显示动作面板的对应物品库存
// ==UserScript==
// @name [银河奶牛]动作界面显示库存
// @version 1.1
// @description 显示动作面板的对应物品库存
// @match https://www.milkywayidle.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));
})();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址