// ==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;
}
})();