Skip to content

Instantly share code, notes, and snippets.

@fzdwx
Last active July 26, 2025 15:35
Show Gist options
  • Select an option

  • Save fzdwx/cad1e72debf8f9998a24c6cd2d98372b to your computer and use it in GitHub Desktop.

Select an option

Save fzdwx/cad1e72debf8f9998a24c6cd2d98372b to your computer and use it in GitHub Desktop.
MoYuIdleHelperPlus 修复
// ==UserScript==
// @name MoYuIdleHelperPlus
// @namespace https://tampermonkey.net/
// @version 2.7.0
// @description 摸鱼放置助手
// @author Mid & Firestream
// @license MIT
// @match https://www.moyu-idle.com/*
// @match https://moyu-idle.com/*
// @run-at document-start
// @grant none
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/pako.min.js
// ==/UserScript==
(function () {
'use strict';
const VERSION = "2.7.0";
const tast = true;
const dbuglistenws = false;
let enableDebugLogging = false;
// --- START: WebSocket 拦截器 ---
if (window.isMoYuHelperWsAttached) {
return;
}
window.isMoYuHelperWsAttached = true;
console.log('🟢 摸鱼放置助手 : 准备部署全入口原型链拦截器。');
const OriginalWebSocket = window.WebSocket;
window._moyuHelperWS = null;
const originalAddEventListener = OriginalWebSocket.prototype.addEventListener;
OriginalWebSocket.prototype.addEventListener = function (type, listener, options) {
window._moyuHelperWS = this;
if (type === 'message') {
const originalListener = listener;
const wrappedListener = (event) => {
if (enableDebugLogging) logMessage(event);
processMessageEvent(event);
if (typeof originalListener === 'function') originalListener.call(this, event);
else if (originalListener && typeof originalListener.handleEvent === 'function') originalListener.handleEvent.call(originalListener, event);
};
if (!this._wrappedListeners) {
this._wrappedListeners = new Map();
}
this._wrappedListeners.set(listener, wrappedListener);
return originalAddEventListener.call(this, type, wrappedListener, options);
}
return originalAddEventListener.call(this, type, listener, options);
};
const originalRemoveEventListener = OriginalWebSocket.prototype.removeEventListener;
OriginalWebSocket.prototype.removeEventListener = function (type, listener, options) {
if (type === 'message' && this._wrappedListeners && this._wrappedListeners.has(listener)) {
const wrappedListener = this._wrappedListeners.get(listener);
this._wrappedListeners.delete(listener);
return originalRemoveEventListener.call(this, type, wrappedListener, options);
}
return originalRemoveEventListener.call(this, type, listener, options);
};
Object.defineProperty(OriginalWebSocket.prototype, 'onmessage', {
get: function () {
return this._onmessageListener || null;
},
set: function (listener) {
window._moyuHelperWS = this;
if (this._onmessageListener) this.removeEventListener('message', this._onmessageListener);
this._onmessageListener = listener;
if (typeof listener === 'function') this.addEventListener('message', listener);
},
configurable: true, enumerable: true
});
const originalSend = OriginalWebSocket.prototype.send;
OriginalWebSocket.prototype.send = function (data) {
window._moyuHelperWS = this;
if (enableDebugLogging) console.log('%c[WebSocket 已发送]', 'color: #03A9F4; font-weight: bold;', data);
if (typeof data === 'string' && data.includes('requestCharacterStatusInfo')) {
setTimeout(requestPlayerSkills, 500); // 延迟请求技能以确保状态更新
}
return originalSend.call(this, data);
};
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';
}
function logMessage(event) {
let messageData = event.data;
if (messageData instanceof ArrayBuffer) {
try {
const format = detectCompression(messageData);
let text = pako.inflate(new Uint8Array(messageData), {to: 'string'});
const obj = JSON.parse(text);
console.log('%c[WebSocket 已接收]', 'color: #4CAF50; font-weight: bold;', `(已解压 ${format})`, obj);
} catch (e) {
console.error('[WS] 解压或解析消息失败', e);
}
} else {
try {
const obj = JSON.parse(messageData);
console.log('%c[WebSocket 已接收]', 'color: #4CAF50; font-weight: bold;', '(JSON)', obj);
} catch (e) {
console.log('%c[WebSocket 已接收]', 'color: #4CAF50; font-weight: bold;', '(文本)', messageData);
}
}
}
const pendingBinaryEvents = [];
function processMessageEvent(event) {
if (typeof event.data === 'string') {
const simpleMatch = event.data.match(/^42(\[.*\])$/);
if (simpleMatch) {
try {
const arr = JSON.parse(simpleMatch[1]);
if (Array.isArray(arr) && typeof arr[0] === 'string') {
processCommand(arr[0], arr[1]);
}
} catch (e) {
}
return;
}
const binaryPlaceholderMatch = event.data.match(/^451-(\[.*\])$/);
if (binaryPlaceholderMatch) {
try {
const arr = JSON.parse(binaryPlaceholderMatch[1]);
pendingBinaryEvents.push(arr);
} catch (e) {
}
return;
}
}
if (event.data instanceof ArrayBuffer) {
const pendingEvent = pendingBinaryEvents.shift();
if (pendingEvent) {
try {
let text = pako.inflate(new Uint8Array(event.data), {to: 'string'});
const binaryData = JSON.parse(text);
processCommand(pendingEvent[0], binaryData);
} catch (e) {
if (enableDebugLogging) console.error(`[MoYuHelper] 二进制包处理失败 (指令: ${pendingEvent[0]}):`, e);
}
}
}
}
// --- END: WebSocket 拦截器 ---
// ===== 全局共享变量 =====
let userInfo = null;
window.currentRoomInfo = null;
const pendingPromises = new Map();
const ASYNC_TIMEOUT = 8000;
// ===== START: 核心数据与常量 =====
const LOCAL_STORAGE_NAME = "MO_YU_IDLE_HELPER_DATA";
const INVENTORY_STORAGE_NAME = "MO_YU_IDLE_HELPER_INVENTORY";
const EARNINGS_STORAGE_NAME = "MO_YU_IDLE_HELPER_EARNINGS";
const SETTINGS_STORAGE_NAME = "MO_YU_IDLE_HELPER_SETTINGS";
const PROJECTION_STABILIZE_COUNT = 50; // 掉落50次后切换到EMA
const EMA_ALPHA = 0.1; // EMA平滑因子
const damageAccum = new Map(), actionCount = new Map(), healAccum = new Map();
let dropStatistics = {gold: 0, goldDropCount: 0}, saveInventoryEnabled = false, lastProcessedTimestamp = null;
let earningsStartTime = null;
let xpStatistics = {
strengthXp: 0, dexterityXp: 0, attackXp: 0,
staminaXp: 0, defenseXp: 0,
skillCasts: {}, totalIntelligenceXp: 0
};
let projections = {
goldEmaPerHour: 0,
strPerHour: 0, dexPerHour: 0, atkPerHour: 0,
staPerHour: 0, defPerHour: 0, intPerHour: 0
};
let playerSkills = new Map();
let playerAttributes = new Map();
const SKILL_LEVEL_UP_XP = [0, 20, 45, 80, 125, 180, 245, 320, 405, 500, 605, 720, 845, 980, 1125, 1280, 1445, 1620, 1805, 2e3, 2205, 2420, 2645, 2880, 3125, 3380, 3645, 3920, 4205, 4500, 4805, 5760, 6825, 8e3, 9285, 10680, 12185, 13800, 15525, 17360, 19305, 21360, 23525, 25800, 28185, 30680, 33285, 36e3, 38825, 41760, 44805, ...Array.from({length: 50}, (e, t) => Math.floor(44805 * Math.pow(1.18, t + 1))), ...Array.from({length: 30}, (e, t) => Math.floor(44805 * Math.pow(1.18, 50) * Math.pow(1.15, t + 1))), ...Array.from({length: 20}, (e, t) => Math.floor(44805 * Math.pow(1.18, 50) * Math.pow(1.15, 30) * Math.pow(1.12, t + 1)))];
const attributeNames = {
battle: "战斗",
strength: "力量",
dexterity: "敏捷",
intelligence: "智力",
stamina: "耐力",
attacking: "攻击",
defencing: "防御"
};
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: "禁忌魔法",
enhanceStrike: "强化冲击",
threadingNeedle: "穿针引线",
ultimateLibraryJudgement: "禁魔审判"
};
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": "女皇披风",
"mithrilSword": "秘银剑",
"mithrilDagger": "秘银匕首",
"mithrilHat": "秘银头盔",
"mithrilCoat": "秘银护甲",
"mithrilGloves": "秘银手套",
"mithrilPants": "秘银护腿",
"steelHammer": "钢制重锤",
"paper": "纸",
"book": "书",
"pencil": "碳笔",
"experienceOfStrength": "力量经验",
"experienceOfDexterity": "敏捷经验",
"experienceOfIntelligence": "智力经验",
"bookOfStrength": "力量之书",
"bookOfDexterity": "敏捷之书",
"bookOfIntelligence": "智力之书",
"magicBook": "魔法书",
"slimeDivideCore": "分裂核心",
"batShadownCape": "蝠影披风",
"fangNecklace": "兽牙项链",
"overloadGuardianCore": "过载核心",
"shadowBlade": "影之刃",
"starDustMagicBook": "星辰魔法书",
"stealthAmulet": "伏击吊坠",
"initiativeAmulet": "先机吊坠",
"nightmarePrisonChest": "噩梦监狱宝箱",
"glassBottles": "玻璃瓶",
"manacrystal": "魔晶石",
"catEyeStone": "猫眼石",
"amberEyeStone": "琥珀瞳石",
"fishscaleMineral": "鱼鳞矿",
"fluffstone": "绒毛岩",
"clawmarkOre": "爪痕矿",
"fishscaleMineralIgnot": "鱼鳞合金",
"shadowSteel": "暗影精铁",
"starforgedAlloy": "星辰合金",
"manacrystalStaff": "魔晶法杖",
"ironPot": "铁锅",
"ironShovel": "铁铲",
"steelPot": "钢锅",
"steelShovel": "钢铲",
"ironMachinistHammer": "铁锤",
"steelMachinistHammer": "钢锤",
"fermentationStirrer": "酿造搅拌器",
"mithrilMachinistHammer": "秘银工匠锤",
"woolArtisanOutfit": "羊毛工匠服",
"silkCuteHat": "丝质可爱帽",
"silkCuteGloves": "丝质可爱手套",
"silkArtisanOutfit": "丝质工匠服",
"silkTailorClothes": "丝质裁缝服",
"silkTailorGloves": "丝质裁缝手套",
"cloudwalkerBoots": "云行靴",
"cloudwalkerCloak": "云行斗篷",
"fishscaleMineralHat": "鱼鳞合金头盔",
"fishscaleMineralCoat": "鱼鳞合金盔甲",
"fishscaleMineralGloves": "鱼鳞合金护手",
"fishscaleMineralPants": "鱼鳞合金护腿",
"berryWine": "浆果酒",
"custardPudding": "蛋奶布丁",
"woodPulp": "木浆",
"sand": "沙子",
"jadeTuna": "翡翠金枪鱼",
"emberEel": "余烬鳗",
"moonlightShrimp": "月光虾",
"crystalCarp": "水晶鲤",
"dawnBlossom": "晨露花",
"amberSap": "琥珀汁",
"luminousMoss": "夜光苔",
"windBellHerb": "风铃草",
"cloudCotton": "云絮",
"rainbowShard": "彩虹碎片",
"autoFeeder": "自动喂食器",
"scratchingPost": "猫抓板"
};
Object.entries(skillNames).forEach(([skillId, skillName]) => {
const bookId = skillId + 'SkillBook';
if (!itemIdNameMap[bookId]) {
itemIdNameMap[bookId] = skillName + '技能书';
}
});
const startTime = Date.now();
let panel, content, tabBar, tabBtns = {}, activeTab = 'combat';
let panelWidth = 420, panelHeight = 500, isCollapsed = false;
// --- START: 核心UI渲染模块 ---
function savePanelSettings() {
const data = {enableDebugLogging, panelWidth, 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 loadPanelSettings() {
try {
const allData = JSON.parse(localStorage.getItem(LOCAL_STORAGE_NAME)) || {};
const saved = allData[SETTINGS_STORAGE_NAME];
if (saved) {
enableDebugLogging = saved.enableDebugLogging || false;
panelWidth = saved.panelWidth || 420;
panelHeight = saved.panelHeight || 500;
if (panel) {
panel.style.width = `${panelWidth}px`;
panel.style.height = `${panelHeight}px`;
}
}
} catch (e) {
console.warn('加载面板设置失败:', e);
}
}
function renderContent() {
if (!content) return;
let panelContent = '';
switch (activeTab) {
case 'combat':
panelContent = renderCombatPanel();
break;
case 'inventory':
panelContent = renderInventoryPanel();
break;
case 'earnings':
panelContent = renderEarningsPanel();
break;
case 'room':
panelContent = renderRoomPanel();
break;
case 'settings':
panelContent = renderSettingsPanel();
break;
}
content.innerHTML = panelContent;
setTimeout(() => attachEventListeners(activeTab), 50);
}
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();
}
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 - 250}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>`;
}
function renderEarningsPanel() {
return `
<div style="display:flex; justify-content: space-between; align-items: center; margin-bottom: 15px; border-bottom:1px solid #555;">
<h4 style="color:#ffd700; margin:0; padding: 5px 0; ">金币收益预期</h4>
<button id="clear-earnings-btn" style="padding:2px 8px;border-radius:4px;border:1px solid #555;background:#3a3a3a;color:#ccc;font-size:11px;cursor:pointer; margin-left:10px; flex-shrink: 0;">清除收益</button>
</div>
<div id="gold-projection-content" style="margin-bottom: 15px;"></div>
<div style="margin-bottom: 15px;">
<h4 style="color:#ffd700; margin:0 0 10px 0; border-bottom:1px solid #555; padding-bottom:5px;">属性经验预期</h4>
<div id="attribute-xp-container" style="font-size: 12px; line-height: 1.6;">
<table style="width:100%;font-size:12px;border-collapse:collapse;">
<thead><tr style="color:#ffd700;"><th style="text-align:left;">属性 (LV)</th><th style="text-align:center;">经验进度</th><th style="text-align:right;">经验/小时</th><th style="text-align:right;">预计下一级</th></tr></thead>
<tbody id="attribute-xp-table"><tr><td colspan="4" style="text-align:center;color:#888;">暂无数据</td></tr></tbody>
</table>
</div>
</div>
<div>
<h4 style="color:#ffd700; margin:0 0 10px 0; border-bottom:1px solid #555; padding-bottom:5px;">技能经验预期</h4>
<table style="width:100%;font-size:12px;border-collapse:collapse; margin-top: 8px;">
<thead><tr style="color:#ffd700;"><th style="text-align:left;">技能 (LV)</th><th style="text-align:center;">经验进度</th><th style="text-align:right;">经验/小时</th><th style="text-align:right;">预计下一级</th></tr></thead>
<tbody id="skill-xp-table"><tr><td colspan="4" style="text-align:center;color:#888;">暂无数据</td></tr></tbody>
</table>
</div>
`;
}
function renderSettingsPanel() {
let tastSection = '';
if (tast) {
tastSection = `<div style='margin-top:20px; border-top: 1px solid #444; padding-top: 12px;'><div style='font-weight:bold;font-size:16px;margin-bottom:12px;'>开发者/测试工具</div><div style="display: flex; gap: 10px; margin-bottom: 8px;"><button id="force-stop-battle-btn" style="padding: 6px 14px; border-radius: 5px; border: 1px solid #e67e22; background-color: #f39c12; color: white; font-weight: bold; cursor: pointer;">强制停战</button><button id="force-leave-room-btn" style="padding: 6px 14px; border-radius: 5px; border: 1px solid #c0392b; background-color: #e74c3c; color: white; font-weight: bold; cursor: pointer;">强制越狱</button></div><div style='font-size:12px;color:#888;'><b>强制停战:</b>3秒后发送停战指令。</div><div style='font-size:12px;color:#888;margin-top: 4px;'><b>强制越狱:</b>3秒后先停战再退房。</div></div>`;
}
let debugWsSection = '';
if (dbuglistenws) {
debugWsSection = `<div style='margin-bottom:16px;'><div style='display:flex;align-items:center;gap:8px;margin-bottom:8px;'><input type="checkbox" id="debug-logging-toggle" ${enableDebugLogging ? 'checked' : ''} style="margin:0;" /><label for="debug-logging-toggle" style="font-size:13px;">启用WebSocket调试日志</label></div><div style='font-size:12px;color:#888;padding-left:26px;'>在浏览器F12控制台中显示所有收发的WebSocket消息。</div></div>`;
}
return `<div style='font-weight:bold;font-size:16px;margin-bottom:12px;'>全局设置</div>
${debugWsSection}
<div style="margin-bottom: 16px; border-top: 1px solid #444; padding-top: 12px;"><div style='font-weight:bold;font-size:16px;margin-bottom:12px;'>外观设置</div><div style="display:flex; gap: 10px; align-items: center;"><label for="panel-width-input">宽度:</label><input type="number" id="panel-width-input" value="${panelWidth}" style="width: 60px; background: #222; color: #fff; border: 1px solid #555; border-radius: 4px; padding: 2px 4px;" /><label for="panel-height-input">高度:</label><input type="number" id="panel-height-input" value="${panelHeight}" style="width: 60px; background: #222; color: #fff; border: 1px solid #555; border-radius: 4px; padding: 2px 4px;" /></div></div>${tastSection}`;
}
// --- END: 核心UI渲染模块 ---
window.userProfileCache = window.userProfileCache || {};
function fetchUserProfile(uuid, cb) {
if (window.userProfileCache[uuid]) {
cb(window.userProfileCache[uuid]);
return;
}
const apiUrl = `${window.location.origin}/api/game/user/profile?uuid=${uuid}`;
fetch(apiUrl).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: {name: "悠闲平原"},
forest_001: {name: "幽暗森林"},
cave_001: {name: "黑石洞窟"},
ruins_001: {name: "遗迹深处"},
snowfield_001: {name: "极寒雪原"},
cat_dungeon_001: {name: "猫影深渊"},
holy_cat_temple_001: {name: "神圣猫咪神殿"},
shadow_paw_hideout: {name: "影爪巢穴"},
astralEmpressTrial: {name: "星辉女帝试炼"},
amusement_park: {name: "游乐园"}
};
function renderRoomPanel() {
const i = window.currentRoomInfo;
if (!i) {
return `<div style='color:#888;text-align:center;'>暂无房间信息</div>`;
}
const m = i.memberIds || [];
let r = '';
m.forEach(u => {
let n = window.userProfileCache[u]?.name || u;
if (!window.userProfileCache[u]) {
fetchUserProfile(u, () => {
if (activeTab === 'room') renderContent()
})
}
const d = i.readyMap && i.readyMap[u];
r += `<li>${n}:<span style='color:${d ? '#0f0' : '#f00'};'>${d ? '已准备' : '未准备'}</span></li>`
});
const a = MapInfos[i.area]?.name || i.area;
return `<div style='font-weight:bold;font-size:16px;margin-bottom:8px;'>${i.name || '房间'}</div><div>房主:${window.userProfileCache[i.ownerId]?.name || i.ownerId}</div><div>成员:${m.length}/${i.maxMembers}</div><div>状态:${i.status}</div><div>类型:${i.type}</div><div>区域:${a}</div><div>轮次:${i.currentRepeat}/${i.repeatCount}</div><div>自动重开:${i.autoRestart ? '是' : '否'}</div><div>创建时间:${new Date(i.createdAt).toLocaleString()}</div><div style='margin-top:8px;font-weight:bold;'>成员准备情况:</div><ul style='padding-left:18px;'>${r}</ul>`;
}
function updateDpsPanel(pu, mm) {
const t = document.querySelector('#dpsTableBody');
if (!t) return;
if (!pu || !mm || pu.length === 0) {
t.innerHTML = `<tr><td colspan="5" style="text-align:center;color:#888;">暂无数据</td></tr>`;
return
}
const r = pu.map(u => {
const tot = damageAccum.get(u) || 0, h = healAccum.get(u) || 0, c = actionCount.get(u) || 1,
n = mm[u]?.name || u, d = Math.round(tot / c), p = Math.round(h / c);
return {name: n, dps: d, hps: p, total: tot, heal: h}
}).sort((a, b) => b.dps - a.dps);
t.innerHTML = r.map(x => `<tr><td>${x.name}</td><td style='text-align:right;'>${x.dps}</td><td style='text-align:right;'>${x.hps}</td><td style='text-align:right;'>${x.total}</td><td style='text-align:right;'>${x.heal}</td></tr>`).join('')
}
const logList = [];
let logAutoScroll = true;
function addBattleLog(l) {
logList.push(...l);
while (logList.length > 200) logList.shift();
const u = document.getElementById('battleLog');
if (u) {
u.innerHTML = logList.map(x => `<li style='margin-bottom:2px;'>${x}</li>`).join('');
if (logAutoScroll) u.scrollTop = u.scrollHeight
}
}
// --- START: 数据处理与统计模块 ---
const formatDecimal = (num) => (num || 0).toFixed(2);
function formatHours(hours) {
if (hours === Infinity || !hours || hours < 0) return "∞";
if (hours < 1) return `${Math.floor(hours * 60)}分`;
if (hours < 24) return `${(Math.floor(hours * 10) / 10).toFixed(1)}时`;
const days = Math.floor(hours / 24);
const remainingHours = hours % 24;
return `${days}天 ${(Math.floor(remainingHours * 10) / 10).toFixed(1)}时`;
}
function formatGold(num) {
if (!num || num < 1000) return formatDecimal(num);
if (num < 1000000) return `${(num / 1000).toFixed(1)}k`;
return `${(num / 1000000).toFixed(1)}m`;
}
function updateInventoryPanel() {
const t = document.querySelector('#inventoryTableBody');
if (!t) return;
const s = Object.entries(dropStatistics).filter(([k]) => k !== 'goldDropCount').filter(([, v]) => v > 0).sort((a, b) => b[1] - a[1]);
t.innerHTML = s.map(([i, v]) => `<tr><td>${itemIdNameMap[i] || i}</td><td style="text-align:right;">${v}</td></tr>`).join('')
}
function updateEarningsPanel() {
const getEl = id => document.getElementById(id);
const goldContentEl = getEl('gold-projection-content');
if (goldContentEl) {
let goldPerHour;
if (dropStatistics.goldDropCount > PROJECTION_STABILIZE_COUNT) {
goldPerHour = projections.goldEmaPerHour;
} else {
const elapsedHours = earningsStartTime ? (Date.now() - earningsStartTime) / 3600000 : 0;
if (elapsedHours > 0 && dropStatistics.goldDropCount > 0) {
const credibility = dropStatistics.goldDropCount / PROJECTION_STABILIZE_COUNT;
const avgGoldPerDrop = dropStatistics.gold / dropStatistics.goldDropCount;
const dropsPerHour = dropStatistics.goldDropCount / elapsedHours;
goldPerHour = (avgGoldPerDrop * dropsPerHour) * credibility;
} else {
goldPerHour = 0;
}
}
goldContentEl.innerHTML = `<p>每小时: <strong style="color:#00e676;">${formatGold(goldPerHour)}</strong> | 每日: <strong style="color:#00e676;">${formatGold(goldPerHour * 24)}</strong></p>`;
}
const attrTableBody = getEl('attribute-xp-table');
if (attrTableBody) {
const attrs = ['battle', 'strength', 'attacking', 'stamina', 'defencing', 'dexterity', 'intelligence'];
attrTableBody.innerHTML = attrs.map(id => {
const baseData = playerAttributes.get(id);
if (!baseData) return '';
const projectionKeyMap = {
attacking: 'atkPerHour',
defencing: 'defPerHour',
strength: 'strPerHour',
dexterity: 'dexPerHour',
stamina: 'staPerHour',
intelligence: 'intPerHour'
};
const xpKeyMap = {
attacking: 'attackXp',
defencing: 'defenseXp',
strength: 'strengthXp',
dexterity: 'dexterityXp',
stamina: 'staminaXp',
intelligence: 'totalIntelligenceXp'
};
const hourly = projections[projectionKeyMap[id]] || 0;
const gainedXp = xpStatistics[xpKeyMap[id]] || 0;
let currentLevel = baseData.level;
let currentTotalXp = baseData.currentExp + gainedXp;
let xpForNextLevel = SKILL_LEVEL_UP_XP[currentLevel];
while (xpForNextLevel !== undefined && currentTotalXp >= xpForNextLevel) {
currentTotalXp -= xpForNextLevel;
currentLevel++;
xpForNextLevel = SKILL_LEVEL_UP_XP[currentLevel];
}
if (xpForNextLevel === undefined) xpForNextLevel = Infinity;
const xpNeeded = xpForNextLevel - currentTotalXp;
const hoursToNextLvl = hourly > 0 ? xpNeeded / hourly : Infinity;
return `<tr>
<td>${attributeNames[id] || id} (LV.${currentLevel})</td>
<td style="text-align:center;">${Math.floor(currentTotalXp)}/${xpForNextLevel === Infinity ? 'MAX' : xpForNextLevel}</td>
<td style="text-align:right;">${formatDecimal(hourly)}</td>
<td style="text-align:right;">${formatHours(hoursToNextLvl)}</td>
</tr>`;
}).join('');
}
const skxEl = getEl('skill-xp-table');
if (skxEl && playerSkills.size > 0 && earningsStartTime) {
const elapsedHours = (Date.now() - earningsStartTime) / 3600000;
const sortedSkills = Array.from(playerSkills.keys()).sort((a, b) => (xpStatistics.skillCasts[b] || 0) - (xpStatistics.skillCasts[a] || 0));
skxEl.innerHTML = sortedSkills.map(id => {
const baseData = playerSkills.get(id);
if (!baseData) return '';
const casts = xpStatistics.skillCasts[id] || 0;
const gainedXp = casts;
const skillXpHour = elapsedHours > 0 ? gainedXp / elapsedHours : 0;
let currentLevel = baseData.level;
let currentTotalXp = baseData.exp + gainedXp;
let xpForNextLevel = SKILL_LEVEL_UP_XP[currentLevel];
while (xpForNextLevel !== undefined && currentTotalXp >= xpForNextLevel) {
currentTotalXp -= xpForNextLevel;
currentLevel++;
xpForNextLevel = SKILL_LEVEL_UP_XP[currentLevel];
}
if (xpForNextLevel === undefined) xpForNextLevel = Infinity;
const xpNeeded = xpForNextLevel - currentTotalXp;
const hoursToNextLvl = skillXpHour > 0 ? xpNeeded / skillXpHour : Infinity;
return `<tr><td>${skillNames[id] || id} (LV.${currentLevel})</td><td style="text-align:center;">${currentTotalXp}/${xpForNextLevel === Infinity ? 'MAX' : xpForNextLevel}</td><td style="text-align:right;">${formatDecimal(skillXpHour)}</td><td style="text-align:right;">${formatHours(hoursToNextLvl)}</td></tr>`;
}).join('');
} else if (skxEl) {
skxEl.innerHTML = `<tr><td colspan="4" style="text-align:center;color:#888;">暂无数据</td></tr>`;
}
}
function saveAllData() {
const inventoryData = {dropStatistics, saveInventoryEnabled, lastProcessedTimestamp};
const earningsData = {
earningsStartTime,
projections,
xpStatistics,
playerSkills: Array.from(playerSkills.entries()),
playerAttributes: Array.from(playerAttributes.entries())
};
let allData = {};
try {
allData = JSON.parse(localStorage.getItem(LOCAL_STORAGE_NAME)) || {};
} catch (e) {
}
allData[INVENTORY_STORAGE_NAME] = inventoryData;
allData[EARNINGS_STORAGE_NAME] = earningsData;
localStorage.setItem(LOCAL_STORAGE_NAME, JSON.stringify(allData));
}
function loadAllData() {
try {
const allData = JSON.parse(localStorage.getItem(LOCAL_STORAGE_NAME)) || {};
const inv = allData[INVENTORY_STORAGE_NAME];
if (inv) {
dropStatistics = inv.dropStatistics || {gold: 0, goldDropCount: 0};
saveInventoryEnabled = inv.saveInventoryEnabled || false;
lastProcessedTimestamp = inv.lastProcessedTimestamp || null;
}
const earn = allData[EARNINGS_STORAGE_NAME];
if (earn) {
earningsStartTime = earn.earningsStartTime || null;
projections = earn.projections || projections;
xpStatistics = earn.xpStatistics || xpStatistics;
playerSkills = new Map(earn.playerSkills || []);
playerAttributes = new Map(earn.playerAttributes || []);
}
} catch (e) {
console.warn('加载全部数据失败:', e);
}
}
function processDropLogs(logs) {
if (!saveInventoryEnabled || !lastProcessedTimestamp) return;
let newLatestTimestamp = lastProcessedTimestamp;
let newDropsCount = 0;
let goldInThisBatch = 0;
for (const log of logs) {
if (log.date > lastProcessedTimestamp) {
newDropsCount++;
if (log.info.gold) {
goldInThisBatch += log.info.gold.count;
dropStatistics.goldDropCount = (dropStatistics.goldDropCount || 0) + 1;
}
for (const [itemId, itemData] of Object.entries(log.info)) {
if (itemId !== 'gold') {
if (itemData.count > 0) dropStatistics[itemId] = (dropStatistics[itemId] || 0) + itemData.count;
}
}
dropStatistics.gold = (dropStatistics.gold || 0) + (log.info.gold?.count || 0);
if (log.date > newLatestTimestamp) newLatestTimestamp = log.date;
} else {
break;
}
}
if (newDropsCount > 0) {
console.log(`[MoYuHelper] 处理了 ${newDropsCount} 条新的掉落记录。`);
const timeDiffHours = (newLatestTimestamp - lastProcessedTimestamp) / 3600000;
if (timeDiffHours > 0 && goldInThisBatch > 0) {
const currentGoldRate = goldInThisBatch / timeDiffHours;
projections.goldEmaPerHour = (projections.goldEmaPerHour || currentGoldRate) * (1 - EMA_ALPHA) + currentGoldRate * EMA_ALPHA;
}
lastProcessedTimestamp = newLatestTimestamp;
saveAllData();
if (activeTab === 'inventory') updateInventoryPanel();
if (activeTab === 'earnings') updateEarningsPanel();
}
}
function requestAndProcessDropLogs() {
if (!saveInventoryEnabled || !window._moyuHelperWS || !userInfo) return;
const command = "inventory:getModifyInfoByType", successCmd = "inventory:getModifyInfoByType:success",
failCmd = "inventory:getModifyInfoByType:fail";
new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
pendingPromises.delete(successCmd);
reject(new Error(`Request for ${command} timed out.`));
}, ASYNC_TIMEOUT);
pendingPromises.set(successCmd, {
resolve: (res) => {
clearTimeout(timeoutId);
pendingPromises.delete(successCmd);
resolve(res);
}, reject: (err) => {
clearTimeout(timeoutId);
pendingPromises.delete(successCmd);
reject(err);
}, failCmd: failCmd
});
window._moyuHelperWS.send(`42["${command}",{"user":${JSON.stringify(userInfo)},"data":{"type":"Battle"}}]`);
}).then(rawData => {
if (rawData && rawData.data) processDropLogs(rawData.data);
}).catch(error => console.error('[MoYuHelper] 异步获取掉落日志失败:', error));
}
function requestPlayerSkills() {
if (!window._moyuHelperWS || !userInfo) return;
const command = "character:getSkills", successCmd = "character:getSkills:success",
failCmd = "character:getSkills:fail";
new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
pendingPromises.delete(successCmd);
reject(new Error(`Request for ${command} timed out.`));
}, ASYNC_TIMEOUT);
pendingPromises.set(successCmd, {
resolve: (res) => {
clearTimeout(timeoutId);
pendingPromises.delete(successCmd);
resolve(res);
}, reject: (err) => {
clearTimeout(timeoutId);
pendingPromises.delete(successCmd);
reject(err);
}, failCmd: failCmd
});
window._moyuHelperWS.send(`42["${command}",{"user":${JSON.stringify(userInfo)},"data":{}}]`);
}).then(rawData => {
if (rawData && rawData.data) {
console.log(`[MoYuHelper] 技能数据已同步。`);
playerSkills.clear();
rawData.data.forEach(skill => playerSkills.set(skill.skillId, {level: skill.level, exp: skill.exp}));
saveAllData();
if (activeTab === 'earnings') updateEarningsPanel();
}
}).catch(error => console.error('[MoYuHelper] 异步获取玩家技能失败:', error));
}
// --- END: 数据处理与统计模块 ---
// ===== START: 卡房处理与手动操作核心逻辑 =====
function forceStopBattleAction() {
if (!userInfo || !window.currentRoomInfo) {
alert('错误:无法执行操作。未获取到用户信息或当前不在任何房间内。');
return;
}
if (!window._moyuHelperWS) {
alert('错误:WebSocket未连接。');
return;
}
if (confirm("确定要强制停止当前战斗吗?\n将在3秒后发送指令。")) {
console.log("[MoYuHelper] 指令已计划:强制停战将在3秒后执行。");
setTimeout(() => {
const roomId = window.currentRoomInfo.uuid;
const stopBattleCommand = `42["battle:stopBattle",{"user":${JSON.stringify(userInfo)},"data":{"roomId":"${roomId}"}}]`;
window._moyuHelperWS.send(stopBattleCommand);
console.log("[MoYuHelper] 强制停战指令已发送。");
}, 3000);
}
}
function forceLeaveRoomAction() {
if (!userInfo || !window.currentRoomInfo) {
alert('错误:无法执行操作。未获取到用户信息或当前不在任何房间内。');
return;
}
if (!window._moyuHelperWS) {
alert('错误:WebSocket未连接。');
return;
}
if (confirm("确定要强制停止战斗并退出牢房吗?\n此操作无法撤销,将在3秒后启动。")) {
console.log("[MoYuHelper] 指令已计划:强制越狱将在3秒后执行。");
setTimeout(() => {
const roomId = window.currentRoomInfo.uuid;
const stopBattleCommand = `42["battle:stopBattle",{"user":${JSON.stringify(userInfo)},"data":{"roomId":"${roomId}"}}]`;
window._moyuHelperWS.send(stopBattleCommand);
setTimeout(() => {
const leaveRoomCommand = `42["battleRoom:leave",{"user":${JSON.stringify(userInfo)},"data":null}]`;
window._moyuHelperWS.send(leaveRoomCommand);
}, 1500);
}, 3000);
}
}
// ===== 核心消息处理与分发 =====
function handleRoomUpdate(command, rawData) {
if (command.endsWith(':leave:success') || (command === 'battleRoom:getCurrentRoom:success' && !rawData.data)) {
window.currentRoomInfo = null;
} else if (rawData.data) {
if (command === 'battleRoom:getCurrentRoom:success') {
window.currentRoomInfo = rawData.data;
} else if (Array.isArray(rawData.data)) {
if (userInfo) {
const room = rawData.data.find(r => r.memberIds && r.memberIds.includes(userInfo.uuid));
if (room) window.currentRoomInfo = room;
}
} else if (typeof rawData.data === 'object' && rawData.data.uuid) {
window.currentRoomInfo = rawData.data;
}
if (window.currentRoomInfo && enableDebugLogging) {
console.log(`[MoYuHelper] 房间信息已更新 (${command})`);
}
}
if (activeTab === 'room') renderContent();
}
function processCommand(command, rawData) {
if (!command || !rawData) return;
if (pendingPromises.has(command)) {
pendingPromises.get(command).resolve(rawData);
return;
} else {
for (const [key, handlers] of pendingPromises.entries()) {
if (handlers.failCmd === command) {
handlers.reject(new Error(rawData.message || `操作失败: ${command}`));
return;
}
}
}
if ((!userInfo || !userInfo.uuid) && rawData.user && rawData.user.uuid) {
userInfo = rawData.user;
console.log(`%c[MoYuHelper] 用户信息已成功捕获: ${userInfo.name} (ID: ${userInfo.uuid})`, 'color: #00e676; font-weight: bold;');
requestPlayerSkills();
}
if (command === 'dispatchCharacterStatusInfo' && rawData.data) {
console.log(`[MoYuHelper] 角色属性数据已同步。`);
Object.entries(rawData.data).forEach(([attrId, attrData]) => {
if (attributeNames[attrId]) {
playerAttributes.set(attrId, {level: attrData.level, currentExp: attrData.currentExp});
}
});
xpStatistics.strengthXp = 0;
xpStatistics.dexterityXp = 0;
xpStatistics.attackXp = 0;
xpStatistics.staminaXp = 0;
xpStatistics.defenseXp = 0;
xpStatistics.totalIntelligenceXp = 0;
if (saveInventoryEnabled) earningsStartTime = Date.now();
saveAllData();
if (activeTab === 'earnings') updateEarningsPanel();
}
if (command.startsWith('battleRoom:')) {
handleRoomUpdate(command, rawData);
if (command === 'battleRoom:startBattle:success') {
requestAndProcessDropLogs();
}
} else if (command.startsWith('battle:fullInfo:success')) {
const d = rawData.data;
if (d && d.battleInfo && d.thisRoundAction) {
const b = d.battleInfo, a = d.thisRoundAction, m = b.members || [];
const p = (b.groups?.player) || [], mm = Object.fromEntries(m.map(x => [x.uuid, x]));
const su = a.sourceUnitUuid, dg = a.damage || {}, hg = a.heal || {};
if (p.includes(su)) {
let ha = false;
for (const v of Object.values(dg)) {
damageAccum.set(su, (damageAccum.get(su) || 0) + Math.floor(v.damage));
ha = true
}
for (const v of Object.values(hg)) {
healAccum.set(su, (healAccum.get(su) || 0) + Math.floor(v));
ha = true
}
if (ha) actionCount.set(su, (actionCount.get(su) || 0) + 1)
}
if (saveInventoryEnabled && userInfo && earningsStartTime) {
const playerUuid = userInfo.uuid;
const alliesUuids = b.groups.player || [];
const summonOwnerMap = new Map();
m.forEach(member => {
if (member.summonerUuid) summonOwnerMap.set(member.uuid, member.summonerUuid);
});
if (a.castSkillId) {
const sourceIsPlayer = su === playerUuid;
const sourceIsPlayerSummon = summonOwnerMap.get(su) === playerUuid;
if (sourceIsPlayer || sourceIsPlayerSummon) {
xpStatistics.skillCasts[a.castSkillId] = (xpStatistics.skillCasts[a.castSkillId] || 0) + 1;
xpStatistics.totalIntelligenceXp += 0.25;
}
}
for (const [targetUuid, damage] of Object.entries(dg)) {
const damageValue = damage.damage
if (damageValue <= 0) continue;
const sourceIsPlayer = su === playerUuid,
sourceIsAlly = alliesUuids.includes(su) && !sourceIsPlayer,
sourceIsPlayerSummon = summonOwnerMap.get(su) === playerUuid;
if (sourceIsPlayer) {
xpStatistics.strengthXp += damageValue * 0.01;
xpStatistics.dexterityXp += damageValue * 0.005;
xpStatistics.attackXp += damageValue * 0.01;
} else if (sourceIsPlayerSummon) {
xpStatistics.strengthXp += damageValue * 0.01 * 0.6;
xpStatistics.dexterityXp += damageValue * 0.005 * 0.6;
xpStatistics.attackXp += damageValue * 0.01 * 0.6;
} else if (sourceIsAlly) {
xpStatistics.strengthXp += damageValue * 0.01 * 0.3;
xpStatistics.dexterityXp += damageValue * 0.005 * 0.3;
xpStatistics.attackXp += damageValue * 0.01 * 0.3;
}
if (targetUuid === playerUuid) {
xpStatistics.staminaXp += damageValue * 0.01;
xpStatistics.dexterityXp += damageValue * 0.005;
xpStatistics.defenseXp += damageValue * 0.01;
}
}
const elapsedHours = (Date.now() - earningsStartTime) / 3600000;
if (elapsedHours > 0) {
projections.intPerHour = xpStatistics.totalIntelligenceXp / elapsedHours;
projections.strPerHour = xpStatistics.strengthXp / elapsedHours;
projections.dexPerHour = xpStatistics.dexterityXp / elapsedHours;
projections.atkPerHour = xpStatistics.attackXp / elapsedHours;
projections.staPerHour = xpStatistics.staminaXp / elapsedHours;
projections.defPerHour = xpStatistics.defenseXp / elapsedHours;
}
saveAllData();
if (activeTab === 'earnings') updateEarningsPanel();
}
window.playerUuids = p;
window.memberMap = mm;
updateDpsPanel(p, mm);
let ll = [];
if (p.includes(su) || a.targetUnitUuidList.some(t => p.includes(t))) {
const sn = mm[su]?.name || su, snm = skillNames[a.castSkillId || '?'] || a.castSkillId;
for (const tu of a.targetUnitUuidList) {
const tn = mm[tu]?.name || tu, dmg = Math.floor(dg[tu].damage || 0), h = Math.floor(hg[tu] || 0);
if (dmg > 0) ll.push(`🗡️ <b>${sn}</b> 用 <b>${snm}</b> 对 <b>${tn}</b> 造成 <span style='color:#ff7675;'>${dmg}</span> 伤害`);
if (h > 0) ll.push(`💚 <b>${sn}</b> 用 <b>${snm}</b> 治疗 <b>${tn}</b> <span style='color:#00e676;'>${h}</span> 生命`);
}
}
if (ll.length) addBattleLog(ll);
}
}
}
console.log('✅ 摸鱼放置助手 : 原型链拦截器已成功部署。');
// ===== UI 初始化与事件绑定 =====
function makeDraggable(handle, el) {
let isDown = false, offsetX = 0, offsetY = 0;
handle.addEventListener('mousedown', e => {
isDown = true;
const rect = el.getBoundingClientRect();
offsetX = e.clientX - rect.left;
offsetY = e.clientY - rect.top;
el.style.right = 'auto';
el.style.bottom = 'auto';
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
e.preventDefault();
});
function onMouseMove(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`;
}
function onMouseUp() {
isDown = false;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
}
}
function initializeHelperUI() {
panel = document.createElement('div');
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: 9999; font-size: 14px; user-select: none; transition: all 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>`;
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: 'earnings',
label: '个人收益'
}, {key: 'room', label: '房间信息'}, {key: 'settings', label: '设置'}];
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;
});
content = document.createElement('div');
content.style.cssText = `flex: 1; padding: 12px; overflow-y: auto; overflow-x: hidden; min-height: 0;`;
function toggleCollapse() {
isCollapsed = !isCollapsed;
content.style.display = isCollapsed ? 'none' : 'block';
tabBar.style.display = isCollapsed ? 'none' : 'flex';
panel.style.height = isCollapsed ? 'auto' : `${panelHeight}px`;
panel.style.minHeight = isCollapsed ? '40px' : '60px';
const collapseBtn = document.querySelector('#moyu-collapse');
if (collapseBtn) {
collapseBtn.textContent = isCollapsed ? '+' : '−';
}
}
document.addEventListener('click', (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;
const runtimeEl = document.getElementById('moyu-runtime');
if (runtimeEl) {
runtimeEl.textContent = `已运行: ${h > 0 ? `${h}小时` : ''}${m > 0 ? `${m}分` : ''}${s}秒`;
}
}
setInterval(updateRuntime, 1000);
setTimeout(updateRuntime, 100);
makeDraggable(titleBar, panel);
const lpc = document.createElement('style');
lpc.innerHTML = `#battleLog::-webkit-scrollbar{width:8px;background:transparent}#battleLog::-webkit-scrollbar-thumb{background:linear-gradient(120deg,#444 30%,#888 100%);border-radius:6px}#battleLog:hover::-webkit-scrollbar-thumb{background:linear-gradient(120deg,#666 30%,#aaa 100%)}#battleLog{scrollbar-width:thin;scrollbar-color:#888 #222}`;
document.head.appendChild(lpc);
setTimeout(() => {
const lp = document.getElementById('battleLog');
if (lp) {
lp.addEventListener('scroll', () => {
logAutoScroll = lp.scrollTop + lp.clientHeight >= lp.scrollHeight - 5
});
lp.addEventListener('mouseenter', () => {
logAutoScroll = false
});
lp.addEventListener('mouseleave', () => {
logAutoScroll = lp.scrollTop + lp.clientHeight >= lp.scrollHeight - 5
});
}
}, 500);
panel.appendChild(titleBar);
panel.appendChild(tabBar);
panel.appendChild(content);
document.body.appendChild(panel);
loadAllData();
loadPanelSettings();
setActiveTab('combat');
window.MoYuHelperAPI = {
getRoomInfo: () => console.log(window.currentRoomInfo || '当前不在任何房间内'),
getUserInfo: () => console.log(userInfo || '用户信息尚未获取'),
};
}
function attachEventListeners(tab) {
const get = (id) => document.getElementById(id);
switch (tab) {
case 'combat':
if (window.playerUuids && window.memberMap) updateDpsPanel(window.playerUuids, window.memberMap);
break;
case 'inventory':
updateInventoryPanel();
const saveToggle = get('save-inventory-toggle');
if (saveToggle) {
saveToggle.onchange = function () {
const wasEnabled = saveInventoryEnabled;
saveInventoryEnabled = this.checked;
if (saveInventoryEnabled && !wasEnabled) {
if (!lastProcessedTimestamp) {
const now = Date.now();
lastProcessedTimestamp = now;
earningsStartTime = now;
alert('统计已开启!\n将从下一场战斗胜利后开始记录掉落与收益。');
} else {
alert('统计已恢复。');
}
} else if (!saveInventoryEnabled && wasEnabled) {
alert('统计已暂停。');
}
saveAllData();
};
}
get('clear-inventory-btn').onclick = () => {
if (confirm('警告:此操作将清除所有物品掉落及个人收益记录!确定吗?')) {
dropStatistics = {gold: 0, goldDropCount: 0};
lastProcessedTimestamp = null;
xpStatistics = {
strengthXp: 0,
dexterityXp: 0,
attackXp: 0,
staminaXp: 0,
defenseXp: 0,
skillCasts: {},
totalIntelligenceXp: 0
};
earningsStartTime = null;
projections = {
goldEmaPerHour: 0,
strPerHour: 0,
dexPerHour: 0,
atkPerHour: 0,
staPerHour: 0,
defPerHour: 0,
intPerHour: 0
};
playerSkills.clear();
playerAttributes.clear();
let d = {};
try {
d = JSON.parse(localStorage.getItem(LOCAL_STORAGE_NAME)) || {};
} catch (e) {
}
if (d[INVENTORY_STORAGE_NAME]) delete d[INVENTORY_STORAGE_NAME];
if (d[EARNINGS_STORAGE_NAME]) delete d[EARNINGS_STORAGE_NAME];
localStorage.setItem(LOCAL_STORAGE_NAME, JSON.stringify(d));
updateInventoryPanel();
if (activeTab === 'earnings') updateEarningsPanel();
alert('所有统计记录已清除。');
}
};
break;
case 'earnings':
updateEarningsPanel();
get('clear-earnings-btn').onclick = () => {
if (confirm('确定要清除所有个人收益记录吗?(这不会影响物品掉落统计)')) {
xpStatistics = {
strengthXp: 0,
dexterityXp: 0,
attackXp: 0,
staminaXp: 0,
defenseXp: 0,
skillCasts: {},
totalIntelligenceXp: 0
};
earningsStartTime = Date.now();
projections = {
goldEmaPerHour: 0,
strPerHour: 0,
dexPerHour: 0,
atkPerHour: 0,
staPerHour: 0,
defPerHour: 0,
intPerHour: 0
};
saveAllData();
updateEarningsPanel();
alert('个人收益记录已清除。');
}
}
break;
case 'room':
break;
case 'settings':
const debugToggle = get('debug-logging-toggle');
if (debugToggle) {
debugToggle.onchange = function () {
enableDebugLogging = this.checked;
savePanelSettings();
};
}
const widthInput = get('panel-width-input');
const heightInput = get('panel-height-input');
const updatePanelSize = () => {
const newWidth = Math.max(300, Math.min(1000, Number(widthInput.value)));
const newHeight = Math.max(200, Math.min(800, Number(heightInput.value)));
panelWidth = newWidth;
panelHeight = newHeight;
panel.style.width = `${panelWidth}px`;
panel.style.height = `${panelHeight}px`;
savePanelSettings();
};
if (widthInput) widthInput.onchange = updatePanelSize;
if (heightInput) heightInput.onchange = updatePanelSize;
if (tast) {
const stopBtn = get('force-stop-battle-btn');
if (stopBtn) {
stopBtn.onclick = forceStopBattleAction;
}
const leaveBtn = get('force-leave-room-btn');
if (leaveBtn) {
leaveBtn.onclick = forceLeaveRoomAction;
}
}
break;
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeHelperUI);
} else {
initializeHelperUI();
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment