// ==UserScript==
// @name MWI-Hit-Tracker-More-Animation
// @namespace http://tampermonkey.net/
// @version 1.9
// @description 战斗过程中实时显示攻击命中目标,增加了更多的特效(伤害数字、粒子拖尾、击中溅射、击中震动)
// @author Artintel (Artintel), Yuk111
// @license MIT
// @match https://www.milkywayidle.com/*
// @match https://test.milkywayidle.com/*
// @icon https://www.milkywayidle.com/favicon.svg
// @grant none
// ==/UserScript==
(function () {
'use strict';
// 状态变量,存储战斗相关信息
const battleState = {
monstersHP: [],
monstersMP: [],
playersHP: [],
playersMP: []
};
// 存储是否已添加窗口大小改变监听器
let isResizeListenerAdded = false;
// 标记脚本是否暂停
let isPaused = false;
// 粒子对象池
const particlePool = [];
// 标记按钮是否已添加
let isCustomColorButtonAdded = false;
// 保存初始颜色
const initialLineColor = [
"rgba(255, 99, 132, 1)", // 浅粉色
"rgba(54, 162, 235, 1)", // 浅蓝色
"rgba(255, 206, 86, 1)", // 浅黄色
"rgba(75, 192, 192, 1)", // 浅绿色
"rgba(153, 102, 255, 1)", // 浅紫色
"rgba(255, 159, 64, 1)", // 浅橙色
"rgba(255, 0, 0, 1)" // 敌人攻击颜色
];
const initialFilterColor = [
"rgba(255, 99, 132, 0.8)", // 浅粉色
"rgba(54, 162, 235, 0.8)", // 浅蓝色
"rgba(255, 206, 86, 0.8)", // 浅黄色
"rgba(75, 192, 192, 0.8)", // 浅绿色
"rgba(153, 102, 255, 0.8)", // 浅紫色
"rgba(255, 159, 64, 0.8)", // 浅橙色
"rgba(255, 0, 0, 0.8)" // 敌人攻击颜色
];
// 存储每个玩家的勾选状态,默认全部勾选
const playerDrawEnabled = new Array(7).fill(true);
// 定义线条颜色数组,用于不同角色的攻击线条颜色
const lineColor = [
"rgba(255, 99, 132, 1)", // 浅粉色
"rgba(54, 162, 235, 1)", // 浅蓝色
"rgba(255, 206, 86, 1)", // 浅黄色
"rgba(75, 192, 192, 1)", // 浅绿色
"rgba(153, 102, 255, 1)", // 浅紫色
"rgba(255, 159, 64, 1)", // 浅橙色
"rgba(255, 0, 0, 1)" // 敌人攻击颜色
];
// 定义滤镜颜色数组,用于线条的外发光效果颜色
const filterColor = [
"rgba(255, 99, 132, 0.8)", // 浅粉色
"rgba(54, 162, 235, 0.8)", // 浅蓝色
"rgba(255, 206, 86, 0.8)", // 浅黄色
"rgba(75, 192, 192, 0.8)", // 浅绿色
"rgba(153, 102, 255, 0.8)", // 浅紫色
"rgba(255, 159, 64, 0.8)", // 浅橙色
"rgba(255, 0, 0, 0.8)" // 敌人攻击颜色
];
// 从 localStorage 加载保存的设置
function readSettings() {
const ls = localStorage.getItem("MWI_Hit_Tracker_Settings");
if (ls) {
const lsObj = JSON.parse(ls);
lineColor.splice(0, lineColor.length, ...lsObj.lineColor);
filterColor.splice(0, filterColor.length, ...lsObj.filterColor);
playerDrawEnabled.splice(0, playerDrawEnabled.length, ...lsObj.playerDrawEnabled);
}
}
// 保存设置到 localStorage
function saveSettings() {
const settings = {
lineColor: lineColor,
filterColor: filterColor,
playerDrawEnabled: playerDrawEnabled
};
localStorage.setItem("MWI_Hit_Tracker_Settings", JSON.stringify(settings));
}
// 在初始化时加载设置
readSettings();
// 创建自定义颜色按钮
function createCustomColorButton() {
// 出警按钮父元素路径,使用 test.js 中的选择器
var tabsContainer = document.querySelector("#root > div > div > div.GamePage_gamePanel__3uNKN > div.GamePage_contentPanel__Zx4FH > div.GamePage_middlePanel__uDts7 > div.GamePage_mainPanel__2njyb > div > div:nth-child(1) > div > div > div > div.TabsComponent_tabsContainer__3BDUp > div > div > div");
var referenceTab = tabsContainer ? tabsContainer.children[1] : null;
if (!tabsContainer || !referenceTab) {
console.log('未找到目标元素,请检查选择器是否正确。');
return;
}
if (tabsContainer.querySelector('.Button_customColor__custom')) return;
// 创建按钮
const customColorButton = document.createElement('button');
// 只使用自定义类名
customColorButton.className = 'Button_customColor__custom css-1q2h7u5';
customColorButton.textContent = 'Hit自定义设置';
// 修改插入逻辑,将按钮插入到最后一个标签之后
var lastTab = tabsContainer.children[tabsContainer.children.length - 1];
lastTab.insertAdjacentElement('afterend', customColorButton);
// 添加按钮样式
const style = document.createElement('style');
style.innerHTML = `
.Button_customColor__custom {
background-color: #546ddb;
color: white;
border-radius: 5px;
padding: 5px 10px;
cursor: pointer;
transition: background-color 0.3s;
}
.Button_customColor__custom:hover {
background-color: #131419;
}`;
document.head.appendChild(style);
// 添加点击事件
customColorButton.addEventListener('click', () => {
// 创建弹出窗口
const popup = document.createElement('div');
popup.style.position = 'fixed';
popup.style.top = '50%';
popup.style.left = '50%';
popup.style.transform = 'translate(-50%, -50%)';
popup.style.backgroundColor = '#f9f9f9';
popup.style.padding = '30px';
popup.style.border = '2px solid #ddd';
popup.style.borderRadius = '10px';
popup.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)';
popup.style.zIndex = '9999';
popup.style.minWidth = '300px';
// 玩家名称数组
const players = ['玩家一', '玩家二', '玩家三', '玩家四', '玩家五', '待定', '敌人'];
// 为每个玩家创建颜色选择器和预览
players.forEach((player, index) => {
const container = document.createElement('div');
container.style.marginBottom = '15px';
container.style.display = 'flex';
container.style.alignItems = 'center';
// 创建勾选框
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = playerDrawEnabled[index];
checkbox.addEventListener('change', (e) => {
playerDrawEnabled[index] = e.target.checked;
});
container.appendChild(checkbox);
const label = document.createElement('span');
label.textContent = `${player}: `;
label.style.flex = '1';
label.style.fontSize = '14px';
label.style.marginLeft = '10px';
container.appendChild(label);
const colorInput = document.createElement('input');
colorInput.type = 'color';
colorInput.value = lineColor[index];
colorInput.addEventListener('input', (e) => {
if (playerDrawEnabled[index]) {
lineColor[index] = e.target.value;
filterColor[index] = e.target.value.replace('1)', '0.8)');
saveSettings(); // 保存设置
}
});
colorInput.style.marginRight = '10px';
const preview = document.createElement('div');
preview.style.width = '30px';
preview.style.height = '30px';
preview.style.border = '1px solid #ccc';
preview.style.borderRadius = '4px';
preview.style.backgroundColor = lineColor[index];
colorInput.addEventListener('input', (e) => {
preview.style.backgroundColor = e.target.value;
});
container.appendChild(colorInput);
container.appendChild(preview);
popup.appendChild(container);
});
// 创建重置按钮
const resetButton = document.createElement('button');
resetButton.textContent = '重置';
resetButton.style.backgroundColor = '#ff4444';
resetButton.style.color = 'white';
resetButton.style.border = 'none';
resetButton.style.borderRadius = '4px';
resetButton.style.padding = '8px 15px';
resetButton.style.marginRight = '10px';
resetButton.style.cursor = 'pointer';
resetButton.addEventListener('click', () => {
lineColor.splice(0, lineColor.length, ...initialLineColor);
filterColor.splice(0, filterColor.length, ...initialFilterColor);
playerDrawEnabled.fill(true); // 重置勾选状态
saveSettings(); // 保存重置后的设置
// 更新颜色选择器和预览
const colorInputs = popup.querySelectorAll('input[type="color"]');
const previews = popup.querySelectorAll('div:last-child');
colorInputs.forEach((input, index) => {
input.value = initialLineColor[index];
previews[index].style.backgroundColor = initialLineColor[index];
});
});
// 创建关闭按钮
const closeButton = document.createElement('button');
closeButton.textContent = '关闭';
closeButton.style.backgroundColor = '#2196F3';
closeButton.style.color = 'white';
closeButton.style.border = 'none';
closeButton.style.borderRadius = '4px';
closeButton.style.padding = '8px 15px';
closeButton.style.cursor = 'pointer';
closeButton.addEventListener('click', () => {
saveSettings()
document.body.removeChild(popup);
});
// 创建按钮容器
const buttonContainer = document.createElement('div');
buttonContainer.style.marginTop = '20px';
buttonContainer.style.display = 'flex';
buttonContainer.style.justifyContent = 'flex-end';
buttonContainer.appendChild(resetButton);
buttonContainer.appendChild(closeButton);
popup.appendChild(buttonContainer);
document.body.appendChild(popup);
});
// 标记按钮已添加
isCustomColorButtonAdded = true;
console.log('自定义颜色按钮已成功添加。');
}
// 循环检查按钮是否创建成功
function checkAndCreateButton() {
const created = createCustomColorButton();
if (!created) {
setTimeout(checkAndCreateButton, 500); // 每 500 毫秒检查一次
}
}
// 修改初始化函数,添加对自定义颜色按钮的调用
function init() {
console.log('初始化函数已调用。');
// 劫持 WebSocket 消息,以便处理战斗相关的消息
hookWS();
// 添加网页可见性变化监听器,当网页从后台恢复时进行清理操作
addVisibilityChangeListener();
// 创建动画样式,用于攻击路径的闪烁效果和目标震动效果
createAnimationStyle();
// 调用循环检查函数
checkAndCreateButton();
}
// 创建动画样式,包括路径闪烁和目标震动效果
function createAnimationStyle() {
// console.log('动画样式函数已调用。');
const style = document.createElement('style');
style.textContent = `
@keyframes lineFlash {
0% { stroke-opacity: 0.7; }
50% { stroke-opacity: 0.3; }
100% { stroke-opacity: 0.7; }
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
50% { transform: translateX(-1px); } /* 减小震动幅度 */
}
.mwht-shake {
animation: shake 0.2s cubic-bezier(.36,.07,.19,.97) forwards; /* 固定0.2秒持续时间 */
transform-origin: center;
position: relative;
z-index: 200;
}
`;
document.head.appendChild(style);
}
// 劫持 WebSocket 消息,拦截并处理战斗相关的消息
function hookWS() {
// console.log('劫持函数已调用。');
const dataProperty = Object.getOwnPropertyDescriptor(MessageEvent.prototype, "data");
const oriGet = dataProperty.get;
dataProperty.get = function hookedGet() {
const socket = this.currentTarget;
if (!(socket instanceof WebSocket)) {
return oriGet.call(this);
}
if (socket.url.indexOf("api.milkywayidle.com/ws") <= -1 && socket.url.indexOf("api-test.milkywayidle.com/ws") <= -1) {
return oriGet.call(this);
}
if (isPaused) {
return oriGet.call(this);
}
const message = oriGet.call(this);
Object.defineProperty(this, "data", { value: message });
return handleMessage(message);
};
Object.defineProperty(MessageEvent.prototype, "data", dataProperty);
}
// 计算元素中心点坐标
function getElementCenter(element) {
const rect = element.getBoundingClientRect();
if (element.innerText.trim() === '') {
return {
x: rect.left + rect.width / 2,
y: rect.top
};
}
return {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2
};
}
// 创建抛物线路径,用于攻击动画的路径显示
function createParabolaPath(startElem, endElem, reversed = false) {
const start = getElementCenter(startElem);
const end = getElementCenter(endElem);
const curveRatio = reversed ? 4 : 2.5;
const curveHeight = -Math.abs(start.x - end.x) / curveRatio;
const controlPoint = {
x: (start.x + end.x) / 2,
y: Math.min(start.y, end.y) + curveHeight
};
if (reversed) {
return `M ${end.x} ${end.y} Q ${controlPoint.x} ${controlPoint.y}, ${start.x} ${start.y}`;
}
return `M ${start.x} ${start.y} Q ${controlPoint.x} ${controlPoint.y}, ${end.x} ${end.y}`;
}
// 为目标元素的第三个父级元素添加震动效果,根据第五个父级元素决定震动方向
function shakeTarget(element) {
if (!element || isPaused) return;
// 向上查找第三个父级元素(用于实际震动)
let shakeElement = element;
for (let i = 0; i < 3 && shakeElement; i++) {
shakeElement = shakeElement.parentElement;
}
// 向上查找第五个父级元素(用于判断震动方向)
let directionElement = element;
for (let i = 0; i < 5 && directionElement; i++) {
directionElement = directionElement.parentElement;
}
// 如果找到了相应的父级元素,应用震动效果
if (shakeElement && directionElement) {
const className = directionElement.className;
let transformValue = 'translate(0, 0)';
// 根据第五个父级元素的类名决定震动方向
if (className.includes('playersArea')) {
transformValue = 'translate(-2px, 2px)';
} else if (className.includes('monstersArea')) {
transformValue = 'translate(2px, 2px)';
}
// 添加震动类并设置动画
shakeElement.classList.add('mwht-shake');
// 使用自定义动画实现不同方向的震动
shakeElement.style.animation = `customShake 0.2s cubic-bezier(.36,.07,.19,.97) forwards`;
shakeElement.style.transformOrigin = 'center';
shakeElement.style.willChange = 'transform';
// 存储原始transform值,动画结束后恢复
const originalTransform = shakeElement.style.transform;
// 动画帧函数
let startTime = null;
const duration = 200; // 200ms = 0.2s
function animate(currentTime) {
if (isPaused) return;
if (!startTime) startTime = currentTime;
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
// 计算动画曲线
const easeOut = 1 - Math.pow(1 - progress, 3);
// 应用变换
if (progress < 0.5) {
// 前半段:从0到目标偏移
const scale = easeOut * 2;
shakeElement.style.transform = `translate(${parseFloat(transformValue.split('(')[1]) * scale}px, ${parseFloat(transformValue.split(',')[1]) * scale}px)`;
} else {
// 后半段:从目标偏移回到0
const scale = 2 - (easeOut * 2);
shakeElement.style.transform = `translate(${parseFloat(transformValue.split('(')[1]) * scale}px, ${parseFloat(transformValue.split(',')[1]) * scale}px)`;
}
if (progress < 1) {
requestAnimationFrame(animate);
} else {
// 动画结束,恢复原始transform
shakeElement.style.transform = originalTransform;
shakeElement.classList.remove('mwht-shake');
shakeElement.style.animation = '';
}
}
// 启动动画
requestAnimationFrame(animate);
}
}
// 创建动画效果,包括攻击路径和伤害数字的动画
function createEffect(startElem, endElem, hpDiff, index, reversed = false) {
if (isPaused) return;
// 检查玩家是否被勾选,如果未勾选则不绘制
if (!playerDrawEnabled[index]) return;
let strokeWidth = '1px';
let filterWidth = '1px';
if (hpDiff >= 1000) {
strokeWidth = '5px';
filterWidth = '6px';
} else if (hpDiff >= 700) {
strokeWidth = '4px';
filterWidth = '5px';
} else if (hpDiff >= 500) {
strokeWidth = '3px';
filterWidth = '4px';
} else if (hpDiff >= 300) {
strokeWidth = '2px';
filterWidth = '3px';
} else if (hpDiff >= 100) {
filterWidth = '2px';
}
if (reversed) {
const dmgDivs = startElem.querySelector('.CombatUnit_splatsContainer__2xcc0')?.querySelectorAll('div') || [];
for (const div of dmgDivs) {
if (div.innerText.trim() === '') {
startElem = div;
break;
}
}
} else {
const dmgDivs = endElem.querySelector('.CombatUnit_splatsContainer__2xcc0')?.querySelectorAll('div') || [];
for (const div of dmgDivs) {
if (div.innerText.trim() === '') {
endElem = div;
break;
}
}
}
const svg = document.getElementById('svg-container');
const frag = document.createDocumentFragment();
// 根据reversed参数决定目标元素
const targetElem = reversed ? startElem : endElem;
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
if (reversed) index = 6;
Object.assign(path.style, {
stroke: lineColor[index],
strokeWidth,
fill: 'none',
strokeLinecap: 'round',
filter: `drop-shadow(0 0 ${filterWidth} ${filterColor[index]})`,
willChange: 'stroke-dashoffset, opacity',
});
path.setAttribute('d', createParabolaPath(startElem, endElem, reversed));
const pathLength = path.getTotalLength();
path.style.strokeDasharray = pathLength;
path.style.strokeDashoffset = pathLength;
frag.appendChild(path);
const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
text.textContent = hpDiff;
const baseFontSize = 5;
const fontSize = Math.floor(200 * Math.pow(hpDiff / (20000 + hpDiff), 0.45)) - baseFontSize;
text.setAttribute('font-size', fontSize);
text.setAttribute('fill', lineColor[index]);
Object.assign(text.style, {
opacity: 0,
filter: `drop-shadow(0 0 5px ${lineColor[index]})`,
transformOrigin: 'center',
fontWeight: 'bold',
willChange: 'transform, opacity, x, y',
});
frag.appendChild(text);
svg.appendChild(frag);
setTimeout(() => {
requestAnimationFrame(() => {
path.style.transition = 'stroke-dashoffset 1s linear';
path.style.strokeDashoffset = '0';
animateText(path, text, pathLength, lineColor[index], () => {
// 伤害数字动画结束后触发震动效果
shakeTarget(targetElem);
});
});
}, 100);
setTimeout(() => {
requestAnimationFrame(() => {
path.style.transition = 'stroke-dashoffset 1s cubic-bezier(0.25, 0.46, 0.45, 0.94), opacity 1s cubic-bezier(0.25, 0.46, 0.45, 0.94)';
path.style.strokeDashoffset = -pathLength;
path.style.opacity = 0;
const removePath = () => {
path.remove();
};
path.addEventListener('transitionend', removePath, { once: true });
});
}, 900);
}
// 从对象池获取粒子元素
function getParticleFromPool() {
if (particlePool.length > 0) {
return particlePool.pop();
}
return document.createElementNS("http://www.w3.org/2000/svg", "circle");
}
// 将粒子元素返回对象池
function returnParticleToPool(particle) {
particle.removeAttribute('r');
particle.removeAttribute('fill');
particle.removeAttribute('cx');
particle.removeAttribute('cy');
particle.style.opacity = 1;
particle.style.transform = 'none';
particle.removeEventListener('transitionend', () => {});
particlePool.push(particle);
}
// 创建粒子特效,在伤害数字消失时显示
function createParticleEffect(x, y, color) {
if (isPaused) return;
const svg = document.getElementById('svg-container');
const numParticles = 20;
const frag = document.createDocumentFragment();
const batchSize = 5;
let batchCount = 0;
function createBatch() {
for (let i = 0; i < batchSize && batchCount * batchSize + i < numParticles; i++) {
const particle = getParticleFromPool();
particle.setAttribute('r', '2');
particle.setAttribute('fill', color);
particle.setAttribute('cx', x);
particle.setAttribute('cy', y);
particle.style.opacity = 1;
particle.style.transformOrigin = 'center';
particle.style.willChange = 'transform, opacity';
const angle = ((batchCount * batchSize + i) / numParticles) * 2 * Math.PI;
const distance = Math.random() * 30 + 10;
const endX = parseFloat(x) + distance * Math.cos(angle);
const endY = parseFloat(y) + distance * Math.sin(angle);
frag.appendChild(particle);
requestAnimationFrame(() => {
particle.style.transition = 'all 0.3s ease-out';
particle.setAttribute('cx', endX);
particle.setAttribute('cy', endY);
particle.style.opacity = 0;
particle.addEventListener('transitionend', () => {
returnParticleToPool(particle);
}, { once: true });
setTimeout(() => {
if (particle.parentNode) {
particle.parentNode.removeChild(particle);
returnParticleToPool(particle);
}
}, 5000);
});
}
batchCount++;
if (batchCount * batchSize < numParticles) {
setTimeout(createBatch, 50);
} else {
svg.appendChild(frag);
}
}
createBatch();
}
// 文本动画函数 - 使用 requestAnimationFrame 实现更流畅的动画
function animateText(path, text, pathLength, color, onComplete) {
const animationConfig = {
duration: 1350,
fadeInStart: 0.0,
fadeInEnd: 0.3,
particleInterval: 3
};
let startTime = null;
let lastParticleFrame = 0;
function animate(currentTime) {
if (isPaused) return;
if (!startTime) startTime = currentTime;
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / animationConfig.duration, 1);
const point = path.getPointAtLength(progress * pathLength);
text.setAttribute('x', point.x);
text.setAttribute('y', point.y);
let opacity = 1;
if (progress < animationConfig.fadeInStart) {
opacity = 0;
} else if (progress < animationConfig.fadeInEnd) {
opacity = 0.7 + 0.3 * ((progress - animationConfig.fadeInStart) / (animationConfig.fadeInEnd - animationConfig.fadeInStart));
}
text.style.opacity = opacity;
if (Math.floor(progress * 100) % animationConfig.particleInterval === 0 && lastParticleFrame !== Math.floor(progress * 100)) {
lastParticleFrame = Math.floor(progress * 100);
const particle = getParticleFromPool();
particle.setAttribute('r', '2');
particle.setAttribute('fill', color);
particle.setAttribute('cx', point.x + (Math.random() - 0.5) * 10);
particle.setAttribute('cy', point.y + (Math.random() - 0.5) * 10);
particle.style.opacity = 1;
particle.style.transition = 'all 0.2s ease-out';
particle.style.willChange = 'opacity, transform';
const svg = document.getElementById('svg-container');
svg.appendChild(particle);
requestAnimationFrame(() => {
particle.style.opacity = 0;
particle.addEventListener('transitionend', () => {
returnParticleToPool(particle);
}, { once: true });
});
}
if (progress < 1) {
requestAnimationFrame(animate);
} else {
text.style.transition = 'all 0.2s ease-out';
text.style.transform = 'scale(1.5)';
text.style.opacity = 0;
setTimeout(() => {
text.remove();
createParticleEffect(text.getAttribute('x'), text.getAttribute('y'), color);
// 调用回调函数触发震动和恢复可见性
if (typeof onComplete === 'function') {
onComplete();
}
}, 100);
}
}
requestAnimationFrame(animate);
}
// 创建线条动画,根据攻击信息创建攻击路径和伤害数字动画
function createLine(from, to, hpDiff, reversed = false) {
if (isPaused) return;
const playerArea = document.querySelector(".BattlePanel_playersArea__vvwlB");
const monsterArea = document.querySelector(".BattlePanel_monstersArea__2dzrY");
const gamePanel = document.querySelector(".GamePage_mainPanel__2njyb");
if (!playerArea || !monsterArea || !gamePanel) return;
const playersContainer = playerArea.firstElementChild;
const monsterContainer = monsterArea.firstElementChild;
const effectFrom = playersContainer?.children[from];
const effectTo = monsterContainer?.children[to];
if (!effectFrom || !effectTo) return;
let svgContainer = document.getElementById('svg-container');
if (!svgContainer) {
const svgNS = 'http://www.w3.org/2000/svg';
svgContainer = document.createElementNS(svgNS, 'svg');
svgContainer.id = 'svg-container';
Object.assign(svgContainer.style, {
position: 'fixed',
top: '0',
left: '0',
width: '100%',
height: '100%',
pointerEvents: 'none',
overflow: 'visible',
zIndex: '190'
});
const setViewBox = () => {
const width = window.innerWidth;
const height = window.innerHeight;
svgContainer.setAttribute('viewBox', `0 0 ${width} ${height}`);
};
setViewBox();
svgContainer.setAttribute('preserveAspectRatio', 'none');
gamePanel.appendChild(svgContainer);
if (!isResizeListenerAdded) {
window.addEventListener('resize', setViewBox);
isResizeListenerAdded = true;
}
}
const originIndex = reversed ? to : from;
createEffect(effectFrom, effectTo, hpDiff, originIndex, reversed);
}
// 处理伤害信息,根据新旧生命值计算伤害差值并创建动画
function processDamage(oldHPArr, newMap, castIndex, attackerIndices, isReverse = false) {
oldHPArr.forEach((oldHP, index) => {
const entity = newMap[index];
if (!entity) return;
const hpDiff = oldHP - entity.cHP;
oldHPArr[index] = entity.cHP;
if (hpDiff > 0 && attackerIndices.length > 0) {
if (attackerIndices.length > 1) {
attackerIndices.forEach(attackerIndex => {
if (attackerIndex === castIndex) {
createLine(attackerIndex, index, hpDiff, isReverse);
}
});
} else {
createLine(attackerIndices[0], index, hpDiff, isReverse);
}
}
});
}
// 检测施法者,通过比较新旧魔法值找出施法者索引
function detectCaster(oldMPArr, newMap) {
let casterIndex = -1;
Object.keys(newMap).forEach(index => {
const newMP = newMap[index].cMP;
if (newMP < oldMPArr[index]) {
casterIndex = index;
}
oldMPArr[index] = newMP;
});
return casterIndex;
}
// 处理 WebSocket 消息,根据消息类型更新战斗状态并创建攻击动画
function handleMessage(message) {
if (isPaused) {
return message;
}
let obj;
try {
obj = JSON.parse(message);
} catch (error) {
console.error('Failed to parse WebSocket message:', error);
return message;
}
if (obj && obj.type === "new_battle") {
battleState.monstersHP = obj.monsters.map((monster) => monster.currentHitpoints);
battleState.monstersMP = obj.monsters.map((monster) => monster.currentManapoints);
battleState.playersHP = obj.players.map((player) => player.currentHitpoints);
battleState.playersMP = obj.players.map((player) => player.currentManapoints);
const svg = document.getElementById('svg-container');
if (svg) {
while (svg.firstChild) {
svg.removeChild(svg.firstChild);
}
}
particlePool.length = 0;
} else if (obj && obj.type === "battle_updated" && battleState.monstersHP.length) {
const mMap = obj.mMap;
const pMap = obj.pMap;
const monsterIndices = Object.keys(obj.mMap);
const playerIndices = Object.keys(obj.pMap);
const castMonster = detectCaster(battleState.monstersMP, mMap);
const castPlayer = detectCaster(battleState.playersMP, pMap);
processDamage(battleState.monstersHP, mMap, castPlayer, playerIndices, false);
processDamage(battleState.playersHP, pMap, castMonster, monsterIndices, true);
}
return message;
}
// 检测网页是否从后台恢复,当网页从后台恢复时清理 SVG 容器中的元素
function addVisibilityChangeListener() {
document.addEventListener('visibilitychange', function () {
if (document.visibilityState === 'hidden') {
isPaused = true;
} else if (document.visibilityState === 'visible') {
isPaused = false;
const svg = document.getElementById('svg-container');
if (svg) {
while (svg.firstChild) {
svg.removeChild(svg.firstChild);
}
}
document.querySelectorAll('[id^="mwi-hit-tracker-"]').forEach(el => {
if (el) {
el.remove();
}
});
document.querySelectorAll('circle[fill^="rgba"]').forEach(el => {
if (el.parentNode === svg) {
el.parentNode.removeChild(el);
}
});
}
});
}
// 启动初始化函数
init();
})();