MoYuIdleHelper

摸鱼放置助手 - 全新UI

目前為 2025-06-20 提交的版本,檢視 最新版本

// ==UserScript==
// @name         MoYuIdleHelper
// @namespace    https://tampermonkey.net/
// @version      1.0
// @description  摸鱼放置助手 - 全新UI
// @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';

    let combatLogEnabled = true;

    const damageAccum = new Map();  // uuid → 累计伤害
    const actionCount = new Map();  // uuid → 动作次数
    const healAccum = new Map();    // uuid → 累计治疗

    // 仓库物品变动跟踪
    let inventory = {};  // 当前仓库状态
    let inventoryChanges = new Map();  // 物品变动记录

    // 技能名称映射
    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: "猫王庇护"
    };

    // 物品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": "女皇披风"
        // ...如有遗漏可继续补充
    };

    // 运行时间
    const startTime = Date.now();

    // 创建主面板
    const panel = document.createElement('div');
    panel.id = 'moyu-helper-panel';
    panel.style.cssText = `
        position: fixed;
        top: 40px;
        left: 40px;
        min-width: 320px;
        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;
    `;

    // 标题栏
    const titleBar = document.createElement('div');
    titleBar.style.cssText = `
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 10px 16px 8px 16px;
        border-bottom: 1px solid #444;
        cursor: move;
        font-weight: bold;
        background: transparent;
    `;
    titleBar.innerHTML = `
        <span>摸鱼放置助手 v1.0</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;
        gap: 2px;
        padding: 0 10px;
        border-bottom: 1px solid #333;
        background: transparent;
    `;
    const tabs = [
        { key: 'combat', label: '战斗统计' },
        { key: 'inventory', 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 = `
        padding: 12px 16px 16px 16px;
        min-height: 120px;
        max-height: 60vh;
        overflow-y: auto;
        font-size: 14px;
    `;

    // 折叠逻辑
    let isCollapsed = false;
    const collapseBtn = titleBar.querySelector('#moyu-collapse');
    collapseBtn.addEventListener('click', toggleCollapse);
    function toggleCollapse() {
        isCollapsed = !isCollapsed;
        content.style.display = isCollapsed ? 'none' : '';
        tabBar.style.display = isCollapsed ? 'none' : 'flex';
        collapseBtn.textContent = isCollapsed ? '+' : '−';
    }

    // 运行时间刷新
    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;
            el.style.left = (e.clientX - offsetX) + 'px';
            el.style.top = (e.clientY - offsetY) + '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);
        }
    }

    // 内容渲染
    function renderContent() {
        if (activeTab === 'combat') {
            content.innerHTML = renderCombatPanel();
        } else if (activeTab === 'inventory') {
            content.innerHTML = renderInventoryPanel();
        }
    }

    // 战斗统计内容(可根据实际数据结构填充)
    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>输出效率</th>
                        <th>治疗效率</th>
                        <th>总伤害</th>
                        <th>总治疗</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:120px;overflow-y:auto;padding-left:18px;font-size:13px;"></ul>
            </div>
        `;
    }

    // 物品统计内容(可根据实际数据结构填充)
    function renderInventoryPanel() {
        return `
            <table style="width:100%;border-collapse:collapse;font-size:13px;">
                <thead>
                    <tr style="color:#ffd700;">
                        <th style="text-align:left;">物品</th>
                        <th>数量</th>
                    </tr>
                </thead>
                <tbody id="inventoryTableBody">
                    <!-- 动态填充 -->
                </tbody>
            </table>
        `;
    }

    // 组装面板
    panel.appendChild(titleBar);
    panel.appendChild(tabBar);
    panel.appendChild(content);
    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 拦截与解压
    const NativeWS = window.WebSocket;
    window.WebSocket = function (url, protocols) {
        const ws = protocols ? new NativeWS(url, protocols) : new NativeWS(url);

        const originalSend = ws.send;
        ws.send = function (data) {
            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;
                            console.log(e)
                        }
                    }
                    return;
                }
                console.log(`[WS 未处理数据]`, text);
            }
            // 根据 lastCmd 分发处理
            if (obj && lastCmd) {
                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);
                        }
                        // 战斗日志输出
                        if (combatLogEnabled) {
                            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 {
                    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];
        }
    });

    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) {
        const ul = document.getElementById('battleLog');
        if (!ul) return;
        for (const line of lines) {
            logList.push(line);
        }
        // 限制日志条数
        while (logList.length > 200) logList.shift();
        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;
        }
        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);
        }
    }
})();