anonymous_you

Endchan 8ball and more

Dec 30th, 2022 (edited)
2,953
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // ==UserScript==
  2. // @name         Endchan 8ball and more
  3. // @namespace    https://endchan.org/
  4. // @version      1.8
  5. // @description  Adds #8ball, #dX and #flip functionality to endchan
  6. // @author       Anonymous (You)
  7. // @match        http://endchan.org/*/res/*
  8. // @match        https://endchan.org/*/res/*
  9. // @match        http://endchan.net/*/res/*
  10. // @match        https://endchan.net/*/res/*
  11. // @match        http://magrathea.endchan.net/*/thread/*
  12. // @match        https://magrathea.endchan.net/*/thread/*
  13. // @grant        none
  14. // @downloadURL  https://pastebin.com/raw/LGpcq1wJ
  15. // @updateURL    https://pastebin.com/raw/LGpcq1wJ
  16. // ==/UserScript==
  17.  
  18. /*
  19. Finds, evaluates and replaces occurrences of # commands in posts.
  20. Supports:
  21.     - #8ball
  22.     - #dX and #XdY (dice)
  23.     - #flip (coin)
  24.     - #slap, #bonk
  25.     - #blur
  26.     - #spoiler
  27. As long as the logic in the randomEngine() function remains intact, the same results should persist everywhere for everyone, even with page refreshes and without having to store anything anywhere.
  28.  
  29. When doing #XdY the maximum X (number of dice) it'll accept is 20 by default. Any more and the command won't evaluate to prevent stupidity like #1000d20 and getting spammed with output.
  30. The default can be adujsted with the maxNumDice const - this should have no effect on the random generation, those with a value set lower simply won't see the results that are out of bounds.
  31.  
  32. Quick styling adjustments can be done with the consts below, for more in depth customization use custom CSS.
  33. The evaluated results are placed in a span that has a class attribute, use the .megu8Ball {} .meguDice {} .meguCoin {} .meguBlur {} .meguSpoiler {} selectors.
  34. Additionally crit fail/success on dice have .meguDiceFail {} and .meguDiceSuccess {} selectors which only apply on single dice rolls.
  35.  
  36. #blur will apply a blur filter to the last quoted post before the command. #spoiler will replace the thumbnails in the quoted post with the spoler image.
  37. Both intentionally only apply to one post so that it's sligtly more difficult to obscure vast portions of the thread.
  38. As with all other commands the blur and spoiler will only apply for people running the script, but they will obscure for everyone with the script not just yourself.
  39. The #blur/#spoiler post probably also needs to not be filtered to apply. Hovering over a blurred post will reveal it for the duration. Spoiler works as it does normally.
  40. */
  41.  
  42. /*
  43. 1.8 - option to disable #spoiler for yourself
  44. 1.7 - added #spoiler
  45. 1.6 - added endchan.net to @match
  46. 1.5 - removed unsafe eval
  47. 1.4 - added #blur
  48. 1.3 - added #slap #bonk
  49. 1.2 - support for magrathea
  50. 1.1 - support for #XdY type dice & #flip
  51. */
  52.  
  53. const defaultColor = "#E0FFFF";
  54. const critFailColor = "#A0522D";
  55. const critSuccessColor = "#B22222";
  56. const fontWeight = "700";
  57.  
  58. const blurStrength = 4; // set to 0 to disable functionality for yourself
  59. const spoilerImage = "/ttg/custom.spoiler"; // set to undefined to disable functionality for yourself
  60.  
  61. const maxNumDice = 20;
  62.  
  63. function addStyle(css) {
  64.   const style = document.getElementById("EC_8ball_addedStyle") || (function() {
  65.     const style = document.createElement('style');
  66.     style.type = 'text/css';
  67.     style.id = "EC_8ball_addedStyle";
  68.     document.head.appendChild(style);
  69.     return style;
  70.   })();
  71.   style.innerHTML = style.innerHTML + css + "\n";
  72. }
  73.  
  74. (function() {
  75.     'use strict';
  76.  
  77.     // add css for flashing text
  78.     addStyle(".flashRed{animation:0.3s 10 ease-in-out alternate flashRed;color:tomato;}");
  79.     addStyle("@keyframes flashRed{from{color:tomato}to{color:white}}");
  80.     addStyle(".flashOrange{animation:0.3s 10 ease-in-out alternate flashOrange;color:orange;}");
  81.     addStyle("@keyframes flashOrange{from{color:orange}to{color:white}}");
  82.     addStyle(".meguBlurrable{filter:blur(" + blurStrength + "px);}");
  83.     addStyle(".meguBlurrable:hover{filter:none;}");
  84.  
  85.     // parse all pre-existing posts when page loads
  86.     processPosts(document.body);
  87.  
  88.     // callback function to execute when mutations are observed
  89.     let callback = function(mutationsList) {
  90.         for(let mutation of mutationsList) {
  91.             for (let node of mutation.addedNodes) {
  92.                 // make sure we can get elements by class name on node
  93.                 if (typeof node.getElementsByClassName === "function") {
  94.                     processPosts(node);
  95.                 }
  96.             }
  97.         }
  98.     };
  99.  
  100.     // observe page for changes to sauce new posts (all articles/posts should be inside the a tag with the id "divThreads")
  101.     var observer = new MutationObserver(callback);
  102.     let observedNode = document.getElementById("divThreads");
  103.     if (!observedNode) {
  104.         observedNode = document.getElementById("threadsContainer"); // magrathea
  105.     }
  106.     observer.observe(observedNode, { childList: true, subtree:true });
  107. })();
  108.  
  109. // process all posts in subtree of given node
  110. function processPosts(node) {
  111.     let posts = node.getElementsByClassName("innerPost");
  112.     let markedPosts = node.getElementsByClassName("markedPost");
  113.     if (!posts || posts.length == 0) {
  114.         posts = node.getElementsByClassName("post-container"); // magrathea
  115.     }
  116.     for (let post of posts) {
  117.         processPost(post);
  118.     }
  119.     for (let markedPost of markedPosts) {
  120.         processPost(markedPost);
  121.     }
  122. }
  123.  
  124. // process a single post
  125. function processPost(post) {
  126.     let linkQuote = post.getElementsByClassName("linkQuote");
  127.     let id = linkQuote && linkQuote.length > 0 ? linkQuote[0].innerHTML : "no-value";
  128.     if (id == "no-value") { // magrathea
  129.         let postInfo = post.getElementsByClassName("post-info");
  130.         if (postInfo && postInfo.length > 0) {
  131.             let match = /No. (\d+)/g.exec(postInfo[0].innerHTML);
  132.             if (match && match.length > 1) {
  133.                 id = match[1];
  134.             }
  135.         }
  136.     }
  137.     let messageDiv = post.getElementsByClassName("divMessage");
  138.     if (!messageDiv || messageDiv.length == 0) {
  139.         messageDiv = post.getElementsByClassName("post-message"); // magrathea
  140.     }
  141.     let message = messageDiv && messageDiv.length > 0 ? messageDiv[0].innerHTML : null;
  142.     if (!message) {
  143.         return;
  144.     }
  145.     // at this point we know there's a message present in the messageDiv & id is set to some value -> find and replace any commands
  146.  
  147.     // the match function can get an index param (idx where in the string the match occurred) but it's safer to keep track of the idx separately - not sure if innerHtml is the same for everyone
  148.     // reseting the index between commands makes them independat from each other (adding commands won't interfere with existing results)
  149.     // reset to random so that commands don't all seed with the same value and produce predictably similar results
  150.     let nextIdx = randomEngine(id, 0);
  151.     let idx = nextIdx();
  152.     message = message.replace(/#8ball/g, function(_) { return eightBall(id, idx++); });
  153.     idx = nextIdx();
  154.     message = message.replace(/#(\d+)?d(\d+)/g, function(match, group1, group2) { return dice(id, idx++, match, group1, group2); });
  155.     idx = nextIdx();
  156.     message = message.replace(/#flip/g, function(_) { return coin(id, idx++); });
  157.     message = message.replace(/#slap/ig, '<span class="flashRed">#SLAP</span>');
  158.     message = message.replace(/#bonk/ig, '<span class="flashOrange">#bonk</span>');
  159.     message = message.replace(/#blur/g, function(_, idx) { return blur(message, idx); });
  160.     message = message.replace(/#spoiler/g, function(_, idx) { return spoiler(message, idx); });
  161.     messageDiv[0].innerHTML = message;
  162. }
  163.  
  164. function getPostNumberBeforeCommand(message, idx) {
  165.     let regexMatch = Array.from(message.slice(0, idx).matchAll(/>&gt;&gt;\/?(\d+)\/?<\/a>/g), m => m[1]); // [1] is the matched group
  166.     if (regexMatch && regexMatch.length > 0) {
  167.         let postNum = regexMatch[regexMatch.length - 1];
  168.         if (postNum) {
  169.             return postNum;
  170.         }
  171.     }
  172.     return undefined;
  173. }
  174.  
  175. function getPost(num) {
  176.     let post = undefined;
  177.     let linkQuotes = Array.from(document.getElementsByClassName("linkQuote"));
  178.     if (linkQuotes && linkQuotes.length > 0) {
  179.         let linkQuote = linkQuotes.find(element => element.innerHTML.includes(num));
  180.         if (linkQuote) {
  181.             post = linkQuote.parentElement;
  182.         }
  183.     } else {
  184.         // magrathea
  185.         let postInfos = document.getElementsByClassName("post-info");
  186.         let link = undefined;
  187.         for (let postInfo of postInfos) {
  188.             let links = postInfo.getElementsByTagName("a");
  189.             if (links && links.length > 0 && links[0].innerHTML.includes(num)) {
  190.                 link = links[0];
  191.             }
  192.             if (link) {
  193.                 post = link.parentElement.parentElement;
  194.                 break;
  195.             }
  196.         }
  197.     }
  198.     return post;
  199. }
  200.  
  201. // blur command needs a reference to the message and index of where the #blur occurred in the msg
  202. function blur(message, idx) {
  203.     let postNumToBlur = getPostNumberBeforeCommand(message, idx)
  204.     if (postNumToBlur) {
  205.         let post = getPost(postNumToBlur);
  206.         if (post) {
  207.             post.classList.add("meguBlurrable");
  208.         }
  209.     }
  210.     return '<span class="meguBlur" style="font-weight: ' + fontWeight + '; color: ' + defaultColor + ';">#blur</span>';
  211. }
  212.  
  213. // spoiler command needs a reference to the message and index of where the #spoiler occurred in the msg
  214. function spoiler(message, idx) {
  215.     let postNumToSpoiler = getPostNumberBeforeCommand(message, idx)
  216.     if (postNumToSpoiler) {
  217.         let post = getPost(postNumToSpoiler);
  218.         if (post && spoilerImage) {
  219.             const images = post.querySelectorAll('img');
  220.             images.forEach(img => { img.src = spoilerImage });
  221.         }
  222.     }
  223.     return '<span class="meguSpoiler" style="font-weight: ' + fontWeight + '; color: ' + defaultColor + ';">#spoiler</span>';
  224. }
  225.  
  226. // 8ball just needs the seed values for random, match is always #8ball
  227. function eightBall(id, idx) {
  228.     let outcome = Math.floor(randomEngine(id, idx)() * 7);
  229.     let replacement = "";
  230.     switch(outcome) {
  231.         case 0:
  232.             replacement = "#8ball (Yes)";
  233.             break;
  234.         case 1:
  235.             replacement = "#8ball (No)";
  236.             break;
  237.         case 2:
  238.             replacement = "#8ball (Maybe)";
  239.             break;
  240.         case 3:
  241.             // pastebin doesn't like using swear words :(
  242.             replacement = "#8ball (Hell Yeah, Mother" + "f" + "u" + "c" + "k" + "e" + "r" + "!)";
  243.             break;
  244.         case 4:
  245.             replacement = "#8ball (Outlook not so good)";
  246.             break;
  247.         case 5:
  248.             replacement = "#8ball (Signs point to yes)";
  249.             break;
  250.         case 6:
  251.             replacement = "#8ball (It can't be helped)";
  252.             break;
  253.         default: // this shouldn't happen
  254.             replacement = "#8ball (There is no spoon)";
  255.             break;
  256.     }
  257.  
  258.     replacement = '<span class="megu8Ball" style="font-weight: ' + fontWeight + '; color: ' + defaultColor + ';">' + replacement + '</span>';
  259.     return replacement;
  260. }
  261.  
  262. // needs seed values for random & the match to determine the number of sides on the dice
  263. function dice(id, idx, match, numDice, sizeDice) {
  264.     let n = numDice;
  265.     if (!n || n < 1) {
  266.         n = 1;
  267.     } else if (n > maxNumDice) {
  268.         return match;
  269.     }
  270.     let sides = sizeDice;
  271.     if (!sides || sides < 1) {
  272.         return match;
  273.     }
  274.  
  275.     let nextRandom = randomEngine(id, idx);
  276.     let outcomes = [];
  277.     let outcomeSum = 0;
  278.     for (let i = 0; i < n; i++) {
  279.         // +1 as 0 is not a dice side
  280.         let value = Math.floor(nextRandom() * sides) + 1;
  281.         outcomes.push(value);
  282.         outcomeSum += value;
  283.     }
  284.     let outcome = outcomeSum;
  285.     if (outcomes.length > 1) {
  286.         outcome = outcomes.join(" + ") + " = " + outcomeSum;
  287.     }
  288.  
  289.     let replacement = match + " (" + outcome + ")";
  290.     let color = defaultColor;
  291.     let additionalClasses = "";
  292.     if (outcomes.length == 1 && outcome == 1) {
  293.         color = critFailColor;
  294.         additionalClasses = " meguDiceFail flashOrange";
  295.     } else if (outcomes.length == 1 && outcome == sides) {
  296.         color = critSuccessColor;
  297.         additionalClasses = " meguDiceSuccess flashRed";
  298.     }
  299.     replacement = '<span class="meguDice' + additionalClasses + '" style="font-weight: ' + fontWeight + '; color: ' + color + ';">' + replacement + '</span>';
  300.     return replacement;
  301. }
  302.  
  303. // coin just needs the seed values for random, match is always #flip
  304. function coin(id, idx) {
  305. let outcome = Math.floor(randomEngine(id, idx)() * 2);
  306.     let replacement = "";
  307.     switch(outcome) {
  308.         case 0:
  309.             replacement = "#flip (Heads)";
  310.             break;
  311.         case 1:
  312.             replacement = "#flip (Tails)";
  313.             break;
  314.         default: // this shouldn't happen
  315.             replacement = "#flip (Edge)";
  316.             break;
  317.     }
  318.  
  319.     replacement = '<span class="meguCoin" style="font-weight: ' + fontWeight + '; color: ' + defaultColor + ';">' + replacement + '</span>';
  320.     return replacement;
  321. }
  322.  
  323.  
  324.  
  325. // don't touch this
  326. // this is the random generator, if it changes for some people and not others then results will desync
  327. function randomEngine(param1, param2) {
  328.     // hash up seed params
  329.     let seedValue = [param1, param2].join("::");
  330.     let seedHash = 0;
  331.     if (seedValue.length === 0) return seedHash;
  332.     for (let i = 0; i < seedValue.length; i++) {
  333.         let char = seedValue.charCodeAt(i);
  334.         seedHash = ((seedHash << 5) - seedHash) + char;
  335.         seedHash = seedHash & seedHash;
  336.     }
  337.     // ensure hash is not negative
  338.     seedHash = seedHash & 0xFFFFFFFF;
  339.     seedHash = seedHash >>> 0;
  340.  
  341.     // init other values used in pseudo-rand calc
  342.     var m = Math.pow(2, 32);
  343.     var a = 1103515245;
  344.     var c = 12345;
  345.  
  346.     // return function that produces a random number between 0 and 1 when called
  347.     return function() {
  348.         seedHash = (a * seedHash + c) % m;
  349.         return seedHash / m;
  350.     }
  351. }
  352.  
Advertisement
Add Comment
Please, Sign In to add comment