您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
猫猫放置-详细战斗日志面板,点击上方中间的按钮展开或者收起
// ==UserScript== // @name 猫猫放置-详细战斗日志面板 // @version v1.45 // @description 猫猫放置-详细战斗日志面板,点击上方中间的按钮展开或者收起 // @author YuoHira // @license MIT // @match https://www.moyu-idle.com/* // @match https://moyu-idle.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=moyu-idle.com // @grant none // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/pako.min.js // @namespace https://greasyfork.org/users/397156 // ==/UserScript== (function() { 'use strict'; // —— 配置变量 —— let isPanelExpanded = true; // 面板展开状态 let panelScale = 100; // 面板缩放百分比 let enableCurrentActionLog = false; // 是否在控制台记录当前回合战斗信息 let hideZeroDamageSkills = true; // 是否屏蔽无伤害技能 // —— 统计数据 —— let battleStartTime = null; // 战斗开始时间 let currentBattleInfo = null; // 当前战斗信息 let playerStats = {}; // 玩家统计数据 let updateTimeout = null; // 更新防抖定时器 // —— 击杀波次统计 —— let killWaveStats = { totalWaves: 0, // 总击杀波次 totalEnemies: 0, // 总击杀敌人数量 firstKillTime: null, // 第一次击杀时间 lastKillTime: null, // 最后一次击杀时间 currentBattleUuid: null, // 当前战斗UUID currentBattleEnemies: new Set(), // 当前战斗中的敌人UUID集合 currentBattleAllEnemies: new Set() // 当前战斗中所有敌人UUID集合(包括已死亡的) }; // —— 技能ID到中文名称的映射 —— const skillNameMap = { 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: "猫王庇护" }; // 获取技能中文名称 function getSkillDisplayName(skillId) { return skillNameMap[skillId] || skillId; } // 初始化玩家统计数据结构 function initPlayerStats(playerUuid, playerName) { if (!playerStats[playerUuid]) { playerStats[playerUuid] = { name: playerName, totalDamage: 0, totalActions: 0, firstActionTime: null, lastActionTime: null, skills: {} // 技能统计: {skillId: {totalDamage, actionCount, firstTime, lastTime}} }; } } // 更新玩家统计数据 function updatePlayerStats(battleData) { const sourceActor = battleData.action.sourceActor; if (!sourceActor || !sourceActor.isPlayer) return; const now = Date.now(); const playerUuid = sourceActor.uuid; const skillId = battleData.action.skillId || 'baseAttack'; const totalDamage = battleData.action.totalDamage; // 初始化玩家数据 initPlayerStats(playerUuid, sourceActor.name); const playerData = playerStats[playerUuid]; // 更新总体统计 playerData.totalDamage += totalDamage; playerData.totalActions++; if (!playerData.firstActionTime) playerData.firstActionTime = now; playerData.lastActionTime = now; // 更新技能统计 if (!playerData.skills[skillId]) { playerData.skills[skillId] = { totalDamage: 0, actionCount: 0, firstTime: null, lastTime: null }; } const skillData = playerData.skills[skillId]; skillData.totalDamage += totalDamage; skillData.actionCount++; if (!skillData.firstTime) skillData.firstTime = now; skillData.lastTime = now; // 保存统计数据到本地存储 savePlayerStats(); } // 计算DPS function calculateDPS(totalDamage, firstTime, lastTime) { if (!firstTime || !lastTime || firstTime === lastTime) return 0; const timeSpan = (lastTime - firstTime) / 1000; // 转换为秒 return timeSpan > 0 ? (totalDamage / timeSpan) : 0; } // 计算WPH(每小时击杀波次) function calculateWPH() { if (!killWaveStats.firstKillTime || !killWaveStats.lastKillTime || killWaveStats.totalWaves === 0) { return 0; } const timeSpan = (killWaveStats.lastKillTime - killWaveStats.firstKillTime) / 1000 / 3600; // 转换为小时 return timeSpan > 0 ? (killWaveStats.totalWaves / timeSpan) : 0; } // 计算EPH(每小时击杀敌人数) function calculateEPH() { if (!killWaveStats.firstKillTime || !killWaveStats.lastKillTime || killWaveStats.totalEnemies === 0) { return 0; } const timeSpan = (killWaveStats.lastKillTime - killWaveStats.firstKillTime) / 1000 / 3600; // 转换为小时 return timeSpan > 0 ? (killWaveStats.totalEnemies / timeSpan) : 0; } // 格式化运行时间 function formatRunningTime(milliseconds) { const totalSeconds = Math.floor(milliseconds / 1000); const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const seconds = totalSeconds % 60; if (hours > 0) { return `${hours}小时${minutes}分钟`; } else if (minutes > 0) { return `${minutes}分钟${seconds}秒`; } else { return `${seconds}秒`; } } // 检测击杀波次(敌人全部死亡) function detectKillWave(battleData) { const battleUuid = battleData.battleUuid; const allMembers = battleData.allMembers; if (!allMembers || allMembers.length === 0) return false; // 如果是新战斗,重置当前战斗的敌人集合 if (battleUuid !== killWaveStats.currentBattleUuid) { killWaveStats.currentBattleUuid = battleUuid; killWaveStats.currentBattleEnemies.clear(); killWaveStats.currentBattleAllEnemies.clear(); // 初始化敌人集合:遍历所有成员,找出敌人 allMembers.forEach(member => { if (!member.isPlayer) { killWaveStats.currentBattleAllEnemies.add(member.uuid); if (member.hp > 0) { killWaveStats.currentBattleEnemies.add(member.uuid); } } }); return false; // 新战斗开始,不检测击杀 } // 更新当前存活敌人状态 killWaveStats.currentBattleEnemies.clear(); allMembers.forEach(member => { if (!member.isPlayer && member.hp > 0) { killWaveStats.currentBattleEnemies.add(member.uuid); } }); // 检查是否所有敌人都死亡(存活敌人集合为空且全部敌人集合不为空) if (killWaveStats.currentBattleEnemies.size === 0 && killWaveStats.currentBattleAllEnemies.size > 0) { const now = Date.now(); const enemyCount = killWaveStats.currentBattleAllEnemies.size; // 更新击杀波次统计 killWaveStats.totalWaves++; killWaveStats.totalEnemies += enemyCount; killWaveStats.lastKillTime = now; // 如果是第一次击杀,记录开始时间 if (!killWaveStats.firstKillTime) { killWaveStats.firstKillTime = now; } // 获取敌人名称列表用于日志显示 const enemyNames = allMembers .filter(member => !member.isPlayer && killWaveStats.currentBattleAllEnemies.has(member.uuid)) .map(member => member.name); // 保存击杀统计 saveKillWaveStats(); console.log(`⚔️ [击杀统计] 击杀第${killWaveStats.totalWaves}波 | 敌人数量: ${enemyCount} | 敌人: ${enemyNames.join(', ')} | 战斗ID: ${battleUuid}`); // 重置当前战斗统计,为下一波做准备 killWaveStats.currentBattleEnemies.clear(); killWaveStats.currentBattleAllEnemies.clear(); killWaveStats.currentBattleUuid = null; return true; } return false; } // 防抖更新UI function debouncedUpdateUI() { if (updateTimeout) { clearTimeout(updateTimeout); } updateTimeout = setTimeout(() => { updateCurrentActionDisplay(); updatePlayerStatsDisplay(); updateTimeout = null; }, 100); // 100ms防抖延迟 } // —— 本地存储键名 —— const STORAGE_KEYS = { PANEL_EXPANDED: 'messageListener_panelExpanded', PANEL_SCALE: 'messageListener_panelScale', PLAYER_STATS: 'messageListener_playerStats', ENABLE_ACTION_LOG: 'messageListener_enableActionLog', HIDE_ZERO_DAMAGE_SKILLS: 'messageListener_hideZeroDamageSkills', KILL_WAVE_STATS: 'messageListener_killWaveStats', IS_MINIMIZED: 'messageListener_isMinimized' }; // —— 界面状态 —— let isMinimized = false; // —— 加载配置 —— function loadConfig() { const savedExpanded = localStorage.getItem(STORAGE_KEYS.PANEL_EXPANDED); const savedScale = localStorage.getItem(STORAGE_KEYS.PANEL_SCALE); const savedStats = localStorage.getItem(STORAGE_KEYS.PLAYER_STATS); const savedActionLog = localStorage.getItem(STORAGE_KEYS.ENABLE_ACTION_LOG); const savedHideZeroDamage = localStorage.getItem(STORAGE_KEYS.HIDE_ZERO_DAMAGE_SKILLS); const savedKillWaveStats = localStorage.getItem(STORAGE_KEYS.KILL_WAVE_STATS); const savedIsMinimized = localStorage.getItem(STORAGE_KEYS.IS_MINIMIZED); if (savedExpanded !== null) { isPanelExpanded = savedExpanded === 'true'; } if (savedIsMinimized !== null) { isMinimized = savedIsMinimized === 'true'; } if (savedScale !== null) { panelScale = parseInt(savedScale) || 100; } if (savedActionLog !== null) { enableCurrentActionLog = savedActionLog === 'true'; } if (savedHideZeroDamage !== null) { hideZeroDamageSkills = savedHideZeroDamage === 'true'; } if (savedStats) { try { const parsedStats = JSON.parse(savedStats); playerStats = parsedStats || {}; } catch (e) { console.warn('加载统计数据失败:', e); playerStats = {}; } } if (savedKillWaveStats) { try { const parsedKillWaveStats = JSON.parse(savedKillWaveStats); // 重新创建Set对象,因为JSON.parse不会恢复Set killWaveStats = { ...killWaveStats, ...parsedKillWaveStats, currentBattleEnemies: new Set(parsedKillWaveStats.currentBattleEnemies || []), currentBattleAllEnemies: new Set(parsedKillWaveStats.currentBattleAllEnemies || []) }; } catch (e) { console.warn('加载击杀波次统计失败:', e); } } } // —— 保存配置 —— function saveConfig() { localStorage.setItem(STORAGE_KEYS.PANEL_EXPANDED, isPanelExpanded); localStorage.setItem(STORAGE_KEYS.PANEL_SCALE, panelScale); localStorage.setItem(STORAGE_KEYS.ENABLE_ACTION_LOG, enableCurrentActionLog); localStorage.setItem(STORAGE_KEYS.HIDE_ZERO_DAMAGE_SKILLS, hideZeroDamageSkills); localStorage.setItem(STORAGE_KEYS.IS_MINIMIZED, isMinimized); } // —— 保存统计数据 —— function savePlayerStats() { try { localStorage.setItem(STORAGE_KEYS.PLAYER_STATS, JSON.stringify(playerStats)); } catch (e) { console.warn('保存统计数据失败:', e); } } // —— 保存击杀波次统计数据 —— function saveKillWaveStats() { try { // 将Set转换为数组进行保存 const statsToSave = { ...killWaveStats, currentBattleEnemies: Array.from(killWaveStats.currentBattleEnemies), currentBattleAllEnemies: Array.from(killWaveStats.currentBattleAllEnemies) }; localStorage.setItem(STORAGE_KEYS.KILL_WAVE_STATS, JSON.stringify(statsToSave)); } catch (e) { console.warn('保存击杀波次统计失败:', e); } } // —— 辅助:检测压缩格式 —— 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 isBattleMessage(data) { if (typeof data === 'string') { try { return data.includes('"battleInfo"') && data.includes('"thisRoundAction"'); } catch (e) { return false; } } return false; } // —— 分割多个JSON对象 —— function splitMultipleJsonObjects(data) { const jsonObjects = []; let depth = 0; let start = 0; let inString = false; let escapeNext = false; for (let i = 0; i < data.length; i++) { const char = data[i]; if (escapeNext) { escapeNext = false; continue; } if (char === '\\') { escapeNext = true; continue; } if (char === '"') { inString = !inString; continue; } if (!inString) { if (char === '{') { depth++; } else if (char === '}') { depth--; if (depth === 0) { // 找到一个完整的JSON对象 const jsonStr = data.substring(start, i + 1); jsonObjects.push(jsonStr); start = i + 1; } } } } return jsonObjects; } // —— 解析战斗消息 —— function parseBattleMessage(data) { try { if (typeof data === 'string') { // 首先尝试直接解析 let jsonData; try { jsonData = JSON.parse(data); } catch (firstError) { // 如果直接解析失败,尝试分割多个JSON对象 const jsonObjects = splitMultipleJsonObjects(data); // 尝试解析每个JSON对象,找到包含战斗信息的那个 for (const jsonStr of jsonObjects) { try { const parsed = JSON.parse(jsonStr); if (parsed.data && parsed.data.battleInfo && parsed.data.thisRoundAction) { jsonData = parsed; break; } } catch (e) { continue; } } // 如果还是没找到,尝试Socket.IO格式 if (!jsonData) { const match = data.match(/\[.*?({.*})\]/); if (match) { jsonData = JSON.parse(match[1]); } else { throw firstError; // 抛出原始错误 } } } if (jsonData && jsonData.data && jsonData.data.battleInfo && jsonData.data.thisRoundAction) { const battleInfo = jsonData.data.battleInfo; const action = jsonData.data.thisRoundAction; // 找到当前行动的角色 const currentTurnIndex = battleInfo.currentTurnIndex; const turnOrder = battleInfo.turnOrder; const currentActorUuid = turnOrder[currentTurnIndex]; // 找到角色信息 const currentActor = battleInfo.members.find(member => member.uuid === currentActorUuid); const sourceActor = battleInfo.members.find(member => member.uuid === action.sourceUnitUuid); // 解析目标信息 const targets = []; if (action.damage) { Object.keys(action.damage).forEach(targetUuid => { const target = battleInfo.members.find(member => member.uuid === targetUuid); if (target) { targets.push({ name: target.name, damage: action.damage[targetUuid], hp: target.hp, maxHp: target.maxHp, isDead: target.hp === 0 }); } }); } return { currentTurn: currentTurnIndex, currentActor: currentActor ? { name: currentActor.name, uuid: currentActor.uuid, isPlayer: currentActor.isPlayer } : null, action: { type: action.type, sourceActor: sourceActor ? { name: sourceActor.name, uuid: sourceActor.uuid, isPlayer: sourceActor.isPlayer } : null, skillId: action.castSkillId, targets: targets, totalDamage: Object.values(action.damage || {}).reduce((sum, dmg) => sum + dmg, 0), attackCount: action.targetUnitUuidList ? action.targetUnitUuidList.length : 0 }, battleUuid: battleInfo.uuid, // 添加完整的成员信息用于击杀检测 allMembers: battleInfo.members }; } } } catch (e) { console.group('❌ [战斗解析] 解析战斗消息失败'); console.error('错误信息:', e); console.log('原始数据长度:', data ? data.length : 'undefined'); console.log('原始数据类型:', typeof data); // 显示原始数据的前后部分,避免控制台过于拥挤 if (typeof data === 'string') { console.log('数据开头 (前500字符):', data.substring(0, 500)); if (data.length > 1000) { console.log('数据结尾 (后500字符):', data.substring(data.length - 500)); } // 尝试找到JSON解析失败的位置 if (e.message.includes('position')) { const match = e.message.match(/position (\d+)/); if (match) { const pos = parseInt(match[1]); console.log(`错误位置周围的字符 (位置${pos}):`, data.substring(Math.max(0, pos - 50), pos + 50)); console.log(`错误位置的字符:`, `"${data[pos]}" (字符码: ${data.charCodeAt(pos)})`); } } // 尝试分割JSON对象进行调试 try { const jsonObjects = splitMultipleJsonObjects(data); console.log(`检测到 ${jsonObjects.length} 个JSON对象:`); jsonObjects.forEach((obj, index) => { console.log(`JSON对象 ${index + 1} (长度: ${obj.length}):`, obj.substring(0, 200) + (obj.length > 200 ? '...' : '')); }); } catch (splitError) { console.log('分割JSON对象时出错:', splitError); } console.log('完整原始数据:', data); } else { console.log('完整原始数据:', data); } console.groupEnd(); } return null; } // —— 记录战斗消息 —— function logBattleMessage(battleData) { // 检测击杀波次 detectKillWave(battleData); // 更新统计数据 updatePlayerStats(battleData); // 始终更新当前行动信息(面板显示用) currentBattleInfo = battleData; // 防抖更新UI显示 debouncedUpdateUI(); // 只有在开关打开时才输出控制台日志 if (enableCurrentActionLog) { const action = battleData.action; const sourceActor = action.sourceActor; console.group(`⚔️ [战斗记录] 第${battleData.currentTurn + 1}次行动 - ${sourceActor.name}`); // 基本信息 console.log(`🎯 行动者: ${sourceActor.name} (${sourceActor.isPlayer ? '玩家' : '敌人'})`); console.log(`🔥 技能: ${getSkillDisplayName(action.skillId || 'baseAttack')}`); console.log(`💥 总伤害: ${action.totalDamage}点`); console.log(`🎲 攻击次数: ${action.attackCount}次`); // 目标详情 if (action.targets.length > 0) { console.log('🎯 攻击目标:'); action.targets.forEach(target => { const status = target.isDead ? '☠️ 死亡' : `❤️ ${target.hp}/${target.maxHp}`; console.log(` • ${target.name}: ${target.damage}伤害 → ${status}`); }); } console.log(`🕐 时间: ${new Date().toLocaleTimeString()}`); console.log(`🆔 战斗ID: ${battleData.battleUuid}`); console.groupEnd(); } } // —— 初始化配置 —— loadConfig(); // —— 暴露全局控制台命令 —— window.toggleBattlePanel = function() { if (!isPanelExpanded) { isPanelExpanded = true; isMinimized = false; } else if (!isMinimized) { isMinimized = true; } else { isMinimized = false; } saveConfig(); updatePanelLayout(); const status = isPanelExpanded ? (isMinimized ? '收起' : '展开') : '关闭'; console.log(`📋 [控制台命令] 面板状态: ${status}`); return `面板状态: ${status}`; }; window.showBattlePanel = function() { isPanelExpanded = true; isMinimized = false; saveConfig(); updatePanelLayout(); console.log('📋 [控制台命令] 面板已展开'); return '面板已展开'; }; window.hideBattlePanel = function() { isPanelExpanded = false; isMinimized = false; saveConfig(); updatePanelLayout(); console.log('📋 [控制台命令] 面板已关闭'); return '面板已关闭'; }; window.minimizeBattlePanel = function() { isPanelExpanded = true; isMinimized = true; saveConfig(); updatePanelLayout(); console.log('📋 [控制台命令] 面板已收起到EPH横条'); return '面板已收起到EPH横条'; }; window.getBattlePanelStatus = function() { const status = isPanelExpanded ? (isMinimized ? '收起(EPH横条)' : '展开') : '关闭'; console.log(`📋 [控制台命令] 当前面板状态: ${status}`); return status; }; window.clearBattleStats = function() { playerStats = {}; currentBattleInfo = null; battleStartTime = null; // 重置击杀波次统计 killWaveStats = { totalWaves: 0, totalEnemies: 0, firstKillTime: null, lastKillTime: null, currentBattleUuid: null, currentBattleEnemies: new Set(), currentBattleAllEnemies: new Set() }; // 清除本地存储 localStorage.removeItem(STORAGE_KEYS.PLAYER_STATS); localStorage.removeItem(STORAGE_KEYS.KILL_WAVE_STATS); // 立即更新显示 updateCurrentActionDisplay(); updatePlayerStatsDisplay(); console.log('📋 [控制台命令] 统计数据和击杀波次数据已清除'); return '统计数据已清除'; }; // 启动提示 console.log('⚔️ [战斗数据面板] 已启动,自动监听战斗消息并保存统计数据'); console.log(`📊 [击杀统计] 当前数据: 波次=${killWaveStats.totalWaves}, 敌人=${killWaveStats.totalEnemies}, 每小时击杀波次=${calculateWPH().toFixed(1)}, 每小时击杀敌人数=${calculateEPH().toFixed(1)}`); console.log(''); console.log('🎮 [控制台命令] 可用的控制台命令:'); console.log(' toggleBattlePanel() - 切换面板状态 (关闭→展开→收起→展开...)'); console.log(' showBattlePanel() - 完全展开面板'); console.log(' hideBattlePanel() - 完全关闭面板'); console.log(' minimizeBattlePanel() - 收起到EPH横条'); console.log(' getBattlePanelStatus() - 获取当前面板状态'); console.log(' clearBattleStats() - 清除所有统计数据'); console.log(''); // —— 创建固定的展开收起按钮 —— const toggleButton = document.createElement('button'); toggleButton.id = 'battlePanel_fixedToggleButton'; // 根据初始状态设置样式 function setToggleButtonStyle() { if (isPanelExpanded && !isMinimized) { // 展开状态:显示收起按钮 toggleButton.style.cssText = ` position: fixed; top: 10px; left: 50%; transform: translateX(-50%); background: rgba(244,67,54,0.25); border: 2px solid rgb(255, 0, 0); color: rgb(255, 147, 23); padding: 8px 24px; border-radius: 6px; font-size: 12px; cursor: pointer; font-weight: bold; z-index: 99999; backdrop-filter: blur(10px); transition: all 0.2s ease; box-shadow: 0 0 10px rgba(255, 0, 0, 0.3); display: block; `; toggleButton.innerHTML = '📐 收起'; } else { // 关闭状态和收起状态:隐藏按钮,由EPH横条代替 toggleButton.style.display = 'none'; } } setToggleButtonStyle(); document.body.appendChild(toggleButton); // —— 创建收起状态的EPH小横条 —— const minimizedBar = document.createElement('div'); minimizedBar.id = 'battlePanel_minimizedEphBar'; minimizedBar.style.cssText = ` position: fixed; top: 10px; left: 50%; transform: translateX(-50%); background: rgba(25,35,45,0.95); border: 1px solid rgba(255,193,7,0.5); color: #FFC107; padding: 8px 16px; border-radius: 6px; font-size: 11px; font-weight: bold; z-index: 99998; backdrop-filter: blur(10px); transition: all 0.3s ease; box-shadow: 0 4px 15px rgba(0,0,0,0.3); display: ${(!isPanelExpanded || isMinimized) ? 'block' : 'none'}; font-family: 'Consolas', 'Monaco', monospace; `; minimizedBar.innerHTML = ` <div style="display: flex; align-items: center; gap: 8px; cursor: pointer;" title="点击展开面板"> <span>⚔️</span> <span id="battlePanel_ephDisplay">${!isPanelExpanded ? '📊 展开 | EPH: ' + calculateEPH().toFixed(1) : 'EPH: ' + calculateWPH().toFixed(1)}</span> </div> `; // 为EPH横条添加点击事件来展开面板 minimizedBar.addEventListener('click', () => { if (!isPanelExpanded) { // 从关闭状态展开 isPanelExpanded = true; isMinimized = false; console.log('📋 [面板] 通过EPH横条从关闭状态展开面板'); } else if (isMinimized) { // 从收起状态展开 isMinimized = false; console.log('📋 [面板] 通过EPH横条从收起状态展开面板'); } saveConfig(); updatePanelLayout(); }); document.body.appendChild(minimizedBar); // —— 添加滚动条样式 —— const style = document.createElement('style'); style.textContent = ` /* 自定义滚动条样式 - Webkit浏览器 */ .battle-panel-scrollbar::-webkit-scrollbar { width: 6px; } .battle-panel-scrollbar::-webkit-scrollbar-track { background: rgba(255,255,255,0.1); border-radius: 3px; } .battle-panel-scrollbar::-webkit-scrollbar-thumb { background: rgba(100,181,246,0.5); border-radius: 3px; } .battle-panel-scrollbar::-webkit-scrollbar-thumb:hover { background: rgba(100,181,246,0.7); } /* Firefox滚动条样式 */ .battle-panel-scrollbar { scrollbar-width: thin; scrollbar-color: rgba(100,181,246,0.5) rgba(255,255,255,0.1); } /* 技能列表容器过渡效果 */ .skill-list-container { transition: opacity 0.1s ease-out; } /* 防止内容闪烁的样式 */ .skill-list-container.updating { pointer-events: none; } /* EPH横条悬停效果 */ #battlePanel_minimizedEphBar:hover { background: rgba(35,45,55,0.98) !important; border-color: rgba(255,193,7,0.8) !important; box-shadow: 0 6px 20px rgba(255,193,7,0.3) !important; transform: translateX(-50%) scale(1.02); } `; document.head.appendChild(style); // —— 创建战斗面板 —— const panel = document.createElement('div'); panel.id = 'battleLogPanel'; // 添加唯一ID function updatePanelStyle() { const shouldShow = isPanelExpanded && !isMinimized; panel.style.cssText = ` position: fixed; top: ${shouldShow ? '50px' : '-1000px'}; left: 50%; transform: translateX(-50%); width: 80vw; height: 80vh; padding: 12px; background: rgba(15,20,25,0.7); color: #fff; font-family: 'Consolas', 'Monaco', monospace; font-size: 10px; border-radius: 8px; z-index: 99997; border: 1px solid rgba(100,200,255,0.4); box-shadow: 0 10px 40px rgba(0,0,0,0.6); backdrop-filter: blur(10px); transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); overflow: hidden; display: ${shouldShow ? 'block' : 'none'}; `; } updatePanelStyle(); function updatePanelContent() { const scale = (panelScale * 1.7) / 100; // 将170的效果作为100%基准 panel.innerHTML = ` <!-- 展开状态内容 --> <div style="display:flex; flex-direction:column; height:100%; --scale: ${scale};"> <!-- 标题栏和控制区 --> <div style="display:flex; align-items:center; justify-content:space-between; padding:${8*scale}px ${16*scale}px; background:rgba(0,0,0,0.3); margin-bottom:${8*scale}px; border-radius:${6*scale}px;"> <div style="font-size:${16*scale}px; font-weight:bold; color:#64B5F6;"> ⚔️ 战斗数据统计面板 </div> <div style="display:flex; align-items:center; gap:${8*scale}px;"> <label style="color:#aaa; font-size:${9*scale}px;"> <input id="battlePanel_actionLogToggle" type="checkbox" ${enableCurrentActionLog ? 'checked' : ''} style="margin-right:${4*scale}px;"> 控制台日志 </label> <label style="color:#aaa; font-size:${9*scale}px;"> <input id="battlePanel_hideZeroDamageToggle" type="checkbox" ${hideZeroDamageSkills ? 'checked' : ''} style="margin-right:${4*scale}px;"> 屏蔽无伤害技能 </label> <div style="width:1px; height:${16*scale}px; background:rgba(255,255,255,0.2);"></div> <label style="color:#aaa; font-size:${9*scale}px;">缩放:</label> <input id="battlePanel_scaleInput" type="number" value="${panelScale}" min="50" max="200" step="10" style=" width:${50*scale}px; padding:${2*scale}px ${4*scale}px; border:1px solid #64B5F6; border-radius:${3*scale}px; background:rgba(0,0,0,0.3); color:#fff; font-size:${9*scale}px; text-align:center; "> <span style="color:#aaa; font-size:${9*scale}px;">%</span> <button id="battlePanel_minimizeBtn" style=" background:rgba(255,193,7,0.15); border:1px solid #FFC107; color:#FFC107; padding:${4*scale}px ${8*scale}px; border-radius:${4*scale}px; font-size:${9*scale}px; cursor:pointer; margin-right:${4*scale}px; ">📌 收起</button> <button id="battlePanel_clearStats" style=" background:rgba(244,67,54,0.15); border:1px solid #f44336; color:#f44336; padding:${4*scale}px ${8*scale}px; border-radius:${4*scale}px; font-size:${9*scale}px; cursor:pointer; ">🗑️ 清除</button> </div> </div> <!-- 主内容区域 --> <div style="display:flex; flex:1; gap:${8*scale}px; overflow:hidden;"> <!-- 左侧:玩家统计面板 (4/5宽度) --> <div id="battlePanel_playerStatsPanel" style=" width:80%; height:100%; background:linear-gradient(135deg, rgba(76,175,80,0.1), rgba(139,195,74,0.05)); border-radius:${8*scale}px; border:1px solid rgba(76,175,80,0.2); padding:${12*scale}px; overflow:hidden; display:none; "> <div style="font-size:${12*scale}px; color:#4CAF50; margin-bottom:${8*scale}px; font-weight:bold; text-align:center;"> 📊 玩家伤害统计数据 </div> <div id="battlePanel_killWaveStatsBar" style=" display:flex; justify-content:center; align-items:center; gap:${8*scale}px; background:rgba(255,193,7,0.1); border:1px solid rgba(255,193,7,0.3); border-radius:${6*scale}px; padding:${6*scale}px; margin-bottom:${8*scale}px; font-size:${9*scale}px; "> <div style="color:#FFC107; font-weight:bold;"> ⚔️ 击杀波次: <span id="battlePanel_totalWaves">${killWaveStats.totalWaves}</span> </div> <div style="color:#FFC107; font-weight:bold;"> 👹 总敌人数: <span id="battlePanel_totalEnemies">${killWaveStats.totalEnemies}</span> </div> <div style="color:#FFC107; font-weight:bold;"> 📊 每小时击杀波次: <span id="battlePanel_wphValue">${calculateWPH().toFixed(1)}</span> </div> <div style="color:#FFC107; font-weight:bold;"> 🎯 每小时击杀敌人数: <span id="battlePanel_ephValue">${calculateEPH().toFixed(1)}</span> </div> <div style="color:#FFC107; font-weight:bold;"> ⏱️ 运行时间: <span id="battlePanel_runningTime">${killWaveStats.firstKillTime ? formatRunningTime(Date.now() - killWaveStats.firstKillTime) : '0分钟'}</span> </div> </div> <div id="battlePanel_playerStatsContent" style=" display:flex; gap:${8*scale}px; overflow-x:auto; overflow-y:hidden; height:calc(100% - ${30*scale}px); padding:${4*scale}px 0; align-items:stretch; "></div> </div> <!-- 右侧:当前出手信息 (1/5宽度) --> <div id="battlePanel_currentActionPanel" style=" width:20%; height:100%; background:linear-gradient(135deg, rgba(100,181,246,0.1), rgba(33,150,243,0.05)); border-radius:${8*scale}px; border:1px solid rgba(100,181,246,0.2); padding:${12*scale}px; overflow-y:auto; display:none; "> <div style="font-size:${11*scale}px; color:#64B5F6; margin-bottom:${8*scale}px; font-weight:bold; text-align:center;"> 🎯 当前行动 </div> <div id="battlePanel_currentActionContent" style="font-size:${9*scale}px; line-height:1.4;"></div> </div> </div> </div> `; } updatePanelContent(); document.body.appendChild(panel); // —— 获取控制元素 —— function getElements() { return { toggleButton: document.getElementById('battlePanel_fixedToggleButton'), actionLogToggle: document.getElementById('battlePanel_actionLogToggle'), hideZeroDamageToggle: document.getElementById('battlePanel_hideZeroDamageToggle'), scaleInput: document.getElementById('battlePanel_scaleInput'), currentActionPanel: document.getElementById('battlePanel_currentActionPanel'), currentActionContent: document.getElementById('battlePanel_currentActionContent'), playerStatsPanel: document.getElementById('battlePanel_playerStatsPanel'), playerStatsContent: document.getElementById('battlePanel_playerStatsContent'), clearStats: document.getElementById('battlePanel_clearStats'), minimizeBtn: document.getElementById('battlePanel_minimizeBtn') }; } // —— 收起/展开功能 —— function toggleMinimize() { isMinimized = !isMinimized; saveConfig(); updatePanelLayout(); updateMinimizedBar(); } // —— 更新EPH横条 —— function updateMinimizedBar() { const minimizedBar = document.getElementById('battlePanel_minimizedEphBar'); const ephDisplay = document.getElementById('battlePanel_ephDisplay'); if (minimizedBar) { const shouldShow = !isPanelExpanded || isMinimized; minimizedBar.style.display = shouldShow ? 'block' : 'none'; if (shouldShow && ephDisplay) { // 根据状态显示不同文本 if (!isPanelExpanded) { ephDisplay.textContent = `📊 展开 | EPH: ${calculateEPH().toFixed(1)}`; } else { // 收起状态:显示EPH标签,但数值使用WPH(每小时击杀波次) ephDisplay.textContent = `EPH: ${calculateWPH().toFixed(1)}`; } } } } // —— 面板展开/收起功能 —— function updatePanelLayout() { // 更新面板显示状态 updatePanelStyle(); // 更新按钮样式 setToggleButtonStyle(); // 更新收起横条 updateMinimizedBar(); // 重新绑定事件 bindEvents(); // 更新显示 updateCurrentActionDisplay(); updatePlayerStatsDisplay(); } // —— 更新面板内容和缩放 —— function updatePanelContentAndScale() { updatePanelContent(); bindEvents(); updateCurrentActionDisplay(); updatePlayerStatsDisplay(); } // —— 事件绑定函数 —— function bindEvents() { const elements = getElements(); // 面板展开/收起 - 固定按钮 if (elements.toggleButton) { elements.toggleButton.removeEventListener('click', togglePanelHandler); elements.toggleButton.addEventListener('click', togglePanelHandler); } // 当前行动记录开关 if (elements.actionLogToggle) { elements.actionLogToggle.removeEventListener('change', actionLogToggleHandler); elements.actionLogToggle.addEventListener('change', actionLogToggleHandler); } // 屏蔽无伤害技能开关 if (elements.hideZeroDamageToggle) { elements.hideZeroDamageToggle.removeEventListener('change', hideZeroDamageToggleHandler); elements.hideZeroDamageToggle.addEventListener('change', hideZeroDamageToggleHandler); } // 缩放输入框 if (elements.scaleInput) { elements.scaleInput.removeEventListener('input', scaleInputHandler); elements.scaleInput.addEventListener('input', scaleInputHandler); } // 清除统计数据 if (elements.clearStats) { elements.clearStats.removeEventListener('click', clearStatsHandler); elements.clearStats.addEventListener('click', clearStatsHandler); } // 收起按钮 if (elements.minimizeBtn) { elements.minimizeBtn.removeEventListener('click', minimizeBtnHandler); elements.minimizeBtn.addEventListener('click', minimizeBtnHandler); } } // 事件处理函数 function togglePanelHandler() { // 顶部红色收起按钮只负责收起到小横条 if (isPanelExpanded && !isMinimized) { isMinimized = true; saveConfig(); updatePanelLayout(); console.log('📋 [面板] 面板已收起到EPH横条'); } } function actionLogToggleHandler(e) { enableCurrentActionLog = e.target.checked; saveConfig(); console.log(`📋 [面板] 控制台战斗日志已${enableCurrentActionLog ? '开启' : '关闭'}`); } function hideZeroDamageToggleHandler(e) { hideZeroDamageSkills = e.target.checked; saveConfig(); updatePlayerStatsDisplay(); // 立即更新显示 console.log(`📋 [面板] 屏蔽无伤害技能已${hideZeroDamageSkills ? '开启' : '关闭'}`); } function scaleInputHandler(e) { const value = parseInt(e.target.value); if (value >= 50 && value <= 200) { panelScale = value; saveConfig(); updatePanelContentAndScale(); console.log(`📋 [面板] 缩放调整为 ${panelScale}%`); } } function clearStatsHandler() { if (confirm('确定要清除所有统计数据吗?这将删除所有保存的战斗数据和遇敌统计!')) { playerStats = {}; currentBattleInfo = null; battleStartTime = null; // 重置击杀波次统计 killWaveStats = { totalWaves: 0, totalEnemies: 0, firstKillTime: null, lastKillTime: null, currentBattleUuid: null, currentBattleEnemies: new Set(), currentBattleAllEnemies: new Set() }; // 清除本地存储 localStorage.removeItem(STORAGE_KEYS.PLAYER_STATS); localStorage.removeItem(STORAGE_KEYS.KILL_WAVE_STATS); // 立即更新显示 updateCurrentActionDisplay(); updatePlayerStatsDisplay(); console.log('📊 [统计] 统计数据和击杀波次数据已清除'); } } function minimizeBtnHandler() { toggleMinimize(); console.log(`📋 [面板] 面板已${isMinimized ? '收起' : '展开'}`); } // 初始绑定事件 bindEvents(); // 初始化状态显示 setTimeout(() => { updateStatusDisplay(); updateCurrentActionDisplay(); updatePlayerStatsDisplay(); updateMinimizedBar(); // 初始化收起横条状态 // 如果有保存的数据,显示统计面板 const elements = getElements(); if ((Object.keys(playerStats).length > 0 || killWaveStats.totalWaves > 0) && elements.playerStatsPanel) { elements.playerStatsPanel.style.display = 'block'; } }, 100); // —— 更新状态显示 —— function updateStatusDisplay() { // 收起状态下不需要状态显示 } // —— 更新当前出手信息显示 —— function updateCurrentActionDisplay() { const elements = getElements(); if (!currentBattleInfo || !elements.currentActionPanel || !elements.currentActionContent) { if (elements.currentActionPanel) { elements.currentActionPanel.style.display = 'none'; } return; } elements.currentActionPanel.style.display = 'block'; const action = currentBattleInfo.action; const sourceActor = action.sourceActor; const scale = (panelScale * 1.7) / 100; let html = ` <div style="color:#64B5F6; font-weight:bold; margin-bottom:${6*scale}px; font-size:${10*scale}px; text-align:center;"> 第${currentBattleInfo.currentTurn + 1}次 - ${sourceActor.name} ${sourceActor.isPlayer ? '👤' : '👹'} </div> <div style="margin-bottom:${6*scale}px;"> <div style="background:rgba(255,255,255,0.05); padding:${4*scale}px; border-radius:${3*scale}px; margin-bottom:${2*scale}px;"> <div style="color:#aaa; font-size:${7*scale}px;">技能</div> <div style="color:#64B5F6; font-weight:bold; font-size:${8*scale}px;">${getSkillDisplayName(action.skillId || 'baseAttack')}</div> </div> <div style="background:rgba(255,255,255,0.05); padding:${4*scale}px; border-radius:${3*scale}px; margin-bottom:${2*scale}px;"> <div style="color:#aaa; font-size:${7*scale}px;">总伤害</div> <div style="color:#f44336; font-weight:bold; font-size:${8*scale}px;">${action.totalDamage.toLocaleString()}</div> </div> <div style="background:rgba(255,255,255,0.05); padding:${4*scale}px; border-radius:${3*scale}px; margin-bottom:${2*scale}px;"> <div style="color:#aaa; font-size:${7*scale}px;">攻击次数</div> <div style="color:#FF9800; font-weight:bold; font-size:${8*scale}px;">${action.attackCount}</div> </div> <div style="background:rgba(255,255,255,0.05); padding:${4*scale}px; border-radius:${3*scale}px;"> <div style="color:#aaa; font-size:${7*scale}px;">时间</div> <div style="color:#4CAF50; font-weight:bold; font-size:${8*scale}px;">${new Date().toLocaleTimeString()}</div> </div> </div> `; if (action.targets.length > 0) { html += `<div style="color:#64B5F6; font-size:${9*scale}px; margin-bottom:${4*scale}px; font-weight:bold; text-align:center;">🎯 目标</div>`; action.targets.forEach(target => { const hpPercent = Math.round((target.hp / target.maxHp) * 100); const statusColor = target.isDead ? '#9E9E9E' : (hpPercent < 20 ? '#f44336' : (hpPercent < 50 ? '#FF9800' : '#4CAF50')); html += ` <div style=" background:rgba(255,255,255,0.05); padding:${4*scale}px; border-radius:${3*scale}px; margin-bottom:${3*scale}px; border-left:${2*scale}px solid ${statusColor}; "> <div style="color:${statusColor}; font-weight:bold; font-size:${8*scale}px; ${target.isDead ? 'text-decoration: line-through;' : ''}">${target.name}</div> <div style="color:#f44336; font-size:${7*scale}px;">${target.damage.toLocaleString()} 伤害</div> <div style="color:${statusColor}; font-size:${7*scale}px;"> ${target.isDead ? '☠️ 死亡' : `❤️ ${target.hp}/${target.maxHp} (${hpPercent}%)`} </div> </div> `; }); } elements.currentActionContent.innerHTML = html; } // —— 保存滚动位置 —— function saveScrollPositions() { const scrollPositions = {}; const skillContainers = document.querySelectorAll('.skill-list-container'); skillContainers.forEach(container => { const playerUuid = container.getAttribute('data-player-uuid'); if (playerUuid) { scrollPositions[playerUuid] = container.scrollTop; } }); return scrollPositions; } // —— 恢复滚动位置 ——(已弃用,改为同步更新) // function restoreScrollPositions - 已移除,现在使用同步方式更新滚动位置 // —— 更新玩家统计显示 —— function updatePlayerStatsDisplay() { const elements = getElements(); const playerCount = Object.keys(playerStats).length; const hasKillStats = killWaveStats.totalWaves > 0; if (playerCount === 0 && !hasKillStats) { if (elements.playerStatsPanel) { elements.playerStatsPanel.style.display = 'none'; } return; } if (!elements.playerStatsPanel || !elements.playerStatsContent) { return; } // 保存当前滚动位置 const scrollPositions = saveScrollPositions(); // 添加更新状态标记,防止用户交互 const existingContainers = document.querySelectorAll('.skill-list-container'); existingContainers.forEach(container => { container.classList.add('updating'); }); elements.playerStatsPanel.style.display = 'block'; const scale = (panelScale * 1.7) / 100; let html = ''; // 按DPS从高到低排序玩家 const sortedPlayersWithUuid = Object.entries(playerStats).map(([uuid, player]) => ({ uuid, ...player })).sort((a, b) => { const aDPS = calculateDPS(a.totalDamage, a.firstActionTime, a.lastActionTime); const bDPS = calculateDPS(b.totalDamage, b.firstActionTime, b.lastActionTime); return bDPS - aDPS; // 从高到低排序 }); sortedPlayersWithUuid.forEach(player => { const avgDamage = player.totalActions > 0 ? Math.round(player.totalDamage / player.totalActions) : 0; const dps = calculateDPS(player.totalDamage, player.firstActionTime, player.lastActionTime); html += ` <div style=" width:${140*scale}px; min-width:${140*scale}px; max-width:${140*scale}px; flex-shrink:0; background:linear-gradient(135deg, rgba(0,0,0,0.3), rgba(100,181,246,0.05)); border-radius:${6*scale}px; padding:${8*scale}px; border:1px solid rgba(100,181,246,0.3); height:100%; box-shadow: 0 ${2*scale}px ${8*scale}px rgba(0,0,0,0.2); display:flex; flex-direction:column; "> <div style="color:#64B5F6; font-weight:bold; margin-bottom:${6*scale}px; font-size:${10*scale}px; text-align:center;"> 👤 ${player.name} </div> <!-- 总体数据 --> <div style="display:grid; grid-template-columns: 1fr 1fr 1fr; gap:${4*scale}px; margin-bottom:${6*scale}px; font-size:${8*scale}px;"> <div style="text-align:center; background:rgba(244,67,54,0.15); padding:${3*scale}px; border-radius:${3*scale}px; border:1px solid rgba(244,67,54,0.3);"> <div style="color:#f44336; font-weight:bold; font-size:${8*scale}px;">${player.totalDamage.toLocaleString()}</div> <div style="color:#ccc; font-size:${6*scale}px;">总伤害</div> </div> <div style="text-align:center; background:rgba(255,152,0,0.15); padding:${3*scale}px; border-radius:${3*scale}px; border:1px solid rgba(255,152,0,0.3);"> <div style="color:#FF9800; font-weight:bold; font-size:${8*scale}px;">${avgDamage.toLocaleString()}</div> <div style="color:#ccc; font-size:${6*scale}px;">平均</div> </div> <div style="text-align:center; background:rgba(76,175,80,0.15); padding:${3*scale}px; border-radius:${3*scale}px; border:1px solid rgba(76,175,80,0.3);"> <div style="color:#4CAF50; font-weight:bold; font-size:${8*scale}px;">${dps.toFixed(1)}</div> <div style="color:#ccc; font-size:${6*scale}px;">DPS</div> </div> </div> <!-- 技能数据 --> <div style="border-top:1px solid rgba(255,255,255,0.1); margin-top:${6*scale}px; padding-top:${6*scale}px; flex:1; min-height:0; display:flex; flex-direction:column;"> <div style="color:#64B5F6; font-size:${8*scale}px; margin-bottom:${4*scale}px; font-weight:bold; text-align:center;">🗡️ 技能统计</div> <div class="battle-panel-scrollbar skill-list-container" data-player-uuid="${player.uuid}" style=" flex:1; overflow-y:auto; overflow-x:hidden; min-height:${150*scale}px; border-radius:${3*scale}px; padding-right:${2*scale}px; "> ${Object.entries(player.skills) .filter(([skillId, skillData]) => !hideZeroDamageSkills || skillData.totalDamage > 0) // 过滤无伤害技能 .sort(([,a], [,b]) => b.totalDamage - a.totalDamage) // 按总伤害从大到小排序 .map(([skillId, skillData]) => { const skillAvg = skillData.actionCount > 0 ? Math.round(skillData.totalDamage / skillData.actionCount) : 0; const skillDps = calculateDPS(skillData.totalDamage, skillData.firstTime, skillData.lastTime); return ` <div style="margin-bottom:${3*scale}px; background:rgba(255,255,255,0.05); padding:${4*scale}px; border-radius:${3*scale}px; border-left:${2*scale}px solid #64B5F6;"> <div style="color:#fff; font-weight:bold; font-size:${7*scale}px; margin-bottom:${2*scale}px;">${skillId === 'baseAttack' ? '⚔️' : '🔥'} ${getSkillDisplayName(skillId)}</div> <div style="display:grid; grid-template-columns: 1fr 1fr 1fr; gap:${2*scale}px; font-size:${6*scale}px;"> <div style="text-align:center; color:#f44336; font-weight:bold;">${skillData.totalDamage.toLocaleString()}</div> <div style="text-align:center; color:#FF9800; font-weight:bold;">${skillAvg.toLocaleString()}</div> <div style="text-align:center; color:#4CAF50; font-weight:bold;">${skillDps.toFixed(1)}</div> </div> <div style="display:grid; grid-template-columns: 1fr 1fr 1fr; gap:${2*scale}px; font-size:${5*scale}px; color:#aaa; margin-top:${1*scale}px;"> <div style="text-align:center;">总伤害</div> <div style="text-align:center;">平均</div> <div style="text-align:center;">DPS</div> </div> </div> `; }).join('')} </div> </div> </div> `; }); // 使用DocumentFragment减少DOM重排 const fragment = document.createDocumentFragment(); const tempDiv = document.createElement('div'); tempDiv.innerHTML = html; // 将tempDiv的所有子节点移动到fragment中 while (tempDiv.firstChild) { fragment.appendChild(tempDiv.firstChild); } // 一次性替换内容 elements.playerStatsContent.innerHTML = ''; elements.playerStatsContent.appendChild(fragment); // 立即恢复滚动位置,不使用延迟 const skillContainers = document.querySelectorAll('.skill-list-container'); skillContainers.forEach(container => { const playerUuid = container.getAttribute('data-player-uuid'); if (playerUuid && scrollPositions[playerUuid] !== undefined) { container.scrollTop = scrollPositions[playerUuid]; } container.classList.remove('updating'); }); // 更新击杀波次统计栏数据 updateKillWaveStatsBar(); } // —— 更新击杀波次统计栏 —— function updateKillWaveStatsBar() { const totalWavesElement = document.getElementById('battlePanel_totalWaves'); const totalEnemiesElement = document.getElementById('battlePanel_totalEnemies'); const wphValueElement = document.getElementById('battlePanel_wphValue'); const ephValueElement = document.getElementById('battlePanel_ephValue'); const runningTimeElement = document.getElementById('battlePanel_runningTime'); if (totalWavesElement) { totalWavesElement.textContent = killWaveStats.totalWaves; } if (totalEnemiesElement) { totalEnemiesElement.textContent = killWaveStats.totalEnemies; } if (wphValueElement) { wphValueElement.textContent = calculateWPH().toFixed(1); } if (ephValueElement) { ephValueElement.textContent = calculateEPH().toFixed(1); } if (runningTimeElement) { const runningTime = killWaveStats.firstKillTime ? formatRunningTime(Date.now() - killWaveStats.firstKillTime) : '0分钟'; runningTimeElement.textContent = runningTime; } // 同时更新收起状态的EPH横条 updateMinimizedBar(); } // —— 拦截全局 WebSocket(战斗面板命名空间) —— const NativeWS = window.WebSocket; // 检查是否已经被其他脚本拦截 if (!window.WebSocket.__battlePanelIntercepted) { const OriginalWebSocket = window.WebSocket; window.WebSocket = function(url, protocols) { const ws = protocols ? new OriginalWebSocket(url, protocols) : new OriginalWebSocket(url); // 保存当前WebSocket实例 window.currentWS = ws; // —— 拦截发送的消息 —— const originalSend = ws.send; ws.send = function(data) { // 正常发送消息 originalSend.call(this, data); }; // —— 拦截接收的消息 —— ws.addEventListener('message', ev => { if (ev.data instanceof ArrayBuffer) { try { const format = detectCompression(ev.data); let text; 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' }); } // 检查是否为战斗消息 if (isBattleMessage(text)) { const battleData = parseBattleMessage(text); if (battleData) { logBattleMessage(battleData); } } } catch (err) { // 解压失败,忽略 console.warn('WebSocket消息解压失败:', err); } } else { // 检查非二进制消息是否为战斗消息 if (isBattleMessage(ev.data)) { const battleData = parseBattleMessage(ev.data); if (battleData) { logBattleMessage(battleData); } } } }); // WebSocket连接状态变化 ws.addEventListener('open', () => { console.log('⚔️ [战斗监听] WebSocket连接已建立'); }); ws.addEventListener('close', () => { console.log('⚔️ [战斗监听] WebSocket连接已断开'); }); ws.addEventListener('error', (error) => { console.error('⚔️ [战斗监听] WebSocket连接错误:', error); }); return ws; }; // 继承原型与静态属性 window.WebSocket.prototype = OriginalWebSocket.prototype; Object.getOwnPropertyNames(OriginalWebSocket).forEach(prop => { if (!(prop in window.WebSocket)) { window.WebSocket[prop] = OriginalWebSocket[prop]; } }); // 标记已被战斗面板拦截 window.WebSocket.__battlePanelIntercepted = true; } })();