MWI-Hit-Tracker-More-Animation

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

// ==UserScript==
// @name         MWI-Hit-Tracker-More-Animation
// @namespace    http://tampermonkey.net/
// @version      1.9.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';

    // 状态变量,存储战斗相关信息
    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);
    //存储特效的勾选状态,默认全勾选
    // 索引含义:0-伤害数字,1-线条绘制,2-粒子拖尾,3-击中特效,4-震动特效
    const effectDrawEnabled = new Array(5).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);
        }

        // 读取特效设置
        const effectLs = localStorage.getItem("MWI_Hit_Tracker_Effect_Settings");
        if (effectLs) {
            const effectLsObj = JSON.parse(effectLs);
            effectDrawEnabled.splice(0, effectDrawEnabled.length, ...effectLsObj.effectDrawEnabled);
        }
    }

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

        // 保存特效设置
        const effectSettings = {
            effectDrawEnabled: effectDrawEnabled
        };
        localStorage.setItem("MWI_Hit_Tracker_Effect_Settings", JSON.stringify(effectSettings));
    }

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

    // 创建自定义颜色按钮
    /**
     * 创建自定义颜色设置按钮,用于打开设置弹出窗口,可设置玩家攻击线条颜色和显示状态。
     */
    function createCustomColorButton() {
        // 使用选择器,查找按钮的父元素
        const 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");
        // 获取参考标签,如果 tabsContainer 存在,则取其第二个子元素
        const 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自定义设置';

        // 获取标签容器中的最后一个标签
        const lastTab = tabsContainer.children[tabsContainer.children.length - 1];

        // 遍历标签容器中的所有标签,检查是否存在文本内容为"商品列表"的标签,如果存在则返回
        for (let i = 0; i < tabsContainer.children.length; i++) {
            if (tabsContainer.children[i].textContent === "商品列表") {
                return;
            }
        }

        // 将自定义颜色设置按钮插入到最后一个标签之后
        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;
            }
            .expandable-section {
                cursor: pointer;
                padding: 10px;
                background-color: #e0e0e0;
                margin-bottom: 10px;
                border-radius: 5px;
            }
            .expandable-content {
                display: none;
                padding-left: 20px;
            }
            .expandable-content.show {
                display: block;
            }
            .draggable {
                cursor: move;
            }
        `;
        // 将样式元素添加到文档头部
        document.head.appendChild(style);

        // 为自定义颜色设置按钮添加点击事件监听器
        customColorButton.addEventListener('click', () => {
            // 检查文档中是否已存在类名为 "自定义菜单" 的元素,如果有则销毁它
            const customMenu = document.querySelector('.自定义菜单');
            if (customMenu) {
                document.body.removeChild(customMenu);
                return;
            }
            // 创建弹出窗口元素并添加类名 "自定义菜单"
            const popup = document.createElement('div');
            popup.classList.add('自定义菜单');
            // 设置弹出窗口的定位方式为固定定位
            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';

            // 使弹出窗口可拖动
            let isDragging = false;
            let startX, startY, initialX, initialY;

            // 鼠标按下事件,开始拖动
            popup.addEventListener('mousedown', (e) => {
                isDragging = true;
                startX = e.clientX;
                startY = e.clientY;
                initialX = popup.offsetLeft;
                initialY = popup.offsetTop;
                // 移除居中 transform 样式
                //popup.style.transform = 'none';
            });

            // 鼠标移动事件,处理拖动
            document.addEventListener('mousemove', (e) => {
                if (isDragging) {
                    e.preventDefault();
                    const dx = e.clientX - startX;
                    const dy = e.clientY - startY;
                    popup.style.left = initialX + dx + 'px';
                    popup.style.top = initialY + dy + 'px';
                }
            });

            // 鼠标释放事件,结束拖动
            document.addEventListener('mouseup', () => {
                isDragging = false;
            });

            // 封装创建可展开部分的函数
            function createExpandableSection(title, contentGenerator = null) {
                // 创建可展开区域元素
                const expandableSection = document.createElement('div');
                // 为可展开区域元素设置类名
                expandableSection.className = 'expandable-section draggable';
                // 初始化展开状态为未展开
                let isExpanded = false;

                // 根据展开状态设置可展开区域的显示文本
                function updateExpandableSectionText() {
                    expandableSection.textContent = isExpanded ? `${title}          ▼` : `${title}           ▶`;
                }

                // 初始设置文本
                updateExpandableSectionText();

                // 创建可展开内容元素
                const expandableContent = document.createElement('div');
                // 为可展开内容元素设置类名
                expandableContent.className = 'expandable-content';

                // 如果有内容生成函数,则调用该函数生成内容
                if (contentGenerator) {
                    contentGenerator(expandableContent);
                }

                // 修改点击事件监听器,更新展开状态并更新文本,同时切换可展开内容的显示状态
                expandableSection.addEventListener('click', () => {
                    isExpanded = !isExpanded;
                    expandableContent.classList.toggle('show');
                    updateExpandableSectionText();
                });

                return { expandableSection, expandableContent };
            }

            // 生成玩家和颜色部分内容的函数
            function generatePlayerColorContent(expandableContent) {
                // 定义玩家名称数组
                const players = ['玩家一', '玩家二', '玩家三', '玩家四', '玩家五', '待定', '敌人'];

                // 封装创建勾选框和标签的通用函数
                function createCheckboxAndLabel(container, labelText, checked, changeHandler) {
                    // 创建勾选框元素
                    const checkbox = document.createElement('input');
                    checkbox.type = 'checkbox';
                    checkbox.checked = checked;
                    checkbox.addEventListener('change', changeHandler);
                    container.appendChild(checkbox);

                    // 创建标签元素
                    const label = document.createElement('span');
                    label.textContent = labelText;
                    label.style.flex = '1';
                    label.style.fontSize = '14px';
                    label.style.marginLeft = '10px';
                    container.appendChild(label);
                }

                // 遍历玩家名称数组,为每个玩家创建颜色选择器和预览
                players.forEach((player, index) => {
                    // 创建容器元素,用于包裹勾选框、标签、颜色选择器和预览
                    const container = document.createElement('div');
                    container.style.marginBottom = '15px';
                    container.style.display = 'flex';
                    container.style.alignItems = 'center';

                    // 创建勾选框和标签
                    createCheckboxAndLabel(container, `${player}: `, playerDrawEnabled[index], (e) => {
                        playerDrawEnabled[index] = e.target.checked;
                    });

                    // 创建颜色选择器元素
                    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);

                    // 将容器元素添加到可展开内容元素中
                    expandableContent.appendChild(container);
                });
            }

            // 生成特效自定义部分内容的函数
            function generateEffectCustomContent(expandableContent) {
                // 定义特效名称集合,方便后期维护(调整顺序,线条绘制移到第一位)
                const effectNames = [
                    '线条绘制',
                    '伤害数字',
                    '击中特效',
                    '震动'
                ];

                // 定义子菜单配置
                const subMenuConfig = {
                    '伤害数字': ['粒子拖尾'] // 粒子拖尾作为伤害数字的子菜单
                };

                // 封装创建勾选框和标签的通用函数
                function createCheckboxAndLabel(container, labelText, checked, changeHandler, indent = false) {
                    // 创建勾选框元素
                    const checkbox = document.createElement('input');
                    checkbox.type = 'checkbox';
                    checkbox.checked = checked;
                    checkbox.addEventListener('change', changeHandler);

                    // 如果需要缩进(用于子菜单项)
                    if (indent) {
                        const indentSpan = document.createElement('span');
                        indentSpan.innerHTML = '&nbsp;&nbsp;&nbsp;&nbsp;';
                        container.appendChild(indentSpan);
                    }

                    container.appendChild(checkbox);

                    // 创建标签元素
                    const label = document.createElement('span');
                    label.textContent = labelText;
                    label.style.flex = '1';
                    label.style.fontSize = '14px';
                    label.style.marginLeft = '10px';
                    container.appendChild(label);

                    return checkbox;
                }

                // 保存子菜单的勾选框引用,用于控制可见性和勾选状态
                const subMenuCheckboxes = {};

                // 处理父菜单项的变更事件,控制子菜单项
                function handleParentMenuChange(parentName, isChecked) {
                    if (subMenuConfig[parentName]) {
                        subMenuConfig[parentName].forEach(subItem => {
                            if (subMenuCheckboxes[`${parentName}_${subItem}`]) {
                                // 当父菜单取消勾选时,禁用子菜单项
                                subMenuCheckboxes[`${parentName}_${subItem}`].disabled = !isChecked;
                                if (!isChecked) {
                                    // 当父菜单取消勾选时,也取消子菜单的勾选
                                    subMenuCheckboxes[`${parentName}_${subItem}`].checked = false;
                                    // 更新对应的特效状态
                                    if (subItem === '粒子拖尾') {
                                        effectDrawEnabled[2] = false;
                                    }
                                }
                            }
                        });
                    }
                    saveSettings();
                }

                // 遍历特效名称数组,为每个特效创建勾选框和标签
                effectNames.forEach((effect, index) => {
                    // 创建容器元素,用于包裹勾选框和标签
                    const container = document.createElement('div');
                    container.style.marginBottom = '15px';
                    container.style.display = 'flex';
                    container.style.alignItems = 'center';

                    // 使线条绘制对应index=1,伤害数字对应index=0,击中特效对应index=3,震动对应index=4
                    let effectIndex;
                    if (effect === '线条绘制') effectIndex = 1;
                    else if (effect === '伤害数字') effectIndex = 0;
                    else if (effect === '击中特效') effectIndex = 3;
                    else if (effect === '震动') effectIndex = 4;

                    // 创建勾选框和标签
                    const checkbox = createCheckboxAndLabel(container, effect, effectDrawEnabled[effectIndex], (e) => {
                        // 更新全局变量effectDrawEnabled的状态
                        effectDrawEnabled[effectIndex] = e.target.checked;

                        // 如果这个特效有子菜单,处理子菜单的可见性和状态
                        handleParentMenuChange(effect, e.target.checked);

                        // 保存设置
                        saveSettings();
                    });

                    // 将容器元素添加到可展开内容元素中
                    expandableContent.appendChild(container);

                    // 处理子菜单
                    if (subMenuConfig[effect]) {
                        subMenuConfig[effect].forEach(subItem => {
                            // 创建子菜单项容器
                            const subContainer = document.createElement('div');
                            subContainer.style.marginBottom = '10px';
                            subContainer.style.marginLeft = '20px';
                            subContainer.style.display = 'flex';
                            subContainer.style.alignItems = 'center';

                            let subEffectIndex;
                            if (subItem === '粒子拖尾') subEffectIndex = 2;

                            // 创建子菜单项勾选框和标签
                            const subCheckbox = createCheckboxAndLabel(subContainer, subItem, effectDrawEnabled[subEffectIndex], (e) => {
                                // 更新全局变量effectDrawEnabled的状态
                                effectDrawEnabled[subEffectIndex] = e.target.checked;
                                // 保存设置
                                saveSettings();
                            }, true);

                            // 如果父菜单未勾选,禁用子菜单项
                            subCheckbox.disabled = !effectDrawEnabled[effectIndex];

                            // 保存子菜单勾选框引用
                            subMenuCheckboxes[`${effect}_${subItem}`] = subCheckbox;

                            // 将子菜单容器添加到可展开内容元素中
                            expandableContent.appendChild(subContainer);
                        });
                    }
                });
            }

            // 创建玩家和颜色可展开部分
            const { expandableSection: playerColorSection, expandableContent: playerColorContent } = createExpandableSection('玩家和颜色', generatePlayerColorContent);
            popup.appendChild(playerColorSection);
            popup.appendChild(playerColorContent);

            // 创建特效自定义可展开部分
            const { expandableSection: effectCustomSection, expandableContent: effectCustomContent } = createExpandableSection('特效自定义', generateEffectCustomContent);
            popup.appendChild(effectCustomSection);
            popup.appendChild(effectCustomContent);

            // 创建特效参数可展开部分
            const { expandableSection: effectParamsSection, expandableContent: effectParamsContent } = createExpandableSection('特效参数');
            popup.appendChild(effectParamsSection);
            popup.appendChild(effectParamsContent);

            // 创建重置按钮元素
            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', () => {
                // 添加确认对话框
                const confirmReset = confirm('确定要重置所有设置吗?这将恢复所有默认值。');

                // 如果用户取消,则不执行重置操作
                if (!confirmReset) {
                    return;
                }

                // 重置线条颜色数组
                lineColor.splice(0, lineColor.length, ...initialLineColor);
                // 重置滤镜颜色数组
                filterColor.splice(0, filterColor.length, ...initialFilterColor);
                // 重置玩家显示状态数组,全部设置为勾选状态
                playerDrawEnabled.fill(true);
                // 重置特效选项数组,全部设置为勾选状态
                effectDrawEnabled.fill(true);

                // 保存重置后的设置
                saveSettings();

                // 更新颜色选择器和预览
                // 获取玩家和颜色可展开内容中的所有颜色选择器元素
                const colorInputs = playerColorContent.querySelectorAll('input[type="color"]');
                // 获取玩家和颜色可展开内容中的所有颜色预览元素
                const previews = playerColorContent.querySelectorAll('div:last-child');
                // 遍历颜色选择器和预览元素,更新其值和背景颜色
                colorInputs.forEach((input, index) => {
                    input.value = initialLineColor[index];
                    previews[index].style.backgroundColor = initialLineColor[index];
                });

                // 更新特效选项的勾选状态
                // 获取所有效果设置部分的勾选框
                const effectCheckboxes = effectCustomContent.querySelectorAll('input[type="checkbox"]');
                // 遍历所有勾选框,将其设置为勾选状态
                effectCheckboxes.forEach(checkbox => {
                    checkbox.checked = true;

                    // 如果这是子菜单项,确保它是启用的
                    if (checkbox.disabled) {
                        checkbox.disabled = false;
                    }
                });

                // 更新玩家勾选框状态
                const playerCheckboxes = playerColorContent.querySelectorAll('input[type="checkbox"]');
                playerCheckboxes.forEach(checkbox => {
                    checkbox.checked = true;
                });

                //关闭菜单后再打开新的菜单
                document.body.removeChild(popup);
                //点击customColorButton
                customColorButton.click();
            });

            // 创建保存按钮元素
            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('初始化函数已调用。');
        // 先加载设置
        readSettings();
        // 劫持 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;

        // 检查震动特效是否启用
        if (!effectDrawEnabled[4]) 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;

        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 effectPosition = {
            x: 0,
            y: 0
        };

        let path = null;
        let text = null;
        let pathLength = 0;
        let hasTextOrPath = false;

        // 只有当线条绘制特效启用时才创建路径
        if (effectDrawEnabled[1]) {
            hasTextOrPath = true;
            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';
            }

            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));
            pathLength = path.getTotalLength();
            path.style.strokeDasharray = pathLength;
            path.style.strokeDashoffset = pathLength;

            // 计算路径终点位置,用于后续触发击中特效
            const endPoint = path.getPointAtLength(pathLength);
            effectPosition.x = endPoint.x;
            effectPosition.y = endPoint.y;

            frag.appendChild(path);
        }

        // 只有当伤害数字特效启用时才创建文本
        if (effectDrawEnabled[0]) {
            hasTextOrPath = true;
            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);
        }

        // 如果没有任何可视化效果但仍然需要处理伤害,直接触发震动和击中特效
        if (!hasTextOrPath) {
            // 获取目标元素的位置用于粒子效果
            const targetCenter = getElementCenter(targetElem);
            effectPosition.x = targetCenter.x;
            effectPosition.y = targetCenter.y;

            // 独立触发震动和击中特效
            if (effectDrawEnabled[3]) {
                createParticleEffect(effectPosition.x, effectPosition.y, lineColor[index]);
            }

            if (effectDrawEnabled[4]) {
                shakeTarget(targetElem);
            }
            return;
        }

        svg.appendChild(frag);

        // 如果创建了路径,设置路径动画
        if (path) {
            setTimeout(() => {
                requestAnimationFrame(() => {
                    path.style.transition = 'stroke-dashoffset 1s linear';
                    path.style.strokeDashoffset = '0';
                });
            }, 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();

                        // 如果没有启用伤害数字特效,在路径移除后触发击中特效和震动
                        if (!text) {
                            if (effectDrawEnabled[3]) {
                                createParticleEffect(effectPosition.x, effectPosition.y, lineColor[index]);
                            }

                            if (effectDrawEnabled[4]) {
                                shakeTarget(targetElem);
                            }
                        }
                    };
                    path.addEventListener('transitionend', removePath, { once: true });
                });
            }, 900);
        }

        // 如果创建了文本,设置文本动画
        if (text) {
            // 如果同时有路径和文本,让文本沿着路径移动
            if (path) {
                setTimeout(() => {
                    requestAnimationFrame(() => {
                        animateText(path, text, pathLength, lineColor[index], targetElem);
                    });
                }, 100);
            } else {
                // 如果只有文本没有路径,创建一个虚拟路径用于文本动画
                const virtualPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
                virtualPath.setAttribute('d', createParabolaPath(startElem, endElem, reversed));
                const virtualPathLength = virtualPath.getTotalLength();

                // 计算虚拟路径终点位置,用于后续触发击中特效
                const endPoint = virtualPath.getPointAtLength(virtualPathLength);
                effectPosition.x = endPoint.x;
                effectPosition.y = endPoint.y;

                // 直接设置文本动画,不添加虚拟路径到DOM
                animateText(virtualPath, text, virtualPathLength, lineColor[index], targetElem);
            }
        }
    }

    // 从对象池获取粒子元素
    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;
        // 击中特效的启用状态已在调用前检查,不需要再检查
        // if (!effectDrawEnabled[3]) 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 实现更流畅的动画
    /**
     * 执行文本动画,让文本沿着指定路径移动,并在移动过程中产生粒子效果。
     *
     * @param {SVGPathElement} path - 文本移动的路径元素。
     * @param {SVGTextElement} text - 要进行动画的文本元素。
     * @param {number} pathLength - 路径的总长度。
     * @param {string} color - 文本和粒子的颜色。
     * @param {HTMLElement} targetElem - 目标元素,用于震动效果。
     */
    function animateText(path, text, pathLength, color, targetElem) {
        // 动画配置对象,包含动画持续时间、淡入开始和结束时间、粒子生成间隔
        const animationConfig = {
            duration: 1350, // 动画总持续时间,单位为毫秒
            fadeInStart: 0.0, // 淡入开始的进度比例
            fadeInEnd: 0.3, // 淡入结束的进度比例
            particleInterval: 3 // 粒子生成的间隔百分比
        };

        let startTime = null; // 动画开始的时间戳
        let lastParticleFrame = 0; // 上一次生成粒子时的进度百分比

        /**
         * 动画循环函数,使用 requestAnimationFrame 不断更新文本和粒子的状态。
         *
         * @param {number} currentTime - 当前的时间戳。
         */
        function animate(currentTime) {
            // 如果脚本处于暂停状态,则停止动画
            if (isPaused) return;

            // 如果动画还未开始,记录当前时间为开始时间
            if (!startTime) startTime = currentTime;

            // 计算从动画开始到现在经过的时间
            const elapsed = currentTime - startTime;
            // 计算动画的进度,取值范围为 0 到 1
            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);
            }

            /**
             * 创建粒子拖尾效果
             * @param {Object} point - 粒子的起始坐标,包含 x 和 y 属性
             * @param {string} color - 粒子的填充颜色
             */
            function createParticleTrail(point, color) {
                // 粒子拖尾特效的启用状态已在调用前检查,不需要再检查
                // if (!effectDrawEnabled[2]) return;

                // 从粒子对象池中获取一个粒子
                const particle = getParticleFromPool();
                // 设置粒子的半径
                particle.setAttribute('r', '2');
                // 设置粒子的填充颜色
                particle.setAttribute('fill', color);
                // 设置粒子的 x 坐标,添加随机偏移
                particle.setAttribute('cx', point.x + (Math.random() - 0.5) * 10);
                // 设置粒子的 y 坐标,添加随机偏移
                particle.setAttribute('cy', point.y + (Math.random() - 0.5) * 10);
                // 设置粒子的初始透明度为 1
                particle.style.opacity = 1;
                // 设置粒子的过渡效果
                particle.style.transition = 'all 0.2s ease-out';
                // 告诉浏览器哪些属性会发生变化,优化性能
                particle.style.willChange = 'opacity, transform';

                // 获取 SVG 容器元素
                const svg = document.getElementById('svg-container');
                // 将粒子添加到 SVG 容器中
                svg.appendChild(particle);

                // 在下一帧请求动画,开始粒子的淡出效果
                requestAnimationFrame(() => {
                    particle.style.opacity = 0;
                    // 监听粒子过渡结束事件,过渡结束后将粒子放回对象池
                    particle.addEventListener('transitionend', () => {
                        returnParticleToPool(particle);
                    }, { once: true });
                });
            }

            // 如果动画进度小于 1,继续请求下一帧动画
            if (progress < 1) {
                requestAnimationFrame(animate);

                // 只在粒子拖尾特效启用时才创建粒子
                if (effectDrawEnabled[2]) {
                    // 调用粒子拖尾效果函数,传入粒子的起始坐标和颜色
                    createParticleTrail(point, color);
                }
            }
            // 动画进度达到 1,执行结束动画
            else {
                // 设置文本的过渡效果
                text.style.transition = 'all 0.2s ease-out';
                // 放大文本
                text.style.transform = 'scale(1.5)';
                // 让文本透明
                text.style.opacity = 0;

                // 保存文本最终位置,用于后续触发击中特效
                const finalX = text.getAttribute('x');
                const finalY = text.getAttribute('y');

                // 延迟 100 毫秒后执行后续操作
                setTimeout(() => {
                    // 移除文本元素
                    text.remove();

                    // 独立触发击中特效和震动
                    if (effectDrawEnabled[3]) {
                        // 在文本的位置创建粒子效果
                        createParticleEffect(finalX, finalY, color);
                    }

                    if (effectDrawEnabled[4]) {
                        // 触发震动效果
                        shakeTarget(targetElem);
                    }
                }, 100);
            }
        }

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

    // 创建线条动画,根据攻击信息创建攻击路径和伤害数字动画
    /**
     * 创建从一个角色到另一个角色的攻击线条,并触发相应的特效。
     *
     * @param {number} from - 攻击发起者的索引。
     * @param {number} to - 攻击目标的索引。
     * @param {number} hpDiff - 伤害值,即生命值的差值。
     * @param {boolean} [reversed=false] - 指示攻击是否是反向的,默认为 false。
     */
    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;

        // 查找 SVG 容器元素
        let svgContainer = document.getElementById('svg-container');

        // 如果 SVG 容器元素不存在,则创建一个新的 SVG 容器
        if (!svgContainer) {
            // 定义 SVG 的命名空间
            const svgNS = 'http://www.w3.org/2000/svg';
            // 创建 SVG 元素
            svgContainer = document.createElementNS(svgNS, '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 属性
            const setViewBox = () => {
                // 获取窗口的宽度
                const width = window.innerWidth;
                // 获取窗口的高度
                const height = window.innerHeight;
                // 设置 SVG 的 viewBox 属性
                svgContainer.setAttribute('viewBox', `0 0 ${width} ${height}`);
            };

            // 首次调用 setViewBox 函数,设置 SVG 的 viewBox 属性
            setViewBox();
            // 设置 SVG 的 preserveAspectRatio 属性
            svgContainer.setAttribute('preserveAspectRatio', 'none');
            // 将 SVG 元素添加到游戏主面板中
            gamePanel.appendChild(svgContainer);

            // 如果窗口大小改变监听器还未添加,则添加该监听器
            if (!isResizeListenerAdded) {
                // 监听窗口大小改变事件,当窗口大小改变时调用 setViewBox 函数
                window.addEventListener('resize', setViewBox);
                // 标记窗口大小改变监听器已添加
                isResizeListenerAdded = true;
            }
        }

        // 根据攻击是否反向,确定攻击发起者的索引
        const originIndex = reversed ? to : from;
        // 调用 createEffect 函数,创建攻击特效
        createEffect(effectFrom, effectTo, hpDiff, originIndex, reversed);
    }

    // 处理伤害信息,根据新旧生命值计算伤害差值并创建动画
    /**
     * 处理伤害数据,根据旧的生命值数组和新的实体映射,计算生命值差值,并在满足条件时创建攻击线条。
     *
     * @param {Array} oldHPArr - 旧的生命值数组,存储每个实体的旧生命值。
     * @param {Object} newMap - 新的实体映射,键为实体索引,值为包含当前生命值(cHP)的实体对象。
     * @param {number} castIndex - 施法者的索引。
     * @param {Array} attackerIndices - 攻击者的索引数组。
     * @param {boolean} [isReverse=false] - 可选参数,指示攻击方向是否反转。
     */
    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;

            // 如果生命值差值大于 0 且攻击者索引数组不为空
            if (hpDiff > 0 && attackerIndices.length > 0) {
                // 如果攻击者索引数组长度大于 1
                if (attackerIndices.length > 1) {
                    // 遍历攻击者索引数组
                    attackerIndices.forEach(attackerIndex => {
                        // 如果攻击者索引等于施法者索引
                        if (attackerIndex === castIndex) {
                            // 调用 createLine 函数创建攻击线条
                            createLine(attackerIndex, index, hpDiff, isReverse);
                        }
                    });
                } else {
                    // 如果攻击者索引数组长度为 1,直接调用 createLine 函数创建攻击线条
                    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();

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址