MH - Golem Visit Stats

Shows golem visit numbers without having to bell your golem or have an idle one. Also adds a small tooltip in the aura to inform you of the hours left until max aura and the number of golems / hats needed for it.

// ==UserScript==
// @name         MH - Golem Visit Stats
// @version      2.1.0
// @description  Shows golem visit numbers without having to bell your golem or have an idle one. Also adds a small tooltip in the aura to inform you of the hours left until max aura and the number of golems / hats needed for it.
// @author       hannadDev
// @namespace    https://greasyfork.org/en/users/1238393-hannaddev
// @match        https://www.mousehuntgame.com/*
// @icon         https://www.mousehuntgame.com/images/items/stats/large/680f6a68612ca9181a90b5719b20ef78.png
// @require      https://cdn.jsdelivr.net/npm/[email protected]/scripts/utils.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/scripts/statics.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/mousehunt-utils.js
// @license      MIT
// ==/UserScript==

(function () {
    "use strict";

    const stylesheetUrl = "https://cdn.jsdelivr.net/npm/[email protected]/stylesheets/main.css";
    const dataUrl = "https://raw.githubusercontent.com/hannadDev/mh-assets/main/data/golem-visit-stats-data.json";

    hd_utils.addStyleElement(stylesheetUrl);

    let data = {
        eventYear: "0",
        eventEndTimestamp: 0,
        shutdownPeriodTimestamp: 0
    };

    // #region Variables
    let isDebug = false;

    const localStorageKey = `mh-golem-visit-stats`;

    let storedData = {};
    // #endregion

    // #region Observers
    const observer = new MutationObserver(function (mutations) {
        if (isDebug) {
            console.log("Mutated");
            for (const mutation of mutations) {
                console.log({ mutation });
                console.log(mutation.target);
            }
        }

        // Only save if something was added.
        if (mutations.some(v => v.type === "childList" && v.addedNodes.length > 0 && v.target.className !== "journaldate")) {
            saveEntries();
        }
    });

    function activateMutationObserver() {
        let observerTarget = document.querySelector(`#journalContainer .content`);

        if (observerTarget !== null && observerTarget !== undefined) {
            observer.observe(observerTarget, {
                childList: true,
                subtree: true
            });
        }
    }

    const xhrObserver = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function () {
        this.addEventListener("load", function () {
            if (this.responseURL === `https://www.mousehuntgame.com/managers/ajax/turns/activeturn.php`) {
                if (isDebug) {
                    console.log("Horn detected");
                }

                saveEntries();
            } else if (this.responseURL === `https://www.mousehuntgame.com/managers/ajax/pages/page.php`) {
                if (isDebug) {
                    console.log("Page load detected");
                }

                activateMutationObserver();
            }
        })
        xhrObserver.apply(this, arguments);
    }
    // #endregion

    // #region Golem Stats Methods
    function getGolemStats(year = data.eventYear) {
        cleanUpEntries();

        let golemStats = {};
        let isCached = false;

        let userQuests = null;
        if (user.quests.QuestIceFortress !== undefined) {
            userQuests = user.quests.QuestIceFortress;
        }

        if (user.quests.QuestCinnamonTreeGrove !== undefined) {
            userQuests = user.quests.QuestCinnamonTreeGrove;
        }

        if (user.quests.QuestGolemWorkshop !== undefined) {
            userQuests = user.quests.QuestGolemWorkshop;
        }

        let total = {
            Golems: 0,
            Hats: 0,
            Scarves: 0
        };

        if (year === data.eventYear && userQuests !== null) {
            for (const k1 in userQuests.destinations) {
                for (const k2 in userQuests.destinations[k1].environments) {
                    if (userQuests.destinations[k1].environments[k2].num_golem_visits !== null) {
                        golemStats[userQuests.destinations[k1].environments[k2].name] = {
                            Golems: userQuests.destinations[k1].environments[k2].num_golem_visits
                        };
                    }
                }
            }

            // Cache data if year is current year
            if (year === data.eventYear) {
                setYearStats(golemStats, data.eventYear);
            }
        } else {
            isCached = true;
            golemStats = (storedData[year] !== undefined) ? storedData[year]["stats"] ?? [] : [];
        }

        for (const k in golemStats) {
            total.Golems += golemStats[k].Golems;

            if (storedData[year] !== undefined && storedData[year]["scrapedStats"] !== undefined && storedData[year]["scrapedStats"][k] !== undefined) {
                golemStats[k].Hats = storedData[year]["scrapedStats"][k].Hats;
                golemStats[k].Scarves = storedData[year]["scrapedStats"][k].Scarves;

                total.Hats += storedData[year]["scrapedStats"][k].Hats;
                total.Scarves += storedData[year]["scrapedStats"][k].Scarves;
            } else {
                golemStats[k].Hats = 0;
                golemStats[k].Scarves = 0;
            }
        }

        if (isDebug) {
            console.log("Golem Stats:");
            console.log(golemStats);

            console.log("Total:");
            console.log(total);
        }

        if (golemStats !== "") {
            createGolemStatsPopup(golemStats, total, isCached, year);
        }
    }

    function showButton() {
        const target = document.querySelector(".mousehuntHud-gameInfo");
        if (target) {
            const link = document.createElement("a");
            link.id = "golem-stats-button";
            link.innerText = "[Golem Stats]";
            link.addEventListener("click", function () {
                getGolemStats();
            });
            target.prepend(link);
        }
    }

    function createGolemStatsPopup(golemStats, totalStats, isCached, year) {
        document.querySelectorAll("#golem-stats-popup-div").forEach(el => el.remove());

        const golemStatsPopup = document.createElement("div");
        golemStatsPopup.id = ("golem-stats-popup-div");
        golemStatsPopup.classList.add("hd-popup");

        // Golem Stats Division
        const golemStatsDiv = document.createElement("div");

        // Title
        const title = document.createElement("h2");
        title.innerText = `Golem Visit Stats - ${year}`
        title.classList.add("hd-bold");
        golemStatsDiv.appendChild(title);

        // Subtitle
        if (isCached) {
            const subtitle = document.createElement("h4");

            if (year === data.eventYear) {
                subtitle.innerText = "Visit event area for latest stats";
            } else {
                subtitle.innerText = "Showing previous year's cached data";
            }

            golemStatsDiv.appendChild(subtitle);
        }

        const spacing = document.createElement("br");
        golemStatsDiv.appendChild(spacing);

        // Table for golem stats
        const golemStatsTable = document.createElement("table");
        golemStatsTable.id = "golem-stats-table"
        golemStatsTable.classList.add("hd-table");

        const headings = ["Locations", "Golems", "Hats", "Scarves"];

        // Create headings
        for (let i = 0; i < headings.length; ++i) {
            const headingElement = document.createElement("th");
            headingElement.id = `golem-stats-${headings[i].toLowerCase()}-heading`;
            headingElement.innerText = headings[i];
            headingElement.classList.add("hd-table-heading");

            golemStatsTable.appendChild(headingElement);
        }

        // Table Body
        const tableBody = document.createElement("tbody");

        var j = 0
        for (const location in golemStats) {
            var tableRow = document.createElement("tr");
            tableRow.id = "golem-stats-table-row-" + j

            for (let i = 0; i < headings.length; ++i) {
                const tdElement = document.createElement("td");
                tdElement.innerText = i === 0 ? location : golemStats[location][headings[i]];
                tdElement.classList.add("hd-table-td");

                tableRow.appendChild(tdElement);
            }

            tableBody.appendChild(tableRow);

            j++;
        }

        // Total Stats
        var totalStatsRow = document.createElement("tr");
        totalStatsRow.id = "golem-stats-table-row-total";
        totalStatsRow.classList.add("hd-table-footer-tr");

        for (let i = 0; i < headings.length; ++i) {
            const tdElement = document.createElement("td");
            tdElement.innerText = i === 0 ? "Total" : totalStats[headings[i]];
            tdElement.classList.add("hd-table-td", "hd-bold");

            totalStatsRow.appendChild(tdElement);
        }

        tableBody.appendChild(totalStatsRow);

        // Final append
        golemStatsTable.appendChild(tableBody)
        golemStatsDiv.appendChild(golemStatsTable);
        golemStatsPopup.appendChild(golemStatsDiv);

        // Years links
        const linksDiv = document.createElement("div");
        linksDiv.classList.add("hd-m-2");
        
        const years = Object.keys(storedData);
        const indexOfFlags = years.indexOf("Flags");
        if (indexOfFlags >= 0) {
            years.splice(indexOfFlags, 1);
        }

        for (const y of years) {
            let yearLink;
            if (y === year) {
                yearLink = document.createElement("span");
            } else {
                yearLink = document.createElement("a");
                yearLink.onclick = function () {
                    getGolemStats(y);
                }
            }

            yearLink.id = `${y}-link`;
            yearLink.innerText = `${y}`;
            yearLink.classList.add("hd-m-2");
            linksDiv.appendChild(yearLink);
        }

        // Append
        golemStatsPopup.appendChild(linksDiv);

        // Close button
        const closeButton = document.createElement("button");
        closeButton.id = "close-button";
        closeButton.textContent = "Close";
        closeButton.classList.add("hd-button");
        closeButton.onclick = function () {
            document.body.removeChild(golemStatsPopup);
        }

        // Append
        golemStatsPopup.appendChild(closeButton);

        // Final Append
        document.body.appendChild(golemStatsPopup);
        hd_utils.dragElement(golemStatsPopup, golemStatsDiv);
    }
    // #endregion

    // #region LocalStorage Methods
    function getStoredData() {
        const savedData = localStorage.getItem(localStorageKey);

        if (savedData !== null) {
            return JSON.parse(savedData);
        }

        return {};
    }

    function setData(stats) {
        localStorage.setItem(localStorageKey, JSON.stringify(stats));
    }

    function setYearStats(stats, year) {
        if (storedData[year] === undefined) {
            storedData[year] = {};
        }

        storedData[year]["stats"] = stats;
        setData(storedData);
    }

    function setYearLogs(logEntries, scrapedStats, year) {
        if (storedData[year] === undefined) {
            storedData[year] = {};
        }

        storedData[year]["logEntries"] = logEntries;
        storedData[year]["scrapedStats"] = scrapedStats;
        setData(storedData);
    }
    // #endregion

    // #region Aura Methods
    function calculateAura() {
        if (document.querySelector(".MiniEventFestiveAura") != null && document.querySelector(".MiniEventFestiveAura").classList.contains("active")) {
            const festiveAuraTooltipElement = document.querySelector(".MiniEventFestiveAura .trapImageView-tooltip-trapAura");

            // #region Current Aura
            const currentAuraTextSplit = festiveAuraTooltipElement.innerText.split("expires on:");
            const currentAuraDateSplit = currentAuraTextSplit[currentAuraTextSplit.length - 1].trim().split(" ");

            if (isDebug) {
                console.log("Current Aura Date Split");
                console.log(currentAuraDateSplit);
            }

            const timeSplit = currentAuraDateSplit[4].split(":");
            let hours = timeSplit[0];
            const minutes = timeSplit[1].slice(0, timeSplit[1].length - 2);
            let period = timeSplit[1].slice(timeSplit[1].length - 2, timeSplit[1].length);

            if (period === "pm" && hours != "12") {
                hours = 12 + parseInt(hours);
            }

            const parsedDate = new Date(`${currentAuraDateSplit[2]}/${hd_statics.monthMap.get(currentAuraDateSplit[0])}/${currentAuraDateSplit[1].replace(",", "")} ${hours}:${minutes}`);

            const parsedDateTimestamp = parsedDate.getTime();

            if (isDebug) {
                console.log("Time Split:");
                console.log(timeSplit);
                console.log(`Hours= ${hours}`);
                console.log(`Minutes= ${minutes}`);
                console.log(`Time Period= ${period}`);
                console.log(`ParsedDate= ${parsedDate}`);
                console.log(`ParsedDate Timestamp= ${parsedDateTimestamp}`);
            }
            // #endregion

            // #region End Date of Aura
            let endAuraTextSplit = festiveAuraTooltipElement.innerText.split("to:")[1].split("Your")[0];
            endAuraTextSplit = endAuraTextSplit.replace("(", "");
            endAuraTextSplit = endAuraTextSplit.replace(")", "");
            let endAuraDateTextSplit = endAuraTextSplit.trim().split(" ");

            const endTimeSplit = endAuraDateTextSplit[4].split(":");
            let endHours = endTimeSplit[0];
            const endMinutes = endTimeSplit[1].slice(0, endTimeSplit[1].length - 2);
            let endPeriod = endTimeSplit[1].slice(endTimeSplit[1].length - 2, endTimeSplit[1].length);

            if (endPeriod === "pm" && endHours != "12") {
                endHours = 12 + parseInt(endHours);
            }

            const parsedEndDate = new Date(`${endAuraDateTextSplit[2]}/${hd_statics.monthMap.get(endAuraDateTextSplit[0])}/${endAuraDateTextSplit[1].replace(",", "")} ${endHours}:${endMinutes}`).getTime();

            if (isDebug) {
                console.log(`EndAuraTextSplit= ${endAuraTextSplit}`);
                console.log(`EndAuraDateTextSplit`);
                console.log(endAuraDateTextSplit);
                console.log(`ParsedEndDate= ${parsedEndDate}`);
            }
            // #endregion

            // #region Golem Stats
            const hoursDifference = (parsedEndDate - parsedDateTimestamp) / 1000 / 60 / 60;

            if (isDebug) {
                console.log(`HoursDifference= ${hoursDifference}`);
            }
            // #endregion

            // #region Max Hunts Stats
            const currentTimestamp = Date.now();

            const maxHunts = (data.eventEndTimestamp - currentTimestamp) / 1000 / 60 / 60 * 5;
            const maxShutdownHunts = (data.shutdownPeriodTimestamp - currentTimestamp) / 1000 / 60 / 60 * 5;

            if (isDebug) {
                console.log(`eventEndTimestamp= ${data.eventEndTimestamp}`);
                console.log(`shutdownPeriodTimestamp= ${data.shutdownPeriodTimestamp}`);
                console.log(`currentTimestamp= ${currentTimestamp}`);
                console.log(`maxHunts= ${maxHunts}`);
                console.log(`maxShutdownHunts= ${maxShutdownHunts}`);
            }
            // #endregion

            // #region Adding tooltip UI
            if (document.getElementById("max-aura-stats-tooltip")) {
                document.getElementById("max-aura-stats-tooltip").remove();
            }

            const maxAuraStatsDiv = document.createElement("div");
            maxAuraStatsDiv.id = "max-aura-stats-tooltip";
            maxAuraStatsDiv.appendChild(document.createElement("br"));

            if (hoursDifference > 0) {
                const hoursRemainingText = document.createTextNode(`Hours needed for max aura: ${hoursDifference.toFixed(2)}`);
                maxAuraStatsDiv.appendChild(hoursRemainingText);

                maxAuraStatsDiv.appendChild(document.createElement("br"));
                const golemsNeededText = document.createTextNode(`${Math.ceil(hoursDifference / 5)} golems in ${Math.ceil(Math.ceil(hoursDifference / 5) / 3) * 25} hunts`);
                maxAuraStatsDiv.appendChild(golemsNeededText);

                maxAuraStatsDiv.appendChild(document.createElement("br"));
                const hatsNeededText = document.createTextNode(`${Math.ceil(hoursDifference / 10)} hats in ${Math.ceil(Math.ceil(hoursDifference / 10) / 3) * 25} hunts`);
                maxAuraStatsDiv.appendChild(hatsNeededText);

                if (maxHunts > 0 || maxShutdownHunts > 0) {
                    maxAuraStatsDiv.appendChild(document.createElement("br"));
                }

                if (maxShutdownHunts > 0) {
                    maxAuraStatsDiv.appendChild(document.createElement("br"));
                    const maxShutdownHuntsText = document.createTextNode(`Larry gets ${Math.floor(maxShutdownHunts)} hunts until shutdown.`);
                    maxAuraStatsDiv.appendChild(maxShutdownHuntsText);
                }

                if (maxHunts > 0) {
                    maxAuraStatsDiv.appendChild(document.createElement("br"));
                    const maxHuntsText = document.createTextNode(`Until GWH ends, Larry gets ${Math.floor(maxHunts)} hunts, can you?`);
                    maxAuraStatsDiv.appendChild(maxHuntsText);
                }
            }
            else {
                const maxAuraText = document.createTextNode("Max aura time reached 🐋 Time to shore🎉");
                maxAuraStatsDiv.appendChild(maxAuraText);
            }

            festiveAuraTooltipElement.appendChild(maxAuraStatsDiv);
            // #endregion
        }
    }
    // #endregion

    // #region Journal Scraping Methods
    function saveEntries() {
        const ownJournal = document.querySelector(`#journalEntries${user.user_id}`);

        if (!ownJournal) {
            if (isDebug) {
                console.log(`Other hunters' profile detected`);
            }

            return;
        }

        const entries = document.querySelectorAll(".entry");

        let savedEntries = {};
        if (storedData[data.eventYear] != undefined && storedData[data.eventYear]["logEntries"] != undefined) {
            savedEntries = storedData[data.eventYear]["logEntries"];
        }

        let savedScrapedStats = {};
        if (storedData[data.eventYear] != undefined && storedData[data.eventYear]["scrapedStats"] != undefined) {
            savedScrapedStats = storedData[data.eventYear]["scrapedStats"];
        }

        let addedNewEntries = false;
        for (const entry of entries) {
            const entryId = entry.dataset.entryId

            if (!entryId) return;

            if (savedEntries[entryId]) {
                if (isDebug) console.log(`Entry ${entryId} already stored`);
            }
            else {
                if (isDebug) console.log(`Stored new entry ${entryId}`);

                if (entry.className.search(/(sendGolem)/) !== -1) {
                    let entryInfo = extractInfoFromEntry(entry);
                    savedEntries[entry.dataset.entryId] = entryInfo;

                    if (savedScrapedStats[entryInfo.LocationName] !== undefined) {
                        if (entryInfo.Hat) savedScrapedStats[entryInfo.LocationName].Hats++;
                        if (entryInfo.Scarf) savedScrapedStats[entryInfo.LocationName].Scarves++;
                    } else {
                        savedScrapedStats[entryInfo.LocationName] = {
                            Hats: entryInfo.Hat ? 1 : 0,
                            Scarves: entryInfo.Scarf ? 1 : 0
                        }
                    }

                    addedNewEntries = true;
                }
            }
        }

        if (addedNewEntries) {
            setYearLogs(savedEntries, savedScrapedStats, data.eventYear);
        }
    }

    function cleanUpEntries() {
        if (!storedData.Flags) {
            storedData.Flags = [];
        }

        if (storedData.CleanedUpEntries20241211) {

            storedData.Flags.push("CleanedUpEntries20241211");
            delete storedData.CleanedUpEntries20241211;
            return;
        }

        if (storedData.Flags.includes("CleanedUpEntries20241211")) {
            return;
        }

        let savedEntries = {};
        if (storedData["2024"] != undefined && storedData["2024"]["logEntries"] != undefined) {
            savedEntries = storedData["2024"]["logEntries"];
        }

        for (const entryId in savedEntries) {
            if (savedEntries[entryId].LocationName.indexOf("the ") === 0) {
                savedEntries[entryId].LocationName = savedEntries[entryId].LocationName.replace("the ", "");
            }
        }

        let savedScrapedStats = {};
        if (storedData["2024"] != undefined && storedData["2024"]["scrapedStats"] != undefined) {
            savedScrapedStats = storedData["2024"]["scrapedStats"];
        }

        for (const locationName in savedScrapedStats) {
            if (locationName.indexOf("the ") === 0) {
                const trimmedLocationName = locationName.replace("the ", "");

                if (!savedScrapedStats[trimmedLocationName]) {
                    savedScrapedStats[trimmedLocationName] = { "Hats": 0, "Scarves": 0 };
                }

                savedScrapedStats[trimmedLocationName]["Hats"] += savedScrapedStats[locationName]["Hats"];
                savedScrapedStats[trimmedLocationName]["Scarves"] += savedScrapedStats[locationName]["Scarves"];
                delete savedScrapedStats[locationName];
            }
        }

        storedData.Flags.push("CleanedUpEntries20241211");
        setYearLogs(savedEntries, savedScrapedStats, data.eventYear);
    }

    function extractInfoFromEntry(entry) {
        const info = {
            LocationName: "",
            Hat: false,
            Scarf: false
        };

        const journalText = entry.querySelector(".journaltext").innerText;
        info.Hat = journalText.includes("Hat");
        info.Scarf = journalText.includes("Scarf");

        const split1 = journalText.split("It lumbered towards ");
        if (split1[1] === undefined) return null;

        split1[1] = split1[1].replace("the ", "");

        const split2 = split1[1].split(" and will be back");
        info.LocationName = split2[0];

        return info;
    }
    // #endregion

    function Initialize() {
        if (isDebug) console.log(`Initializing.`);

        storedData = getStoredData();

        showButton();
        calculateAura();
        activateMutationObserver();

        // #region Listeners
        onPageChange({
            change: () => { calculateAura(); }
        });

        onRequest(() => {
            calculateAura();
        }, "managers/ajax/turns/activeturn.php");
        //#endregion
    }

    hd_utils.readJson(dataUrl, (obj) => {
        data = obj;
        Initialize();
    });
})();