Skip to content

Instantly share code, notes, and snippets.

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

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

Select an option

Save fzdwx/a20e1412ad08d1300743da0158cf4e28 to your computer and use it in GitHub Desktop.
战斗面板
// ==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