Greasy Fork is available in English.

MWI-Hit-Tracker-More-Animation

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

// ==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();

})();