// ==UserScript==
// @name MoYuIdleHelper
// @namespace https://tampermonkey.net/
// @version 1.2.2
// @description 摸鱼放置助手
// @author Mid
// @license MIT
// @match https://www.moyu-idle.com/*
// @match https://moyu-idle.com/*
// @grant none
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/pako.min.js
// ==/UserScript==
(function () {
'use strict';
const VERSION = "v1.2.2";
const DEBUG_MODE = false; // 调试模式开关
const LOCAL_STORAGE_NAME = "MO_YU_IDLE_HELPER_DATA";
const INVENTORY_STORAGE_NAME = "MO_YU_IDLE_HELPER_INVENTORY";
const SETTINGS_STORAGE_NAME = "MO_YU_IDLE_HELPER_SETTINGS";
const damageAccum = new Map(); // uuid → 累计伤害
const actionCount = new Map(); // uuid → 动作次数
const healAccum = new Map(); // uuid → 累计治疗
// 仓库物品变动跟踪
let inventory = {}; // 当前仓库状态
let inventoryChanges = new Map(); // 物品变动记录
let userInfo = null;
let saveInventoryEnabled = false; // 是否保存掉落信息
// 技能名称映射
const skillNames = {
baseAttack: "普通攻击",
boneShield: "骨盾",
corrosiveBreath: "腐蚀吐息",
summonBerryBird: "召唤浆果鸟",
baseHeal: "基础治疗",
poison: "中毒",
selfHeal: "自我疗愈",
sweep: "横扫",
baseGroupHeal: "基础群体治疗",
powerStrike: "重击",
guardianLaser: "守护者激光",
lavaBreath: "熔岩吐息",
dragonRoar: "龙之咆哮",
doubleStrike: "双重打击",
lowestHpStrike: "弱点打击",
explosiveShot: "爆炸射击",
freeze: "冻结",
iceBomb: "冰弹",
lifeDrain: "吸血",
roar: "咆哮",
blizzard: "暴风雪",
ironWall: "铁壁",
curse: "诅咒",
shadowBurst: "暗影爆发",
groupCurse: "群体诅咒",
holyLight: "神圣之光",
bless: "祝福",
revive: "复活",
groupRegen: "群体再生",
astralBarrier: "星辉结界",
astralBlast: "星辉冲击",
groupSilence: "群体沉默",
selfRepair: "自我修复",
cleanse: "驱散",
cometStrike: "彗星打击",
armorBreak: "破甲",
starTrap: "星辰陷阱",
emperorCatFinale_forAstralEmpressBoss: "星辉终极裁决",
astralStorm: "星辉风暴",
groupShield: "群体护盾",
sneak: "潜行",
ambush: "偷袭",
poisonClaw: "毒爪",
shadowStep: "暗影步",
silenceStrike: "沉默打击",
slientSmokeScreen: "静默烟雾弹",
mirrorImage: "镜像影分身",
shadowAssassinUlt: "绝影连杀",
stardustMouseSwap: "偷天换日",
dizzySpin: "眩晕旋转",
carouselOverdrive: "失控加速",
candyBomb: "糖果爆裂",
prankSmoke: "恶作剧烟雾",
plushTaunt: "毛绒嘲讽",
starlightSanctuary: "星光治愈",
ghostlyStrike: "鬼影冲锋",
paradeHorn: "狂欢号角",
clownSummon: "小丑召集令",
kingAegis: "猫王庇护",
sealMagic: "封印魔法",
banish: "驱逐",
bind: "束缚",
detectMagic: "识破魔法",
punish: "惩戒",
confuse: "扰乱",
forbiddenMagic: "禁忌魔法",
ultimateLibraryJudgement: "禁魔审判"
};
// 物品ID到名称映射
const itemIdNameMap = {
"__satiety": "饱食度",
"__cat": "小猫咪",
"gold": "金币",
"catPawCoin": "猫爪古钱币",
"wood": "木材",
"stone": "矿石",
"coal": "煤炭",
"iron": "铁",
"steel": "钢",
"silverOre": "银矿",
"silverIngot": "银锭",
"mithrilOre": "秘银矿",
"mithrilIngot": "秘银锭",
"bamboo": "竹子",
"fish": "鱼",
"mushroom": "蘑菇",
"berry": "浆果",
"chickenEgg": "鸡蛋",
"milk": "牛奶",
"salmon": "鲑鱼",
"tuna": "金枪鱼",
"honey": "蜂蜜",
"herb": "草药",
"wool": "羊毛",
"silk": "蚕丝",
"cashmere": "羊绒布料",
"silkFabric": "丝绸布料",
"axe": "斧头",
"pickaxe": "铁镐",
"baseHealSkillBook": "基础治疗技能书",
"sweepSkillBook": "横扫",
"collectRing": "采集戒指",
"collectRing2": "附魔采集戒指",
"catTailorClothes": "毛毛裁缝服",
"catTailorGloves": "毛毛裁缝手套",
"woolTailorClothes": "羊毛裁缝服",
"woolTailorGloves": "羊毛裁缝手套",
"goblinDaggerPlus": "哥布林匕首·改",
"wolfPeltArmor": "狼皮甲",
"skeletonShieldPlus": "骷髅盾·强化",
"trollClubPlus": "巨魔木棒·重型",
"scorpionStingerSpear": "巨蝎毒矛",
"guardianCoreAmulet": "守护者核心护符",
"moonlightGuardianCoreAmulet": "月光守护者",
"dragonScaleArmor": "龙鳞甲",
"woolCoat": "羊毛衣",
"woolHat": "羊毛帽",
"woolGloves": "羊毛手套",
"woolPants": "羊毛裤",
"ironCoat": "铁甲衣",
"ironHat": "铁头盔",
"ironGloves": "铁护手",
"ironPants": "铁护腿",
"steelCoat": "钢甲衣",
"steelHat": "钢头盔",
"steelGloves": "钢护手",
"steelPants": "钢护腿",
"silverSword": "银质剑",
"silverDagger": "银质匕首",
"silverCoat": "银护甲",
"silverHat": "银头盔",
"silverGloves": "银护手",
"silverPants": "银护腿",
"simpleSalad": "野草沙拉",
"wildFruitMix": "野果拼盘",
"fishSoup": "鱼汤",
"berryPie": "浆果派",
"mushroomStew": "蘑菇炖汤",
"catMint": "猫薄荷饼干",
"catSnack": "猫咪零食",
"luxuryCatFood": "豪华猫粮",
"sashimiPlatter": "鲜鱼刺身拼盘",
"catGiftBag": "猫猫礼袋",
"luckyCatBox": "幸运猫盒",
"mysteryCan": "神秘罐头",
"catnipSurprise": "猫薄荷惊喜包",
"meowEnergyBall": "喵能量球",
"dreamFeatherBag": "梦羽袋",
"woodSword": "木剑",
"ironSword": "铁剑",
"steelSword": "钢剑",
"catFurCoat": "毛毛衣",
"catFurHat": "毛毛帽",
"catFurGloves": "毛毛手套",
"catFurPants": "毛毛裤",
"collectingBracelet": "采集手环",
"fishingHat": "钓鱼帽",
"miningBelt": "采矿工作服",
"farmingGloves": "园艺手套",
"heavyMinerGloves": "重型矿工手套",
"agileGatherBoots": "灵巧采集靴",
"focusedFishingCap": "钓鱼专注帽",
"woodFishingRod": "木钓竿",
"chefHat": "厨师帽",
"ancientFishboneNecklace": "远古鱼骨项链",
"moonlightPendant": "月光吊坠",
"testResource": "测试资源",
"forestDagger": "冰霜匕首",
"snowWolfCloak": "雪狼皮披风",
"iceFeatherBoots": "冰羽靴",
"icePickaxe": "冰稿",
"woolBurqa": "羊毛罩袍",
"woolMageHat": "羊毛法师帽",
"woolMageLongGloves": "羊毛法师手套",
"woolMagePants": "羊毛法师裤",
"silkMageBurqa": "丝质罩袍",
"silkMageHat": "丝质法师帽",
"silkMageLongGloves": "丝质法师手套",
"silkMagePants": "丝质法师裤",
"woolTightsCloth": "羊毛紧身衣",
"woolDexHeadScarf": "羊毛裹头巾",
"woolDexGloves": "羊毛绑带手套",
"woolTightsPants": "羊毛紧身裤",
"silkTightsCloth": "丝质夜行衣",
"silkDexHeadScarf": "丝质裹头巾",
"silkDexGloves": "丝质绑带手套",
"silkTightsPants": "丝质宽松裤",
"woodStaff": "木法杖",
"ironDagger": "铁匕首",
"moonlightStaff": "月光法杖",
"mewShadowStaff": "喵影法杖",
"groupShieldSkillBook": "群体护盾技能书",
"silverNecklace": "银项链",
"silverBracelet": "银手链",
"catPotionSilverBracelet": "猫薄荷手链",
"catFurCuteHat": "毛毛可爱帽",
"woolCuteHat": "羊毛可爱帽",
"catPawStamp": "猫爪印章",
"rareCatfish": "稀有猫鱼",
"mysticalKoi": "神秘锦鲤",
"treasureMap": "藏宝图",
"catPawFossil": "猫爪化石",
"catStatue": "猫雕像",
"mysteriousBell": "神秘铃铛",
"ancientCatBowl": "古代猫碗",
"catScroll": "猫之卷轴",
"catAntiqueShard": "猫咪文物碎片",
"catHairball": "猫毛球",
"luckyCatCharm": "招财猫护符",
"catnipGem": "猫薄荷宝石",
"ancientFishBone": "远古鱼骨",
"whiskerFeather": "胡须羽毛",
"moonlightBell": "月光铃铛",
"shell": "贝壳",
"mysticalEssence": "神秘精华",
"catPotion": "猫薄荷药剂",
"magicScroll": "魔法卷轴",
"catRelic": "猫咪圣物",
"blessedBell": "祝福铃铛",
"slimeGel": "史莱姆凝胶",
"slimeCore": "史莱姆核心",
"goblinEar": "哥布林耳朵",
"goblinDagger": "哥布林匕首",
"batWing": "蝙蝠翅膀",
"batTooth": "蝙蝠牙",
"wolfFang": "狼牙",
"wolfPelt": "狼皮",
"skeletonBone": "骷髅骨",
"skeletonShield": "骷髅残盾",
"toxicSpore": "毒孢子",
"mushroomCap": "蘑菇怪帽",
"lizardScale": "蜥蜴鳞片",
"lizardTail": "蜥蜴尾巴",
"spiritEssence": "幽灵精华",
"ectoplasm": "灵质",
"trollHide": "巨魔兽皮",
"trollClub": "巨魔木棒",
"scorpionStinger": "巨蝎毒针",
"scorpionCarapace": "巨蝎甲壳",
"guardianCore": "守护者核心",
"ancientGear": "古代齿轮",
"lavaHeart": "熔岩之心",
"dragonScale": "龙鳞",
"venomDagger": "剧毒匕首",
"emberAegis": "余烬庇护",
"iceGel": "冰霜凝胶",
"frostCrystal": "霜之结晶",
"snowWolfFur": "雪狼皮",
"frostDagger": "冰霜匕首",
"iceBomb": "冰弹",
"iceBatWing": "冰蝙蝠翅膀",
"snowRabbitFur": "雪兔皮",
"frostEssence": "霜之精华",
"snowBeastFang": "巨兽獠牙",
"snowBeastHide": "巨兽皮",
"frostCrown": "霜之王冠",
"shadowFur": "暗影猫皮",
"catShadowGem": "猫影宝石",
"dungeonKey": "地牢钥匙",
"ironPawArmor": "铁爪护甲",
"phantomWhisker": "幻影胡须",
"curseWing": "诅咒之翼",
"golemCore": "猫偶核心",
"witchHat": "巫术猫帽",
"shadowOrb": "暗影法球",
"abyssalCloak": "深渊披风",
"ancestorCrown": "猫祖王冠",
"starEssence": "星辉精华",
"starShard": "星辰碎片",
"trapParts": "陷阱零件",
"starDust": "星尘",
"starCrown": "星辉王冠",
"starRelic": "星辉遗物",
"nightEyeGem": "夜瞳宝石",
"toxicFur": "剧毒皮毛",
"whiskerCharm": "胡须护符",
"shadowCape": "暗影披风",
"rareClaw": "稀有利爪",
"smokeBall": "烟雾弹",
"candyBomb": "糖果炸弹",
"plushFur": "毛绒绒",
"ghostEssence": "幽灵精华",
"loadOfamusementPark": "游乐园之王",
"paradeCape": "游行披风",
"empressCloak": "女皇披风"
// ...如有遗漏可继续补充
};
// 自动为每个技能生成技能书条目
Object.entries(skillNames).forEach(([skillId, skillName]) => {
const bookId = skillId + 'SkillBook';
if (!itemIdNameMap[bookId]) {
itemIdNameMap[bookId] = skillName + '技能书';
}
});
// 运行时间
const startTime = Date.now();
// 创建主面板
const panel = document.createElement('div');
let panelWidth = 420;
let panelHeight = 500;
panel.id = 'moyu-helper-panel';
panel.style.cssText = `
position: fixed;
top: 40px;
left: 40px;
width: ${panelWidth}px;
height: ${panelHeight}px;
max-width: 90vw;
min-height: 60px;
background: rgba(30, 32, 40, 0.96);
color: #fff;
border-radius: 12px;
box-shadow: 0 4px 24px 0 rgba(0,0,0,0.25);
z-index: 99999;
font-size: 14px;
user-select: none;
transition: box-shadow 0.2s;
display: flex;
flex-direction: column;
`;
// 标题栏
const titleBar = document.createElement('div');
titleBar.style.cssText = `
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: rgba(0,0,0,0.3);
border-radius: 12px 12px 0 0;
cursor: move;
font-weight: bold;
font-size: 14px;
flex-shrink: 0;
`;
titleBar.innerHTML = `
<span>摸鱼放置助手 ${VERSION}</span>
<div style="display:flex;align-items:center;gap:12px;">
<span id="moyu-runtime" style="font-size:12px;color:#ffd700;"></span>
<button id="moyu-collapse" style="background:none;border:none;color:#fff;font-size:18px;cursor:pointer;">−</button>
</div>
`;
// 标签栏
const tabBar = document.createElement('div');
tabBar.style.cssText = `
display: flex;
background: rgba(0,0,0,0.2);
border-radius: 12px 12px 0 0;
flex-shrink: 0;
`;
const tabs = [
{ key: 'combat', label: '战斗统计' },
{ key: 'inventory', label: '物品统计' },
{ key: 'room', label: '房间信息' },
{ key: 'settings', label: '设置' }
];
let activeTab = 'combat';
const tabBtns = {};
tabs.forEach(tab => {
const btn = document.createElement('button');
btn.textContent = tab.label;
btn.style.cssText = `
flex: 1;
padding: 6px 0;
background: none;
border: none;
border-bottom: 2px solid transparent;
color: #fff;
font-size: 15px;
cursor: pointer;
transition: border-color 0.2s, color 0.2s;
`;
btn.addEventListener('click', () => setActiveTab(tab.key));
tabBar.appendChild(btn);
tabBtns[tab.key] = btn;
});
// 内容区域
const content = document.createElement('div');
content.style.cssText = `
flex: 1;
padding: 12px;
overflow-y: auto;
overflow-x: hidden;
min-height: 0;
`;
// WebSocket发送栏
const wsSendBar = document.createElement('div');
wsSendBar.style.cssText = `
padding: 8px 12px;
background: rgba(0,0,0,0.2);
border-top: 1px solid #333;
display: flex;
gap: 8px;
align-items: center;
flex-shrink: 0;
`;
wsSendBar.innerHTML = `
<select id="moyu-ws-cmd-type" style="padding:4px 8px;border-radius:6px;border:1px solid #444;background:#222;color:#fff;font-size:13px;">
<option value="character:getProfile">character:getProfile</option>
<option value="character:getProfile">character:getProfile</option>
<option value="chat:createPublicChatMsg">chat:createPublicChatMsg</option>
<!-- 可扩展更多指令 -->
</select>
<input id="moyu-ws-cmd" type="text" placeholder="输入WebSocket指令(JSON或文本)" style="flex:1;padding:4px 8px;border-radius:6px;border:1px solid #444;background:#222;color:#fff;outline:none;font-size:13px;" />
<button id="moyu-ws-send" style="padding:4px 16px;border-radius:6px;border:none;background:#ffd700;color:#222;font-weight:bold;cursor:pointer;">发送</button>
`;
// 自动填充格式
wsSendBar.querySelector('#moyu-ws-cmd-type').onchange = function () {
const cmd = this.value;
if (cmd === 'character:getProfile') {
wsSendBar.querySelector('#moyu-ws-cmd').value =
'42["character:getProfile",' + JSON.stringify({ user: userInfo, data: null }) + ']';
}
};
// 默认填充
wsSendBar.querySelector('#moyu-ws-cmd-type').onchange();
// 主动发送指令频率限制
let lastSendTime = 0;
const MIN_SEND_INTERVAL = 1000; // 1秒
// 统一频率限制的发送函数
function sendWithRateLimit(msg) {
const now = Date.now();
if (now - lastSendTime < MIN_SEND_INTERVAL) {
if (DEBUG_MODE) console.warn('发送过于频繁,已被限制');
return false;
}
if (window._moyuHelperWS && window._moyuHelperWS.readyState === 1) {
window._moyuHelperWS.send(msg);
lastSendTime = now;
return true;
} else {
alert('WebSocket未连接');
return false;
}
}
wsSendBar.querySelector('#moyu-ws-send').onclick = function () {
const val = wsSendBar.querySelector('#moyu-ws-cmd').value.trim();
if (!val) return;
if (!sendWithRateLimit(val)) {
alert('发送过于频繁,请稍后再试');
}
};
// 折叠逻辑
let isCollapsed = false;
function toggleCollapse() {
isCollapsed = !isCollapsed;
if (isCollapsed) {
// 折叠时隐藏内容和标签栏,调整面板高度
content.style.display = 'none';
tabBar.style.display = 'none';
panel.style.height = 'auto';
panel.style.minHeight = '40px';
} else {
// 展开时显示内容和标签栏,恢复面板高度
content.style.display = '';
tabBar.style.display = 'flex';
panel.style.height = panelHeight + 'px';
panel.style.minHeight = '60px';
}
const collapseBtn = document.querySelector('#moyu-collapse');
if (collapseBtn) {
collapseBtn.textContent = isCollapsed ? '+' : '−';
}
}
// 绑定折叠按钮事件(使用事件委托)
document.addEventListener('click', function (e) {
if (e.target && e.target.id === 'moyu-collapse') {
toggleCollapse();
}
});
// 运行时间刷新
function updateRuntime() {
const now = Date.now();
let diff = Math.floor((now - startTime) / 1000);
const h = Math.floor(diff / 3600);
diff %= 3600;
const m = Math.floor(diff / 60);
const s = diff % 60;
document.getElementById('moyu-runtime').textContent =
`已运行: ${h > 0 ? h + '小时' : ''}${m > 0 ? m + '分' : ''}${s}秒`;
}
setInterval(updateRuntime, 1000);
setTimeout(updateRuntime, 100);
// 拖动逻辑(仅标题栏可拖动)
(function makeDraggable(handle, el) {
let isDown = false, offsetX = 0, offsetY = 0;
handle.addEventListener('mousedown', e => {
isDown = true;
el.style.right = 'auto';
offsetX = e.clientX - el.offsetLeft;
offsetY = e.clientY - el.offsetTop;
e.preventDefault();
});
document.addEventListener('mousemove', e => {
if (!isDown) return;
let newLeft = e.clientX - offsetX;
let newTop = e.clientY - offsetY;
// 边界检测,防止拖出屏幕
const maxLeft = window.innerWidth - el.offsetWidth;
const maxTop = window.innerHeight - el.offsetHeight;
newLeft = Math.max(0, Math.min(newLeft, maxLeft));
newTop = Math.max(0, Math.min(newTop, maxTop));
el.style.left = newLeft + 'px';
el.style.top = newTop + 'px';
});
document.addEventListener('mouseup', () => { isDown = false; });
})(titleBar, panel);
// 标签切换逻辑
function setActiveTab(key) {
activeTab = key;
for (const k in tabBtns) {
tabBtns[k].style.borderBottomColor = (k === key) ? '#ffd700' : 'transparent';
tabBtns[k].style.color = (k === key) ? '#ffd700' : '#fff';
}
renderContent();
// 切换后刷新对应数据
if (activeTab === 'combat') {
setTimeout(() => {
// 需要有 playerUuids 和 memberMap 数据
if (typeof updateDpsPanel === 'function' && window.playerUuids && window.memberMap) {
updateDpsPanel(window.playerUuids, window.memberMap);
}
}, 0);
} else if (activeTab === 'inventory') {
setTimeout(() => {
if (typeof updateInventoryPanel === 'function') {
updateInventoryPanel();
}
}, 0);
} else if (activeTab === 'room') {
setTimeout(() => {
if (typeof renderRoomPanel === 'function') {
renderRoomPanel();
}
}, 0);
} else if (activeTab === 'settings') {
renderSettingsPanel();
}
}
// 面板尺寸设置
let panelSettings = {
width: panelWidth,
height: panelHeight
};
// 加载面板设置
function loadPanelSettings() {
try {
const allData = JSON.parse(localStorage.getItem(LOCAL_STORAGE_NAME)) || {};
const saved = allData[SETTINGS_STORAGE_NAME];
if (saved) {
if (saved.panelWidth && saved.panelHeight) {
panelWidth = saved.panelWidth;
panelHeight = saved.panelHeight;
panelSettings = saved;
updatePanelSize();
}
}
} catch (e) {
console.warn('加载面板设置失败:', e);
}
}
// 保存面板设置
function savePanelSettings() {
const data = {
panelWidth: panelWidth,
panelHeight: panelHeight
};
let allData = {};
try {
allData = JSON.parse(localStorage.getItem(LOCAL_STORAGE_NAME)) || {};
} catch (e) { allData = {}; }
allData[SETTINGS_STORAGE_NAME] = data;
localStorage.setItem(LOCAL_STORAGE_NAME, JSON.stringify(allData));
}
// 更新面板尺寸
function updatePanelSize() {
panel.style.width = panelWidth + 'px';
panel.style.height = panelHeight + 'px';
// 重新渲染内容以适应新尺寸
renderContent();
}
// 设置面板内容
function renderSettingsPanel() {
return `
<div style='font-weight:bold;font-size:16px;margin-bottom:12px;'>面板设置</div>
<div style='margin-bottom:16px;'>
<div style='display:flex;align-items:center;gap:8px;margin-bottom:8px;'>
<label style="font-size:13px;min-width:60px;">面板宽度:</label>
<input type="number" id="panel-width" value="${panelWidth}" min="200" max="800" style="width:80px;padding:4px 8px;border-radius:4px;border:1px solid #444;background:#222;color:#fff;font-size:13px;" />
<span style="font-size:12px;color:#888;">px</span>
</div>
<div style='display:flex;align-items:center;gap:8px;margin-bottom:8px;'>
<label style="font-size:13px;min-width:60px;">面板高度:</label>
<input type="number" id="panel-height" value="${panelHeight}" min="200" max="800" style="width:80px;padding:4px 8px;border-radius:4px;border:1px solid #444;background:#222;color:#fff;font-size:13px;" />
<span style="font-size:12px;color:#888;">px</span>
</div>
<div style='font-size:12px;color:#888;margin-top:8px;'>调整后会自动保存设置</div>
</div>
`;
}
// 内容渲染
function renderContent() {
if (activeTab === 'combat') {
content.innerHTML = renderCombatPanel();
// 渲染历史日志
setTimeout(() => {
const ul = document.getElementById('battleLog');
if (ul) {
ul.innerHTML = logList.map(l => `<li style='margin-bottom:2px;'>${l}</li>`).join('');
// 自动滚动到底部
if (logAutoScroll) ul.scrollTop = ul.scrollHeight;
}
}, 0);
} else if (activeTab === 'inventory') {
content.innerHTML = renderInventoryPanel();
// 绑定物品统计设置事件
setTimeout(() => {
const saveToggle = document.getElementById('save-inventory-toggle');
const clearBtn = document.getElementById('clear-inventory-btn');
if (saveToggle) {
saveToggle.onchange = function () {
saveInventoryEnabled = this.checked;
if (saveInventoryEnabled) {
saveInventoryData();
}
};
}
if (clearBtn) {
clearBtn.onclick = function () {
if (confirm('确定要清除所有物品统计记录吗?此操作不可恢复。')) {
// 清除内存中的数据
inventory = {};
// 清除本地存储
let allData = {};
try {
allData = JSON.parse(localStorage.getItem(LOCAL_STORAGE_NAME)) || {};
} catch (e) { allData = {}; }
delete allData[INVENTORY_STORAGE_NAME];
localStorage.setItem(LOCAL_STORAGE_NAME, JSON.stringify(allData));
// 更新显示
updateInventoryPanel();
alert('物品统计记录已清除');
}
};
}
}, 100);
} else if (activeTab === 'room') {
content.innerHTML = renderRoomPanel();
// 绑定自动准备设置事件
setTimeout(() => {
const toggle = document.getElementById('auto-ready-toggle');
const threshold = document.getElementById('auto-ready-threshold');
const stopBattleToggle = document.getElementById('auto-stop-battle-toggle');
const kickUnreadyToggle = document.getElementById('auto-kick-unready-toggle');
if (toggle) {
toggle.onchange = function () {
autoReadyEnabled = this.checked;
};
}
if (threshold) {
threshold.onchange = function () {
let v = parseInt(this.value) || 2;
if (v < 2) v = 2;
if (v > 6) v = 6;
autoReadyThreshold = v;
this.value = v;
};
}
if (stopBattleToggle) {
stopBattleToggle.onchange = function () {
autoStopBattleEnabled = this.checked;
};
}
if (kickUnreadyToggle) {
kickUnreadyToggle.onchange = function () {
autoKickUnreadyEnabled = this.checked;
renderContent();
};
}
// 绑定自动招募设置事件
const recruitToggle = document.getElementById('auto-recruit-toggle');
const recruitMsg = document.getElementById('auto-recruit-msg');
if (recruitToggle) {
recruitToggle.onchange = function () {
autoRecruitEnabled = this.checked;
renderContent();
};
}
if (recruitMsg) {
recruitMsg.oninput = function () {
autoRecruitMsg = this.value;
};
}
}, 100);
} else if (activeTab === 'settings') {
content.innerHTML = renderSettingsPanel();
// 绑定设置事件
setTimeout(() => {
const widthInput = document.getElementById('panel-width');
const heightInput = document.getElementById('panel-height');
if (widthInput) {
widthInput.onchange = function () {
panelWidth = Math.max(200, Math.min(800, parseInt(this.value) || 320));
this.value = panelWidth;
updatePanelSize();
savePanelSettings();
};
}
if (heightInput) {
heightInput.onchange = function () {
panelHeight = Math.max(200, Math.min(800, parseInt(this.value) || 400));
this.value = panelHeight;
updatePanelSize();
savePanelSettings();
};
}
}, 100);
}
}
// 战斗统计内容(可根据实际数据结构填充)
function renderCombatPanel() {
return `
<table style="width:100%;border-collapse:collapse;font-size:13px;">
<thead>
<tr style="color:#ffd700;">
<th style="text-align:left;">玩家</th>
<th style="text-align:right;">输出效率</th>
<th style="text-align:right;">治疗效率</th>
<th style="text-align:right;">总伤害</th>
<th style="text-align:right;">总治疗</th>
</tr>
</thead>
<tbody id="dpsTableBody">
<!-- 动态填充 -->
</tbody>
</table>
<div style="margin-top:12px;">
<div style="font-weight:bold;margin-bottom:4px;">战斗日志</div>
<ul id="battleLog" style="max-height:${panelHeight - 300}px;overflow-y:auto;padding-left:18px;font-size:13px;"></ul>
</div>
`;
}
// 物品统计内容(可根据实际数据结构填充)
function renderInventoryPanel() {
return `
<div style='margin-bottom:12px;'>
<div style='display:flex;align-items:center;gap:8px;margin-bottom:8px;'>
<input type="checkbox" id="save-inventory-toggle" ${saveInventoryEnabled ? 'checked' : ''} style="margin:0;" />
<label for="save-inventory-toggle" style="font-size:13px;">保存掉落信息</label>
<button id="clear-inventory-btn" style="margin-left:auto;padding:4px 12px;border-radius:4px;border:1px solid #666;background:#333;color:#fff;font-size:12px;cursor:pointer;">清除记录</button>
</div>
<div style='font-size:12px;color:#888;'>启用后,物品统计数据将保存到本地,页面刷新后不会丢失</div>
</div>
<table style="width:100%;border-collapse:collapse;font-size:13px;">
<thead>
<tr style="color:#ffd700;">
<th style="text-align:left;">物品</th>
<th style="text-align:right;">数量</th>
</tr>
</thead>
<tbody id="inventoryTableBody">
<!-- 动态填充 -->
</tbody>
</table>
`;
}
// 用户信息缓存
window.userProfileCache = window.userProfileCache || {};
// 获取用户信息(异步)
function fetchUserProfile(uuid, cb) {
if (window.userProfileCache[uuid]) {
cb(window.userProfileCache[uuid]);
return;
}
fetch(`https://moyu-idle.com/api/game/user/profile?uuid=${uuid}`)
.then(r => r.json())
.then(res => {
if (res && res.code === 200 && res.data) {
window.userProfileCache[uuid] = res.data;
cb(res.data);
} else {
cb({ uuid, name: uuid });
}
})
.catch(() => cb({ uuid, name: uuid }));
}
// 地图信息
const MapInfos = {
plain_001: { id: "plain_001", name: "悠闲平原", description: "阳光明媚、草地广阔的平原,适合新手猫咪冒险者探索。这里有许多温顺的小动物和丰富的浆果、蘑菇资源。" },
forest_001: { id: "forest_001", name: "幽暗森林", description: "一片常年被迷雾笼罩的森林,适合新手冒险者历练。" },
cave_001: { id: "cave_001", name: "黑石洞窟", description: "阴暗潮湿的洞窟,哥布林和蝙蝠出没其间。" },
ruins_001: { id: "ruins_001", name: "遗迹深处", description: "古老遗迹的深处,危险与宝藏并存。" },
snowfield_001: { id: "snowfield_001", name: "极寒雪原", description: "一片终年被冰雪覆盖的荒原,危机四伏,只有最勇敢的冒险者敢于挑战。" },
cat_dungeon_001: { id: "cat_dungeon_001", name: "猫影深渊", description: "传说中猫族先祖封印的地牢,阴影与魔力交织,只有最强的冒险者敢于挑战。" },
holy_cat_temple_001: { id: "holy_cat_temple_001", name: "神圣猫咪神殿", description: "传说中猫族信仰的圣地,光辉与神秘并存,只有最虔诚的冒险者才能踏足此地。" },
shadow_paw_hideout: { id: "shadow_paw_hideout", name: "影爪巢穴", description: "传说中只有最敏捷、最隐秘的猫咪刺客才敢踏足的黑暗巢穴。这里聚集着猫族刺客高手,步步杀机,只有最机警的冒险猫才能全身而退。" },
astralEmpressTrial: { id: "astralEmpressTrial", name: "星辉女帝试炼", description: "在星辉流转的神秘殿堂,猫族勇者们将接受女帝与其星辉守卫的考验。唯有心怀信仰与勇气,才能在星辰的见证下,获得女帝的认可与祝福。传说通过试炼者,将成为喵界真正的星辉之子。" },
amusement_park: { id: "amusement_park", name: "游乐园", description: "一座欢乐的猫猫游乐园。旋转木马、糖果小丑、毛绒玩偶常见游乐设施这里都有哦!" }
};
// 自动准备设置
let autoReadyEnabled = false;
let autoReadyThreshold = 6;
// 自动招募设置
let autoRecruitEnabled = false;
let autoRecruitInterval = 300; // 秒,代码中直接设置
let autoRecruitMsg = ''; // 用户自定义招募模板
let autoRecruitTimer = null;
function startAutoRecruit(roomId) {
stopAutoRecruit();
if (!autoRecruitEnabled || !roomId) return;
autoRecruitTimer = setInterval(() => {
const info = window.currentRoomInfo;
const memberIds = info?.memberIds || [];
if (memberIds.length >= autoReadyThreshold) {
stopAutoRecruit();
if (DEBUG_MODE) console.log('人数已达阈值,自动停止招募');
return;
}
if (window._moyuHelperWS && window._moyuHelperWS.readyState === 1 && userInfo) {
// 只更新人数信息
const currentMembers = memberIds.length;
const maxMembers = info.maxMembers;
const updatedMsg = autoRecruitMsg.replace(/\d+\/\d+人/, `${currentMembers}/${maxMembers}人`);
const finalMsg = `[😸摸鱼放置助手-自动招募]${updatedMsg}`;
const msg = `42["chat:createPublicChatMsg",{"user":${JSON.stringify(userInfo)},"data":{"content":${JSON.stringify(finalMsg)}}}]`;
if (sendWithRateLimit(msg)) {
if (DEBUG_MODE) console.log('自动发送招募信息:', msg);
}
}
}, autoRecruitInterval * 1000);
}
function stopAutoRecruit() {
if (autoRecruitTimer) {
clearInterval(autoRecruitTimer);
autoRecruitTimer = null;
}
}
// 自动停止战斗设置
let autoStopBattleEnabled = false;
// 自动踢人设置
let autoKickUnreadyEnabled = false;
let unreadyKickTimer = null;
const UNREADY_KICK_INTERVAL = 30; // 检查间隔秒
const UNREADY_KICK_TIMEOUT = 5 * 60 * 1000; // 5分钟
let unreadyKickMap = {};
function startUnreadyKick(roomId) {
stopUnreadyKick();
if (!autoKickUnreadyEnabled || !roomId) return;
unreadyKickTimer = setInterval(() => {
const info = window.currentRoomInfo;
if (!info || !info.readyMap) return;
const now = Date.now();
(info.memberIds || []).forEach(uid => {
if (uid === userInfo?.uuid) return; // 不踢自己
const ready = info.readyMap[uid];
if (ready) {
delete unreadyKickMap[uid];
} else {
if (!unreadyKickMap[uid]) {
unreadyKickMap[uid] = now;
}
if (now - unreadyKickMap[uid] > UNREADY_KICK_TIMEOUT) {
// 踢出
const kickCmd = `42["battleRoom:kick",{"user":${JSON.stringify(userInfo)},"data":{"roomId":"${info.uuid}","targetUserId":"${uid}"}}]`;
if (sendWithRateLimit(kickCmd)) {
if (DEBUG_MODE) console.log('自动踢出未准备玩家:', uid);
delete unreadyKickMap[uid];
}
}
}
});
}, UNREADY_KICK_INTERVAL * 1000);
}
function stopUnreadyKick() {
if (unreadyKickTimer) {
clearInterval(unreadyKickTimer);
unreadyKickTimer = null;
}
unreadyKickMap = {};
}
// 新增:房间信息面板渲染
function renderRoomPanel() {
const info = window.currentRoomInfo;
if (!info) {
stopAutoRecruit();
stopUnreadyKick();
return `<div style='color:#888;text-align:center;'>暂无房间信息</div>`;
}
// 渲染成员准备情况,优先显示名称
const memberIds = info.memberIds || [];
let readyListHtml = '';
let needAsync = false;
memberIds.forEach(uid => {
let name = uid;
if (window.userProfileCache[uid]) {
name = window.userProfileCache[uid].name;
} else {
needAsync = true;
fetchUserProfile(uid, () => {
if (activeTab === 'room') renderContent();
});
}
const ready = info.readyMap && info.readyMap[uid];
readyListHtml += `<li>${name}:<span style='color:${ready ? '#0f0' : '#f00'};'>${ready ? '已准备' : '未准备'}</span></li>`;
});
// 匹配区域名称
const areaName = MapInfos[info.area]?.name || info.area;
// 自动生成招募信息
if (!autoRecruitMsg) {
const roomName = info.name || '房间';
const currentMembers = memberIds.length;
const maxMembers = info.maxMembers;
autoRecruitMsg = `【房间名:${roomName}】${areaName} ${currentMembers}/${maxMembers}人 来人一起摸鱼~`;
}
// 检查是否需要自动准备
if (autoReadyEnabled && memberIds.length >= autoReadyThreshold && !info.readyMap?.[userInfo.uuid]) {
setTimeout(() => {
const readyCmd = `42["battleRoom:updateReadyState",{"user":${JSON.stringify(userInfo)},"data":{"roomId":"${info.uuid}","ready":true}}]`;
if (sendWithRateLimit(readyCmd)) {
if (DEBUG_MODE) console.log('自动发送准备指令:', readyCmd);
}
}, 1000);
}
// 检查自动招募
if (autoRecruitEnabled) {
startAutoRecruit(info.uuid);
} else {
stopAutoRecruit();
}
// 检查是否需要自动停止战斗
if (autoStopBattleEnabled && memberIds.length < autoReadyThreshold) {
setTimeout(() => {
const stopCmd = `42["battle:stopBattle",{"user":${JSON.stringify(userInfo)},"data":{"roomId":"${info.uuid}"}}]`;
if (sendWithRateLimit(stopCmd)) {
if (DEBUG_MODE) console.log('自动发送停止战斗指令:', stopCmd);
}
}, 1000);
}
// 检查自动踢人
if (autoKickUnreadyEnabled) {
startUnreadyKick(info.uuid);
} else {
stopUnreadyKick();
}
// <div>房间ID:${info.uuid}</div>
return `
<div style='font-weight:bold;font-size:16px;margin-bottom:8px;'>${info.name || '房间'}</div>
<div>房主:${window.userProfileCache[info.ownerId]?.name || info.ownerId}</div>
<div>成员:${memberIds.length} / ${info.maxMembers}</div>
<div>状态:${info.status}</div>
<div>类型:${info.type}</div>
<div>区域:${areaName}</div>
<div>轮次:${info.currentRepeat} / ${info.repeatCount}</div>
<div>自动重开:${info.autoRestart ? '是' : '否'}</div>
<div>创建时间:${new Date(info.createdAt).toLocaleString()}</div>
<div style='margin-top:8px;font-weight:bold;'>成员准备情况:</div>
<ul style='padding-left:18px;'>${readyListHtml}</ul>
<div style='margin-top:12px;padding-top:8px;border-top:1px solid #333;'>
<div style='font-weight:bold;margin-bottom:6px;'>自动准备设置:</div>
<div style='display:flex;align-items:center;gap:8px;margin-bottom:4px;'>
<input type="checkbox" id="auto-ready-toggle" ${autoReadyEnabled ? 'checked' : ''} style="margin:0;" />
<label for="auto-ready-toggle" style="font-size:13px;">启用自动准备</label>
</div>
<div style='display:flex;align-items:center;gap:8px;margin-bottom:4px;'>
<input type="checkbox" id="auto-stop-battle-toggle" ${autoStopBattleEnabled ? 'checked' : ''} style="margin:0;" />
<label for="auto-stop-battle-toggle" style="font-size:13px;">人数不足时自动停止战斗</label>
</div>
<div style='display:flex;align-items:center;gap:8px;margin-bottom:4px;'>
<input type="checkbox" id="auto-kick-unready-toggle" ${autoKickUnreadyEnabled ? 'checked' : ''} style="margin:0;" />
<label for="auto-kick-unready-toggle" style="font-size:13px;">自动踢出5分钟未准备玩家</label>
</div>
<div style='display:flex;align-items:center;gap:8px;'>
<label style="font-size:13px;">人数阈值:</label>
<input type="number" id="auto-ready-threshold" value="${autoReadyThreshold}" min="2" max="6" style="width:60px;padding:2px 4px;border-radius:4px;border:1px solid #444;background:#222;color:#fff;font-size:12px;" />
</div>
</div>
<div style='margin-top:12px;padding-top:8px;border-top:1px solid #333;'>
<div style='font-weight:bold;margin-bottom:6px;'>自动招募设置:</div>
<div style='display:flex;align-items:center;gap:8px;margin-bottom:4px;'>
<input type="checkbox" id="auto-recruit-toggle" ${autoRecruitEnabled ? 'checked' : ''} style="margin:0;" />
<label for="auto-recruit-toggle" style="font-size:13px;">启用自动招募</label>
</div>
<div style='display:flex;align-items:center;gap:8px;'>
<label style="font-size:13px;">招募模板:</label>
<input type="text" id="auto-recruit-msg" value="${autoRecruitMsg}" style="flex:1;padding:2px 8px;border-radius:4px;border:1px solid #444;background:#222;color:#fff;font-size:13px;" maxlength="100" placeholder="输入招募模板,人数用0/6人表示" />
</div>
</div>
`;
}
// 组装面板
panel.appendChild(titleBar);
panel.appendChild(tabBar);
panel.appendChild(content);
// panel.appendChild(wsSendBar);
document.body.appendChild(panel);
// 初始化
setActiveTab('combat');
// 你可以在此处挂载实际的统计数据渲染逻辑
// 例如:动态填充dpsTableBody、battleLog、inventoryTableBody等
// 判断压缩格式
function detectCompression(buf) {
const b = new Uint8Array(buf);
if (b.length >= 2) {
if (b[0] === 0x1f && b[1] === 0x8b) return 'gzip';
if (b[0] === 0x78 && (((b[0] << 8) | b[1]) % 31) === 0) return 'zlib';
}
return 'deflate';
}
// 记录当前WebSocket实例
window._moyuHelperWS = null;
// WebSocket 拦截与解压
const NativeWS = window.WebSocket;
window.WebSocket = function (url, protocols) {
const ws = protocols ? new NativeWS(url, protocols) : new NativeWS(url);
window._moyuHelperWS = ws;
const originalSend = ws.send;
ws.send = function (data) {
if (DEBUG_MODE) console.log('[WS 发送]', data);
return originalSend.call(this, data);
};
let messageID = 0;
let lastCmd = null;
ws.addEventListener('message', ev => {
messageID++;
let obj = null;
let text = null;
if (ev.data instanceof ArrayBuffer) {
const format = detectCompression(ev.data);
switch (format) {
case 'gzip':
text = pako.ungzip(new Uint8Array(ev.data), { to: 'string' });
break;
case 'zlib':
text = pako.inflate(new Uint8Array(ev.data), { to: 'string' });
break;
default:
text = pako.inflateRaw(new Uint8Array(ev.data), { to: 'string' });
}
obj = JSON.parse(text);
} else {
text = ev.data
if (typeof text === 'string' && /^\d+-/.test(text)) {
const idx = text.indexOf('-');
if (idx !== -1) {
try {
const arr = JSON.parse(text.slice(idx + 1));
if (Array.isArray(arr) && typeof arr[0] === 'string') {
lastCmd = arr[0];
}
} catch (e) {
lastCmd = null;
if (DEBUG_MODE) console.log(e)
}
}
return;
}
if (DEBUG_MODE) console.log(`[WS 未处理数据]`, text);
}
// 根据 lastCmd 分发处理
if (obj && lastCmd) {
if (userInfo == null && obj.user) {
userInfo = obj.user;
}
if (lastCmd.startsWith('inventory:increase:success')) {
updateInventory(obj.data);
} else if (lastCmd.startsWith('battle:fullInfo:success')) {
// battle:fullInfo:success、battle:round:success等
obj = obj.data
if (obj && obj.battleInfo && obj.thisRoundAction) {
const battle = obj.battleInfo;
const action = obj.thisRoundAction;
const members = battle.members || [];
const playerUuids = (battle.groups?.player) || [];
const memberMap = Object.fromEntries(
members.map(m => [m.uuid, m])
);
const srcUuid = action.sourceUnitUuid;
const dmgObj = action.damage || {}; // { uuid: amount, … }
const healObj = action.heal || {}; // { uuid: amount, … }
const skill = action.castSkillId || '未知技能';
const targets = action.targetUnitUuidList || [];
// 只统计玩家组
if (playerUuids.includes(srcUuid)) {
let hasAction = false;
for (const [tUuid, amt] of Object.entries(dmgObj)) {
damageAccum.set(srcUuid, (damageAccum.get(srcUuid) || 0) + amt);
hasAction = true;
}
for (const [tUuid, amt] of Object.entries(healObj)) {
healAccum.set(srcUuid, (healAccum.get(srcUuid) || 0) + amt);
hasAction = true;
}
if (hasAction) {
actionCount.set(srcUuid, (actionCount.get(srcUuid) || 0) + 1);
}
window.playerUuids = playerUuids;
window.memberMap = memberMap;
updateDpsPanel(playerUuids, memberMap);
}
// 战斗日志输出
let logLines = [];
if (playerUuids.includes(srcUuid) || targets.some(t => playerUuids.includes(t))) {
const srcName = memberMap[srcUuid]?.name || srcUuid;
for (const tUuid of targets) {
const tName = memberMap[tUuid]?.name || tUuid;
const dmg = dmgObj[tUuid] || 0;
const heal = healObj[tUuid] || 0;
const skillName = skillNames[skill] || skill;
if (dmg > 0) logLines.push(`🗡️ <b>${srcName}</b> 用 <b>${skillName}</b> 对 <b>${tName}</b> 造成 <span style='color:#ff7675;'>${dmg}</span> 伤害`);
if (heal > 0) logLines.push(`💚 <b>${srcName}</b> 用 <b>${skillName}</b> 治疗 <b>${tName}</b> <span style='color:#00e676;'>${heal}</span> 生命`);
}
}
if (logLines.length) addBattleLog(logLines);
}
} else if (lastCmd.startsWith('battleRoom:update') || lastCmd.startsWith('battleRoom:create:success')) {
const roomInfo = obj.data;
window.currentRoomInfo = roomInfo;
if (activeTab === 'room') renderContent();
} else if (lastCmd.startsWith('battleRoom:leave:success')) {
window.currentRoomInfo = null;
if (activeTab === 'room') renderContent();
}
else {
if (DEBUG_MODE) {
console.log(`[WS 未处理指令]`, lastCmd);
console.log(`[WS 未处理对象]`, obj);
}
}
// 其他指令可在此扩展
lastCmd = null;
} else {
// 没有指令时,打印日志
}
});
return ws;
};
window.WebSocket.prototype = NativeWS.prototype;
Object.getOwnPropertyNames(NativeWS).forEach(prop => {
if (!(prop in window.WebSocket)) {
window.WebSocket[prop] = NativeWS[prop];
}
});
if (DEBUG_MODE) console.log('✅ MoYuIdleHelper 已启动');
// 新增:DPS面板渲染函数
function updateDpsPanel(playerUuids, memberMap) {
const tbody = document.querySelector('#dpsTableBody');
if (!tbody) return;
if (!playerUuids || !memberMap || playerUuids.length === 0) {
tbody.innerHTML = `<tr><td colspan="5" style="text-align:center;color:#888;">暂无数据</td></tr>`;
return;
}
// 收集玩家数据
const rows = playerUuids.map(uuid => {
const total = damageAccum.get(uuid) || 0;
const heal = healAccum.get(uuid) || 0;
const cnt = actionCount.get(uuid) || 1;
const name = memberMap[uuid]?.name || uuid;
const dps = Math.round(total / cnt); // 输出效率
const hps = Math.round(heal / cnt); // 治疗效率
return { name, dps, hps, total, heal };
});
// 按输出效率降序
rows.sort((a, b) => b.dps - a.dps);
tbody.innerHTML = rows.map(r => `<tr><td>${r.name}</td><td style='text-align:right;'>${r.dps}</td><td style='text-align:right;'>${r.hps}</td><td style='text-align:right;'>${r.total}</td><td style='text-align:right;'>${r.heal}</td></tr>`).join('');
}
// 新增:战斗日志管理
const logList = [];
// 日志自动滚动控制
let logAutoScroll = true;
// 日志面板增加滚动条美化和事件监听
const logPanelCss = document.createElement('style');
logPanelCss.innerHTML = `
#logPanel::-webkit-scrollbar {
width: 8px;
background: transparent;
}
#logPanel::-webkit-scrollbar-thumb {
background: linear-gradient(120deg, #444 30%, #888 100%);
border-radius: 6px;
}
#logPanel:hover::-webkit-scrollbar-thumb {
background: linear-gradient(120deg, #666 30%, #aaa 100%);
}
#logPanel {
scrollbar-width: thin;
scrollbar-color: #888 #222;
}
`;
document.head.appendChild(logPanelCss);
setTimeout(() => {
const logPanel = document.getElementById('battleLog');
if (logPanel) {
logPanel.addEventListener('scroll', function () {
// 判断是否在底部
const atBottom = logPanel.scrollTop + logPanel.clientHeight >= logPanel.scrollHeight - 2;
logAutoScroll = atBottom;
});
logPanel.addEventListener('mouseenter', () => { logAutoScroll = false; });
logPanel.addEventListener('mouseleave', () => {
// 如果离开时已在底部,则恢复自动滚动
const atBottom = logPanel.scrollTop + logPanel.clientHeight >= logPanel.scrollHeight - 2;
logAutoScroll = atBottom;
});
}
}, 500);
function addBattleLog(lines) {
// 始终记录到logList
for (const line of lines) {
logList.push(line);
}
// 限制日志条数
while (logList.length > 200) logList.shift();
// 如果面板已打开,刷新显示
const ul = document.getElementById('battleLog');
if (ul) {
ul.innerHTML = logList.map(l => `<li style='margin-bottom:2px;'>${l}</li>`).join('');
// 自动滚动到底部(仅在自动滚动开启时)
const logPanel = document.getElementById('battleLog');
if (logPanel && logAutoScroll) {
logPanel.scrollTop = logPanel.scrollHeight;
}
}
}
// 更新物品变动统计
function updateInventory(newInventory) {
for (const [itemId, itemData] of Object.entries(newInventory)) {
const oldCount = inventory[itemId] || 0;
const newCount = itemData.count;
inventory[itemId] = oldCount + newCount;
}
if (saveInventoryEnabled) {
saveInventoryData();
}
updateInventoryPanel();
}
// 更新物品变动面板
function updateInventoryPanel() {
const tbody = document.querySelector('#inventoryTableBody');
if (!tbody) return;
tbody.innerHTML = '';
const sortedItems = Object.entries(inventory)
.filter(([itemId, total]) => total > 0)
.sort((a, b) => b[1] - a[1]);
for (const [itemId, total] of sortedItems) {
const row = document.createElement('tr');
row.innerHTML = `
<td>${itemIdNameMap[itemId] || itemId}</td>
<td style="text-align:right;">${total}</td>
`;
tbody.appendChild(row);
}
}
// 保存物品数据到本地存储
function saveInventoryData() {
if (saveInventoryEnabled) {
const data = {
inventory: inventory,
saveInventoryEnabled: saveInventoryEnabled,
timestamp: Date.now()
};
let allData = {};
try {
allData = JSON.parse(localStorage.getItem(LOCAL_STORAGE_NAME)) || {};
} catch (e) { allData = {}; }
allData[INVENTORY_STORAGE_NAME] = data;
localStorage.setItem(LOCAL_STORAGE_NAME, JSON.stringify(allData));
}
}
// 从本地存储加载物品数据
function loadInventoryData() {
try {
const allData = JSON.parse(localStorage.getItem(LOCAL_STORAGE_NAME)) || {};
const saved = allData[INVENTORY_STORAGE_NAME];
if (saved) {
const data = saved;
if (data.inventory) {
inventory = data.inventory;
if (DEBUG_MODE) console.log('已加载保存的物品数据');
}
if (typeof data.saveInventoryEnabled === 'boolean') {
saveInventoryEnabled = data.saveInventoryEnabled;
if (DEBUG_MODE) console.log('已加载保存选项状态:', saveInventoryEnabled);
}
}
} catch (e) {
if (DEBUG_MODE) console.warn('加载物品数据失败:', e);
}
}
// 初始化时加载数据
loadInventoryData();
loadPanelSettings();
})();