idlepoe战斗分析

点击按钮打开模态框 只能分析统计中获取的数据 切换角色, 刷新页面会丢失数据(或者你想清理数据就可以这么做)

// ==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或关注我们的公众号极客氢云获取最新地址