Last active
July 27, 2025 04:35
-
-
Save fzdwx/a20e1412ad08d1300743da0158cf4e28 to your computer and use it in GitHub Desktop.
战斗面板
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name 猫猫放置-详细战斗日志面板 | |
| // @version v2.0.1 | |
| // @description 猫猫放置-详细战斗日志面板,点击上方中间的按钮展开或者收起 | |
| // @author fzdwx<[email protected]>,YuoHira | |
| // @license MIT | |
| // @match https://www.moyu-idle.com/* | |
| // @match https://moyu-idle.com/* | |
| // @grant none | |
| // ==/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: "猫王庇护", | |
| forbiddenMagic: "禁忌魔法", | |
| detectMagic: "识破", | |
| banish: "驱逐" | |
| }; | |
| // 获取技能中文名称 | |
| function getSkillDisplayName(skillId) { | |
| return skillNameMap[skillId] || skillId; | |
| } | |
| // 初始化玩家统计数据结构 | |
| function initPlayerStats(playerUuid, playerName) { | |
| if (!playerStats[playerUuid]) { | |
| playerStats[playerUuid] = { | |
| name: playerName, | |
| totalDamage: 0, | |
| totalDamageMagical: 0, | |
| totalDamagePhysical: 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; | |
| const totalDamageMagical = battleData.action.totalDamageMagical; | |
| const totalDamagePhysical = battleData.action.totalDamagePhysical; | |
| // 初始化玩家数据 | |
| initPlayerStats(playerUuid, sourceActor.name); | |
| const playerData = playerStats[playerUuid]; | |
| // 更新总体统计 | |
| playerData.totalDamage += totalDamage; | |
| playerData.totalDamageMagical += totalDamageMagical; | |
| playerData.totalDamagePhysical += totalDamagePhysical; | |
| 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(); | |
| // 重置当前战斗统计,为下一波做准备 | |
| 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 parseBattleMessage(data) { | |
| const jsonData = data; | |
| if (jsonData && jsonData.data && jsonData.data.battleInfo && jsonData.data.thisRoundAction) { | |
| const battleInfo = jsonData.data.battleInfo; | |
| const action = jsonData.data.thisRoundAction; | |
| const castUnit = battleInfo.members.find(t => t.uuid === action.sourceUnitUuid); | |
| // 找到当前行动的角色 | |
| 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) { | |
| let damageElement = action.damage[targetUuid]; | |
| let damage = damageElement.damage; | |
| let damagePhysical = 0; | |
| let damageMagical = 0; | |
| if (damageElement.damageType === 'physical') { | |
| damagePhysical = damage | |
| } | |
| if (damageElement.damageType === 'magical') { | |
| damageMagical = damage | |
| } | |
| let items = { | |
| name: target.name, | |
| damage: damage, | |
| damageMagical: damageMagical, | |
| damagePhysical: damagePhysical, | |
| hp: target.hp, | |
| maxHp: target.maxHp, | |
| isDead: target.hp === 0 | |
| }; | |
| targets.push(items); | |
| } | |
| }); | |
| } | |
| return { | |
| castUnit: castUnit, | |
| 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.damage, 0), | |
| totalDamagePhysical: targets.reduce((sum, dmg) => sum + dmg.damagePhysical, 0), | |
| totalDamageMagical: targets.reduce((sum, dmg) => sum + dmg.damageMagical, 0), | |
| attackCount: action.targetUnitUuidList ? action.targetUnitUuidList.length : 0 | |
| }, | |
| battleUuid: battleInfo.uuid, | |
| // 添加完整的成员信息用于击杀检测 | |
| allMembers: battleInfo.members | |
| }; | |
| } | |
| } | |
| // —— 记录战斗消息 —— | |
| function logBattleMessage(battleData) { | |
| // 检测击杀波次 | |
| detectKillWave(battleData); | |
| // 更新统计数据 | |
| updatePlayerStats(battleData); | |
| // 始终更新当前行动信息(面板显示用) | |
| currentBattleInfo = battleData; | |
| // 防抖更新UI显示 | |
| debouncedUpdateUI(); | |
| // 只有在开关打开时才输出控制台日志 | |
| if (enableCurrentActionLog) { | |
| const action = battleData.action; | |
| const sourceActor = action.sourceActor; | |
| // 基本信息 | |
| // 目标详情 | |
| if (action.targets.length > 0) { | |
| action.targets.forEach(target => { | |
| const status = target.isDead ? '☠️ 死亡' : `❤️ ${target.hp}/${target.maxHp}`; | |
| }); | |
| } | |
| } | |
| } | |
| // —— 初始化配置 —— | |
| loadConfig(); | |
| // —— 暴露全局控制台命令 —— | |
| window.toggleBattlePanel = function () { | |
| if (!isPanelExpanded) { | |
| isPanelExpanded = true; | |
| isMinimized = false; | |
| } else if (!isMinimized) { | |
| isMinimized = true; | |
| } else { | |
| isMinimized = false; | |
| } | |
| saveConfig(); | |
| updatePanelLayout(); | |
| const status = isPanelExpanded ? (isMinimized ? '收起' : '展开') : '关闭'; | |
| return `面板状态: ${status}`; | |
| }; | |
| window.showBattlePanel = function () { | |
| isPanelExpanded = true; | |
| isMinimized = false; | |
| saveConfig(); | |
| updatePanelLayout(); | |
| return '面板已展开'; | |
| }; | |
| window.hideBattlePanel = function () { | |
| isPanelExpanded = false; | |
| isMinimized = false; | |
| saveConfig(); | |
| updatePanelLayout(); | |
| return '面板已关闭'; | |
| }; | |
| window.minimizeBattlePanel = function () { | |
| isPanelExpanded = true; | |
| isMinimized = true; | |
| saveConfig(); | |
| updatePanelLayout(); | |
| return '面板已收起到EPH横条'; | |
| }; | |
| window.getBattlePanelStatus = function () { | |
| const status = isPanelExpanded ? (isMinimized ? '收起(EPH横条)' : '展开') : '关闭'; | |
| 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(); | |
| return '统计数据已清除'; | |
| }; | |
| // —— 创建固定的展开收起按钮 —— | |
| 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; | |
| } else if (isMinimized) { | |
| // 从收起状态展开 | |
| isMinimized = false; | |
| } | |
| 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(); | |
| } | |
| } | |
| function actionLogToggleHandler(e) { | |
| enableCurrentActionLog = e.target.checked; | |
| saveConfig(); | |
| } | |
| function hideZeroDamageToggleHandler(e) { | |
| hideZeroDamageSkills = e.target.checked; | |
| saveConfig(); | |
| updatePlayerStatsDisplay(); // 立即更新显示 | |
| } | |
| function scaleInputHandler(e) { | |
| const value = parseInt(e.target.value); | |
| if (value >= 50 && value <= 200) { | |
| panelScale = value; | |
| saveConfig(); | |
| updatePanelContentAndScale(); | |
| } | |
| } | |
| function clearStatsHandler() { | |
| 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(); | |
| } | |
| function minimizeBtnHandler() { | |
| toggleMinimize(); | |
| } | |
| // 初始绑定事件 | |
| 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 damageType = "" | |
| if (action.totalDamageMagical > 0) { | |
| damageType = "魔法" | |
| } | |
| if (action.totalDamagePhysical > 0) { | |
| damageType = "物理" | |
| } | |
| 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()} (${damageType})</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); | |
| console.log("111111", player) | |
| 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="display:flex; flex-direction:column; gap:${4 * scale}px; margin-bottom:${6 * scale}px;"> | |
| <div style="display:flex; justify-content:space-between; gap:${4 * scale}px;"> | |
| <div style="flex:1; text-align:center; background:rgba(33,150,243,0.15); padding:${3 * scale}px; border-radius:${3 * scale}px; border:1px solid rgba(33,150,243,0.3);"> | |
| <div style="color:#2196F3; font-weight:bold; font-size:${8 * scale}px;">${player.totalDamagePhysical.toFixed(1)}</div> | |
| <div style="color:#ccc; font-size:${6 * scale}px;">物理伤害</div> | |
| ${player.totalDamage > 0 ? | |
| `<div style="color:#81D4FA; font-size:${6 * scale}px;">${((player.totalDamagePhysical / player.totalDamage) * 100).toFixed(1)}%</div>` : | |
| `<div style="color:#81D4FA; font-size:${6 * scale}px;">0.0%</div>`} | |
| </div> | |
| <div style="flex:1; text-align:center; background:rgba(156,39,176,0.15); padding:${3 * scale}px; border-radius:${3 * scale}px; border:1px solid rgba(156,39,176,0.3);"> | |
| <div style="color:#9C27B0; font-weight:bold; font-size:${8 * scale}px;">${player.totalDamageMagical.toFixed(1)}</div> | |
| <div style="color:#ccc; font-size:${6 * scale}px;">魔法伤害</div> | |
| ${player.totalDamage > 0 ? | |
| `<div style="color:#E1BEE7; font-size:${6 * scale}px;">${((player.totalDamageMagical / player.totalDamage) * 100).toFixed(1)}%</div>` : | |
| `<div style="color:#E1BEE7; font-size:${6 * scale}px;">0.0%</div>`} | |
| </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(); | |
| } | |
| let prevSpan = null; | |
| let prevNextText = null; | |
| function updateSkillDisplay(characterName, skillId) { | |
| if (prevSpan != null) { | |
| prevSpan.innerHTML = prevNextText; | |
| } | |
| const ourSideDiv = document.querySelector('.text-blue-600.text-xl'); // 通过独特class定位 | |
| if (ourSideDiv) { | |
| // 2. 获取「我方」所在的父容器(战斗单位区域) | |
| const battleUnitsContainer = ourSideDiv.nextElementSibling; | |
| // 3. 在容器内查找目标角色名 | |
| const targetNameSpan = [...battleUnitsContainer.querySelectorAll('span.font-medium')] | |
| .find(span => positionAwareMatch(characterName, span.textContent.trim())); | |
| if (targetNameSpan) { | |
| const skillName = skillNameMap[skillId] ?? skillId; | |
| const realName = nameDic[characterName]; | |
| // 在这里修改文本或执行其他操作 | |
| targetNameSpan.innerHTML = `${realName}<br><span style="font-size:14px;opacity:1;color:#f43535">🎯 ${skillName}</span>`; | |
| prevNextText = `${realName}<br><span style="font-size:14px;opacity:0.4;color:black">🎯 ${skillName}</span>`; | |
| prevSpan = targetNameSpan; | |
| } else { | |
| //console.warn('未找到角色名元素'); | |
| } | |
| } else { | |
| //console.warn('未找到「我方」标识'); | |
| } | |
| } | |
| let nameDic = {}; | |
| //相对位置匹配,应对乱码问题 | |
| function positionAwareMatch(dirtyStr, targetStr) { | |
| let result = false; | |
| const dicName = dirtyStr in nameDic ? nameDic[dirtyStr] : null; | |
| //第一次匹配 | |
| if (!dicName) { | |
| //初次匹配时,若存在乱码则名字必定不同 | |
| if (dirtyStr !== targetStr) { | |
| // 提取有效字符及其位置(过滤乱码) | |
| // 每4个乱码字节占用一个位置 | |
| const validChars = []; | |
| let validIndex = 0; | |
| for (let i = 0; i < dirtyStr.length; i++) { | |
| const char = dirtyStr[i]; | |
| if (/[\u4E00-\u9FA5]/.test(char)) { // 仅保留中文 | |
| validChars.push({char, index: validIndex}); | |
| validIndex++; | |
| } else if (i % 4 === 0) | |
| validIndex++; | |
| } | |
| // 如果没有有效字符,返回false | |
| if (validChars.length === 0) return false; | |
| // 检查每个有效字符是否在目标字符串的对应位置 | |
| // 允许±1的偏移量来应对乱码字符数不一致的情况 | |
| result = validChars.every(vc => { | |
| const pos = vc.index; | |
| return ( | |
| targetStr[pos] === vc.char || // 精确位置匹配 | |
| (pos > 0 && targetStr[pos - 1] === vc.char) || // 前一位 | |
| (pos < targetStr.length - 1 && targetStr[pos + 1] === vc.char) // 后一位 | |
| ); | |
| }); | |
| } else | |
| result = true; | |
| if (result) | |
| nameDic[dirtyStr] = targetStr; | |
| } else { | |
| result = targetStr.includes(dicName); | |
| } | |
| return result; | |
| } | |
| // —— 拦截全局 WebSocket(战斗面板命名空间) —— | |
| window.addEventListener("moyu-socket-event", (e) => { | |
| // 检查是否为战斗消息 | |
| if (e.detail.event === 'battle:fullInfo:success') { | |
| const battleData = parseBattleMessage(e.detail.data); | |
| if (battleData) { | |
| logBattleMessage(battleData); | |
| } | |
| } | |
| }); | |
| } | |
| ) | |
| (); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment