Greasy Fork

Greasy Fork is available in English.

[银河奶牛]动作界面显示库存

显示动作面板的对应物品库存

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         [银河奶牛]动作界面显示库存
// @version      1.2
// @description  显示动作面板的对应物品库存
// @match        https://www.milkywayidle.com/*
// @match        https://www.milkywayidlecn.com/*
// @match        https://test.milkywayidle.com/*
// @icon         https://www.milkywayidle.com/favicon.svg
// @author       GPT-DiamondMoo
// @license      MIT
// @namespace    http://tampermonkey.net/
// ==/UserScript==

(function () {
    'use strict';

    // ====== 配置:动作名与物品中文名映射(仅在名称不一致时手动补充) ======
    const nameMap = {
    "奶牛": "牛奶",
    "翠绿奶牛": "翠绿牛奶",
    "蔚蓝奶牛": "蔚蓝牛奶",
    "深紫奶牛": "深紫牛奶",
    "绛红奶牛": "绛红牛奶",
    "彩虹奶牛": "彩虹牛奶",
    "神圣奶牛": "神圣牛奶",
    "树": "原木",
    "桦树": "白桦原木",
    "雪松树": "雪松原木",
    "紫心树": "紫心原木",
    "银杏树": "银杏原木",
    "红杉树": "红杉原木",
    "奥秘树": "神秘原木",
    };

    // ====== 全局缓存:背包数量(按 itemHrid 存储)与中文名称映射 ======
    let itemCountsByHrid = {};
    let itemNameByHrid = {};

    // ====== 功能:根据页面的 React Fiber 根节点,获取游戏根 stateNode ======
    function getGameRootStateNode() {
        const sel = document.querySelector('[class^="GamePage"]');
        if (!sel) return null;
        return (el =>
            el?.[Object.keys(el).find(k => k.startsWith('__reactFiber$'))]?.return?.stateNode
        )(sel);
    }

    // ====== 功能:从 React state.characterItemMap 读取背包数量(只统计强化等级为 0 的物品) ======
    function readInventoryFromState() {
        const root = getGameRootStateNode();
        if (!root) return;

        let state = root.state || root?.return?.stateNode?.state || root?.props?.state || root;
        let props = root.props || root;

        let charMap = state?.characterItemMap || props?.state?.characterItemMap || null;
        let i18nMap = props?.i18n?.options?.resources?.zh?.translation?.itemNames ||
                      root?.props?.i18n?.options?.resources?.zh?.translation?.itemNames || null;

        const result = {};
        const addEntry = (entry) => {
            let v = null;
            if (Array.isArray(entry) && entry.length >= 2) v = entry[1];
            else if (entry && entry.value) v = entry.value;
            else if (entry && entry.itemHrid) v = entry;
            if (!v) return;
            if (v.enhancementLevel === 0 && v.itemHrid) {
                result[v.itemHrid] = (result[v.itemHrid] || 0) + Number(v.count || 0);
            }
        };

        // 兼容 Map、Array、Object 三种数据结构
        if (charMap instanceof Map) {
            charMap.forEach((v, k) => addEntry([k, v]));
        } else if (Array.isArray(charMap)) {
            charMap.forEach(e => addEntry(e));
        } else if (typeof charMap === 'object') {
            Object.values(charMap).forEach(v => addEntry(v));
        }

        // 写入缓存
        itemCountsByHrid = result;
        if (i18nMap && typeof i18nMap === 'object') {
            itemNameByHrid = Object.assign({}, i18nMap);
        }
    }

    // ====== 功能:格式化数量(5字符内不截断;超长缩写为 K/M/B/T;极大数以 xxxxT 显示) ======
    function formatCount(n) {
        n = Math.floor(Number(n) || 0);
        const s = String(n);
        if (s.length <= 5) return s;

        const units = [
            { v: 1e3, s: 'K' },
            { v: 1e6, s: 'M' },
            { v: 1e9, s: 'B' },
            { v: 1e12, s: 'T' }
        ];

        for (let i = 0; i < units.length; i++) {
            const u = units[i];
            const scaled = Math.floor(n / u.v);
            const cand = String(scaled) + u.s;
            if (cand.length <= 5) return cand;
        }

        const scaledT = Math.floor(n / 1e12);
        return String(scaledT) + 'T';
    }

    // ====== 功能:移除旧的数量显示,避免重复叠加 ======
    function removeAllOverlays() {
        document.querySelectorAll('.tm-count-overlay').forEach(n => n.remove());
    }

    // ====== 功能:在动作按钮右下角显示数量(使用原脚本样式) ======
    function displayCountsOnActions() {
        removeAllOverlays();

        const actions = document.querySelectorAll('.SkillActionGrid_skillActionGrid__1tJFk .SkillAction_skillAction__1esCp');
        if (!actions || actions.length === 0) return;

        const hridByName = {};
        for (const hrid in itemNameByHrid) {
            if (itemNameByHrid.hasOwnProperty(hrid)) hridByName[itemNameByHrid[hrid]] = hrid;
        }

        actions.forEach(skill => {
            const nameElem = skill.querySelector('.SkillAction_name__2VPXa');
            if (!nameElem) return;
            const skillName = nameElem.textContent.trim();
            if (!skillName) return;

            let matchedHrid = null;
            if (hridByName[skillName]) matchedHrid = hridByName[skillName];
            if (!matchedHrid && nameMap[skillName] && hridByName[nameMap[skillName]]) {
                matchedHrid = hridByName[nameMap[skillName]];
            }
            if (!matchedHrid && nameMap[skillName] && nameMap[skillName].startsWith('/items')) {
                matchedHrid = nameMap[skillName];
            }
            if (!matchedHrid) return;

            const count = itemCountsByHrid[matchedHrid] || 0;
            if (count <= 0) return;

            const txt = formatCount(count);

            skill.style.position = skill.style.position || 'relative';
            skill.style.overflow = 'visible';

            const div = document.createElement('div');
            div.className = 'tm-count-overlay';
            div.textContent = txt;

            Object.assign(div.style, {
                gridArea: '1/1',
                display: 'flex',
                alignItems: 'flex-end',
                justifyContent: 'flex-end',
                margin: '0 2px -1px 0',
                textShadow: '-1px 0 var(--color-background-game),0 1px var(--color-background-game),1px 0 var(--color-background-game),0 -1px var(--color-background-game)',
                color: '#fff',
                fontWeight: 'bold',
                position: 'relative',
                zIndex: '10',
                pointerEvents: 'none',
            });

            skill.appendChild(div);
        });
    }

    // ====== 功能:监听页面 DOM 变化,捕捉背包变更与动作面板出现并即时刷新 ======
    const observer = new MutationObserver((mutations) => {
        let trigger = false;
        let panel = false;

        for (const m of mutations) {
            if (!m.addedNodes) continue;
            for (const n of m.addedNodes) {
                if (!(n instanceof Element)) continue;

                if (n.matches?.('.SkillActionGrid_skillActionGrid__1tJFk') ||
                    n.querySelector?.('.SkillActionGrid_skillActionGrid__1tJFk') ||
                    n.matches?.('.SkillAction_skillAction__1esCp') ||
                    n.querySelector?.('.SkillAction_skillAction__1esCp')) {
                    panel = true;
                }
                if (n.querySelector?.('.Inventory_itemGrid__20YAH') ||
                    n.querySelector?.('.Item_itemContainer__x7kH1') ||
                    n.querySelector?.('.SkillActionGrid_skillActionGrid__1tJFk')) {
                    trigger = true;
                }
            }
        }

        if (trigger) {
            readInventoryFromState();
            displayCountsOnActions();
        }
        if (panel) {
            displayCountsOnActions();
        }
    });

    observer.observe(document.body, { childList: true, subtree: true });

    // ====== 初始化(页面加载后执行一次) ======
    function init() {
        readInventoryFromState();
        displayCountsOnActions();
    }

    window.addEventListener('load', () => setTimeout(init, 1000));
})();