Greasy Fork

Greasy Fork is available in English.

MWI-Hit-Tracker-More-Animation

战斗过程中实时显示攻击命中目标,增加了更多的特效

当前为 2025-05-08 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         MWI-Hit-Tracker-More-Animation
// @namespace    http://tampermonkey.net/
// @version      1.8.1
// @description  战斗过程中实时显示攻击命中目标,增加了更多的特效
// @author       Artintel (Artintel), Yuk111
// @license MIT
// @match        https://www.milkywayidle.com/*
// @match        https://test.milkywayidle.com/*
// @icon         https://www.milkywayidle.com/favicon.svg
// @grant        none
// ==/UserScript==

// 立即执行函数,创建独立作用域,避免全局变量污染
(function () {
    'use strict';

    // 状态变量,存储战斗相关信息
    // monstersHP: 存储怪物的当前生命值
    // monstersMP: 存储怪物的当前魔法值
    // playersHP: 存储玩家的当前生命值
    // playersMP: 存储玩家的当前魔法值
    const battleState = {
        monstersHP: [],
        monstersMP: [],
        playersHP: [],
        playersMP: []
    };

    // 存储是否已添加窗口大小改变监听器
    let isResizeListenerAdded = false;

    // 标记脚本是否暂停
    let isPaused = false;

    // 初始化函数,用于启动脚本逻辑
    function init() {
        // 劫持 WebSocket 消息,以便处理战斗相关的消息
        hookWS();
        // 添加网页可见性变化监听器,当网页从后台恢复时进行清理操作
        addVisibilityChangeListener();
        // 创建动画样式,用于攻击路径的闪烁效果
        createAnimationStyle();
    }

    // 创建 lineFlash 动画样式,使攻击路径产生闪烁效果
    function createAnimationStyle() {
        // 创建一个 style 元素
        const style = document.createElement('style');
        // 设置 style 元素的文本内容为 lineFlash 动画的定义
        style.textContent = `
            @keyframes lineFlash {
                0% {
                    /* 起始时路径的透明度为 1 */
                    stroke-opacity: 1;
                }
                50% {
                    /* 中间时路径的透明度为 0.3 */
                    stroke-opacity: 0.3;
                }
                100% {
                    /* 结束时路径的透明度恢复为 1 */
                    stroke-opacity: 1;
                }
            }
        `;
        // 将 style 元素添加到文档的头部
        document.head.appendChild(style);
    }

    // 劫持 WebSocket 消息,拦截并处理战斗相关的消息
    function hookWS() {
        // 获取 MessageEvent 原型上的 data 属性描述符
        const dataProperty = Object.getOwnPropertyDescriptor(MessageEvent.prototype, "data");
        // 保存原始的 data 属性的 getter 函数
        const oriGet = dataProperty.get;

        // 将 data 属性的 getter 函数替换为自定义的 hookedGet 函数
        dataProperty.get = function hookedGet() {
            // 获取当前的 WebSocket 对象
            const socket = this.currentTarget;
            // 如果当前对象不是 WebSocket 实例,使用原始的 getter 函数获取数据
            if (!(socket instanceof WebSocket)) {
                return oriGet.call(this);
            }
            // 如果 WebSocket 的 URL 不包含指定的 API 地址,使用原始的 getter 函数获取数据
            if (socket.url.indexOf("api.milkywayidle.com/ws") <= -1 && socket.url.indexOf("api-test.milkywayidle.com/ws") <= -1) {
                return oriGet.call(this);
            }

            // 如果脚本暂停,直接返回原始消息
            if (isPaused) {
                return oriGet.call(this);
            }

            // 使用原始的 getter 函数获取消息数据
            const message = oriGet.call(this);
            // 重新定义 data 属性,防止循环调用
            Object.defineProperty(this, "data", { value: message });

            // 调用 handleMessage 函数处理消息
            return handleMessage(message);
        };

        // 重新定义 MessageEvent 原型上的 data 属性
        Object.defineProperty(MessageEvent.prototype, "data", dataProperty);
    }

    // 计算元素中心点坐标
    function getElementCenter(element) {
        // 获取元素的边界矩形信息
        const rect = element.getBoundingClientRect();
        // 如果元素内文本为空,将中心点的 y 坐标设置为元素顶部
        if (element.innerText.trim() === '') {
            return {
                x: rect.left + rect.width / 2,
                y: rect.top
            };
        }
        // 否则,将中心点的 y 坐标设置为元素垂直居中位置
        return {
            x: rect.left + rect.width / 2,
            y: rect.top + rect.height / 2
        };
    }

    // 创建抛物线路径,用于攻击动画的路径显示
    function createParabolaPath(startElem, endElem, reversed = false) {
        // 获取起始元素的中心点坐标
        const start = getElementCenter(startElem);
        // 获取结束元素的中心点坐标
        const end = getElementCenter(endElem);

        // 根据是否反转调整抛物线的弧度比例
        const curveRatio = reversed ? 4 : 2.5;
        // 计算抛物线的高度
        const curveHeight = -Math.abs(start.x - end.x) / curveRatio;

        // 计算抛物线的控制点坐标
        const controlPoint = {
            x: (start.x + end.x) / 2,
            y: Math.min(start.y, end.y) + curveHeight
        };

        // 如果是反转的情况,生成反转的抛物线路径
        if (reversed) {
            return `M ${end.x} ${end.y} Q ${controlPoint.x} ${controlPoint.y}, ${start.x} ${start.y}`;
        }
        // 正常情况生成正向的抛物线路径
        return `M ${start.x} ${start.y} Q ${controlPoint.x} ${controlPoint.y}, ${end.x} ${end.y}`;
    }

    // 定义线条颜色数组,用于不同角色的攻击线条颜色
    const lineColor = [
        "rgba(255, 99, 132, 1)", // 浅粉色
        "rgba(54, 162, 235, 1)", // 浅蓝色
        "rgba(255, 206, 86, 1)", // 浅黄色
        "rgba(75, 192, 192, 1)", // 浅绿色
        "rgba(153, 102, 255, 1)", // 浅紫色
        "rgba(255, 159, 64, 1)", // 浅橙色
        "rgba(255, 0, 0, 1)" // 敌人攻击颜色
    ];
    // 定义滤镜颜色数组,用于线条的外发光效果颜色
    const filterColor = [
        "rgba(255, 99, 132, 0.8)", // 浅粉色
        "rgba(54, 162, 235, 0.8)", // 浅蓝色
        "rgba(255, 206, 86, 0.8)", // 浅黄色
        "rgba(75, 192, 192, 0.8)", // 浅绿色
        "rgba(153, 102, 255, 0.8)", // 浅紫色
        "rgba(255, 159, 64, 0.8)", // 浅橙色
        "rgba(255, 0, 0, 0.8)" // 敌人攻击颜色
    ];

    // 创建动画效果,包括攻击路径和伤害数字的动画
    function createEffect(startElem, endElem, hpDiff, index, reversed = false) {
        // 如果脚本暂停,不创建效果
        if (isPaused) return;

        // 初始化线条的宽度
        let strokeWidth = '1px';
        // 初始化滤镜的宽度
        let filterWidth = '1px';
        // 根据伤害值调整线条和滤镜的宽度
        if (hpDiff >= 1000) {
            strokeWidth = '5px';
            filterWidth = '6px';
        } else if (hpDiff >= 700) {
            strokeWidth = '4px';
            filterWidth = '5px';
        } else if (hpDiff >= 500) {
            strokeWidth = '3px';
            filterWidth = '4px';
        } else if (hpDiff >= 300) {
            strokeWidth = '2px';
            filterWidth = '3px';
        } else if (hpDiff >= 100) {
            filterWidth = '2px';
        }

        // 尝试定位伤害数字 div
        if (reversed) {
            const dmgDivs = startElem.querySelector('.CombatUnit_splatsContainer__2xcc0').querySelectorAll('div');
            for (const div of dmgDivs) {
                if (div.innerText.trim() === '') {
                    startElem = div;
                    break;
                }
            }
        } else {
            const dmgDivs = endElem.querySelector('.CombatUnit_splatsContainer__2xcc0').querySelectorAll('div');
            for (const div of dmgDivs) {
                if (div.innerText.trim() === '') {
                    endElem = div;
                    break;
                }
            }
        }

        // 获取 SVG 容器元素
        const svg = document.getElementById('svg-container');
        // 创建一个 SVG 路径元素
        const path = document.createElementNS("http://www.w3.org/2000/svg", "path");

        // 如果是反转的情况,使用敌人攻击颜色的索引
        if (reversed) {
            index = 6;
        }
        // 设置路径元素的样式
        Object.assign(path.style, {
            stroke: lineColor[index],
            strokeWidth: strokeWidth,
            fill: 'none',
            strokeLinecap: 'round',
            filter: 'drop-shadow(0 0 ' + filterWidth + ' ' + filterColor[index] + ')',
            animation: 'lineFlash 0.6s linear'
        });
        // 设置路径元素的 d 属性,即路径的形状
        path.setAttribute('d', createParabolaPath(startElem, endElem, reversed));
        // 计算路径的总长度
        const length = path.getTotalLength();
        // 设置路径的虚线样式,使其初始不可见
        path.style.strokeDasharray = length;
        path.style.strokeDashoffset = length;

        // 将路径元素添加到 SVG 容器中
        svg.appendChild(path);

        // 请求下一帧动画时执行以下操作
        requestAnimationFrame(() => {
            // 设置路径的过渡效果,使其在 0.1 秒内以线性方式显示
            path.style.transition = 'stroke-dashoffset 0.1s linear';
            // 使路径逐渐显示
            path.style.strokeDashoffset = '0';
        });
        // 0.6 秒后执行以下操作
        setTimeout(() => {
            // 先重置路径的过渡效果
            path.style.transition = 'none';

            // 请求下一帧动画时执行以下操作
            requestAnimationFrame(() => {
                // 保持路径当前的可见状态
                path.style.strokeDasharray = length;
                path.style.strokeDashoffset = '0';

                // 设置路径的过渡效果,使其在 0.3 秒内以 cubic-bezier 方式消失
                path.style.transition = 'stroke-dashoffset 0.3s cubic-bezier(0.4, 0, 1, 1)';
                // 使路径逐渐消失
                path.style.strokeDashoffset = -length;

                // 定义路径动画结束后移除元素的函数
                const removeElement = () => {
                    // 从 SVG 容器中移除路径元素
                    if (path.parentNode) {
                        path.parentNode.removeChild(path);
                    }
                    // 移除过渡结束事件的监听器
                    path.removeEventListener('transitionend', removeElement);
                };
                // 监听路径的过渡结束事件,触发移除元素的函数
                path.addEventListener('transitionend', removeElement);
            });
        }, 600);

        // 创建伤害数字元素
        const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
        // 设置伤害数字元素的文本内容为伤害值
        text.textContent = hpDiff;
        // 定义基础字号
        const baseFontSize = 10;
        // 根据伤害值计算字号的增量
        const fontSizeIncrement = Math.floor(200 * math.pow(hpDiff / (100 + hpDiff), 0.45));
        // 计算最终的字号
        const fontSize = fontSizeIncrement - baseFontSize;
        // 设置伤害数字元素的字号
        text.setAttribute('font-size', fontSize);
        // 设置伤害数字元素的填充颜色
        text.setAttribute('fill', lineColor[index]);
        // 初始时伤害数字元素透明度0.7
        text.style.opacity = 0.7;
        // 为伤害数字元素添加外发光特效
        text.style.filter = `drop-shadow(0 0 5px ${lineColor[index]})`;
        // 设置伤害数字元素的变换原点为中心
        text.style.transformOrigin = 'center';
        // 设置伤害数字元素的字体加粗
        text.style.fontWeight = 'bold';
        // 将伤害数字元素添加到 SVG 容器中
        svg.appendChild(text);

        // 定义伤害数字动画的总帧数
        const numFrames = 60;
        // 定义伤害数字动画的总时长为 0.8 秒
        const totalDuration = 800;
        // 计算每帧的时间间隔
        const frameDuration = totalDuration / numFrames;
        // 初始化当前帧数为 0
        let currentFrame = 0;
        // 获取路径的总长度
        const pathLength = path.getTotalLength();

        // 定义伤害数字动画函数
        const animateText = () => {
            // 如果脚本暂停,停止动画
            if (isPaused) return;

            // 检查当前帧是否小于总帧数
            if (currentFrame < numFrames) {
                // 根据当前帧计算在路径上的位置
                const point = path.getPointAtLength((currentFrame / numFrames) * pathLength);
                // 设置伤害数字元素的 x 坐标为路径上当前点的 x 坐标
                text.setAttribute('x', point.x);
                // 设置伤害数字元素的 y 坐标为路径上当前点的 y 坐标
                text.setAttribute('y', point.y);
                // 根据当前帧计算伤害数字元素的透明度,使其逐渐显示
                text.style.opacity = 0.7 + 0.3 * (currentFrame / numFrames);

                // 生成粒子拖尾
                const numParticles = 2; // 粒子数量
                for (let i = 0; i < numParticles; i++) {
                    const particle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
                    particle.setAttribute('r', '2');
                    particle.setAttribute('fill', lineColor[index]);
                    particle.style.opacity = 1;
                    particle.style.transformOrigin = 'center';

                    // 随机偏移粒子位置
                    const offsetX = (Math.random() - 0.4) * 10;
                    const offsetY = (Math.random() - 0.4) * 10;
                    particle.setAttribute('cx', parseFloat(point.x) + offsetX);
                    particle.setAttribute('cy', parseFloat(point.y) + offsetY);

                    svg.appendChild(particle);

                    setTimeout(() => {
                        particle.style.transition = 'all 0.2s ease-out';
                        particle.style.opacity = 0;

                        const removeParticle = () => {
                            if (particle.parentNode) {
                                particle.parentNode.removeChild(particle);
                            }
                            particle.removeEventListener('transitionend', removeParticle);
                        };
                        particle.addEventListener('transitionend', removeParticle);
                    }, 0);
                }

                // 当前帧序号加 1
                currentFrame++;
                // 递归调用 animateText 函数,在指定的帧间隔时间后执行
                setTimeout(animateText, frameDuration);
            } else {
                // 当动画结束后,开始执行魔法击中的消失动画
                text.style.transition = 'all 0.2s ease-out'; // 减少消失动画时长
                text.style.transform = 'scale(1.5)';
                text.style.opacity = 0;

                setTimeout(() => {
                    if (text.parentNode) {
                        text.parentNode.removeChild(text);
                    }
                    // 添加粒子特效
                    createParticleEffect(text.getAttribute('x'), text.getAttribute('y'), lineColor[index]);
                }, 200); // 减少等待时间
            }
        };
        // 调用动画函数开始执行伤害数字动画
        animateText();

        // 设置5秒后强制移除元素
        const removeAfter5Seconds = () => {
            if (path.parentNode) {
                path.parentNode.removeChild(path);
            }
            if (text.parentNode) {
                text.parentNode.removeChild(text);
            }
        };
        setTimeout(removeAfter5Seconds, 5000);
    }

    // 创建粒子特效,在伤害数字消失时显示
    function createParticleEffect(x, y, color) {
        // 如果脚本暂停,不创建粒子特效
        if (isPaused) return;

        // 获取 SVG 容器元素
        const svg = document.getElementById('svg-container');
        // 定义粒子的数量
        const numParticles = 20; // 减少粒子数量
        // 循环创建粒子
        for (let i = 0; i < numParticles; i++) {
            // 创建一个 SVG 圆形元素作为粒子
            const particle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
            // 设置粒子的半径
            particle.setAttribute('r', '2');
            // 设置粒子的填充颜色
            particle.setAttribute('fill', color);
            // 初始时粒子完全不透明
            particle.style.opacity = 1;
            // 设置粒子的变换原点为中心
            particle.style.transformOrigin = 'center';

            // 计算粒子的角度
            const angle = (i / numParticles) * 2 * Math.PI;
            // 随机生成粒子的移动距离
            const distance = Math.random() * 30 + 10; // 减少移动距离
            // 计算粒子的结束位置的 x 坐标
            const endX = parseFloat(x) + distance * Math.cos(angle);
            // 计算粒子的结束位置的 y 坐标
            const endY = parseFloat(y) + distance * Math.sin(angle);

            // 设置粒子的初始 x 坐标
            particle.setAttribute('cx', x);
            // 设置粒子的初始 y 坐标
            particle.setAttribute('cy', y);
            // 将粒子添加到 SVG 容器中
            svg.appendChild(particle);

            // 请求下一帧动画时执行以下操作
            requestAnimationFrame(() => {
                // 设置粒子的过渡效果,使其在 0.3 秒内以 ease-out 方式移动和消失
                particle.style.transition = 'all 0.3s ease-out'; // 减少过渡时间
                // 设置粒子的结束位置的 x 坐标
                particle.setAttribute('cx', endX);
                // 设置粒子的结束位置的 y 坐标
                particle.setAttribute('cy', endY);
                // 使粒子逐渐消失
                particle.style.opacity = 0;

                // 定义粒子动画结束后移除元素的函数
                const removeParticle = () => {
                    if (particle.parentNode) {
                        particle.parentNode.removeChild(particle);
                    }
                    particle.removeEventListener('transitionend', removeParticle);
                };
                particle.addEventListener('transitionend', removeParticle);
            });

            // 设置5秒后强制移除粒子
            const removeParticleAfter5Seconds = () => {
                if (particle.parentNode) {
                    particle.parentNode.removeChild(particle);
                }
            };
            setTimeout(removeParticleAfter5Seconds, 5000);
        }
    }

    // 创建线条动画,根据攻击信息创建攻击路径和伤害数字动画
    function createLine(from, to, hpDiff, reversed = false) {
        // 如果脚本暂停,不创建线条动画
        if (isPaused) return;

        // 获取玩家区域的容器元素
        const container = document.querySelector(".BattlePanel_playersArea__vvwlB");
        // 检查容器元素是否存在且有子元素
        if (container && container.children.length > 0) {
            // 获取玩家容器元素
            const playersContainer = container.children[0];
            // 获取起始元素
            const effectFrom = playersContainer.children[from];
            // 获取怪物容器元素
            const monsterContainer = document.querySelector(".BattlePanel_monstersArea__2dzrY").children[0];
            // 获取结束元素
            const effectTo = monsterContainer.children[to];
            // 获取 SVG 容器元素
            const svg = document.getElementById('svg-container');
            // 如果 SVG 容器元素不存在
            if (!svg) {
                // 创建一个 SVG 容器元素
                const svgContainer = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
                // 设置 SVG 容器元素的 ID
                svgContainer.id = 'svg-container';
                // 设置 SVG 容器元素的样式
                Object.assign(svgContainer.style, {
                    position: 'fixed',
                    top: '0',
                    left: '0',
                    width: '100%',
                    height: '100%',
                    pointerEvents: 'none',
                    overflow: 'visible',
                    zIndex: '190'
                });

                // 设置 SVG 容器元素的 viewBox 属性
                svgContainer.setAttribute('viewBox', `0 0 ${window.innerWidth} ${window.innerHeight}`);
                // 设置 SVG 容器元素的 preserveAspectRatio 属性
                svgContainer.setAttribute('preserveAspectRatio', 'none');
                // 定义更新 viewBox 的函数
                const updateViewBox = () => {
                    svgContainer.setAttribute('viewBox', `0 0 ${window.innerWidth} ${window.innerHeight}`);
                };
                // 初始化 viewBox
                updateViewBox();
                // 将 SVG 容器元素添加到游戏主面板中
                document.querySelector(".GamePage_mainPanel__2njyb").appendChild(svgContainer);
                // 如果还没有添加窗口大小改变的监听器
                if (!isResizeListenerAdded) {
                    // 监听窗口大小改变事件,触发更新 viewBox 的函数
                    window.addEventListener('resize', () => {
                        updateViewBox();
                    });
                    // 标记已经添加了监听器
                    isResizeListenerAdded = true;
                }
            }

            // 如果是反转的情况,调用 createEffect 函数创建反转的动画效果
            if (reversed) {
                createEffect(effectFrom, effectTo, hpDiff, to, reversed);
            } else {
                // 正常情况调用 createEffect 函数创建正向的动画效果
                createEffect(effectFrom, effectTo, hpDiff, from, reversed);
            }
        }
    }

    // 处理 WebSocket 消息,根据消息类型更新战斗状态并创建攻击动画
    function handleMessage(message) {
        // 如果脚本暂停,直接返回原始消息
        if (isPaused) {
            return message;
        }

        let obj;
        try {
            // 将 JSON 字符串解析为 JavaScript 对象
            obj = JSON.parse(message);
        } catch (error) {
            console.error('Failed to parse WebSocket message:', error);
            return message;
        }
        // 如果消息类型是新战斗开始
        if (obj && obj.type === "new_battle") {
            // 初始化怪物的生命值数组
            battleState.monstersHP = obj.monsters.map((monster) => monster.currentHitpoints);
            // 初始化怪物的魔法值数组
            battleState.monstersMP = obj.monsters.map((monster) => monster.currentManapoints);
            // 初始化玩家的生命值数组
            battleState.playersHP = obj.players.map((player) => player.currentHitpoints);
            // 初始化玩家的魔法值数组
            battleState.playersMP = obj.players.map((player) => player.currentManapoints);
        } else if (obj && obj.type === "battle_updated" && battleState.monstersHP.length) {
            // 获取怪物信息的映射
            const mMap = obj.mMap;
            // 获取玩家信息的映射
            const pMap = obj.pMap;
            // 获取怪物的索引数组
            const monsterIndices = Object.keys(obj.mMap);
            // 获取玩家的索引数组
            const playerIndices = Object.keys(obj.pMap);

            // 标记释放技能的怪物索引
            let castMonster = -1;
            // 遍历怪物索引
            monsterIndices.forEach((monsterIndex) => {
                // 如果怪物的当前魔法值小于之前记录的魔法值,标记该怪物为释放技能的怪物
                if (mMap[monsterIndex].cMP < battleState.monstersMP[monsterIndex]) {
                    castMonster = monsterIndex;
                }
                // 更新怪物的当前魔法值
                battleState.monstersMP[monsterIndex] = mMap[monsterIndex].cMP;
            });
            // 标记释放技能的玩家索引
            let castPlayer = -1;
            // 遍历玩家索引
            playerIndices.forEach((userIndex) => {
                // 如果玩家的当前魔法值小于之前记录的魔法值,标记该玩家为释放技能的玩家
                if (pMap[userIndex].cMP < battleState.playersMP[userIndex]) {
                    castPlayer = userIndex;
                }
                // 更新玩家的当前魔法值
                battleState.playersMP[userIndex] = pMap[userIndex].cMP;
            });

            // 遍历怪物的生命值数组
            battleState.monstersHP.forEach((mHP, mIndex) => {
                // 获取当前怪物的信息
                const monster = mMap[mIndex];
                // 如果怪物信息存在
                if (monster) {
                    // 计算怪物失去的生命值
                    const hpDiff = mHP - monster.cHP;
                    // 更新怪物的当前生命值
                    battleState.monstersHP[mIndex] = monster.cHP;
                    // 如果怪物失去了生命值且有玩家存在
                    if (hpDiff > 0 && playerIndices.length > 0) {
                        // 如果有多个玩家
                        if (playerIndices.length > 1) {
                            // 遍历玩家索引
                            playerIndices.forEach((userIndex) => {
                                // 如果该玩家是释放技能的玩家
                                if (userIndex === castPlayer) {
                                    // 调用 createLine 函数创建攻击动画
                                    createLine(userIndex, mIndex, hpDiff);
                                }
                            });
                        } else {
                            // 如果只有一个玩家,调用 createLine 函数创建攻击动画
                            createLine(playerIndices[0], mIndex, hpDiff);
                        }
                    }
                }
            });

            // 遍历玩家的生命值数组
            battleState.playersHP.forEach((pHP, pIndex) => {
                // 获取当前玩家的信息
                const player = pMap[pIndex];
                // 如果玩家信息存在
                if (player) {
                    // 计算玩家失去的生命值
                    const hpDiff = pHP - player.cHP;
                    // 更新玩家的当前生命值
                    battleState.playersHP[pIndex] = player.cHP;
                    // 如果玩家失去了生命值且有怪物存在
                    if (hpDiff > 0 && monsterIndices.length > 0) {
                        // 如果有多个怪物
                        if (monsterIndices.length > 1) {
                            // 遍历怪物索引
                            monsterIndices.forEach((monsterIndex) => {
                                // 如果该怪物是释放技能的怪物
                                if (monsterIndex === castMonster) {
                                    // 调用 createLine 函数创建攻击动画(反转)
                                    createLine(pIndex, monsterIndex, hpDiff, true);
                                }
                            });
                        } else {
                            // 如果只有一个怪物,调用 createLine 函数创建攻击动画(反转)
                            createLine(pIndex, monsterIndices[0], hpDiff, true);
                        }
                    }
                }
            });
        }
        // 返回原始消息
        return message;
    }

    // 检测网页是否从后台恢复,当网页从后台恢复时清理 SVG 容器中的元素
    function addVisibilityChangeListener() {
        document.addEventListener('visibilitychange', function () {
            if (document.visibilityState === 'hidden') {
                // 网页隐藏时,暂停脚本
                isPaused = true;
            } else if (document.visibilityState === 'visible') {
                // 网页从后台恢复时,解除暂停
                isPaused = false;
                // 移除SVG容器内所有元素(包括路径、伤害数字、粒子拖尾和击中粒子特效)
                const svg = document.getElementById('svg-container');
                if (svg) {
                    // 递归清空所有子节点
                    while (svg.firstChild) {
                        svg.removeChild(svg.firstChild);
                    }
                }
                // 额外清理可能残留的未被SVG容器包含的元素(防御性清理)
                document.querySelectorAll('[id^="mwi-hit-tracker-"]').forEach(el => el.remove());
                // 清理可能存在的粒子拖尾和击中粒子特效元素
                document.querySelectorAll('circle[fill^="rgba"]').forEach(el => {
                    if (el.parentNode === svg) {
                        el.parentNode.removeChild(el);
                    }
                });
            }
        });
    }

    // 启动初始化函数
    init();

})();