GC Flask Calculator

Displays probabilities of different Flask of Rainbow Fountain Water results at the Rainbow Pool at grundos.cafe.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         GC Flask Calculator
// @namespace    http://devipotato.net/
// @version      1
// @description  Displays probabilities of different Flask of Rainbow Fountain Water results at the Rainbow Pool at grundos.cafe.
// @author       DeviPotato (Devi on GC, devi on Discord)
// @license MIT
// @match        https://www.grundos.cafe/rainbowpool/neopetcolours/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=grundos.cafe
// @grant        GM.setValue
// @grant        GM.getValue
// @run-at document-end
// ==/UserScript==

(async function() {
    const carbonatedRemovedColors = ["Blue", "Green", "Red", "Yellow", "White", "Purple", "Brown", "Pink", "Orange", "Invisible"];

    function urlParam(name){
        var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(window.location.href);
        if (results==null) {
            return null;
        }
        return decodeURI(results[1]) || 0;
    }

    function querySelectorIncludesText (selector, text){
        return Array.from(document.querySelectorAll(selector))
            .filter(el => el.textContent.includes(text));
    }
    function capitalizeFirstLetter(string) {
        return string.charAt(0).toUpperCase() + string.slice(1);
    }

    async function countColors(species) {
        let colorData = JSON.parse(await GM.getValue("colorData", "{}"))
        let normal = 0;
        let carbonated = 0;
        for (const color in colorData[species]) {
            normal += colorData[species][color];
            if(!carbonatedRemovedColors.includes(color)) {
                carbonated += 1;
            }
        }
        return { normal, carbonated };
    }

    async function calculateColorChances(species, color) {
        let colorData = JSON.parse(await GM.getValue("colorData", "{}"))
        let capsColor = capitalizeFirstLetter(color);
        let totalNormalColors = 0;
        let totalCarbonatedColors = 0;
        for (const color in colorData[species]) {
            totalNormalColors += colorData[species][color];
            if(!carbonatedRemovedColors.includes(color)) {
                totalCarbonatedColors += 1;
            }
        }
        let normalChance = colorData[species][capsColor] / totalNormalColors;
        let specificNormalChance = 1 / totalNormalColors;
        let carbonatedChance = (carbonatedRemovedColors.includes(capsColor)?0:1/totalCarbonatedColors);
        return { normalChance, specificNormalChance, carbonatedChance }
    }

    function calculateMultiflaskChances(colorChances, flasks) {
        let normalChance = 1 - Math.pow(1 - colorChances.normalChance, flasks);
        let specificNormalChance = 1 - Math.pow(1 - colorChances.specificNormalChance, flasks);
        let carbonatedChance = 1 - Math.pow(1 - colorChances.carbonatedChance, flasks);
        return { normalChance, specificNormalChance, carbonatedChance }
    }

    function colorStats(colorChances, colorName, isAlt=false) {
        let hasAlts = colorChances.normalChance != colorChances.specificNormalChance;
        let container = document.createElement("div");
        container.classList.add("flask_statscontainer");
        let stats = document.createElement("div");
        stats.classList.add("flask_stats");
        container.append(stats);
        let form = document.createElement("div");
        form.classList.add("multiflask_form");
        form.textContent = "Number of flasks: ";
        container.appendChild(form);
        let flaskCount = document.createElement("input");
        flaskCount.type = "text";
        flaskCount.inputmode = "numeric";
        flaskCount.pattern = "[0-9]*";
        flaskCount.value = 1;
        flaskCount.classList.add("multiflask_count","form-control");
        flaskCount.size = 5;
        //workaround to prevent auto-focus on mobile
        flaskCount.disabled = true;
        flaskCount.autofocus = false;
        form.appendChild(flaskCount);
        form.appendChild(document.createTextNode(" "));
        let calculateButton = document.createElement("input");
        calculateButton.type = "button";
        calculateButton.classList.add("multiflask_button","form-control");
        calculateButton.value = "Calculate";
        form.appendChild(calculateButton);
        flaskCount.addEventListener("input", (event) => {
            if (isNaN(flaskCount.value) || !Number.isInteger(flaskCount.value)) {
                flaskCount.value = flaskCount.value.replace(/\D/g, '');
            }
        }, false);
        flaskCount.addEventListener("keydown", (event) => {
            if(event.key === "Enter") {
                calculateButton.click();
            }
        }, false);
        calculateButton.addEventListener("click", async () => {
            if(flaskCount.value == 0) flaskCount.value = 1;
            let multiflaskChances = calculateMultiflaskChances(colorChances, flaskCount.value);
            let hasAlts = colorChances.normalChance != colorChances.specificNormalChance;
            stats.innerHTML = formatFlaskStats(multiflaskChances, colorName, isAlt);
        }, false);
        stats.innerHTML = formatFlaskStats(colorChances, colorName, isAlt);
        return container;
    }

    //workaround to prevent auto-focus on mobile
    function enableFlaskBoxes() {
        let boxes = document.querySelectorAll(".multiflask_count");
        boxes.forEach(box => {
            box.disabled = false;
        })
    }

    function miniStats(colorChances, isAlt=false) {
        let miniStats = document.createElement("span");
        miniStats.classList.add("flask_ministats");
        miniStats.innerHTML = `(${(colorChances.normalChance*100).toFixed(2)}% | ✨${(colorChances.carbonatedChance*100).toFixed(2)}%)`
        return miniStats;
    }

    function formatFlaskStats(colorChances, colorName, isAlt=false) {
        let hasAlts = colorChances.normalChance != colorChances.specificNormalChance;
        if(isAlt) {
            return `Normal Flasks: <b>${(colorChances.normalChance*100).toFixed(2)}%</b> ${hasAlts?`(any ${colorName}) |  <b>${(colorChances.specificNormalChance*100).toFixed(2)}%</b> (this version)`:""}<br>
    ✨Carbonated Flasks: <b>${(colorChances.carbonatedChance*100).toFixed(2)}%</b> (base ${colorName}) | <b>${(0).toFixed(2)}%</b> (this version)`
        }
        else {
            return `Normal Flasks: <b>${(colorChances.normalChance*100).toFixed(2)}%</b> ${hasAlts?`(any ${colorName}) |  <b>${(colorChances.specificNormalChance*100).toFixed(2)}%</b> (this version)`:""}<br>
    ✨Carbonated Flasks: <b>${(colorChances.carbonatedChance*100).toFixed(2)}%</b>`
        }
    }

    function formatColorTotals(totals) {
        return `There are <b>${totals.normal}</b> possible results for normal flasks and <b>${totals.carbonated}</b> possible results for ✨carbonated flasks.`
    }

    //count the colors and update data, return if a new color was found
    async function parseColors(content, species) {
        let colorData = JSON.parse(await GM.getValue("colorData", "{}"))
        if(!colorData[species]) {
            colorData[species] = {};
        }
        let newColors = false;
        let colorElements = content.querySelectorAll('.flex-column.small-gap');
        colorElements.forEach(element => {
            let colorName = element.getElementsByTagName("span")[0].innerText.trim();
            if(element.innerHTML.includes("https://grundoscafe.b-cdn.net/items/100012.gif")) {
                if(colorData[species] && colorData[species][colorName]==1) {
                    newColors = true; // there is an alt icon for a color we don't know the alts for
                }
            }
            if(!colorData[species][colorName]) {
                newColors = true; // this is a color we did not know about before
                colorData[species][colorName] = 1;
            }
        })
        await GM.setValue("colorData", JSON.stringify(colorData));
        return newColors;
    }

    async function parseAlts(content, species) {
        let colorData = JSON.parse(await GM.getValue("colorData", "{}"))
        if(!colorData[species]) {
            colorData[species] = {};
        }
        let colorElements = content.querySelectorAll('.flex-column.small-gap');
        let totals = {};
        let newColors = false;
        colorElements.forEach(element => {
            let colorName = element.getElementsByTagName("span")[0].innerText.trim();
            let imgSrc = element.getElementsByTagName("img")[0].src;
            if(colorName == "Royal" || colorName == "Usuki" || colorName == "Quiguki") {
                if(imgSrc.includes("boy")) {
                    colorName = colorName + "boy";
                }
                else if(imgSrc.includes("girl")) {
                    colorName = colorName + "girl";
                }
            }
            totals[colorName] = (totals[colorName] || 0) + 1;

        })
        for (const color in totals) {
            let newTotal = totals[color] + 1;
            if(colorData[species] && colorData[species][color] != newTotal) {
                newColors = true; // this is a different total of alts than we had before
                colorData[species][color] = newTotal;
            }
        }
        await GM.setValue("colorData", JSON.stringify(colorData));
        return newColors;
    }

    async function attachMiniStats(elements, species) {
        for(let element of elements) {
            let colorName = element.getElementsByTagName("span")[0].innerText.trim();
            let imgSrc = element.getElementsByTagName("img")[0].src;
            let isAlt = imgSrc.includes("alt") || imgSrc.includes("classic")
            if(colorName == "Royal" || colorName == "Usuki" || colorName == "Quiguki") {
                if(imgSrc.includes("boy")) {
                    colorName = colorName + "boy";
                }
                else if(imgSrc.includes("girl")) {
                    colorName = colorName + "girl";
                }
            }
            let colorChances = await calculateColorChances(species, colorName)
            element.querySelector("span").after(miniStats(colorChances,isAlt));
        }
    }

    // thank you twiggies!
    async function fetchPage(url) {
        try {
            const response = await fetch(url);
            if(response.ok) {
                const html = await response.text();
                const node = await new DOMParser().parseFromString(html, "text/html");
                // show RE if the fetched page contains a RE
                if (node.getElementById('page_event').innerHTML.trim() != '') {
                    document.getElementById('page_event').innerHTML += node.getElementById('page_event').innerHTML
                }
                return node;
            } else {
                throw new Error(`${response.status} ${response.statusText}`);
            }
        }
        catch(err) {
            console.log(`Encountered error fetching url ${url}:`)
            console.log(err)
        }
    }

    async function fetchColors(species) {
        const colorPage = await fetchPage(`https://www.grundos.cafe/rainbowpool/neopetcolours/?species=${species}`);
        return await parseColors(colorPage, species);
    }

    async function fetchAlts(species) {
        const altPage = await fetchPage(`https://www.grundos.cafe/rainbowpool/neopetcolours/?species=${species}&altsonly=true`);
        return await parseAlts(altPage, species);
    }

    if(document.querySelectorAll(".errorpage").length==0)
    {
        if(urlParam("species")) {
            let species = urlParam("species");
            // for all color/all alt color pages
            if(!urlParam("colour")) {
                let isAlts = urlParam("altsonly")=="true";
                let newColors = false;
                let colorElements = document.querySelectorAll('.flex-column.small-gap');
                if(!isAlts) {
                    newColors = await parseColors(document, species);
                    if(newColors) await fetchAlts(species);
                } else {
                    newColors = await parseAlts(document, species);
                    if(newColors) await fetchColors(species)
                }
                let colorData = JSON.parse(await GM.getValue("colorData", "{}"));
                let flaskTotals = document.createElement("p");
                let totals = await countColors(species);
                flaskTotals.innerHTML = formatColorTotals(totals);
                querySelectorIncludesText("strong","Colours")[0].after(flaskTotals);
                await attachMiniStats(colorElements, species);
            }
            // for specific color pages
            else {
                let colorData = JSON.parse(await GM.getValue("colorData", "{}"))
                if(!colorData[species]) {
                    await fetchColors(species);
                    await fetchAlts(species);
                    colorData = JSON.parse(await GM.getValue("colorData", "{}"))
                }
                if(colorData[species]) {
                    let color = urlParam("colour");
                    let capsSpecies = species=="jubjub"?"JubJub":capitalizeFirstLetter(species);
                    let capsColor = capitalizeFirstLetter(color);
                    capsColor = capsColor.split("_")[0]
                    if(!colorData[species][capsColor]) {
                        await fetchColors(species);
                        await fetchAlts(species);
                        colorData = JSON.parse(await GM.getValue("colorData", "{}"))
                    }
                    let colorChances = await calculateColorChances(species, capsColor)
                    if(!color.includes("alt") && !color.includes("classic")) {
                        let altHeaders = querySelectorIncludesText("strong",`Alternate`);
                        let classicHeaders = querySelectorIncludesText("strong",`Classic`);
                        let allAlts = [...altHeaders, ...classicHeaders];

                        if(colorData[species][capsColor] != allAlts.length+1) {
                            await fetchColors(species);
                            await fetchAlts(species);
                            colorChances = await calculateColorChances(species, capsColor)
                        }

                        querySelectorIncludesText("strong",`${capsColor} ${capsSpecies}`)[0].after(colorStats(colorChances,capsColor,false));

                        allAlts.forEach(header => {
                            header.after(colorStats(colorChances,capsColor,true));
                        })
                    }
                    else {
                        querySelectorIncludesText("strong",capsSpecies)[0].after(colorStats(colorChances,capsColor,true));
                    }

                }
                //workaround to prevent auto-focus on mobile
                setTimeout(() => {
                    enableFlaskBoxes();
                }, 300)
            }
        }
        // color page of all species
        else if(urlParam("colour")) {
            let colorData = JSON.parse(await GM.getValue("colorData", "{}"))
            let color = urlParam("colour");
            let capsColor = capitalizeFirstLetter(color);
            let speciesElements = document.querySelectorAll('.flex-column.small-gap');
            let missingData = false;
            for(let element of speciesElements) {
                let species = element.getElementsByTagName("span")[0].innerText.trim().toLowerCase();
                if(colorData[species] && colorData[species][capsColor]) {
                    let colorChances = await calculateColorChances(species, capsColor)
                    element.querySelector("span").after(miniStats(colorChances));
                }
                else {
                    missingData = true;
                    let noData = document.createElement("span");
                    noData.innerHTML = `(???%)`
                    element.querySelector("span").after(noData);
                }
            }
            if(missingData) {
                let warning = document.createElement("p");
                warning.innerHTML = `Colour data for some of these species is missing or out of date. Please visit their pages to see their flask chances.`
                querySelectorIncludesText("strong","Available")[0].after(warning);
            }
        }
    }
})();