// ==UserScript==
// @name MWI-Hit-Tracker
// @namespace http://tampermonkey.net/
// @version 0.6
// @description 战斗过程中实时显示攻击命中目标
// @author Artintel
// @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();
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);
}
}
// 动画效果
function getElementCenter(element) {
const rect = element.getBoundingClientRect();
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';
}
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);
// 绘制动画
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);
path.removeEventListener('transitionend', removeElement);
};
path.addEventListener('transitionend', removeElement);
});
}, 600);
// 自动移除
//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) {
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;
}
})();