您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
点击按钮打开模态框 只能分析统计中获取的数据 切换角色, 刷新页面会丢失数据(或者你想清理数据就可以这么做)
// ==UserScript== // @name idlepoe战斗分析 // @namespace // @version 1.028 // @description 点击按钮打开模态框 只能分析统计中获取的数据 切换角色, 刷新页面会丢失数据(或者你想清理数据就可以这么做) // @description 目前功能 // @description 1. 战斗失败日志 回放最后一幕 // @description 2. 人物使用技能统计,伤害, 次数, dps // @description 3. 玩家受到伤害统计 // @description 4. 遇怪分析, 怪物血量,护盾, 战斗时长等 // @author 群友 // @run-at document-idle // @match *://*.idlepoe.com/* // @match *://idlepoe.com/* // @match *://poe.faith.wang/* // @icon https://www.google.com/s2/favicons?domain=idlepoe.com // @grant unsafeWindow // @license MIT // @namespace https://idlepoe.com // ==/UserScript== (async function() { 'use strict'; //获取 当前玩家token const token = localStorage.getItem("token"); const classEmojis = { 2: "👑", 3: "🪄", 4: "🏹", 7: "🗡️", 1: "🪓", 5: "⚔️", 6: "🛡️" };`` let playerClassEmj = "🦸♂️"; // 默认 emoji let playerId = "xxx"; // 应该会读取真正的id const playerInfoUrl = "https://poe.faith.wang/api/character"; //获取玩家职业 async function getPlayerInfo() { const classElements = document.getElementsByClassName("class"); const data = await fetchPlayerInfo(); playerId = data.data.name; let key = data.data.class; playerClassEmj = classEmojis[key]; console.log(playerClassEmj + playerId); } async function fetchPlayerInfo() { const res = await fetch(playerInfoUrl, { headers: { 'Authorization': 'Bearer ' + token } }); return await res.json(); } await getPlayerInfo(); // 统计基础类,管理 min, max, count, total class StatTracker { constructor() { this.min = Infinity; this.max = -Infinity; this.count = 0; this.total = 0; } // 更新统计数据 update(value) { if (typeof value !== 'number' || isNaN(value)) { // 非法值直接忽略 return; } this.total += value; this.count++; if (value < this.min) this.min = value; if (value > this.max) this.max = value; } // 计算平均值 getAvg() { return this.count === 0 ? 0 : this.total / this.count; } toString() { if (this.count === 0) return "🚫 无数据"; const avg = this.total / this.count; return `⚖️ 均值: ${formatNumberSmart(avg)} | 🔽 最小: ${formatNumberSmart(this.min)} | 🔼 最大: ${formatNumberSmart(this.max)} | 🔁 次数: ${this.count} | 🌙 累计: ${formatNumberSmart(this.total)}`; } } //根据key 分类统计 class StatTrackerByKey { constructor(keyNames = []) { this.map = {}; this.keyNames = keyNames; // 用于 toString 排序和显示 } update(key, value) { // 自动初始化,如果 key 不在 keyNames 中也能统计,但 getAllStats()/toString() 不会显示 if (!this.map[key]) { this.map[key] = new StatTracker(); } this.map[key].update(value); } getStats(key) { return this.map[key] || new StatTracker(); } getAllStats() { return this.keyNames.map(key => ({ key, stats: this.getStats(key) })); } toString() { return this.keyNames.map(key => `${key}: ${this.getStats(key).toString()}` ).join('\n'); } } //分稀有度的 const RARITY_NORMAL = 1; const RARITY_MAGIC = 2; const RARITY_RARE = 3; const RARITY_BOSS = 4; const rarityLabels = { [RARITY_NORMAL]: "⚪ 普通 ", [RARITY_MAGIC]: "🔵 魔法 ", [RARITY_RARE]: "🟡 稀有 ", [RARITY_BOSS]: "🔴 头目 " }; //根据稀有度分类统计 class StatTrackerByRarity extends StatTrackerByKey { constructor() { super([RARITY_NORMAL, RARITY_MAGIC, RARITY_RARE, RARITY_BOSS]); } toString() { return this.keyNames.map(key => ` ${rarityLabels[key]}: ${this.getStats(key).toString()}` ).join('\n'); } } //根据伤害类型分类统计 const DAMAGE_TYPE_INDEX_PHYSICAL = 1; const DAMAGE_TYPE_INDEX_FIRE = 2; const DAMAGE_TYPE_INDEX_COLD = 3; const DAMAGE_TYPE_INDEX_LIGHTNING = 4; const DAMAGE_TYPE_INDEX_CHAOS = 5; const damageLabels = { [DAMAGE_TYPE_INDEX_PHYSICAL]: "⚔️ 物理", [DAMAGE_TYPE_INDEX_FIRE]: "🔥 火焰", [DAMAGE_TYPE_INDEX_COLD]: "❄️ 冰霜", [DAMAGE_TYPE_INDEX_LIGHTNING]: "⚡ 闪电", [DAMAGE_TYPE_INDEX_CHAOS]: "☠️ 混沌" }; class StatTrackerByType extends StatTrackerByKey { constructor() { super([ DAMAGE_TYPE_INDEX_PHYSICAL, DAMAGE_TYPE_INDEX_FIRE, DAMAGE_TYPE_INDEX_COLD, DAMAGE_TYPE_INDEX_LIGHTNING, DAMAGE_TYPE_INDEX_CHAOS ]); } toString() { const lines = this.keyNames .filter(key => this.getStats(key).count > 0) .map(key => ` ${damageLabels[key]}: ${this.getStats(key).toString()}` ); if (lines.length === 0) { return ' 🚫 无数据'; } return lines.join('\n'); } } //根据技能分类统计 class SkillStat { constructor(name,stoneId) { this.name = name; this.stoneId = stoneId; this.totalTracker = new StatTracker(); // 总伤害 this.typeTracker = new StatTrackerByType(); // 按类型的伤害 this.speedTracker = new StatTracker(); // 释放间隔 this.critCount = 0; // 暴击次数 this.nonCritCount = 0; // 非暴击次数 this.miss = 0; // 未命中次数 } updateDamage(totalDamage = 0, damageByType = {}, isCrit = false) { this.totalTracker.update(totalDamage); for (const type in damageByType) { const dmg = damageByType[type]; if (typeof dmg === "number" && !isNaN(dmg)) { this.typeTracker.update(parseInt(type), dmg); } } if (isCrit) { this.critCount++; } else { this.nonCritCount++; } if (totalDamage === 0){ this.miss ++; } } updateInterval(number,interval){ this.speedTracker.update(interval); } getCritRate() { const total = this.critCount + this.nonCritCount - this.miss; return total > 0 ? formatNumberSmart(this.critCount / total * 100) : "N/A"; } getAverageDPS(){ let totalDamage = this.totalTracker.total; let totalTime = this.speedTracker.total; return formatNumberSmart(totalDamage/totalTime); } getAverageHitDPS(){ let averageDamage = this.totalTracker.getAvg(); let averageTime = this.speedTracker.getAvg(); return formatNumberSmart(averageDamage/averageTime); } getHitRate(){ let miss = this.miss; let total = this.critCount + this.nonCritCount; return total > 0 ? formatNumberSmart((1 - miss / total) * 100) : "N/A"; } toString() { return `🧪 技能: ${this.name} (🆔 ${this.stoneId}) \n 💥 伤害: ${this.totalTracker.toString()} \n 🗡️ 单体击中DPS: ${this.getAverageHitDPS()} | ⚔️ 实际释放DPS: ${this.getAverageDPS()} | ⏱️ 平均释放间隔: ${this.speedTracker.count > 0 ? formatNumberSmart(this.speedTracker.getAvg()) + " 秒" : "无"} \n 🧿 命中率: ${this.getHitRate()}% | 💫 暴击率: ${this.getCritRate()}% | 🎯 暴击: ${this.critCount} 次 | 🟢 普通命中: ${this.nonCritCount - this.miss} 次 | ❌ 未命中: ${this.miss} 次 \n 📊 按类型伤害:\n${this.typeTracker.toString()}`; } } // 记录上条json时间 主要为了计算技能释放间隔, 在前面的init部分初始化 let lastTime = 0; // 最近的战斗记录 let lastBattle = {}; //失败的战斗记录 let lossBattle = {}; //总战斗次数 好像没啥用 let totalBattles = 0; //是否是连接后的第一场战斗 (去除不完整部分) let firstBattle = true; // 处理 battle searching 的时间统计 const searchTime = new StatTracker(); let totalWins = 0; // 胜利总次数 let totalLosses = 0; // 失败总次数 //战斗时间数据统计 const battleTimeStat = new StatTracker(); // 战斗时长记录 根据稀有度分类 const battleTimeStatByRarity = new StatTrackerByRarity(); //经验值数据统计 const expStats = new StatTracker(); // 经验值分稀有度(好像没办法获取单个怪物经验) // 敌人数 生命值 es 统计 const enemyCount = new StatTracker(); const enemyHp = new StatTracker(); const enemyEs = new StatTracker(); // 敌人数按稀有度分类 const enemyCountByRarity = new StatTrackerByRarity(); const enemyHpByRarity = new StatTrackerByRarity(); const enemyEsByRarity = new StatTrackerByRarity(); const healStatsBySkill = {}; // 治疗技能统计 const playerSkillStats = {}; // 所有玩家技能 const enemySkillStats = {}; //敌人所有技能 const totalDamageByType = new StatTrackerByType(); // 所有技能总的按类型统计 const takenDamageByType = new StatTrackerByType(); // 受到伤害按类型统计 const absorbedDamage = new StatTracker(); let extraSearchingTime = 5000; function connectSSEWithAuth({url, token, onMessage, onError, retryInterval = 3000}) { let controller = null; let isStopped = false; let heartbeatTimer = null; let lastMessageTime = Date.now(); const heartbeatInterval = 5000; const maxSilentTime = 5000; let hasConnectedOnce = false; // ✅ 新增:是否曾经成功连接过 let hasConnectedThisTime = false; // ✅ 当前连接是否已成功 function clearHeartbeat() { if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; } } function start() { controller = new AbortController(); lastMessageTime = Date.now(); hasConnectedThisTime = false; // ✅ 重置本轮连接状态 fetch(url, { method: "GET", headers: { Authorization: `Bearer ${token}`, Accept: "text/event-stream" }, signal: controller.signal }).then(response => { if (response.status === 401) { myLog.error("❌ ERROR: 授权失败(401 Unauthorized)"); onError?.(new Error("Unauthorized")); updateBtnStatus("disconnected"); return; } if (!response.ok) { myLog.error(`❌ ERROR: HTTP状态错误 ${response.status}`); onError?.(new Error(`Connection failed: ${response.status}`)); updateBtnStatus("disconnected"); retry(); return; } const reader = response.body.getReader(); const decoder = new TextDecoder("utf-8"); function read() { reader.read().then(({done, value}) => { if (done) { myLog.warn("⚠️ DISCONNECTED (auto): 服务器主动关闭连接,准备重连..."); updateBtnStatus("disconnected"); clearHeartbeat(); retry(); return; } const text = decoder.decode(value, {stream: true}); lastMessageTime = Date.now(); // ✅ 第一次收到消息,认为连接已成功 if (!hasConnectedThisTime) { hasConnectedThisTime = true; if (!hasConnectedOnce) { myLog.log("✅ 连接成功, 可点击统计分析获取数据"); } else { myLog.log("🔁 重连成功"); } hasConnectedOnce = true; updateBtnStatus("connected"); } onMessage?.({event: "message", data: text.trim()}); read(); // 继续监听 }).catch(err => { if (err.name === 'AbortError') return; myLog.error(`❌ ERROR: 数据读取失败: ${err.message}`); onError?.(err); clearHeartbeat(); retry(); }); } // 启动静默检测 clearHeartbeat(); heartbeatTimer = setInterval(() => { const silentFor = Date.now() - lastMessageTime; if (silentFor > maxSilentTime + extraSearchingTime) { myLog.warn(`⚠️ ${silentFor / 1000}s 未收到消息,尝试重连...`); controller?.abort(); // 触发 catch 或 retry clearHeartbeat(); retry(); } }, heartbeatInterval); read(); }).catch(err => { myLog.error(`❌ ERROR: 请求连接失败 - ${err.message}`); onError?.(err); clearHeartbeat(); retry(); }); } function retry() { if (isStopped) return; myLog.log(`🔁 ${retryInterval / 1000}s 后重试连接...`); updateBtnStatus("connecting"); setTimeout(start, retryInterval); } start(); return { stop() { isStopped = true; controller?.abort(); clearHeartbeat(); updateBtnStatus("disconnected"); myLog.log("🛑 DISCONNECTED (manual): 手动断开连接"); } }; } let sseConnection = null; function startSSE() { if (sseConnection) { myLog.warn("⚠️ 已经建立连接,无需重复连接!"); return; } myLog.log("🔌 正在尝试连接战斗数据服务中,请稍候..."); updateBtnStatus("connecting"); firstBattle = true; const timeoutMs = 10_000; let hasConnected = false; const timeoutId = setTimeout(() => { if (!hasConnected) { myLog.error(`⏱️ 连接超时!${timeoutMs / 1000} 秒未收到响应`); stopSSE(); // 确保断开连接 } }, timeoutMs); sseConnection = connectSSEWithAuth({ url: "https://poe.faith.wang/api/battle/sse", token, onMessage: ({event, data}) => { if (!hasConnected) { hasConnected = true; clearTimeout(timeoutId); // ✅ 不再重复 updateBtnStatus,这个已在 connectSSEWithAuth 中处理 } try { const {dataList} = parseSSEChunk(data); dataList.forEach(json => analyzeBattleType(json)); } catch (e) { myLog.error(`❌ 单条数据解析错误:${e}`); } }, onError: (err) => { myLog.error(`[${new Date().toLocaleString()}] ❌ SSE连接失败:\n`, err); updateBtnStatus("disconnected"); } }); } function stopSSE() { if (sseConnection) { sseConnection.stop(); sseConnection = null; myLog.log("🛑 已断开连接"); updateBtnStatus("disconnected"); } else { myLog.warn("⚠️ 当前没有活动连接"); } } //ssechunk 解析工具 function parseSSEChunk(chunkText) { const lines = chunkText.split("\n"); let event = "message"; // 默认事件名 let dataLines = []; for (const line of lines) { if (line.startsWith("event:")) { event = line.slice(6).trim(); } else if (line.startsWith("data:")) { dataLines.push(line.slice(5).trim()); } // 忽略其他行(如id:、retry:) } const dataStr = dataLines.join("\n"); const parsedObjects = extractJSONObjects(dataStr); return {event, dataList: parsedObjects, rawData: dataStr}; } //处理可能包含的多个json对象 function extractJSONObjects(text) { const objects = []; let depth = 0; let start = -1; for (let i = 0; i < text.length; i++) { if (text[i] === '{') { if (depth === 0) start = i; depth++; } else if (text[i] === '}') { depth--; if (depth === 0 && start !== -1) { const jsonStr = text.slice(start, i + 1); try { objects.push(JSON.parse(jsonStr)); } catch (e) { myLog.warn("⚠️ JSON 解析失败:", jsonStr); } start = -1; } } } return objects; } /** * 根据传入的战斗相关 JSON 数据类型,调用对应的分析函数。 * 用于区分:寻路、初始化、战斗事件、结算等不同阶段的数据。 * * @param {Object} json - 一条战斗相关的数据记录(来源于日志) * 可包含字段: * - battleId : 标识战斗阶段(没有时代表寻路) * - isWin : 战斗结算标志(true/false) * - targets : 战斗过程中的目标数据(攻击对象等) */ function analyzeBattleType(json) { //全部的log日志 /* if (json.battleId === undefined){ if (!first){ myLog.groupEnd() }else { first = !first; } myLog.group("寻路中, 准备战斗") } myLog.log(json)*/ // return; // 配置参数 const MAX_LOSS_BATTLE_RECORDS = 20; // 最多保存的失败战斗记录数 const MAX_LAST_BATTLE_TIME = 6 * 60 * 1000; // 6分钟未结算的战斗自动清理 // 定时清理函数 function setupBattleCleanup() { // 每5分钟清理一次未结算的战斗记录 setInterval(() => { const now = Date.now(); for (const battleId in lastBattle) { const battle = lastBattle[battleId]; // 清理超过6分钟未结算的战斗 if (now - battle.startTime > MAX_LAST_BATTLE_TIME) { myLog.warn(`自动清理战斗详情: ${battleId}`); delete lastBattle[battleId]; } } }, MAX_LAST_BATTLE_TIME); // 6分钟检查一次 // 每30分钟清理一次旧失败记录 setInterval(() => { cleanupLossBattle(); }, 30 * 60 * 1000); // 30分钟清理一次 } // 清理旧失败记录 function cleanupLossBattle() { const keys = Object.keys(lossBattle); if (keys.length > MAX_LOSS_BATTLE_RECORDS) { // 按时间排序(最早的在前面) keys.sort((a, b) => lossBattle[a].endTime - lossBattle[b].endTime); // 计算需要删除的数量 const deleteCount = keys.length - MAX_LOSS_BATTLE_RECORDS; for (let i = 0; i < deleteCount; i++) { const id = keys[i]; //myLog.log(`清理旧失败记录: ${id}`); delete lossBattle[id]; } } } // 在程序初始化时调用 setupBattleCleanup(); // 修改后的核心逻辑 if (json.battleId === undefined) { analyzeBattleSearching(json); let searchTime = json.time; extraSearchingTime = searchTime; lastTime = 0; } else if (firstBattle) { // 跳过处理 } else if (json.totalTime !== undefined) { const battleId = json.battleId; // 战斗结算处理 analyzeBattleResult(json); // 战斗结算处理, 统计遇怪时间 let rightTeam = lastBattle[battleId][0].rightTeam; let rarity = new Set(rightTeam.map(one => one.rarity)); rarity.forEach(r => battleTimeStatByRarity.update(r, json.totalTime)); if (lastBattle[battleId]) { // 添加结算事件 lastBattle[battleId].push(json); // 记录结束时间(用于后续清理) lastBattle[battleId].endTime = Date.now(); if (!json.isWin) { // 记录失败战斗 myLog.group("战斗失败! 详细战斗日志:"); _analyzeLoss(lastBattle[battleId]); myLog.groupEnd(); // 保存失败记录 lossBattle[battleId] = lastBattle[battleId]; // 记录结束时间(用于后续清理) lossBattle[battleId].endTime = Date.now(); // 检查是否需要清理旧记录 if (Object.keys(lossBattle).length > MAX_LOSS_BATTLE_RECORDS) { cleanupLossBattle(); } } } // 无论胜败,清理战斗记录 delete lastBattle[battleId]; } else if (json.targets !== undefined) { // 战斗事件处理 analyzeBattleEvent(json); const battleId = json.battleId; if (lastBattle[battleId]) { lastBattle[battleId].push(json); } } else { // 战斗初始化 const battleId = json.battleId; // 清理可能存在的旧记录 if (lastBattle[battleId]) { delete lastBattle[battleId]; } // 创建新战斗记录 lastBattle[battleId] = [json]; // 记录开始时间(用于超时清理) lastBattle[battleId].startTime = Date.now(); analyzeBattleInit(json); } } /** * 分析一次战斗搜索所用时间,并更新平均值、最小值和最大值 * @param {Object} json - 包含搜索时间的对象,需包含 json.time 字段 */ function analyzeBattleSearching(json) { searchTime.update(json.time / 1000) // 增加总战斗数 totalBattles++; firstBattle = false; } function analyzeBattleInit(json) { //获取敌人数组 const enemies = json.rightTeam; //更新敌人数量 enemyCount.update(enemies.length); //新建数据 根据稀有度统计数量 let enemyCountArray = [0,0,0,0,0]; //遍历敌人, 获取其生命,es信息 enemies.forEach(enemy => { const { hpMax, esMax, rarity } = enemy; // 统计 ES 值(有esMax即视为存在ES) if (esMax !== undefined) { enemyEs.update(esMax); enemyEsByRarity.update(rarity,esMax) } // 统计 HP 值 enemyHp.update(hpMax); enemyHpByRarity.update(rarity,hpMax); enemyCountArray[rarity] += 1; }); //统计数量 for (let i = 0; i < enemyCountArray.length; i++) { enemyCountByRarity.update(i,enemyCountArray[i]) } } // 处理 battle result(战斗结果)的时间、胜负、经验统计 /** * 分析单场战斗的结果并更新统计信息 * @param {Object} json - 包含战斗数据的 JSON 对象 * json.isWin - 是否胜利(true/false) * json.totalTime - 战斗时长 * json.trophy.exp - 战斗获得的经验值(数值) */ function analyzeBattleResult(json) { // 判断胜负 if (json.isWin) { totalWins++; } else { totalLosses++; } // 读取战斗时长与经验值 battleTimeStat.update(json.totalTime); expStats.update(json.trophy?.exp ?? 0) // 安全读取 trophy.exp,默认值为 0 //todo 分析掉落 } //分析造成伤害 function _analyzeDamage(target, skillName, stoneId = skillName) { // 生成唯一键,避免同名技能冲突 const key = `${skillName}::${stoneId}`; if (!playerSkillStats[key]) { playerSkillStats[key] = new SkillStat(skillName,stoneId); } const isCrit = !!target.isCritical; const totalDamage = target.totalDamage ?? 0; const damageByType = target.damages ?? {}; playerSkillStats[key].updateDamage(totalDamage, damageByType, isCrit); // 更新总类型统计 for (const type in damageByType) { totalDamageByType.update(parseInt(type), damageByType[type]); } } //分析玩家技能 function _analyzeSkill(targets, skillName, stoneId = skillName, interval){ // 生成唯一键,避免同名技能冲突 const key = `${skillName}::${stoneId}`; if (!playerSkillStats[key]) { playerSkillStats[key] = new SkillStat(skillName,stoneId); } playerSkillStats[key].updateInterval(targets.length, interval); } //分析治疗 function _analyzeHeal(target, skillName) { if (!healStatsBySkill[skillName]) { healStatsBySkill[skillName] = new StatTracker(); } healStatsBySkill[skillName].update(target.totalHeal); } //分析承受伤害 function _analyzeTaken(target,skillName){ if (!enemySkillStats[skillName]) { enemySkillStats[skillName] = new SkillStat(skillName); } const isCrit = !!target.isCritical; // 转换为布尔值,确保可靠 enemySkillStats[skillName].updateDamage(target.totalDamage, target.damages, isCrit); //计算被吸收的伤害 if (target.absorbed !== undefined){ absorbedDamage.update(target.absorbed); } // 总体统计也记录 for (const type in target.damages) { takenDamageByType.update(parseInt(type), target.damages[type]); } } //分析战斗事件 function analyzeBattleEvent(json) { //没有攻击来源的伤害 if (json.actor === undefined) { // DOT json.targets.forEach(target => { if (target.name !== playerId) { _analyzeDamage(target, target.reason); }else if (target.damages !== undefined && target.damages !== null){ _analyzeTaken(target, target.reason); }else { //todo 奉献地面治疗效果等 } }); } else if (json.actor.name === playerId) { // 玩家造成 const skillName = json.skill.name; const skillId = json.skill.stoneId; _analyzeSkill(json.targets,skillName,skillId,json.time-lastTime); lastTime = json.time; json.targets.forEach(target => { if (target.name === playerId) { // 治疗自己 if (target.totalHeal !== undefined) { _analyzeHeal(target, skillName); } } else { //玩家使用伤害技能 _analyzeDamage(target, skillName,skillId); } }); } else { // 敌人造成伤害 if (json.targets[0].name === playerId){ _analyzeTaken(json.targets[0],json.skill.name) } } } //展示数据 function showData() { if (totalBattles <= 1){ myLog.log("🔮 目前数据不足, 需获取数据且等待至少一场战斗结束..."); return } const size = Object.keys(lossBattle).reduce((total, key) => { return total + lossBattle[key].length; }, 0); if (size > 100000) { myLog.log(`🛰️ 数据量极多,正在分析海量战斗记录 (可能需要十五分钟或更久)`); } else if (size > 10000) { myLog.log(`🧬 数据量较大,正在分析与渲染,喝口水缓一缓 ☕(可能需要数分钟)`); } else if (size > 1000) { myLog.log(`🔍 正在分析与渲染数据中, 请稍等...`); } else if (size > 100) { myLog.log(`⌛ 快速分析中`); } myLog.group(`📊 基本统计(🕒 ${new Date().toLocaleString()})=================`); const total = totalWins + totalLosses; const winRate = total > 0 ? (totalWins / total * 100).toFixed(2) : "N/A"; myLog.log(`${playerClassEmj} 玩家: ${playerId}`); myLog.log(`🎖️ 总战斗场数: ${total}`); myLog.log(`✅ 胜利: ${totalWins} 次 | ❌ 失败: ${totalLosses} 次 | 🏆 胜率: ${winRate}%`); myLog.log(`⏳ 寻路时间: ${searchTime.toString()}`); myLog.log(`🧭 战斗时间: ${battleTimeStat.toString()}`); myLog.log(`⭐ 获取经验: ${expStats.toString()}`); myLog.groupEnd(); myLog.group("死亡回放"); Object.values(lossBattle).forEach(battle => { _analyzeLoss(battle); }); myLog.groupEnd(); myLog.group("=== 玩家造成伤害 ============================================================================"); myLog.group("▶ 技能统计"); for (const [name, stat] of Object.entries(playerSkillStats)) { myLog.log(stat.toString()); myLog.log("\n"); } myLog.groupEnd(); myLog.group("▶ 造成伤害分类型统计--------------------------------------"); myLog.log(totalDamageByType.toString()); myLog.groupEnd(); myLog.groupEnd(); myLog.group("=== 玩家承受伤害 ============================================================================"); myLog.group("▶ 承受伤害分类型统计"); myLog.log(takenDamageByType.toString()); myLog.groupEnd(); myLog.groupEnd(); myLog.group("=== 敌人技能伤害 ============================================================================"); for (const [name, stat] of Object.entries(enemySkillStats)) { myLog.log(stat.toString()); } myLog.groupEnd(); myLog.group("=== 敌人信息统计 ============================================================================"); myLog.group("▶ 数量--------------------------------------------------"); myLog.log(enemyCountByRarity.toString()); myLog.groupEnd(); myLog.group("▶ 生命值统计(HP)-----------------------------------------"); myLog.log(enemyHpByRarity.toString()); myLog.groupEnd(); myLog.group("▶ 能量护盾统计(ES)----------------------------------------"); myLog.log(enemyEsByRarity.toString()); myLog.groupEnd(); myLog.group("▶ 战斗时长统计--------------------------------------------"); myLog.log(battleTimeStatByRarity.toString()); myLog.groupEnd(); myLog.groupEnd(); myLog.log("\n\n"); } //死亡回放显示 function _analyzeLoss(lossJsonArray) { myLog.array(lossJsonArray); const finalScene = lossJsonArray[lossJsonArray.length - 2]; myLog.group(`📉 失败战斗分析 (🆔 battleId: ${finalScene.battleId})`); myLog.logWithScroll(`🎭 最后行动角色: ${finalScene.actor?finalScene.actor.name:null}`); myLog.logWithScroll(`🧪 使用技能: ${finalScene.skill?finalScene.skill.name:null}`); //展示最后一击 const targets = finalScene.targets; for (let i = 0; i < targets.length; i++) { const target = targets[i]; myLog.group(`🎯 技能目标: ${target.name}`); if (target.reason !== undefined && target.reason !== null && target.reason !== ''){ myLog.logWithScroll(`❓ 原因: ${target.reason}`) } const isCrit = !!target.isCritical; const critText = isCrit ? " 💥暴击" : " ❌未暴击"; if (target.damages !== undefined && target.damages !== null) { const finalDamageStat = new StatTrackerByType(); for (const type in target.damages) { const dmg = target.damages[type]; if (typeof dmg === "number" && !isNaN(dmg)) { finalDamageStat.update(parseInt(type), dmg); } } myLog.logWithScroll(`🧨 技能${critText},造成的伤害如下:\n${finalDamageStat.toString()}`); } else { myLog.logWithScroll("⚠️ 未记录伤害数据"); } myLog.groupEnd(); } //展示敌方队伍 rightTeam 是敌人 数组 myLog.group(`👹 敌方队伍详细信息`) const rightTeam = finalScene.rightTeam; rightTeam.forEach(enemy => { const rarityEmoji = rarityLabels[enemy.rarity] || "❓ 未知"; const isDead = enemy.hp === 0; const deathEmoji = isDead ? "🪦" : ""; const name = enemy.name || "(无名)"; const nameWithDeath = `${deathEmoji}${name}`; // 第一行: 稀有度 + 名字 const line1 = `${rarityEmoji} ${nameWithDeath}`; // 第二行: ES 和 HP,用 | 分隔 let line2Parts = []; if (enemy.hpMax !== undefined) { const hp = enemy.hp ?? 0; const hpPct = ((hp / enemy.hpMax) * 100).toFixed(1); line2Parts.push(`❤️ HP: ${hp} / ${enemy.hpMax} (${hpPct}%)`); } if (enemy.esMax !== undefined) { const es = enemy.es ?? 0; const esPct = ((es / enemy.esMax) * 100).toFixed(1); line2Parts.push(`🛡️ ES: ${es} / ${enemy.esMax} (${esPct}%)`); } const line2 = line2Parts.join(' | '); // 拼接两行,注意要换行 myLog.logWithScroll(` ${line1}\n\t\t\t\t\t\t${line2}`); }); myLog.groupEnd(); myLog.logWithScroll(`⏱️ 最终战斗时间: ${finalScene.time.toFixed(3)}`); myLog.groupEnd(); myLog.logWithScroll("\n\n") } /** * 智能格式化数字: * - 如果是整数,无小数部分,直接显示整数。 * - 如果数值 >= 10000,转为以 K(千)为单位显示。 * - 根据数值大小智能决定保留几位小数: * < 10 → 保留3位小数 * < 100 → 保留2位小数 * < 10000 → 保留1位小数 * >= 10000 → 使用 K 格式 */ function formatNumberSmart(value) { // 处理非有限数字(例如 NaN、Infinity) if (!isFinite(value)) return 'NaN'; const absVal = Math.abs(value); // 单位换算优先级(T > B > M > K) if (absVal >= 1e12) { return `${formatWithPrecision(value / 1e12)}T`; } else if (absVal >= 1e9) { return `${formatWithPrecision(value / 1e9)}B`; } else if (absVal >= 1e6) { return `${formatWithPrecision(value / 1e6)}M`; } else if (absVal >= 1e3) { return `${formatWithPrecision(value / 1e3)}K`; } // 对小于 1e3 的数值,使用普通精度格式化 return formatWithPrecision(value); /** * 根据数值的绝对值范围决定保留的小数位数: * <10 → 保留 3 位 * <100 → 保留 2 位 * <10000 → 保留 1 位 * ≥10000 → 已处理过,不会触发这段代码 */ function formatWithPrecision(num) { const absNum = Math.abs(num); if (absNum < 10) { return num.toFixed(3); } else if (absNum < 100) { return num.toFixed(2); } else if (absNum < 1000) { return num.toFixed(1); } else { // 理论上不会执行到这里(已在主函数中处理 ≥10000) return Math.round(num).toString(); } } } //todo 绘制前端样式 (() => { const theme = localStorage.getItem("theme") ?? "dark"; let currentTheme = theme; // ----- 样式注入 ----- function injectStyle(theme) { const style = document.createElement('style'); style.id = 'myLogStyle'; style.textContent = ` #myLogModal { background: ${theme === 'light' ? '#fafafa' : '#1e1e1e'}; color: ${theme === 'light' ? '#222' : '#eee'}; border: 2px solid ${theme === 'light' ? '#ccc' : '#666'}; box-shadow: 0 0 10px ${theme === 'light' ? '#ccc' : '#000'}; position: fixed; top: 5%; left: 5%; width: 90%; height: 90%; overflow-y: auto; font-family: Consolas, monospace; font-size: 13px; white-space: pre-wrap; user-select: text; border-radius: 6px; padding: 10px; display: none; z-index: 999999; } #myLogModal header { display: flex; justify-content: flex-end; height: 30px; } #myLogModal .myLog-group { border-left: 2px solid ${theme === 'light' ? '#bbb' : '#444'}; margin-left: 16px; padding-left: 8px; margin-bottom: 4px; } #myLogModal .myLog-info { color: ${theme === 'light' ? '#007acc' : '#4fc3f7'}; } #myLogModal .myLog-warn { color: ${theme === 'light' ? '#b28500' : '#f9a825'}; } #myLogModal .myLog-error { color: ${theme === 'light' ? '#c62828' : '#e53935'}; } #myLogModal .myLog-debug { color: ${theme === 'light' ? '#4a90e2' : '#90caf9'}; } #myLogFloatingButtons { position: sticky; justify-content: space-between; /* 左右两侧分开 */ top: 0; right: 0; display: flex; gap: 8px; width: 100%; /* 确保撑满容器宽度 */ padding: 8px 10px; background: ${theme === 'light' ? '#fafafa' : '#1e1e1e'}; border-bottom: 1px solid ${theme === 'light' ? '#ddd' : '#444'}; z-index: 10002; } .myLog-left-buttons, .myLog-right-buttons { display: flex; gap: 8px; } #myLogFloatingButtons button { background: transparent; color: ${theme === 'light' ? '#333' : '#ccc'}; border: none; padding: 6px 12px; font-size: 14px; border-radius: 6px; cursor: pointer; transition: 0.2s ease; opacity: 0.75; } #myLogFloatingButtons button:hover { opacity: 1; background: ${theme === 'light' ? 'rgba(0,0,0,0.05)' : 'rgba(255,255,255,0.1)'}; color: ${theme === 'light' ? '#000' : '#fff'}; } #logToggleBtn { position: fixed; top: 70px; right: 210px; padding: 8px 16px; font-size: 14px; border-radius: 6px; cursor: pointer; z-index: 9999; background: ${theme === 'light' ? '#ddd' : '#333'}; color: ${theme === 'light' ? '#222' : '#eee'}; border: none; } #logToggleBtn:hover { background: ${theme === 'light' ? '#bbb' : '#555'}; } `; const old = document.getElementById('myLogStyle'); if (old) old.remove(); document.head.appendChild(style); } // ----- DOM结构 ----- const modal = document.createElement('div'); modal.id = 'myLogModal'; modal.innerHTML = ` <div id="myLogFloatingButtons"> <div class="myLog-left-buttons"> <button id="btnAnalyzeBattle">📡 获取数据</button> <button id="btnPrintData">📊 统计分析</button> <button id="myLogClearBtn">🗑️ 清空面板</button> <button id="btnTest">测试别点</button> </div> <div class="myLog-right-buttons"> <button id="myLogCloseBtn">❌ 关闭</button> </div> </div> <div id="myLogContent"></div> `; document.body.appendChild(modal); const content = modal.querySelector('#myLogContent'); const clearBtn = modal.querySelector('#myLogClearBtn'); const closeBtn = modal.querySelector('#myLogCloseBtn'); const analyzeBtn = modal.querySelector('#btnAnalyzeBattle'); const printBtn = modal.querySelector('#btnPrintData'); const btnTest = modal.querySelector("#btnTest"); analyzeBtn.id = "_my_analyzeBtn"; btnTest.style.display = "none"; // 创建按钮 const toggleButton = document.createElement("button"); toggleButton.id = "logToggleBtn"; toggleButton.textContent = "战斗分析"; toggleButton.onclick = () => { modal.style.display = modal.style.display === 'block' ? 'none' : 'block'; modal.scrollTop = modal.scrollHeight; }; document.body.appendChild(toggleButton); // 动态更新状态 emoji /* function updateSSEStatusEmoji(status) { const emojiMap = { connected: '🟢', disconnected: '🔴', connecting: '🟡', null: '' }; const emoji = emojiMap[status] || ''; toggleButton.textContent = `${emoji} 战斗分析`; }*/ // ----- 按钮功能 ----- clearBtn.onclick = () => { content.innerHTML = ''; logState.currentGroup = content; }; closeBtn.onclick = () => modal.style.display = 'none'; analyzeBtn.onclick = () => { if (sseConnection) { stopSSE(); } else { startSSE?.(); } }; printBtn.onclick = () => { if (typeof showData === 'function') { showData(); } else { myLog.warn("⚠️ showData 函数不存在"); } }; btnTest.onclick = () => { const keys = Object.keys(lastBattle); if (keys.length > 0) { _analyzeLoss(lastBattle[keys[0]]) } }; // ----- 日志状态 ----- const logState = { currentGroup: content, groupStack: [], }; let autoScroll = true; // 绑定滚动事件,判断用户是否手动滚动离开底部 modal.addEventListener('scroll', () => { const nearBottom = modal.scrollHeight - modal.scrollTop - modal.clientHeight < 10; autoScroll = nearBottom; }); // 格式化日志参数 function formatArg(arg) { if (typeof arg === 'string') return arg; try { return JSON.stringify(arg, null, 2); } catch { return String(arg); } } function outputLog(message, type = 'info',autoScrollAllowed = true) { const el = document.createElement('div'); el.textContent = message; el.className = `myLog-${type}`; logState.currentGroup.appendChild(el); // 如果全局自动滚动开着,且当前允许自动滚动才滚动到底部 if (autoScroll && autoScrollAllowed) { modal.scrollTop = modal.scrollHeight; } } function formatArg(arg) { if (typeof arg === 'string') return arg; try { return JSON.stringify(arg, null, 2); } catch { return String(arg); } } // ----- myLog 接口 ----- window.myLog = window.myLog = { setTheme(newTheme) { if (newTheme === 'dark' || newTheme === 'light') { currentTheme = newTheme; localStorage.setItem("theme", newTheme); injectStyle(newTheme); } }, log(...args) { const msg = args.map(formatArg).join(' '); outputLog(msg, 'info',false); }, logWithScroll(...args) { const msg = args.map(formatArg).join(' '); outputLog(msg, 'info', true); // true 表示允许自动滚动 }, warn(...args) { const msg = args.map(formatArg).join(' '); outputLog(msg, 'warn'); }, error(...args) { const msg = args.map(formatArg).join(' '); outputLog(msg, 'error'); }, debug(...args) { const msg = args.map(formatArg).join(' '); outputLog(msg, 'debug'); }, group(title) { const group = document.createElement('div'); group.className = 'myLog-group'; const label = document.createElement('div'); label.textContent = title; label.style.fontWeight = 'bold'; group.appendChild(label); logState.currentGroup.appendChild(group); logState.groupStack.push(logState.currentGroup); logState.currentGroup = group; }, groupEnd() { if (logState.groupStack.length > 0) { logState.currentGroup = logState.groupStack.pop(); } }, // 新增 array 方法 array(obj) { if (!logState.currentGroup) return; function renderCollapsible(data, level = 0) { const container = document.createElement('div'); container.style.marginLeft = level * 12 + 'px'; container.style.fontFamily = 'monospace'; if (typeof data !== 'object' || data === null) { container.textContent = String(data); return container; } const isArray = Array.isArray(data); const summary = document.createElement('div'); summary.style.cursor = 'pointer'; summary.style.userSelect = 'none'; summary.style.fontWeight = 'bold'; summary.textContent = isArray ? `Array(${data.length})` : 'Object'; const content = document.createElement('div'); content.style.display = isArray ? 'none' : 'block'; // ✅ 关键修改:数组默认折叠,对象默认展开 content.style.marginLeft = '12px'; summary.onclick = () => { content.style.display = content.style.display === 'none' ? 'block' : 'none'; }; container.appendChild(summary); container.appendChild(content); if (isArray) { data.forEach((item, idx) => { const child = renderCollapsible(item, level + 1); const wrapper = document.createElement('div'); wrapper.style.display = 'flex'; const indexSpan = document.createElement('span'); indexSpan.textContent = idx + ': '; indexSpan.style.fontWeight = 'normal'; indexSpan.style.marginRight = '6px'; wrapper.appendChild(indexSpan); wrapper.appendChild(child); content.appendChild(wrapper); }); } else { for (const key in data) { if (!Object.hasOwnProperty.call(data, key)) continue; const child = renderCollapsible(data[key], level + 1); const wrapper = document.createElement('div'); const keySpan = document.createElement('span'); keySpan.textContent = `${key}: `; keySpan.style.fontWeight = 'normal'; keySpan.style.marginRight = '6px'; wrapper.appendChild(keySpan); wrapper.appendChild(child); content.appendChild(wrapper); } } return container; } const el = document.createElement('div'); el.className = 'myLog-info'; el.appendChild(renderCollapsible(obj)); logState.currentGroup.appendChild(el); const modal = document.getElementById('myLogModal'); if (modal) modal.scrollTop = modal.scrollHeight; } }; // 初始化样式 injectStyle(currentTheme); })(); function updateBtnStatus(status) { _updateToggleButtonEmoji(status); const analyzeBtn = document.getElementById("_my_analyzeBtn"); const textMap = { connected: '🛑 停止获取', disconnected: '📡 获取数据', connecting: '🟡 连接中', null: '' } const text = textMap[status] || '📡 获取数据'; analyzeBtn.textContent = text; } function _updateToggleButtonEmoji(status) { const emojiMap = { connected: '🟢', disconnected: '', connecting: '🟡', null: '' }; const emoji = emojiMap[status] || ''; const toggleButton = document.getElementById("logToggleBtn"); toggleButton.textContent = `${emoji} 战斗分析`; } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址