MilkyWayIdle Consumable Stock Alert (Team Channel Only)

Alert clan chat when consumables stock is low in MilkyWayIdle

// ==UserScript==
// @name         MilkyWayIdle Consumable Stock Alert (Team Channel Only)
// @namespace    http://tampermonkey.net/
// @version      1.9.3
// @description  Alert clan chat when consumables stock is low in MilkyWayIdle
// @author       roastrabbit
// @match        https://www.milkywayidle.com/*
// @match        https://test.milkywayidle.com/*
// @icon         https://www.milkywayidle.com/favicon.svg
// @license      CC-BY-NC-SA-4.0
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    const CONSUMABLE_THRESHOLD = 100;
    const CHECK_INTERVAL_MS = 30000;
    const warnedItems = new Set();

    // --- Hook init_character_data via MessageEvent patch ---
    const origDesc = Object.getOwnPropertyDescriptor(MessageEvent.prototype, 'data');
    const isAlreadyHooked = origDesc?.get?.name === 'MWI_hookedGet';

    if (!isAlreadyHooked) {
        const originalGetter = origDesc?.get;
        Object.defineProperty(MessageEvent.prototype, 'data', {
            get: function MWI_hookedGet() {
                const value = originalGetter?.call(this);
                const socket = this.currentTarget;

                if (socket instanceof WebSocket && socket.url.includes("milkywayidle.com/ws")) {
                    try {
                        const parsed = JSON.parse(value);
                        if (parsed?.type === 'init_character_data') {
                            // console.log('[MCSAlert] Hooked init_character_data:', parsed);
                            const names = Object.values(parsed.partyInfo?.sharableCharacterMap || {})
                            .map(player => player.name).filter(Boolean);
                            localStorage.setItem('MCSAlert_activeCombatPlayers', JSON.stringify(names));
                        }
                    } catch (_) {}
                }

                Object.defineProperty(this, 'data', { value }); // prevent re-entry
                return value;
            },
            configurable: true,
            enumerable: true
        });
    }

    const init_Client_Data = localStorage.getItem('initClientData');
    const init_Client_Data_ = JSON.parse(init_Client_Data);
    const item_hrid_to_name = {};
    const item_name_to_hrid = {};

    for (const [hrid, item] of Object.entries(init_Client_Data_.itemDetailMap || {})) {
        if (item && typeof item === 'object' && item.name) {
            item_hrid_to_name[hrid] = item.name;
            item_name_to_hrid[item.name] = hrid;
        }
    }

    const e2c = {
        "Donut": "甜甜圈",
        "Blueberry Donut": "蓝莓甜甜圈",
        "Blackberry Donut": "黑莓甜甜圈",
        "Strawberry Donut": "草莓甜甜圈",
        "Mooberry Donut": "哞莓甜甜圈",
        "Marsberry Donut": "火星莓甜甜圈",
        "Spaceberry Donut": "太空莓甜甜圈",
        "Cupcake": "纸杯蛋糕",
        "Blueberry Cake": "蓝莓蛋糕",
        "Blackberry Cake": "黑莓蛋糕",
        "Strawberry Cake": "草莓蛋糕",
        "Mooberry Cake": "哞莓蛋糕",
        "Marsberry Cake": "火星莓蛋糕",
        "Spaceberry Cake": "太空莓蛋糕",
        "Gummy": "软糖",
        "Apple Gummy": "苹果软糖",
        "Orange Gummy": "橙子软糖",
        "Plum Gummy": "李子软糖",
        "Peach Gummy": "桃子软糖",
        "Dragon Fruit Gummy": "火龙果软糖",
        "Star Fruit Gummy": "杨桃软糖",
        "Yogurt": "酸奶",
        "Apple Yogurt": "苹果酸奶",
        "Orange Yogurt": "橙子酸奶",
        "Plum Yogurt": "李子酸奶",
        "Peach Yogurt": "桃子酸奶",
        "Dragon Fruit Yogurt": "火龙果酸奶",
        "Star Fruit Yogurt": "杨桃酸奶",
        "Stamina Coffee": "耐力咖啡",
        "Intelligence Coffee": "智力咖啡",
        "Defense Coffee": "防御咖啡",
        "Attack Coffee": "攻击咖啡",
        "Power Coffee": "力量咖啡",
        "Ranged Coffee": "远程咖啡",
        "Magic Coffee": "魔法咖啡",
        "Super Stamina Coffee": "超级耐力咖啡",
        "Super Intelligence Coffee": "超级智力咖啡",
        "Super Defense Coffee": "超级防御咖啡",
        "Super Attack Coffee": "超级攻击咖啡",
        "Super Power Coffee": "超级力量咖啡",
        "Super Ranged Coffee": "超级远程咖啡",
        "Super Magic Coffee": "超级魔法咖啡",
        "Ultra Stamina Coffee": "究极耐力咖啡",
        "Ultra Intelligence Coffee": "究极智力咖啡",
        "Ultra Defense Coffee": "究极防御咖啡",
        "Ultra Attack Coffee": "究极攻击咖啡",
        "Ultra Power Coffee": "究极力量咖啡",
        "Ultra Ranged Coffee": "究极远程咖啡",
        "Ultra Magic Coffee": "究极魔法咖啡",
        "Wisdom Coffee": "经验咖啡",
        "Lucky Coffee": "幸运咖啡",
        "Swiftness Coffee": "迅捷咖啡",
        "Channeling Coffee": "吟唱咖啡",
        "Critical Coffee": "暴击咖啡"
    };

    function activateClanChannel() {
        const chatContainer = document.querySelector('div.Chat_chat__3DQkj');
        if (!chatContainer) {
            console.warn("[MCSAlert] Chat container not found.");
            return false;
        }

        const tabButtons = chatContainer.querySelectorAll('button.MuiTab-root');

        for (const btn of tabButtons) {
            const badge = btn.querySelector('span.TabsComponent_badge__1Du26');
            const labelText = badge?.childNodes?.[0]?.textContent?.trim();

            if (labelText === '队伍') {
                btn.click();
                console.log("[MCSAlert] Team channel tab activated.");
                return true;
            }
        }

        console.warn("[MCSAlert] Team tab not found in chat area.");
        return false;
    }


    function getCombatPlayerAlerts() {
        const edibleTools = JSON.parse(localStorage.getItem('Edible_Tools')) || {};
        const activeCombatPlayers = localStorage.getItem('MCSAlert_activeCombatPlayers');
        const combatPlayers = edibleTools?.Combat_Data?.Combat_Player_Data || {};
        const alertList = [];

        for (const [playerName, playerData] of Object.entries(combatPlayers)) {
            if (activeCombatPlayers.includes(playerName)) {
                // console.log(`Checking active combat player ${playerName}`);
                const inventory = playerData?.Food_Data?.End?.Food || {};
                for (const [itemID, quantity] of Object.entries(inventory)) {
                    if (quantity <= CONSUMABLE_THRESHOLD) {
                        const itemName = item_hrid_to_name[itemID] || itemID;
                        const localizedName = e2c[itemName] || itemName;
                        alertList.push(`[${playerName}] ${localizedName} (${quantity})`);
                    }
                }
            }
        }
        console.log(alertList);
        return alertList;
    }

    async function sendClanMessage(msg) {
        if (!activateClanChannel()) return;

        await new Promise(r => setTimeout(r, 600));
        const input = document.querySelector('.Chat_chatInputContainer__2euR8 form input.Chat_chatInput__16dhX[placeholder="输入消息..."]');
        const button = document.querySelector('.Chat_chatInputContainer__2euR8 form button.Button_button__1Fe9z');
        if (!input || !button) return;

        const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
        nativeInputValueSetter.call(input, msg);
        const ev = new Event("input", { bubbles: true });
        input.dispatchEvent(ev);

        button.click(); // Uncomment to send messages automatically
        console.log("[MCSAlert] Prepared message:", msg);
    }

    async function checkAndNotify() {
        console.log("[MCSAlert] Checking consumables...");
        const lowItems = getCombatPlayerAlerts();

        const newlyLow = lowItems.filter(item => !warnedItems.has(item));
        newlyLow.forEach(item => warnedItems.add(item));

        for (const item of Array.from(warnedItems)) {
            if (!lowItems.includes(item)) warnedItems.delete(item);
        }

        if (newlyLow.length > 0) {
            const msg = `[⚠️消耗品警報⚠️] ${newlyLow.join(', ')}`;
            await sendClanMessage(msg);
        } else {
            console.log("[MCSAlert] No new consumables below threshold.");
        }
    }

    function waitForPageReady(callback) {
        const checkInterval = setInterval(() => {
            if (document.querySelector('.Chat_chatInputContainer__2euR8 form input.Chat_chatInput__16dhX[placeholder="输入消息..."]')) {
                clearInterval(checkInterval);
                callback();
            }
        }, 1000);
        setTimeout(() => clearInterval(checkInterval), 20000);
    }

    function setupMonitor() {
        console.log("[MCSAlert] Started monitoring consumables.");
        checkAndNotify();
        setInterval(() => checkAndNotify(), CHECK_INTERVAL_MS);
    }

    if (window.top === window.self) {
        waitForPageReady(setupMonitor);
    }
})();