Greasy Fork

Greasy Fork is available in English.

MWI-Hit-Tracker-change

战斗过程中实时显示攻击命中目标

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    // 存储怪物当前的生命值
    let monstersHP = [];
    // 存储怪物当前的魔法值
    let monstersMP = [];
    // 存储玩家当前的生命值
    let playersHP = [];
    // 存储玩家当前的魔法值
    let playersMP = [];
    // 调用 hookWS 函数,用于劫持 WebSocket 消息
    hookWS();

    // 创建 lineFlash 动画样式,用于路径闪烁效果
    const style = document.createElement('style');
    style.textContent = `
        @keyframes lineFlash {
            0% {
                stroke-opacity: 1; // 起始时路径的透明度为 1
            }
            50% {
                stroke-opacity: 0.3; // 中间时路径的透明度为 0.3
            }
            100% {
                stroke-opacity: 1; // 结束时路径的透明度恢复为 1
            }
        }
    `;
    document.head.appendChild(style);

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

        // 将 data 属性的 getter 函数替换为自定义的 hookedGet 函数
        dataProperty.get = hookedGet;
        // 重新定义 MessageEvent 原型上的 data 属性
        Object.defineProperty(MessageEvent.prototype, "data", dataProperty);

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

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

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

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

    // 创建抛物线路径的函数
    function createParabolaPath(startElem, endElem, reversed = false) {
        // 获取起始元素的中心点坐标
        const start = getElementCenter(startElem);
        // 获取结束元素的中心点坐标
        const end = getElementCenter(endElem);

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

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

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

    // 定义线条颜色数组,用于不同角色的攻击线条颜色
    const lineColor = [
        "rgba(255, 99, 132, 1)", // 浅粉色
        "rgba(54, 162, 235, 1)", // 浅蓝色
        "rgba(255, 206, 86, 1)", // 浅黄色
        "rgba(75, 192, 192, 1)", // 浅绿色
        "rgba(153, 102, 255, 1)", // 浅紫色
        "rgba(255, 159, 64, 1)", // 浅橙色
        "rgba(255, 0, 0, 1)", // 敌人攻击颜色
    ];
    // 定义滤镜颜色数组,用于线条的外发光效果颜色
    const filterColor = [
        "rgba(255, 99, 132, 0.8)", // 浅粉色
        "rgba(54, 162, 235, 0.8)", // 浅蓝色
        "rgba(255, 206, 86, 0.8)", // 浅黄色
        "rgba(75, 192, 192, 0.8)", // 浅绿色
        "rgba(153, 102, 255, 0.8)", // 浅紫色
        "rgba(255, 159, 64, 0.8)", // 浅橙色
        "rgba(255, 0, 0, 0.8)", // 敌人攻击颜色
    ];
    // 创建动画效果的函数
    function createEffect(startElem, endElem, hpDiff, index, reversed = false) {
        // 初始化线条的宽度
        let strokeWidth = '1px';
        // 初始化滤镜的宽度
        let filterWidth = '1px';
        // 根据伤害值调整线条和滤镜的宽度
        if (hpDiff >= 1000) {
            strokeWidth = '5px';
            filterWidth = '6px';
        } else if (hpDiff >= 700) {
            strokeWidth = '4px';
            filterWidth = '5px';
        } else if (hpDiff >= 500) {
            strokeWidth = '3px';
            filterWidth = '4px';
        } else if (hpDiff >= 300) {
            strokeWidth = '2px';
            filterWidth = '3px';
        } else if (hpDiff >= 100) {
            filterWidth = '2px';
        }
        // 尝试定位伤害数字 div
        if (reversed) {
            const dmgDivs = startElem.querySelector('.CombatUnit_splatsContainer__2xcc0').querySelectorAll('div');
            for (const div of dmgDivs) {
                if (div.innerText.trim() === '') {
                    startElem = div;
                    break;
                }
            }
        } else {
            const dmgDivs = endElem.querySelector('.CombatUnit_splatsContainer__2xcc0').querySelectorAll('div');
            for (const div of dmgDivs) {
                if (div.innerText.trim() === '') {
                    endElem = div;
                    break;
                }
            }
        }

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

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

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

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

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

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

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

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

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

        // 定义伤害数字动画函数
        const animateText = () => {
            // 检查当前帧是否小于总帧数
            if (currentFrame < numFrames) {
                // 根据当前帧计算在路径上的位置
                const point = path.getPointAtLength((currentFrame / numFrames) * pathLength);
                // 设置伤害数字元素的 x 坐标为路径上当前点的 x 坐标
                text.setAttribute('x', point.x);
                // 设置伤害数字元素的 y 坐标为路径上当前点的 y 坐标
                text.setAttribute('y', point.y);
                // 根据当前帧计算伤害数字元素的透明度,使其逐渐显示
                text.style.opacity = 0.7 + 0.3 * (currentFrame / numFrames);
                // 当前帧序号加 1
                currentFrame++;
                // 递归调用 animateText 函数,在指定的帧间隔时间后执行
                setTimeout(animateText, frameDuration);
            } else {
                // 当动画结束后,开始执行魔法击中的消失动画
                text.style.transition = 'all 0.2s ease-out'; // 减少消失动画时长
                text.style.transform = 'scale(1.5)';
                text.style.opacity = 0;

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

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

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

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

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

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

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

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

    // 标记是否已经添加了窗口大小改变的监听器
    let isResizeListenerAdded = false;
    // 创建线条动画的函数
    function createLine(from, to, hpDiff, reversed = false) {
        // 获取玩家区域的容器元素
        const container = document.querySelector(".BattlePanel_playersArea__vvwlB");
        // 检查容器元素是否存在且有子元素
        if (container && container.children.length > 0) {
            // 获取玩家容器元素
            const playersContainer = container.children[0];
            // 获取起始元素
            const effectFrom = playersContainer.children[from];
            // 获取怪物容器元素
            const monsterContainer = document.querySelector(".BattlePanel_monstersArea__2dzrY").children[0];
            // 获取结束元素
            const effectTo = monsterContainer.children[to];
            // 获取 SVG 容器元素
            const svg = document.getElementById('svg-container');
            // 如果 SVG 容器元素不存在
            if (!svg) {
                // 创建一个 SVG 容器元素
                const svgContainer = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
                // 设置 SVG 容器元素的 ID
                svgContainer.id = 'svg-container';
                // 设置 SVG 容器元素的样式
                Object.assign(svgContainer.style, {
                    position: 'fixed',
                    top: '0',
                    left: '0',
                    width: '100%',
                    height: '100%',
                    pointerEvents: 'none',
                    overflow: 'visible',
                    zIndex: '190'
                });

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

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

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

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

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

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

    // 检测网页是否从后台恢复的函数
    function addVisibilityChangeListener() {
        document.addEventListener('visibilitychange', function() {
            if (document.visibilityState === 'visible') {
                // 移除之前创建的所有元素
                const svg = document.getElementById('svg-container');
                if (svg) {
                    while (svg.firstChild) {
                        svg.removeChild(svg.firstChild);
                    }
                }
            }
        });
    }

    // 添加监听器
    addVisibilityChangeListener();

})();