Greasy Fork

来自缓存

Greasy Fork is available in English.

MWI-Hit-Tracker-More-Animation

战斗过程中实时显示攻击命中目标,增加了更多的特效(伤害数字、粒子拖尾、击中溅射、击中震动)

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

您需要先安装一款用户脚本管理器扩展,例如 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.9
// @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';

    // 状态变量,存储战斗相关信息
    const battleState = {
        monstersHP: [],
        monstersMP: [],
        playersHP: [],
        playersMP: []
    };

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

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

    // 粒子对象池
    const particlePool = [];

    // 标记按钮是否已添加
    let isCustomColorButtonAdded = false;

    // 保存初始颜色
    const initialLineColor = [
        "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 initialFilterColor = [
        "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)" // 敌人攻击颜色
    ];

    // 存储每个玩家的勾选状态,默认全部勾选
    const playerDrawEnabled = new Array(7).fill(true);

    // 定义线条颜色数组,用于不同角色的攻击线条颜色
    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)" // 敌人攻击颜色
    ];

    // 从 localStorage 加载保存的设置
    function readSettings() {
        const ls = localStorage.getItem("MWI_Hit_Tracker_Settings");
        if (ls) {
            const lsObj = JSON.parse(ls);
            lineColor.splice(0, lineColor.length, ...lsObj.lineColor);
            filterColor.splice(0, filterColor.length, ...lsObj.filterColor);
            playerDrawEnabled.splice(0, playerDrawEnabled.length, ...lsObj.playerDrawEnabled);
        }
    }

    // 保存设置到 localStorage
    function saveSettings() {
        const settings = {
            lineColor: lineColor,
            filterColor: filterColor,
            playerDrawEnabled: playerDrawEnabled
        };
        localStorage.setItem("MWI_Hit_Tracker_Settings", JSON.stringify(settings));
    }

    // 在初始化时加载设置
    readSettings();

    // 创建自定义颜色按钮
    function createCustomColorButton() {
        // 出警按钮父元素路径,使用 test.js 中的选择器
        var tabsContainer = document.querySelector("#root > div > div > div.GamePage_gamePanel__3uNKN > div.GamePage_contentPanel__Zx4FH > div.GamePage_middlePanel__uDts7 > div.GamePage_mainPanel__2njyb > div > div:nth-child(1) > div > div > div > div.TabsComponent_tabsContainer__3BDUp > div > div > div");
        var referenceTab = tabsContainer ? tabsContainer.children[1] : null;

        if (!tabsContainer || !referenceTab) {
            console.log('未找到目标元素,请检查选择器是否正确。');
            return;
        }
        if (tabsContainer.querySelector('.Button_customColor__custom')) return;

        // 创建按钮
        const customColorButton = document.createElement('button');
        // 只使用自定义类名
        customColorButton.className = 'Button_customColor__custom css-1q2h7u5';
        customColorButton.textContent = 'Hit自定义设置';

        // 修改插入逻辑,将按钮插入到最后一个标签之后
        var lastTab = tabsContainer.children[tabsContainer.children.length - 1];
        lastTab.insertAdjacentElement('afterend', customColorButton);

        // 添加按钮样式
        const style = document.createElement('style');
        style.innerHTML = `
            .Button_customColor__custom {
                background-color: #546ddb;
                color: white;
                border-radius: 5px;
                padding: 5px 10px;
                cursor: pointer;
                transition: background-color 0.3s;
            }
            .Button_customColor__custom:hover {
                background-color: #131419;
            }`;
        document.head.appendChild(style);

        // 添加点击事件
        customColorButton.addEventListener('click', () => {
            // 创建弹出窗口
            const popup = document.createElement('div');
            popup.style.position = 'fixed';
            popup.style.top = '50%';
            popup.style.left = '50%';
            popup.style.transform = 'translate(-50%, -50%)';
            popup.style.backgroundColor = '#f9f9f9';
            popup.style.padding = '30px';
            popup.style.border = '2px solid #ddd';
            popup.style.borderRadius = '10px';
            popup.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)';
            popup.style.zIndex = '9999';
            popup.style.minWidth = '300px';

            // 玩家名称数组
            const players = ['玩家一', '玩家二', '玩家三', '玩家四', '玩家五', '待定', '敌人'];

            // 为每个玩家创建颜色选择器和预览
            players.forEach((player, index) => {
                const container = document.createElement('div');
                container.style.marginBottom = '15px';
                container.style.display = 'flex';
                container.style.alignItems = 'center';

                // 创建勾选框
                const checkbox = document.createElement('input');
                checkbox.type = 'checkbox';
                checkbox.checked = playerDrawEnabled[index];
                checkbox.addEventListener('change', (e) => {
                    playerDrawEnabled[index] = e.target.checked;
                });
                container.appendChild(checkbox);

                const label = document.createElement('span');
                label.textContent = `${player}: `;
                label.style.flex = '1';
                label.style.fontSize = '14px';
                label.style.marginLeft = '10px';
                container.appendChild(label);

                const colorInput = document.createElement('input');
                colorInput.type = 'color';
                colorInput.value = lineColor[index];
                colorInput.addEventListener('input', (e) => {
                    if (playerDrawEnabled[index]) {
                        lineColor[index] = e.target.value;
                        filterColor[index] = e.target.value.replace('1)', '0.8)');
                        saveSettings(); // 保存设置
                    }
                });
                colorInput.style.marginRight = '10px';

                const preview = document.createElement('div');
                preview.style.width = '30px';
                preview.style.height = '30px';
                preview.style.border = '1px solid #ccc';
                preview.style.borderRadius = '4px';
                preview.style.backgroundColor = lineColor[index];
                colorInput.addEventListener('input', (e) => {
                    preview.style.backgroundColor = e.target.value;
                });

                container.appendChild(colorInput);
                container.appendChild(preview);

                popup.appendChild(container);
            });

            // 创建重置按钮
            const resetButton = document.createElement('button');
            resetButton.textContent = '重置';
            resetButton.style.backgroundColor = '#ff4444';
            resetButton.style.color = 'white';
            resetButton.style.border = 'none';
            resetButton.style.borderRadius = '4px';
            resetButton.style.padding = '8px 15px';
            resetButton.style.marginRight = '10px';
            resetButton.style.cursor = 'pointer';
            resetButton.addEventListener('click', () => {
                lineColor.splice(0, lineColor.length, ...initialLineColor);
                filterColor.splice(0, filterColor.length, ...initialFilterColor);
                playerDrawEnabled.fill(true); // 重置勾选状态
                saveSettings(); // 保存重置后的设置
                // 更新颜色选择器和预览
                const colorInputs = popup.querySelectorAll('input[type="color"]');
                const previews = popup.querySelectorAll('div:last-child');
                colorInputs.forEach((input, index) => {
                    input.value = initialLineColor[index];
                    previews[index].style.backgroundColor = initialLineColor[index];
                });
            });

            // 创建关闭按钮
            const closeButton = document.createElement('button');
            closeButton.textContent = '关闭';
            closeButton.style.backgroundColor = '#2196F3';
            closeButton.style.color = 'white';
            closeButton.style.border = 'none';
            closeButton.style.borderRadius = '4px';
            closeButton.style.padding = '8px 15px';
            closeButton.style.cursor = 'pointer';
            closeButton.addEventListener('click', () => {
                saveSettings()
                document.body.removeChild(popup);
            });

            // 创建按钮容器
            const buttonContainer = document.createElement('div');
            buttonContainer.style.marginTop = '20px';
            buttonContainer.style.display = 'flex';
            buttonContainer.style.justifyContent = 'flex-end';
            buttonContainer.appendChild(resetButton);
            buttonContainer.appendChild(closeButton);

            popup.appendChild(buttonContainer);

            document.body.appendChild(popup);
        });

        // 标记按钮已添加
        isCustomColorButtonAdded = true;
        console.log('自定义颜色按钮已成功添加。');
    }

    // 循环检查按钮是否创建成功
    function checkAndCreateButton() {
        const created = createCustomColorButton();
        if (!created) {
            setTimeout(checkAndCreateButton, 500); // 每 500 毫秒检查一次
        }
    }

    // 修改初始化函数,添加对自定义颜色按钮的调用
    function init() {
        console.log('初始化函数已调用。');
        // 劫持 WebSocket 消息,以便处理战斗相关的消息
        hookWS();
        // 添加网页可见性变化监听器,当网页从后台恢复时进行清理操作
        addVisibilityChangeListener();
        // 创建动画样式,用于攻击路径的闪烁效果和目标震动效果
        createAnimationStyle();
        // 调用循环检查函数
        checkAndCreateButton();
    }
    // 创建动画样式,包括路径闪烁和目标震动效果
    function createAnimationStyle() {
        // console.log('动画样式函数已调用。');
        const style = document.createElement('style');
        style.textContent = `
            @keyframes lineFlash {
                0% { stroke-opacity: 0.7; }
                50% { stroke-opacity: 0.3; }
                100% { stroke-opacity: 0.7; }
            }

            @keyframes shake {
                0%, 100% { transform: translateX(0); }
                50% { transform: translateX(-1px); } /* 减小震动幅度 */
            }

            .mwht-shake {
                animation: shake 0.2s cubic-bezier(.36,.07,.19,.97) forwards; /* 固定0.2秒持续时间 */
                transform-origin: center;
                position: relative;
                z-index: 200;
            }
        `;
        document.head.appendChild(style);
    }

    // 劫持 WebSocket 消息,拦截并处理战斗相关的消息
    function hookWS() {
        // console.log('劫持函数已调用。');
        const dataProperty = Object.getOwnPropertyDescriptor(MessageEvent.prototype, "data");
        const oriGet = dataProperty.get;

        dataProperty.get = function hookedGet() {
            const socket = this.currentTarget;
            if (!(socket instanceof WebSocket)) {
                return oriGet.call(this);
            }
            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);
            }

            const message = oriGet.call(this);
            Object.defineProperty(this, "data", { value: message });

            return handleMessage(message);
        };

        Object.defineProperty(MessageEvent.prototype, "data", dataProperty);
    }

    // 计算元素中心点坐标
    function getElementCenter(element) {
        const rect = element.getBoundingClientRect();
        if (element.innerText.trim() === '') {
            return {
                x: rect.left + rect.width / 2,
                y: rect.top
            };
        }
        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}`;
    }

    // 为目标元素的第三个父级元素添加震动效果,根据第五个父级元素决定震动方向
    function shakeTarget(element) {
        if (!element || isPaused) return;

        // 向上查找第三个父级元素(用于实际震动)
        let shakeElement = element;
        for (let i = 0; i < 3 && shakeElement; i++) {
            shakeElement = shakeElement.parentElement;
        }

        // 向上查找第五个父级元素(用于判断震动方向)
        let directionElement = element;
        for (let i = 0; i < 5 && directionElement; i++) {
            directionElement = directionElement.parentElement;
        }

        // 如果找到了相应的父级元素,应用震动效果
        if (shakeElement && directionElement) {
            const className = directionElement.className;
            let transformValue = 'translate(0, 0)';

            // 根据第五个父级元素的类名决定震动方向
            if (className.includes('playersArea')) {
                transformValue = 'translate(-2px, 2px)';
            } else if (className.includes('monstersArea')) {
                transformValue = 'translate(2px, 2px)';
            }

            // 添加震动类并设置动画
            shakeElement.classList.add('mwht-shake');

            // 使用自定义动画实现不同方向的震动
            shakeElement.style.animation = `customShake 0.2s cubic-bezier(.36,.07,.19,.97) forwards`;
            shakeElement.style.transformOrigin = 'center';
            shakeElement.style.willChange = 'transform';

            // 存储原始transform值,动画结束后恢复
            const originalTransform = shakeElement.style.transform;

            // 动画帧函数
            let startTime = null;
            const duration = 200; // 200ms = 0.2s

            function animate(currentTime) {
                if (isPaused) return;

                if (!startTime) startTime = currentTime;
                const elapsed = currentTime - startTime;
                const progress = Math.min(elapsed / duration, 1);

                // 计算动画曲线
                const easeOut = 1 - Math.pow(1 - progress, 3);

                // 应用变换
                if (progress < 0.5) {
                    // 前半段:从0到目标偏移
                    const scale = easeOut * 2;
                    shakeElement.style.transform = `translate(${parseFloat(transformValue.split('(')[1]) * scale}px, ${parseFloat(transformValue.split(',')[1]) * scale}px)`;
                } else {
                    // 后半段:从目标偏移回到0
                    const scale = 2 - (easeOut * 2);
                    shakeElement.style.transform = `translate(${parseFloat(transformValue.split('(')[1]) * scale}px, ${parseFloat(transformValue.split(',')[1]) * scale}px)`;
                }

                if (progress < 1) {
                    requestAnimationFrame(animate);
                } else {
                    // 动画结束,恢复原始transform
                    shakeElement.style.transform = originalTransform;
                    shakeElement.classList.remove('mwht-shake');
                    shakeElement.style.animation = '';
                }
            }

            // 启动动画
            requestAnimationFrame(animate);
        }
    }

    // 创建动画效果,包括攻击路径和伤害数字的动画
    function createEffect(startElem, endElem, hpDiff, index, reversed = false) {
        if (isPaused) return;
        // 检查玩家是否被勾选,如果未勾选则不绘制
        if (!playerDrawEnabled[index]) 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';
        }

        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;
                }
            }
        }

        const svg = document.getElementById('svg-container');
        const frag = document.createDocumentFragment();

        // 根据reversed参数决定目标元素
        const targetElem = reversed ? startElem : endElem;

        const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
        if (reversed) index = 6;
        Object.assign(path.style, {
            stroke: lineColor[index],
            strokeWidth,
            fill: 'none',
            strokeLinecap: 'round',
            filter: `drop-shadow(0 0 ${filterWidth} ${filterColor[index]})`,
            willChange: 'stroke-dashoffset, opacity',
        });
        path.setAttribute('d', createParabolaPath(startElem, endElem, reversed));
        const pathLength = path.getTotalLength();
        path.style.strokeDasharray = pathLength;
        path.style.strokeDashoffset = pathLength;

        frag.appendChild(path);

        const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
        text.textContent = hpDiff;
        const baseFontSize = 5;
        const fontSize = Math.floor(200 * Math.pow(hpDiff / (20000 + hpDiff), 0.45)) - baseFontSize;
        text.setAttribute('font-size', fontSize);
        text.setAttribute('fill', lineColor[index]);
        Object.assign(text.style, {
            opacity: 0,
            filter: `drop-shadow(0 0 5px ${lineColor[index]})`,
            transformOrigin: 'center',
            fontWeight: 'bold',
            willChange: 'transform, opacity, x, y',
        });
        frag.appendChild(text);

        svg.appendChild(frag);

        setTimeout(() => {
            requestAnimationFrame(() => {
                path.style.transition = 'stroke-dashoffset 1s linear';
                path.style.strokeDashoffset = '0';

                animateText(path, text, pathLength, lineColor[index], () => {
                    // 伤害数字动画结束后触发震动效果
                    shakeTarget(targetElem);
                });
            });
        }, 100);

        setTimeout(() => {
            requestAnimationFrame(() => {
                path.style.transition = 'stroke-dashoffset 1s cubic-bezier(0.25, 0.46, 0.45, 0.94), opacity 1s cubic-bezier(0.25, 0.46, 0.45, 0.94)';
                path.style.strokeDashoffset = -pathLength;
                path.style.opacity = 0;

                const removePath = () => {
                    path.remove();
                };
                path.addEventListener('transitionend', removePath, { once: true });
            });
        }, 900);
    }

    // 从对象池获取粒子元素
    function getParticleFromPool() {
        if (particlePool.length > 0) {
            return particlePool.pop();
        }
        return document.createElementNS("http://www.w3.org/2000/svg", "circle");
    }

    // 将粒子元素返回对象池
    function returnParticleToPool(particle) {
        particle.removeAttribute('r');
        particle.removeAttribute('fill');
        particle.removeAttribute('cx');
        particle.removeAttribute('cy');
        particle.style.opacity = 1;
        particle.style.transform = 'none';
        particle.removeEventListener('transitionend', () => {});
        particlePool.push(particle);
    }

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

        const svg = document.getElementById('svg-container');
        const numParticles = 20;
        const frag = document.createDocumentFragment();

        const batchSize = 5;
        let batchCount = 0;
        function createBatch() {
            for (let i = 0; i < batchSize && batchCount * batchSize + i < numParticles; i++) {
                const particle = getParticleFromPool();
                particle.setAttribute('r', '2');
                particle.setAttribute('fill', color);
                particle.setAttribute('cx', x);
                particle.setAttribute('cy', y);
                particle.style.opacity = 1;
                particle.style.transformOrigin = 'center';
                particle.style.willChange = 'transform, opacity';

                const angle = ((batchCount * batchSize + i) / numParticles) * 2 * Math.PI;
                const distance = Math.random() * 30 + 10;
                const endX = parseFloat(x) + distance * Math.cos(angle);
                const endY = parseFloat(y) + distance * Math.sin(angle);

                frag.appendChild(particle);

                requestAnimationFrame(() => {
                    particle.style.transition = 'all 0.3s ease-out';
                    particle.setAttribute('cx', endX);
                    particle.setAttribute('cy', endY);
                    particle.style.opacity = 0;

                    particle.addEventListener('transitionend', () => {
                        returnParticleToPool(particle);
                    }, { once: true });

                    setTimeout(() => {
                        if (particle.parentNode) {
                            particle.parentNode.removeChild(particle);
                            returnParticleToPool(particle);
                        }
                    }, 5000);
                });
            }
            batchCount++;
            if (batchCount * batchSize < numParticles) {
                setTimeout(createBatch, 50);
            } else {
                svg.appendChild(frag);
            }
        }
        createBatch();
    }

    // 文本动画函数 - 使用 requestAnimationFrame 实现更流畅的动画
    function animateText(path, text, pathLength, color, onComplete) {
        const animationConfig = {
            duration: 1350,
            fadeInStart: 0.0,
            fadeInEnd: 0.3,
            particleInterval: 3
        };

        let startTime = null;
        let lastParticleFrame = 0;

        function animate(currentTime) {
            if (isPaused) return;

            if (!startTime) startTime = currentTime;

            const elapsed = currentTime - startTime;
            const progress = Math.min(elapsed / animationConfig.duration, 1);

            const point = path.getPointAtLength(progress * pathLength);

            text.setAttribute('x', point.x);
            text.setAttribute('y', point.y);

            let opacity = 1;
            if (progress < animationConfig.fadeInStart) {
                opacity = 0;
            } else if (progress < animationConfig.fadeInEnd) {
                opacity = 0.7 + 0.3 * ((progress - animationConfig.fadeInStart) / (animationConfig.fadeInEnd - animationConfig.fadeInStart));
            }
            text.style.opacity = opacity;

            if (Math.floor(progress * 100) % animationConfig.particleInterval === 0 && lastParticleFrame !== Math.floor(progress * 100)) {
                lastParticleFrame = Math.floor(progress * 100);

                const particle = getParticleFromPool();
                particle.setAttribute('r', '2');
                particle.setAttribute('fill', color);
                particle.setAttribute('cx', point.x + (Math.random() - 0.5) * 10);
                particle.setAttribute('cy', point.y + (Math.random() - 0.5) * 10);
                particle.style.opacity = 1;
                particle.style.transition = 'all 0.2s ease-out';
                particle.style.willChange = 'opacity, transform';

                const svg = document.getElementById('svg-container');
                svg.appendChild(particle);

                requestAnimationFrame(() => {
                    particle.style.opacity = 0;
                    particle.addEventListener('transitionend', () => {
                        returnParticleToPool(particle);
                    }, { once: true });
                });
            }

            if (progress < 1) {
                requestAnimationFrame(animate);
            } else {
                text.style.transition = 'all 0.2s ease-out';
                text.style.transform = 'scale(1.5)';
                text.style.opacity = 0;

                setTimeout(() => {
                    text.remove();
                    createParticleEffect(text.getAttribute('x'), text.getAttribute('y'), color);

                    // 调用回调函数触发震动和恢复可见性
                    if (typeof onComplete === 'function') {
                        onComplete();
                    }
                }, 100);
            }
        }

        requestAnimationFrame(animate);
    }

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

        const playerArea = document.querySelector(".BattlePanel_playersArea__vvwlB");
        const monsterArea = document.querySelector(".BattlePanel_monstersArea__2dzrY");
        const gamePanel = document.querySelector(".GamePage_mainPanel__2njyb");

        if (!playerArea || !monsterArea || !gamePanel) return;

        const playersContainer = playerArea.firstElementChild;
        const monsterContainer = monsterArea.firstElementChild;

        const effectFrom = playersContainer?.children[from];
        const effectTo = monsterContainer?.children[to];

        if (!effectFrom || !effectTo) return;

        let svgContainer = document.getElementById('svg-container');

        if (!svgContainer) {
            const svgNS = 'http://www.w3.org/2000/svg';
            svgContainer = document.createElementNS(svgNS, 'svg');
            svgContainer.id = 'svg-container';

            Object.assign(svgContainer.style, {
                position: 'fixed',
                top: '0',
                left: '0',
                width: '100%',
                height: '100%',
                pointerEvents: 'none',
                overflow: 'visible',
                zIndex: '190'
            });

            const setViewBox = () => {
                const width = window.innerWidth;
                const height = window.innerHeight;
                svgContainer.setAttribute('viewBox', `0 0 ${width} ${height}`);
            };

            setViewBox();
            svgContainer.setAttribute('preserveAspectRatio', 'none');
            gamePanel.appendChild(svgContainer);

            if (!isResizeListenerAdded) {
                window.addEventListener('resize', setViewBox);
                isResizeListenerAdded = true;
            }
        }

        const originIndex = reversed ? to : from;
        createEffect(effectFrom, effectTo, hpDiff, originIndex, reversed);
    }

    // 处理伤害信息,根据新旧生命值计算伤害差值并创建动画
    function processDamage(oldHPArr, newMap, castIndex, attackerIndices, isReverse = false) {
        oldHPArr.forEach((oldHP, index) => {
            const entity = newMap[index];
            if (!entity) return;

            const hpDiff = oldHP - entity.cHP;
            oldHPArr[index] = entity.cHP;

            if (hpDiff > 0 && attackerIndices.length > 0) {
                if (attackerIndices.length > 1) {
                    attackerIndices.forEach(attackerIndex => {
                        if (attackerIndex === castIndex) {
                            createLine(attackerIndex, index, hpDiff, isReverse);
                        }
                    });
                } else {
                    createLine(attackerIndices[0], index, hpDiff, isReverse);
                }
            }
        });
    }

    // 检测施法者,通过比较新旧魔法值找出施法者索引
    function detectCaster(oldMPArr, newMap) {
        let casterIndex = -1;
        Object.keys(newMap).forEach(index => {
            const newMP = newMap[index].cMP;
            if (newMP < oldMPArr[index]) {
                casterIndex = index;
            }
            oldMPArr[index] = newMP;
        });
        return casterIndex;
    }

    // 处理 WebSocket 消息,根据消息类型更新战斗状态并创建攻击动画
    function handleMessage(message) {
        if (isPaused) {
            return message;
        }

        let obj;
        try {
            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);

            const svg = document.getElementById('svg-container');
            if (svg) {
                while (svg.firstChild) {
                    svg.removeChild(svg.firstChild);
                }
            }
            particlePool.length = 0;
        } 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);

            const castMonster = detectCaster(battleState.monstersMP, mMap);
            const castPlayer = detectCaster(battleState.playersMP, pMap);

            processDamage(battleState.monstersHP, mMap, castPlayer, playerIndices, false);
            processDamage(battleState.playersHP, pMap, castMonster, monsterIndices, true);
        }
        return message;
    }

    // 检测网页是否从后台恢复,当网页从后台恢复时清理 SVG 容器中的元素
    function addVisibilityChangeListener() {
        document.addEventListener('visibilitychange', function () {
            if (document.visibilityState === 'hidden') {
                isPaused = true;
            } else if (document.visibilityState === 'visible') {
                isPaused = false;
                const svg = document.getElementById('svg-container');
                if (svg) {
                    while (svg.firstChild) {
                        svg.removeChild(svg.firstChild);
                    }
                }
                document.querySelectorAll('[id^="mwi-hit-tracker-"]').forEach(el => {
                    if (el) {
                        el.remove();
                    }
                });
                document.querySelectorAll('circle[fill^="rgba"]').forEach(el => {
                    if (el.parentNode === svg) {
                        el.parentNode.removeChild(el);
                    }
                });
            }
        });
    }
    // 启动初始化函数
    init();

})();