Blaseball TTS

Add a "Speak" button to Blaseball live games

// ==UserScript==
// @name         Blaseball TTS
// @namespace    https://freshbreath.zone
// @version      2.1
// @description  Add a "Speak" button to Blaseball live games
// @author       MOS Technology 6502
// @match        https://*.blaseball.com/
// @icon         https://www.google.com/s2/favicons?sz=64&domain=blaseball.com
// @grant        none
// @license      CC0; https://creativecommons.org/share-your-work/public-domain/cc0/
// ==/UserScript==

(function() {
    "use strict";

    // This variable tracks which log we are "listening" to currently.
    let audibleLog;

    // Speak a string
    function speak(message) {

        // Reformat some of the messages for better speech playback.
        //  Fix inning ordinals (1, 2, 3 -> 1st, 2nd, 3rd...)
        function ordinal(n) {
            let s = ["th", "st", "nd", "rd"];
            let v = n%100;
            return (s[(v-20)%10] || s[v] || s[0]);
        }
        let inning = message.match(/End of the (?:top|bottom) of the (\d+)\./);
        if (inning != null) {
            message = message.slice(0, -1) + ordinal(inning[1]);
        }

        // Make pitch counts more phonetic
        message = message.replace(' 0-1', ' oh-n-one');
        message = message.replace(' 0-2', ' oh-n-two');
        message = message.replace(' 1-0', ' one-n-oh');
        if (Math.random() < 0.5) {
            message = message.replace(' 1-1', ' count\'s even at one');
        } else {
            message = message.replace(' 1-1', ' one-n-one');
        }
        message = message.replace(' 1-2', ' one-n-two');
        message = message.replace(' 2-0', ' two-n-oh');
        message = message.replace(' 2-1', ' two-n-one');
        if (Math.random() < 0.5) {
            message = message.replace(' 2-2', ' count\'s even at two');
        } else {
            message = message.replace(' 2-2', ' two-n-two');
        }
        message = message.replace(' 3-0', ' three-n-oh');
        message = message.replace(' 3-1', ' three-n-one');
        message = message.replace(' 3-2', ' full count');

        console.log("Blaseball TTS: Speaking '" + message + "'");

        // Create the TTS object
        let msg = new SpeechSynthesisUtterance();
        msg.text = message;
        // Other fun voice options
        // msg.lang to change language
        // msg.pitch to sets the pitch (tone)
        // msg.rate  to set rate (how fast they talk)
        // msg.voice to change voice (should be one of window.SpeechSynthesis.getVoices())
        // msg.volume to change volume

        // Send the msg to the speech engine
        window.speechSynthesis.speak(msg);
    }

    // Callback for when a button is clicked.
    //  This should try to find the sibling game-widget__log, and set audibleLog to it.
    const buttonCallback = (event) => {
        let node = event.target.parentNode;

        // Reset the audible log
        audibleLog = undefined;
        // Try to set audible log to the game log, if it exists
        for (const subNode of node.childNodes) {
            if (subNode.classList?.contains("game-widget__log")) {
                audibleLog = subNode;
                break;
            }
        }
        console.log("Blaseball TTS: Updating audibleLog to " + audibleLog)
    };

    // Observer added to document which looks for changes
    const callback = (mutationList, observer) => {
        for (const mutation of mutationList) {
            if (mutation.type === "childList") {
                for (const node of mutation.addedNodes) {
                    // Checking for the appearance of the game-widget__status, so we can add our button
                    if (node.classList?.contains("game-widget__status")) {
                        // found it!  create button and add to main list
                        let btn = document.createElement("button");
                        btn.textContent = '🔊';
                        btn.classList.add('schedule__day');
                        btn.addEventListener("click", buttonCallback);
                        node.appendChild(btn);
                    }
                }
            } else if (mutation.type === "characterData") {
                // Verify that the owner of this changed text is the audibleLog.
                //  If so, send the new text to TTS.
                if (audibleLog && (mutation.target.parentNode === audibleLog)) {
                    speak(mutation.target.data);
                }
            }
        }
    };

    // Finally, attach the observer to the HTML document, and we are ready to go!
    const observer = new MutationObserver(callback);
    observer.observe(document, { subtree: true, childList: true, characterData: true });
})();