// ==UserScript== // @name MWI-Hit-Tracker // @namespace http://tampermonkey.net/ // @version 0.8 // @description 战斗过程中实时显示攻击命中目标 // @author Artintel // @license MIT // @match https://www.milkywayidle.com/* // @match https://test.milkywayidle.com/* // @icon https://www.milkywayidle.com/favicon.svg // @grant none // @downloadURL none // ==/UserScript== (function() { 'use strict'; let monstersHP = []; let monstersMP = []; let playersHP = []; let playersMP = []; hookWS(); function hookWS() { const dataProperty = Object.getOwnPropertyDescriptor(MessageEvent.prototype, "data"); const oriGet = dataProperty.get; dataProperty.get = hookedGet; Object.defineProperty(MessageEvent.prototype, "data", dataProperty); 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); } const message = oriGet.call(this); Object.defineProperty(this, "data", { value: message }); // Anti-loop return handleMessage(message); } } // 动画效果 const AnimationManager = { maxPaths: 50, // 最大同时存在path数 activePaths: new Set(), // 当前活动路径集合 canCreate() { // 数量检查 return this.activePaths.size < this.maxPaths; }, addPath(path) { this.activePaths.add(path); }, removePath(path) { this.activePaths.delete(path); } }; 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 curveHeight = -120; // 数值越大弧度越高(负值向上弯曲) 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'); // 获取所有 div for (const div of dmgDivs) { if (div.innerText.trim() === '') { startElem = div; break; } } } else { const dmgDivs = endElem.querySelector('.CombatUnit_splatsContainer__2xcc0').querySelectorAll('div'); // 获取所有 div for (const div of dmgDivs) { if (div.innerText.trim() === '') { endElem = div; break; } } } const svg = document.getElementById('svg-container'); 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]+')' }); path.setAttribute('d', createParabolaPath(startElem, endElem, reversed)); // 入场动画 const length = path.getTotalLength(); path.style.strokeDasharray = length; path.style.strokeDashoffset = length; svg.appendChild(path); // 注册到管理器 AnimationManager.addPath(path); // 移除逻辑 const cleanUp = () => { try { if (path.parentNode) { svg.removeChild(path); } AnimationManager.removePath(path); } catch(e) { console.error('Svg path cleanup error:', e); } }; // 绘制动画 requestAnimationFrame(() => { path.style.transition = 'stroke-dashoffset 0.1s linear'; path.style.strokeDashoffset = '0'; }); // 自动移除 setTimeout(() => { // 1. 先重置transition path.style.transition = 'none'; // 2. 重新设置dasharray实现反向动画 requestAnimationFrame(() => { // 保持当前可见状态 path.style.strokeDasharray = length; path.style.strokeDashoffset = '0'; // 3. 开始消失动画 path.style.transition = 'stroke-dashoffset 0.3s cubic-bezier(0.4, 0, 1, 1)'; path.style.strokeDashoffset = -length; // 4. 动画结束后移除 const removeElement = () => { //svg.removeChild(path); cleanUp(); path.removeEventListener('transitionend', removeElement); }; path.addEventListener('transitionend', removeElement); }); }, 600); // 强制清理保护 const forceCleanupTimer = setTimeout(cleanUp, 5000); // 5秒后强制移除 path.addEventListener('transitionend', () => clearTimeout(forceCleanupTimer)); // 自动移除 //setTimeout(() => { // path.style.opacity = '0'; // path.style.transition = 'opacity 0.1s linear'; // setTimeout(() => svg.removeChild(path), 500); //}, 800); } // 添加窗口resize监听 let isResizeListenerAdded = false; function createLine(from, to, hpDiff, reversed = false) { if (!AnimationManager.canCreate()) { return null; // 同时存在数量超出上限 } 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]; const svg = document.getElementById('svg-container'); if(!svg){ const svgContainer = document.createElementNS('http://www.w3.org/2000/svg', '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' }); // 设置SVG原生属性 svgContainer.setAttribute('viewBox', `0 0 ${window.innerWidth} ${window.innerHeight}`); svgContainer.setAttribute('preserveAspectRatio', 'none'); // 初始化viewBox const updateViewBox = () => { svgContainer.setAttribute('viewBox', `0 0 ${window.innerWidth} ${window.innerHeight}`); }; updateViewBox(); //playersContainer.appendChild(svgContainer); document.querySelector(".GamePage_mainPanel__2njyb").appendChild(svgContainer); //document.body.appendChild(svgContainer); // 添加resize监听(确保只添加一次) if (!isResizeListenerAdded) { window.addEventListener('resize', () => { updateViewBox(); }); isResizeListenerAdded = true; } } if (reversed) { createEffect(effectFrom, effectTo, hpDiff, to, reversed); } else { createEffect(effectFrom, effectTo, hpDiff, from, reversed); } } } function handleMessage(message) { 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(userIndex, mIndex, hpDiff); } }); } else { 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(pIndex, monsterIndex, hpDiff, true); } }); } else { createLine(pIndex, monsterIndices[0], hpDiff, true); } } } }); } return message; } })();