Greasy Fork is available in English.
读取装备信息并模拟战斗
当前为
// ==UserScript==
// @name Battle Simulation
// @namespace http://tampermonkey.net/
// @version 1.0
// @description 读取装备信息并模拟战斗
// @author Lunaris
// @match https://aring.cc/awakening-of-war-soul-ol/
// @grant none
// @icon https://aring.cc/awakening-of-war-soul-ol/favicon.ico
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// 全局变量存储人物属性和怪物设置
let playerStats = {
攻击: 0,
破防: 0,
命中率: 100,
暴击率: 0,
暴击伤害: 0,
暴击重击: 0,
暴击固定减少: 0,
暴击百分比减少: 0,
不暴击减免: 1.0,
攻速: 1.0,
攻击属性: '无',
元素伤害加成: 0,
元素伤害Map: {
wind: 0,
fire: 0,
water: 0,
earth: 0
},
追击伤害: 0,
追击词条: [],
影刃词条: [],
虚无词条: [],
重击词条: [],
裂创词条: [],
重创词条: [],
分裂词条: [],
爆发词条: [],
碎骨词条: [],
冲击词条: [],
冲锋词条: [],
收割词条: [],
收尾词条: [],
全伤害加成: 0,
常驻显示词条: [],
精准减闪系数: 1,
残忍减防: 0,
残忍防御系数: 1,
残忍百分比词条: [],
残忍固定词条: []
};
// 保存怪物设置
let monsterSettings = {
血量: 0,
防御: 0,
闪避率: 0,
抗暴率: 0,
承伤系数: 150,
战斗时间: 180
};
// 创建悬浮按钮
const panelState = {
isLoading: false,
isReady: false,
userAttrs: {},
equipmentData: []
};
const helperPanelState = {
hasData: false,
isMinimized: false,
isClosed: false
};
const helperPanel = document.createElement('div');
helperPanel.style.cssText = `
position: fixed;
right: 16px;
width: min(160px, 92vw);
background: rgba(5, 6, 10, 0.95);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 14px;
box-shadow: 0 8px 24px rgba(0,0,0,0.45);
padding: 14px;
color: #f5f6ff;
font-family: Arial, sans-serif;
font-size: 12px;
line-height: 1.4;
z-index: 99998;
backdrop-filter: blur(8px);
`;
function setHelperPanelCompact(compact) {
if (compact) {
helperPanel.style.top = '50%';
helperPanel.style.bottom = 'auto';
helperPanel.style.transform = 'translateY(-50%)';
} else {
helperPanel.style.top = 'auto';
helperPanel.style.bottom = '16px';
helperPanel.style.transform = 'none';
}
}
const panelHeader = document.createElement('div');
panelHeader.style.cssText = `
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
`;
const panelTitle = document.createElement('span');
panelTitle.textContent = '战斗模拟';
panelTitle.style.cssText = `
font-size: 12px;
letter-spacing: 1px;
color: #d1d8ff;
`;
const panelActions = document.createElement('div');
panelActions.style.cssText = 'display: flex; gap: 6px;';
const minimizePanelBtn = document.createElement('button');
minimizePanelBtn.textContent = '━';
minimizePanelBtn.style.cssText = `
width: 28px;
height: 28px;
border-radius: 999px;
border: 1px solid rgba(255,255,255,0.15);
background: rgba(255,255,255,0.04);
color: #f5f6ff;
cursor: pointer;
font-size: 14px;
line-height: 1;
`;
const closePanelBtn = document.createElement('button');
closePanelBtn.textContent = '×';
closePanelBtn.style.cssText = `
width: 28px;
height: 28px;
border-radius: 999px;
border: 1px solid rgba(255,255,255,0.15);
background: rgba(240,96,96,0.18);
color: #ffbaba;
cursor: pointer;
font-size: 16px;
line-height: 1;
`;
panelActions.appendChild(minimizePanelBtn);
panelActions.appendChild(closePanelBtn);
panelHeader.appendChild(panelTitle);
panelHeader.appendChild(panelActions);
const panelBody = document.createElement('div');
panelBody.style.cssText = `
display: flex;
flex-direction: column;
`;
const mainActionBtn = document.createElement('button');
mainActionBtn.textContent = '加载战斗模拟';
mainActionBtn.style.cssText = `
width: 100%;
background: linear-gradient(135deg, #364269 0%, #7151d8 100%);
border: none;
border-radius: 10px;
padding: 10px 12px;
font-size: 13px;
font-weight: bold;
color: #fff;
cursor: pointer;
transition: transform 0.2s ease;
box-shadow: 0 6px 16px rgba(0,0,0,0.35);
`;
mainActionBtn.onmouseover = () => {
if (!panelState.isLoading) {
mainActionBtn.style.transform = 'scale(1.02)';
}
};
mainActionBtn.onmouseout = () => mainActionBtn.style.transform = 'scale(1)';
const statusHint = document.createElement('div');
statusHint.style.cssText = `
margin-top: 6px;
font-size: 11px;
color: #8f9bc4;
text-align: center;
display: none;
cursor: default;
`;
const reopenPanelBtn = document.createElement('button');
reopenPanelBtn.textContent = '打开战斗助手';
reopenPanelBtn.style.cssText = `
position: fixed;
right: 16px;
bottom: 16px;
padding: 6px 14px;
border-radius: 20px;
border: 1px solid rgba(255,255,255,0.2);
background: rgba(5,6,10,0.9);
color: #d1d8ff;
font-size: 12px;
cursor: pointer;
z-index: 99998;
display: none;
`;
function updateHelperPanelVisibility() {
if (helperPanelState.isClosed) {
if (helperPanel.parentElement) {
helperPanel.remove();
}
if (reopenPanelBtn.parentElement) {
reopenPanelBtn.remove();
}
return;
}
if (helperPanelState.isMinimized) {
helperPanel.style.display = 'none';
if (!reopenPanelBtn.parentElement) {
document.body.appendChild(reopenPanelBtn);
}
reopenPanelBtn.style.display = 'block';
return;
}
if (!helperPanel.parentElement) {
document.body.appendChild(helperPanel);
}
helperPanel.style.display = 'block';
panelBody.style.display = 'flex';
reopenPanelBtn.style.display = 'none';
minimizePanelBtn.textContent = '━';
setHelperPanelCompact(!helperPanelState.hasData);
}
minimizePanelBtn.onclick = (event) => {
event.stopPropagation();
if (helperPanelState.isClosed) return;
helperPanelState.isMinimized = true;
updateHelperPanelVisibility();
};
closePanelBtn.onclick = (event) => {
event.stopPropagation();
helperPanelState.isClosed = true;
updateHelperPanelVisibility();
};
reopenPanelBtn.onclick = () => {
if (helperPanelState.isClosed) return;
helperPanelState.isMinimized = false;
updateHelperPanelVisibility();
};
const personalSection = document.createElement('div');
personalSection.style.cssText = `
margin-top: 14px;
border: 1px solid rgba(255,255,255,0.08);
border-radius: 10px;
padding: 10px;
background: rgba(255,255,255,0.03);
display: none;
`;
const personalTitle = document.createElement('div');
personalTitle.textContent = '个人属性';
personalTitle.style.cssText = `
font-weight: bold;
margin-bottom: 6px;
font-size: 12px;
color: #d1d8ff;
`;
const personalContent = document.createElement('div');
personalContent.style.cssText = `
display: flex;
flex-direction: column;
gap: 6px;
`;
personalSection.appendChild(personalTitle);
personalSection.appendChild(personalContent);
const equipmentSection = document.createElement('div');
equipmentSection.style.cssText = `
margin-top: 12px;
border: 1px solid rgba(255,255,255,0.08);
border-radius: 10px;
padding: 10px;
background: rgba(255,255,255,0.02);
display: none;
`;
const equipmentToggle = document.createElement('button');
equipmentToggle.textContent = '装备词条';
equipmentToggle.style.cssText = `
width: 100%;
background: none;
border: none;
color: #f5f6ff;
font-size: 12px;
font-weight: bold;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
padding: 0;
`;
const toggleIcon = document.createElement('span');
toggleIcon.textContent = '▼';
toggleIcon.style.cssText = 'font-size: 11px; color: #8aa4ff;';
equipmentToggle.appendChild(toggleIcon);
const equipmentContent = document.createElement('div');
equipmentContent.style.cssText = `
margin-top: 8px;
overflow: hidden;
max-height: 0;
opacity: 0;
transition: max-height 0.25s ease, opacity 0.25s ease;
`;
let equipmentExpanded = false;
equipmentToggle.onclick = () => {
equipmentExpanded = !equipmentExpanded;
toggleIcon.textContent = equipmentExpanded ? '▲' : '▼';
if (equipmentExpanded) {
equipmentContent.style.maxHeight = equipmentContent.scrollHeight + 'px';
equipmentContent.style.opacity = '1';
} else {
equipmentContent.style.maxHeight = '0px';
equipmentContent.style.opacity = '0';
}
};
equipmentSection.appendChild(equipmentToggle);
equipmentSection.appendChild(equipmentContent);
panelBody.appendChild(mainActionBtn);
panelBody.appendChild(statusHint);
panelBody.appendChild(personalSection);
panelBody.appendChild(equipmentSection);
helperPanel.appendChild(panelHeader);
helperPanel.appendChild(panelBody);
document.body.appendChild(helperPanel);
document.body.appendChild(reopenPanelBtn);
updateHelperPanelVisibility();
const simulatePanel = document.createElement('div');
simulatePanel.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 99999;
width: min(420px, 94vw);
max-height: 84vh;
overflow-y: auto;
background: rgba(7, 9, 14, 0.98);
border-radius: 14px;
border: 1px solid rgba(255,255,255,0.08);
box-shadow: 0 20px 40px rgba(0,0,0,0.55);
display: none;
padding: 22px;
font-family: Arial, sans-serif;
color: #f5f6ff;
`;
document.body.appendChild(simulatePanel);
// 解析人物基本属性
// 解析人物基本属性
function parseUserAttrs() {
const userAttrsDiv = document.querySelector('.user-attrs');
const attrs = {};
if (userAttrsDiv) {
const paragraphs = userAttrsDiv.querySelectorAll('.text-wrap p');
paragraphs.forEach(p => {
const spans = p.querySelectorAll('span');
if (spans.length >= 2) {
const key = spans[0].textContent.replace(':', '').trim();
const value = spans[1].textContent.trim();
attrs[key] = value;
}
});
}
// 更新全局玩家属性
playerStats.攻击 = parseFloat(attrs['攻击'] || 0);
playerStats.破防 = parseFloat(attrs['破防'] || 0);
playerStats.命中率 = parseFloat(attrs['命中率']?.replace('%', '') || 100);
playerStats.暴击率 = parseFloat(attrs['暴击率']?.replace('%', '') || 0);
playerStats.暴击伤害 = parseFloat(attrs['暴击伤害']?.replace('%', '') || 150) / 100;
// 尝试读取"攻击速度"或"攻速"
playerStats.攻速 = parseFloat(attrs['攻击速度'] || attrs['攻速'] || 1.0);
playerStats.全伤害加成 = parseFloat(attrs['全伤害加成']?.replace('%', '') || 0) / 100;
playerStats.元素伤害Map = {
wind: 0,
fire: 0,
water: 0,
earth: 0
};
const elementAttrMap = {
wind: '风伤害加成',
fire: '火伤害加成',
water: '水伤害加成',
earth: '土伤害加成'
};
Object.entries(elementAttrMap).forEach(([key, label]) => {
const value = attrs[label];
playerStats.元素伤害Map[key] = value ? parseFloat(value.replace('%', '') || 0) / 100 : 0;
});
playerStats.元素伤害加成 = 0;
return attrs;
}
// 解析装备信息
function parseEquipment(equipDiv) {
const info = {
affixes: [],
specialAttrs: []
};
const paragraphs = equipDiv.querySelectorAll('p');
let currentSection = '';
paragraphs.forEach(p => {
const text = p.textContent.trim();
if (text === '暗金属性:') {
currentSection = 'darkGold';
} else if (text === '刻印属性:') {
currentSection = 'affix';
} else if (text === '特殊属性:') {
currentSection = 'special';
} else if (text && !text.endsWith(':')) {
const specialSpan = p.querySelector('.special');
if (specialSpan) {
const affixName = specialSpan.textContent.trim();
const darkGoldSpan = p.querySelector('.darkGold');
const percentage = darkGoldSpan ? darkGoldSpan.textContent.trim() : '';
let description = '';
const tempDiv = document.createElement('div');
tempDiv.innerHTML = p.innerHTML;
tempDiv.querySelectorAll('.awaken').forEach(span => span.remove());
tempDiv.querySelectorAll('.darkGold').forEach(span => span.remove());
const specialClone = tempDiv.querySelector('.special');
if (specialClone) {
specialClone.remove();
}
let descText = tempDiv.textContent || '';
const colonIndex = descText.search(/[::]/);
if (colonIndex !== -1) {
descText = descText.slice(colonIndex + 1);
}
description = descText.trim();
info.affixes.push({
name: affixName,
percentage: percentage,
description: description
});
} else if (currentSection === 'special') {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = p.innerHTML;
const awakenSpans = tempDiv.querySelectorAll('.awaken');
awakenSpans.forEach(span => span.remove());
info.specialAttrs.push(tempDiv.textContent.trim());
}
}
});
return info;
}
// 格式化展示人物属性
function buildPersonalAttrHTML(attrs) {
const entries = Object.entries(attrs || {});
if (entries.length === 0) {
return '<div style="color: #8f9bc4; font-size: 11px;">暂未读取到属性,请在角色界面触发</div>';
}
return entries.map(([key, value]) => `
<div style="
display: flex;
justify-content: space-between;
align-items: center;
font-size: 11px;
padding: 4px 6px;
border-radius: 6px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.04);
">
<span style="color: #9ea8d5;">${key}</span>
<span style="color: #f5f6ff; font-weight: bold;">${value}</span>
</div>
`).join('');
}
function buildEquipmentTraitsHTML(equipmentData) {
if (!equipmentData || equipmentData.length === 0) {
return '<div style="color: #8f9bc4; font-size: 9px;">暂无装备词条</div>';
}
let entries = [];
equipmentData.forEach(eq => {
entries = entries.concat(
(eq.affixes || []).map(affix => ({
type: 'affix',
name: affix.name || '',
chance: affix.percentage || '',
description: affix.description || ''
}))
);
entries = entries.concat(
(eq.specialAttrs || []).map(attr => ({
type: 'special',
description: attr
}))
);
});
if (entries.length === 0) {
return '<div style="color: #8f9bc4; font-size: 9px;">暂未检测到可展示的词条</div>';
}
return entries.map(entry => {
if (entry.type === 'special') {
return `
<div style="
padding: 6px;
border-radius: 8px;
margin-bottom: 6px;
background: rgba(255,255,255,0.03);
border: 1px dashed rgba(255,255,255,0.1);
font-size: 9px;
color: #ffe3a7;
">${entry.description}</div>
`;
}
const triggerRate = entry.chance ? entry.chance : '100%';
return `
<div style="
padding: 6px;
border-radius: 8px;
margin-bottom: 6px;
background: rgba(255,255,255,0.02);
border: 1px solid rgba(122,145,255,0.3);
font-size: 9px;
">
<div style="display: flex; justify-content: space-between; font-weight: bold; color: #9fb4ff;">
<span>${entry.name}</span>
<span>${triggerRate}</span>
</div>
<div style="margin-top: 4px; color: #d7dbff;">${entry.description}</div>
</div>
`;
}).join('');
}
function getElementIcon(elementName) {
switch (elementName) {
case '风属性':
return '🌪️';
case '火属性':
return '🔥';
case '水属性':
return '💧';
case '土属性':
return '🌱';
default:
return '';
}
}
// 将装备词条转化为角色属性加成
function applyEquipmentEffects(equipmentData) {
playerStats.追击伤害 = 0;
playerStats.追击词条 = [];
playerStats.影刃词条 = [];
playerStats.虚无词条 = [];
playerStats.重击词条 = [];
playerStats.裂创词条 = [];
playerStats.重创词条 = [];
playerStats.分裂词条 = [];
playerStats.爆发词条 = [];
playerStats.碎骨词条 = [];
playerStats.冲击词条 = [];
playerStats.冲锋词条 = [];
playerStats.收割词条 = [];
playerStats.收尾词条 = [];
playerStats.常驻显示词条 = [];
playerStats.精准减闪系数 = 1;
playerStats.残忍减防 = 0;
playerStats.残忍防御系数 = 1;
playerStats.残忍百分比词条 = [];
playerStats.残忍固定词条 = [];
equipmentData.forEach(eq => {
eq.affixes.forEach(affix => {
if (!affix.name) return;
if (affix.name.includes('精准')) {
const preciseName = affix.name.trim() || '精准';
playerStats.常驻显示词条.push(preciseName);
const percentMatch = (affix.description || '').match(/([\d.]+)\s*%/);
if (percentMatch) {
const percentValue = parseFloat(percentMatch[1]);
if (!isNaN(percentValue)) {
const multiplier = Math.max(0, 1 - (percentValue / 100));
playerStats.精准减闪系数 *= multiplier;
}
}
}
if (affix.name.includes('追击')) {
const desc = affix.description || '';
const guaranteedTrigger = /每次(攻击|命中)/.test(desc);
let normalizedChance = 100;
if (!guaranteedTrigger) {
const chanceText = affix.percentage || '';
const chanceValue = parseFloat(chanceText.replace(/[^\d.]/g, '')) || 100;
normalizedChance = Math.max(0, Math.min(100, chanceValue));
}
let damageValue = 0;
const numberMatches = desc.match(/[\d.]+/g);
if (numberMatches && numberMatches.length > 0) {
damageValue = parseFloat(numberMatches[numberMatches.length - 1]);
}
if (!isNaN(damageValue)) {
const affixData = {
type: '追击',
name: affix.name.trim() || '追击',
chance: normalizedChance,
damage: damageValue
};
playerStats.追击词条.push(affixData);
playerStats.追击伤害 += affixData.damage * (affixData.chance / 100);
}
} else if (affix.name.includes('分裂')) {
const percentMatches = affix.description.match(/([\d.]+)\s*%/g);
let chanceValue = null;
if (percentMatches && percentMatches.length > 0) {
const lastPercent = percentMatches[percentMatches.length - 1];
chanceValue = parseFloat(lastPercent);
}
if ((chanceValue === null || isNaN(chanceValue)) && affix.percentage) {
chanceValue = parseFloat(affix.percentage.replace(/[^\d.]/g, ''));
}
const digitSegmentMatch = affix.description.match(/(\d+)\s*段/);
let segmentCount = digitSegmentMatch ? parseInt(digitSegmentMatch[1], 10) : null;
if (!segmentCount) {
const chineseSegmentMatch = affix.description.match(/([一二两三四五六七八九十百千]+)\s*段/);
if (chineseSegmentMatch) {
segmentCount = parseChineseNumeral(chineseSegmentMatch[1]);
}
}
if (!segmentCount) {
segmentCount = 3;
}
if (!isNaN(chanceValue) && chanceValue > 0) {
playerStats.分裂词条.push({
type: '分裂',
name: affix.name.trim() || '分裂',
chance: Math.max(0, Math.min(100, chanceValue)),
segments: Math.max(2, segmentCount)
});
}
} else if (affix.name.includes('裂创')) {
const desc = affix.description || '';
const damageMatch = desc.match(/([\d.]+)\s*(?:点)?\s*真实伤害/);
let damageValue = damageMatch ? parseFloat(damageMatch[1]) : null;
if (damageValue === null) {
const numberMatch = desc.match(/[\d.]+/);
if (numberMatch) {
damageValue = parseFloat(numberMatch[0]);
}
}
if (!isNaN(damageValue) && damageValue !== null) {
playerStats.裂创词条.push({
type: '裂创',
name: affix.name.trim() || '裂创',
damage: damageValue
});
}
} else if (affix.name.includes('重创')) {
const desc = affix.description || '';
const damageMatch = desc.match(/([\d.]+)\s*(?:点)?\s*伤害/);
let damageValue = damageMatch ? parseFloat(damageMatch[1]) : null;
if (damageValue === null) {
const numberMatch = desc.match(/[\d.]+/);
if (numberMatch) {
damageValue = parseFloat(numberMatch[0]);
}
}
if (!isNaN(damageValue) && damageValue !== null) {
playerStats.重创词条.push({
type: '重创',
name: affix.name.trim() || '重创',
damage: damageValue
});
}
} else if (affix.name.includes('影刃')) {
// 影刃默认每次攻击必定判定,不使用词条标题中的百分比
const normalizedChance = 100;
const percentMatch = affix.description.match(/([\d.]+)\s*%/);
const fixedMatch = affix.description.match(/([\d.]+)\s*(?:点|真实伤害)/);
let damageValue = null;
if (fixedMatch) {
damageValue = parseFloat(fixedMatch[1]);
}
let percentValue = null;
if (percentMatch) {
percentValue = parseFloat(percentMatch[1]);
}
if (damageValue !== null || percentValue !== null) {
playerStats.影刃词条.push({
type: '影刃',
name: affix.name.trim() || '影刃',
chance: normalizedChance,
damage: damageValue,
percent: percentValue
});
}
} else if (affix.name.includes('虚无')) {
const desc = affix.description || '';
const conversionMatch = desc.match(/([\d.]+)\s*%[^%]*真实伤害/);
const conversionPercent = conversionMatch ? parseFloat(conversionMatch[1]) : NaN;
if (!isNaN(conversionPercent) && conversionPercent > 0) {
playerStats.虚无词条.push({
type: '虚无',
name: affix.name.trim() || '虚无',
chance: 100,
percent: conversionPercent
});
}
} else if (affix.name.includes('重击')) {
const desc = affix.description || '';
const chanceMatch = desc.match(/([\d.]+)\s*%(?:\s*的)?\s*(?:概率|几率)/);
let chanceValue = chanceMatch ? parseFloat(chanceMatch[1]) : NaN;
if (isNaN(chanceValue) && affix.percentage) {
const fallbackChance = parseFloat(affix.percentage.replace(/[^\d.]/g, ''));
if (!isNaN(fallbackChance)) {
chanceValue = fallbackChance;
}
}
const normalizedChance = isNaN(chanceValue) ? 100 : Math.max(0, Math.min(100, chanceValue));
const percentDamageMatch = desc.match(/(?:造成|附加)[^%]*?([\d.]+)\s*%[^。]*当前攻击力/);
const percentDamageMatchAlt = desc.match(/当前攻击力[^%]*?([\d.]+)\s*%/);
const flatDamageMatch = desc.match(/(?:造成|附加)\s*([\d.]+)\s*(?:点)?(?:固定)?伤害/);
let percentValue = percentDamageMatch ? parseFloat(percentDamageMatch[1]) : NaN;
if (isNaN(percentValue) && percentDamageMatchAlt) {
percentValue = parseFloat(percentDamageMatchAlt[1]);
}
const flatValue = flatDamageMatch ? parseFloat(flatDamageMatch[1]) : NaN;
const hasPercent = !isNaN(percentValue);
const hasFlat = !isNaN(flatValue);
if (hasPercent || hasFlat) {
playerStats.重击词条.push({
type: '重击',
name: affix.name.trim() || '重击',
chance: normalizedChance,
percent: hasPercent ? percentValue : null,
flat: hasFlat ? flatValue : null
});
}
} else if (affix.name.includes('残忍')) {
const desc = affix.description || '';
const chanceMatch = desc.match(/([\d.]+)\s*%[^,。,、;]*?(?:几率|概率|触发)/);
const triggerChance = chanceMatch ? parseFloat(chanceMatch[1]) : 100;
const percentEffectMatches = Array.from(desc.matchAll(/([\d.]+)\s*%[^,。,、;]*?(?:防御|护甲)/g));
if (percentEffectMatches.length > 0) {
percentEffectMatches.forEach(match => {
const percentValue = parseFloat(match[1]);
if (!isNaN(percentValue)) {
playerStats.残忍百分比词条.push({
name: affix.name.trim() || '残忍',
chance: isNaN(triggerChance) ? 100 : triggerChance,
percent: percentValue
});
}
});
} else {
const flatMatches = Array.from(desc.matchAll(/([\d.]+)\s*(?:点)?\s*防御/g));
flatMatches.forEach(match => {
const ignoreValue = parseFloat(match[1]);
if (!isNaN(ignoreValue)) {
playerStats.残忍固定词条.push({
name: affix.name.trim() || '残忍',
chance: isNaN(triggerChance) ? 100 : triggerChance,
value: ignoreValue
});
}
});
}
} else if (affix.name.includes('爆发')) {
const triggerChance = Math.max(0, Math.min(100, parseFloat((affix.percentage || '').replace(/[^\d.]/g, '')) || 100));
const desc = affix.description || '';
const extraCritMatch = desc.match(/([\d.]+)\s*%/);
const extraCritChance = extraCritMatch ? Math.max(0, Math.min(100, parseFloat(extraCritMatch[1]))) : 0;
if (extraCritChance > 0) {
playerStats.爆发词条.push({
name: affix.name.trim() || '爆发',
triggerChance,
extraCritChance
});
}
} else if (affix.name.includes('碎骨')) {
const desc = affix.description || '';
// 标题中的百分比仅为展示,触发概率以描述为准
const triggerChance = 100;
const effectChanceMatch = desc.match(/([\d.]+)\s*%[^,。,、;]*?(?:概率|几率)/);
const effectChance = effectChanceMatch ? Math.max(0, Math.min(100, parseFloat(effectChanceMatch[1]))) : 100;
const percentPattern = /忽略(?:敌方)?\s*([\d.]+)\s*%[^,。,、;]*?(?:防御|护甲)/;
const flatPattern = /忽略(?:敌方)?\s*([\d.]+)\s*(?:点)?\s*(?:防御|护甲)/;
const ignorePercentMatch = desc.match(percentPattern);
const ignoreFlatMatch = (!ignorePercentMatch) ? desc.match(flatPattern) : null;
const percentValue = ignorePercentMatch ? parseFloat(ignorePercentMatch[1]) : null;
const flatValue = ignoreFlatMatch ? parseFloat(ignoreFlatMatch[1]) : null;
if ((!isNaN(percentValue) && percentValue > 0) || (!isNaN(flatValue) && flatValue > 0)) {
playerStats.碎骨词条.push({
name: affix.name.trim() || '碎骨',
triggerChance,
effectChance,
percent: !isNaN(percentValue) ? percentValue : null,
flat: !isNaN(flatValue) ? flatValue : null
});
}
} else if (affix.name.includes('冲击')) {
const desc = affix.description || '';
const thresholdMatch = desc.match(/血量(?:高于|大于|超过)?\s*([\d.]+)\s*%/);
const thresholdPercent = thresholdMatch ? parseFloat(thresholdMatch[1]) : null;
const percentPattern = /忽略(?:敌方)?\s*([\d.]+)\s*%[^,。,、;]*?(?:防御|护甲)/;
const flatPattern = /忽略(?:敌方)?\s*([\d.]+)\s*(?:点)?\s*(?:防御|护甲)/;
const percentMatch = desc.match(percentPattern);
const flatMatch = (!percentMatch) ? desc.match(flatPattern) : null;
const percentValue = percentMatch ? parseFloat(percentMatch[1]) : null;
const ignoreValue = flatMatch ? parseFloat(flatMatch[1]) : null;
if ((!isNaN(ignoreValue) && ignoreValue > 0) || (!isNaN(percentValue) && percentValue > 0)) {
playerStats.冲击词条.push({
name: affix.name.trim() || '冲击',
chance: 100,
thresholdPercent: !isNaN(thresholdPercent) ? thresholdPercent : null,
ignoreValue: !isNaN(ignoreValue) ? ignoreValue : null,
percent: !isNaN(percentValue) ? percentValue : null
});
}
} else if (affix.name.includes('冲锋')) {
const desc = affix.description || '';
const thresholdMatch = desc.match(/血量(?:高于|大于|超过)?\s*([\d.]+)\s*%/);
const thresholdPercent = thresholdMatch ? parseFloat(thresholdMatch[1]) : null;
const bonusMatch = desc.match(/额外(?:造成)?\s*([\d.]+)\s*%[^,。,、;]*?(?:伤害|输出)/);
const bonusPercent = bonusMatch ? parseFloat(bonusMatch[1]) : null;
if (!isNaN(bonusPercent) && bonusPercent > 0) {
playerStats.冲锋词条.push({
name: affix.name.trim() || '冲锋',
chance: 100,
thresholdPercent: !isNaN(thresholdPercent) ? thresholdPercent : null,
bonusPercent
});
}
} else if (affix.name.includes('收割')) {
const desc = affix.description || '';
const thresholdMatch = desc.match(/(?:血量|生命)[^,。,、;]*?(?:低于|少于|小于)\s*([\d.]+)\s*%/);
const thresholdPercent = thresholdMatch ? parseFloat(thresholdMatch[1]) : NaN;
const bonusMatch = desc.match(/额外(?:造成)?\s*([\d.]+)\s*%[^,。,、;]*?(?:伤害|输出)/);
const bonusPercent = bonusMatch ? parseFloat(bonusMatch[1]) : NaN;
let triggerChance = NaN;
const namePercentMatch = affix.name.match(/([\d.]+)\s*%/);
if (namePercentMatch) {
triggerChance = parseFloat(namePercentMatch[1]);
}
if ((isNaN(triggerChance) || triggerChance <= 0) && affix.percentage) {
const percentValue = parseFloat(affix.percentage.replace(/[^\d.]/g, ''));
if (!isNaN(percentValue)) {
triggerChance = percentValue;
}
}
if (isNaN(triggerChance) || triggerChance <= 0) {
const descChanceMatch = desc.match(/([\d.]+)\s*%[^,。,、;]*?(?:概率|几率|触发)/);
if (descChanceMatch) {
triggerChance = parseFloat(descChanceMatch[1]);
}
}
const normalizedChance = isNaN(triggerChance) ? 100 : Math.max(0, Math.min(100, triggerChance));
if (!isNaN(bonusPercent) && bonusPercent > 0 && !isNaN(thresholdPercent)) {
playerStats.收割词条.push({
name: affix.name.trim() || '收割',
chance: normalizedChance,
thresholdPercent,
bonusPercent
});
}
} else if (affix.name.includes('收尾')) {
const desc = affix.description || '';
const thresholdMatch = desc.match(/(?:血量|生命)[^,。,、;]*?(?:低于|不足|少于|小于)\s*([\d.]+)\s*%/);
const thresholdPercent = thresholdMatch ? parseFloat(thresholdMatch[1]) : NaN;
const percentPattern = /忽略(?:敌方)?\s*([\d.]+)\s*%[^,。,、;]*?(?:防御|护甲)/;
const flatPattern = /忽略(?:敌方)?\s*([\d.]+)\s*(?:点)?\s*(?:防御|护甲)/;
const percentMatch = desc.match(percentPattern);
const flatMatch = desc.match(flatPattern);
const percentValue = percentMatch ? parseFloat(percentMatch[1]) : NaN;
const ignoreValue = flatMatch ? parseFloat(flatMatch[1]) : NaN;
let triggerChance = NaN;
const namePercentMatch = affix.name.match(/([\d.]+)\s*%/);
if (namePercentMatch) {
triggerChance = parseFloat(namePercentMatch[1]);
}
if ((isNaN(triggerChance) || triggerChance <= 0) && affix.percentage) {
const percentValueFromTitle = parseFloat(affix.percentage.replace(/[^\d.]/g, ''));
if (!isNaN(percentValueFromTitle)) {
triggerChance = percentValueFromTitle;
}
}
if (isNaN(triggerChance) || triggerChance <= 0) {
const descChanceMatch = desc.match(/([\d.]+)\s*%[^,。,、;]*?(?:概率|几率|触发)/);
if (descChanceMatch) {
triggerChance = parseFloat(descChanceMatch[1]);
}
}
const normalizedChance = isNaN(triggerChance) ? 100 : Math.max(0, Math.min(100, triggerChance));
if ((!isNaN(ignoreValue) && ignoreValue > 0) || (!isNaN(percentValue) && percentValue > 0)) {
playerStats.收尾词条.push({
name: affix.name.trim() || '收尾',
chance: normalizedChance,
thresholdPercent: isNaN(thresholdPercent) ? null : thresholdPercent,
ignoreValue: isNaN(ignoreValue) ? null : ignoreValue,
percent: isNaN(percentValue) ? null : percentValue
});
}
}
});
});
}
function parseChineseNumeral(text) {
if (!text) {
return null;
}
const map = { '零': 0, '一': 1, '二': 2, '两': 2, '三': 3, '四': 4, '五': 5, '六': 6, '七': 7, '八': 8, '九': 9 };
let total = 0;
let current = 0;
for (const char of text) {
if (char === '十') {
if (current === 0) {
current = 1;
}
total += current * 10;
current = 0;
} else if (Object.prototype.hasOwnProperty.call(map, char)) {
current = map[char];
}
}
total += current;
return total || null;
}
function getSplitResult(player) {
const splitAffixes = player.分裂词条 || [];
const triggered = [];
let extraSegments = 0;
splitAffixes.forEach(affix => {
const chance = Math.max(0, Math.min(100, affix.chance || 0));
if (chance > 0 && Math.random() * 100 < chance) {
triggered.push(affix);
const segments = Math.max(2, affix.segments || 2);
extraSegments += segments - 1;
}
});
const totalSegments = 1 + extraSegments;
return {
segments: Math.max(1, totalSegments),
triggered
};
}
function formatSplitDescriptor(splitResult, segmentCount, segmentIndex, extraTags = []) {
const ratioText = segmentCount > 1 ? `(${segmentIndex}/${segmentCount})` : '';
const splitNames = splitResult.triggered.map(affix => affix.name || '分裂');
const otherTags = extraTags.filter(Boolean);
const splitNamesText = splitNames.length > 0 ? splitNames.join(' ') : '';
const otherTagsText = otherTags.length > 0 ? otherTags.join(' ') : '';
let descriptor = '';
if (ratioText) {
descriptor += ratioText;
}
if (splitNamesText) {
descriptor += splitNamesText;
}
if (otherTagsText) {
descriptor = descriptor ? `${descriptor} ${otherTagsText}` : otherTagsText;
}
return descriptor.trim();
}
function parseDescriptorParts(descriptor) {
if (!descriptor) {
return { ratio: '', tags: [] };
}
let ratio = '';
let remaining = descriptor.trim();
const ratioMatch = remaining.match(/^(\d+\/\d+)/);
if (ratioMatch) {
ratio = ratioMatch[0];
remaining = remaining.slice(ratio.length).trim();
}
const tags = remaining ? remaining.split(/\s+/).filter(Boolean) : [];
return { ratio, tags };
}
// 战斗伤害计算
function calculateDamage(player, monster, isCrit, options = {}) {
const baseDamageScale = options.baseDamageScale ?? 1;
const clampChance = (value) => {
if (typeof value !== 'number' || isNaN(value)) {
return 100;
}
return Math.max(0, Math.min(100, value));
};
const shouldTrigger = (chance) => {
if (typeof chance !== 'number' || isNaN(chance)) {
return true;
}
const normalized = Math.max(0, Math.min(100, chance));
if (normalized >= 100) {
return true;
}
return Math.random() * 100 < normalized;
};
const currentMonsterHP = typeof options.currentMonsterHP === 'number' ? options.currentMonsterHP : null;
const maxMonsterHP = typeof options.maxMonsterHP === 'number'
? options.maxMonsterHP
: (typeof monster.血量 === 'number' ? monster.血量 : null);
const monsterHpPercent = (currentMonsterHP !== null && typeof maxMonsterHP === 'number' && maxMonsterHP > 0)
? (currentMonsterHP / maxMonsterHP) * 100
: null;
let fractureDefenseReduction = 0;
let shockDefenseReduction = 0;
let finisherDefenseReduction = 0;
const triggeredEffectTags = [];
if (Array.isArray(player.碎骨词条)) {
player.碎骨词条.forEach(affix => {
const percentValue = typeof affix.percent === 'number' ? affix.percent : parseFloat(affix.percent);
const flatValue = typeof affix.flat === 'number' ? affix.flat : parseFloat(affix.flat);
if ((isNaN(percentValue) || percentValue <= 0) && (isNaN(flatValue) || flatValue <= 0)) {
return;
}
const triggerChance = clampChance(affix.triggerChance);
const effectChance = clampChance(affix.effectChance ?? 100);
if (triggerChance <= 0 || effectChance <= 0) {
return;
}
if (Math.random() * 100 < triggerChance && Math.random() * 100 < effectChance) {
let reduction = 0;
if (!isNaN(percentValue) && percentValue > 0) {
reduction = monster.防御 * (percentValue / 100);
} else if (!isNaN(flatValue) && flatValue > 0) {
reduction = flatValue;
}
fractureDefenseReduction += reduction;
triggeredEffectTags.push(affix.name || '碎骨');
}
});
}
if (monsterHpPercent !== null && Array.isArray(player.冲击词条)) {
player.冲击词条.forEach(affix => {
const thresholdPercent = typeof affix.thresholdPercent === 'number'
? affix.thresholdPercent
: parseFloat(affix.thresholdPercent);
if (!isNaN(thresholdPercent) && monsterHpPercent <= thresholdPercent) {
return;
}
const ignoreValue = typeof affix.ignoreValue === 'number' ? affix.ignoreValue : parseFloat(affix.ignoreValue);
const percentValue = typeof affix.percent === 'number' ? affix.percent : parseFloat(affix.percent);
if ((isNaN(ignoreValue) || ignoreValue <= 0) && (isNaN(percentValue) || percentValue <= 0)) {
return;
}
if (shouldTrigger(affix.chance)) {
let reduction = 0;
if (!isNaN(percentValue) && percentValue > 0) {
reduction += monster.防御 * (percentValue / 100);
}
if (!isNaN(ignoreValue) && ignoreValue > 0) {
reduction += ignoreValue;
}
shockDefenseReduction += reduction;
triggeredEffectTags.push(affix.name || '冲击');
}
});
}
if (monsterHpPercent !== null && Array.isArray(player.收尾词条)) {
player.收尾词条.forEach(affix => {
const thresholdPercent = typeof affix.thresholdPercent === 'number'
? affix.thresholdPercent
: parseFloat(affix.thresholdPercent);
if (!isNaN(thresholdPercent) && monsterHpPercent > thresholdPercent) {
return;
}
const ignoreValue = typeof affix.ignoreValue === 'number' ? affix.ignoreValue : parseFloat(affix.ignoreValue);
const percentValue = typeof affix.percent === 'number' ? affix.percent : parseFloat(affix.percent);
if ((isNaN(ignoreValue) || ignoreValue <= 0) && (isNaN(percentValue) || percentValue <= 0)) {
return;
}
let reduction = 0;
if (!isNaN(percentValue) && percentValue > 0) {
reduction += monster.防御 * (percentValue / 100);
}
if (!isNaN(ignoreValue) && ignoreValue > 0) {
reduction += ignoreValue;
}
finisherDefenseReduction += reduction;
triggeredEffectTags.push(affix.name || '收尾');
});
}
const baseDefense = Math.max(0, monster.防御 - player.破防 - fractureDefenseReduction - shockDefenseReduction - finisherDefenseReduction);
const damageCurveConst = (typeof monster.承伤系数 === 'number' && monster.承伤系数 > 0)
? monster.承伤系数
: 150;
const baseDamageMultiplier = damageCurveConst / (damageCurveConst + baseDefense);
const baseAttackDamage = baseDamageMultiplier * player.攻击;
let baseDamage = 0;
let preDefenseBaseDamage = 0;
let extraDamagePortion = 0;
const pendingExtraSegments = [];
const pendingVoidConversions = [];
const damageBonusMultiplier = 1
+ (player.全伤害加成 || 0)
+ (player.元素伤害加成 || 0);
let crueltyFlatReduction = 0;
let crueltyPercentReduction = 0;
let critDamageMultiplier = baseDamageMultiplier;
if (isCrit) {
if (Array.isArray(player.残忍百分比词条)) {
player.残忍百分比词条.forEach(affix => {
const percentValue = typeof affix.percent === 'number' ? affix.percent : parseFloat(affix.percent);
if (isNaN(percentValue) || percentValue <= 0) {
return;
}
if (shouldTrigger(affix.chance)) {
crueltyPercentReduction += monster.防御 * (percentValue / 100);
triggeredEffectTags.push(affix.name || '残忍');
}
});
}
if (Array.isArray(player.残忍固定词条)) {
player.残忍固定词条.forEach(affix => {
const value = typeof affix.value === 'number' ? affix.value : parseFloat(affix.value);
if (isNaN(value) || value <= 0) {
return;
}
if (shouldTrigger(affix.chance)) {
crueltyFlatReduction += value;
triggeredEffectTags.push(affix.name || '残忍');
}
});
}
// 暴击后的防御 = 怪物防御 - 怪物防御*百分比减少 - 暴击固定减少 - 人物破防 等
const percentRemaining = Math.max(0, 1 - (player.暴击百分比减少 || 0));
let defenseAfterPercent = monster.防御 * percentRemaining;
let critDefense = defenseAfterPercent - player.暴击固定减少 - player.破防 - (player.残忍减防 || 0) - crueltyFlatReduction - crueltyPercentReduction - fractureDefenseReduction - shockDefenseReduction - finisherDefenseReduction;
critDefense = Math.max(0, critDefense);
// 暴击承伤公式 = 承伤系数/(承伤系数+暴击后的实际防御)
critDamageMultiplier = damageCurveConst / (damageCurveConst + critDefense);
// 暴击时的实际伤害 = 人物攻击*人物暴击伤害*暴击承伤公式 + 暴击重击*暴击承伤公式
const critPreDamage = player.攻击 * player.暴击伤害 + player.暴击重击;
preDefenseBaseDamage = critPreDamage;
baseDamage = critPreDamage * critDamageMultiplier;
} else {
// 不暴击时的实际伤害 = 150/(150+怪物防御-破防) * 攻击 * 不暴击减免
const nonCritPreDamage = player.攻击 * player.不暴击减免;
preDefenseBaseDamage = nonCritPreDamage;
baseDamage = baseAttackDamage * player.不暴击减免;
}
baseDamage *= baseDamageScale;
preDefenseBaseDamage *= baseDamageScale;
if (player.追击词条 && player.追击词条.length > 0) {
player.追击词条.forEach(affix => {
const chance = Math.max(0, Math.min(100, affix.chance));
if (Math.random() * 100 < chance) {
const chaseDamage = affix.damage * baseDamageMultiplier;
extraDamagePortion += chaseDamage;
pendingExtraSegments.push({
name: affix.name || '追击',
rawDamage: chaseDamage,
type: '追击'
});
}
});
}
if (player.影刃词条 && player.影刃词条.length > 0) {
player.影刃词条.forEach(affix => {
const chance = Math.max(0, Math.min(100, affix.chance));
if (Math.random() * 100 < chance) {
let extraDamage = 0;
if (typeof affix.damage === 'number') {
extraDamage += affix.damage;
}
if (typeof affix.percent === 'number') {
extraDamage += player.攻击 * (affix.percent / 100);
}
extraDamagePortion += extraDamage;
pendingExtraSegments.push({
name: affix.name || '影刃',
rawDamage: extraDamage,
type: '影刃'
});
}
});
}
if (player.重击词条 && player.重击词条.length > 0) {
player.重击词条.forEach(affix => {
const chance = clampChance(affix.chance ?? 100);
if (Math.random() * 100 < chance) {
let extraAttackPortion = 0;
if (typeof affix.flat === 'number' && !isNaN(affix.flat)) {
extraAttackPortion += affix.flat;
}
if (typeof affix.percent === 'number' && !isNaN(affix.percent)) {
extraAttackPortion += player.攻击 * (affix.percent / 100);
}
const extraDamage = extraAttackPortion * baseDamageMultiplier * baseDamageScale;
if (extraDamage > 0) {
extraDamagePortion += extraDamage;
pendingExtraSegments.push({
name: affix.name || '重击',
rawDamage: extraDamage,
type: '重击'
});
}
}
});
}
if (isCrit && player.裂创词条 && player.裂创词条.length > 0) {
player.裂创词条.forEach(affix => {
const extraDamage = typeof affix.damage === 'number' ? affix.damage : 0;
if (extraDamage > 0) {
extraDamagePortion += extraDamage;
pendingExtraSegments.push({
name: affix.name || '裂创',
rawDamage: extraDamage,
type: '裂创'
});
}
});
}
if (isCrit && player.重创词条 && player.重创词条.length > 0) {
player.重创词条.forEach(affix => {
const extraDamage = typeof affix.damage === 'number' ? affix.damage : 0;
if (extraDamage > 0) {
const scaledExtra = extraDamage * critDamageMultiplier * baseDamageScale;
extraDamagePortion += scaledExtra;
pendingExtraSegments.push({
name: affix.name || '重创',
rawDamage: scaledExtra,
type: '重创'
});
}
});
}
if (player.虚无词条 && player.虚无词条.length > 0) {
player.虚无词条.forEach(affix => {
const chance = clampChance(affix.chance ?? 100);
if (chance <= 0) {
return;
}
if (Math.random() * 100 < chance) {
const percentValue = typeof affix.percent === 'number' ? affix.percent : parseFloat(affix.percent);
if (!isNaN(percentValue) && percentValue > 0) {
pendingVoidConversions.push({
name: affix.name || '虚无',
percent: percentValue
});
}
}
});
}
if (pendingVoidConversions.length > 0) {
const totalConvertedPercent = Math.min(100, pendingVoidConversions
.map(affix => typeof affix.percent === 'number' ? affix.percent : parseFloat(affix.percent))
.reduce((sum, value) => {
const sanitized = isNaN(value) ? 0 : Math.max(0, value);
return sum + sanitized;
}, 0));
const remainingRatio = Math.max(0, 1 - totalConvertedPercent / 100);
baseDamage *= remainingRatio;
}
const scaledBaseDamage = Math.ceil(baseDamage * damageBonusMultiplier);
if (pendingVoidConversions.length > 0 && preDefenseBaseDamage > 0) {
pendingVoidConversions.forEach(affix => {
const convertedPreDamage = preDefenseBaseDamage * (affix.percent / 100);
if (convertedPreDamage > 0) {
extraDamagePortion += convertedPreDamage;
pendingExtraSegments.push({
name: affix.name,
rawDamage: convertedPreDamage,
type: '虚无'
});
}
});
}
let executionBonusPercent = 0;
if (monsterHpPercent !== null && Array.isArray(player.收割词条)) {
player.收割词条.forEach(affix => {
const thresholdPercent = typeof affix.thresholdPercent === 'number'
? affix.thresholdPercent
: parseFloat(affix.thresholdPercent);
if (isNaN(thresholdPercent) || monsterHpPercent > thresholdPercent) {
return;
}
const bonusPercent = typeof affix.bonusPercent === 'number'
? affix.bonusPercent
: parseFloat(affix.bonusPercent);
if (isNaN(bonusPercent) || bonusPercent <= 0) {
return;
}
executionBonusPercent += bonusPercent;
triggeredEffectTags.push(affix.name || '收割');
});
}
const scaledExtraDamage = Math.round(extraDamagePortion * damageBonusMultiplier);
let totalDamage = Math.max(0, scaledBaseDamage + scaledExtraDamage);
if (executionBonusPercent > 0) {
totalDamage *= (1 + executionBonusPercent / 100);
totalDamage = Math.max(0, Math.floor(totalDamage));
}
const triggeredChargeTags = [];
let totalChargeBonusPercent = 0;
if (monsterHpPercent !== null && Array.isArray(player.冲锋词条)) {
player.冲锋词条.forEach(affix => {
const thresholdPercent = typeof affix.thresholdPercent === 'number'
? affix.thresholdPercent
: parseFloat(affix.thresholdPercent);
if (!isNaN(thresholdPercent) && monsterHpPercent <= thresholdPercent) {
return;
}
const bonusPercent = typeof affix.bonusPercent === 'number' ? affix.bonusPercent : parseFloat(affix.bonusPercent);
if (isNaN(bonusPercent) || bonusPercent <= 0) {
return;
}
if (shouldTrigger(affix.chance)) {
totalChargeBonusPercent += bonusPercent;
triggeredChargeTags.push(affix.name || '冲锋');
}
});
}
if (totalChargeBonusPercent > 0) {
totalDamage *= (1 + totalChargeBonusPercent / 100);
triggeredChargeTags.forEach(name => triggeredEffectTags.push(name));
totalDamage = Math.max(0, Math.round(totalDamage));
}
const trueDamageDetails = pendingExtraSegments.map(segment => ({
name: segment.name,
damage: Math.max(0, Math.round(segment.rawDamage * damageBonusMultiplier)),
type: segment.type
}));
return {
damage: totalDamage,
trueDamageDetails,
extraTags: triggeredEffectTags
};
}
// 模拟战斗(加入时间概念)
function simulateBattle(player, monster, battleTime) {
const battleLog = [];
let monsterHP = monster.血量;
let totalDamage = 0;
let critCount = 0;
let hitCount = 0;
let missCount = 0;
// 实际暴击率与命中率
const actualCritRate = Math.max(0, Math.min(100, player.暴击率 - monster.抗暴率));
const dodgeMultiplier = player.精准减闪系数 ?? 1;
const effectiveMonsterDodge = Math.max(0, monster.闪避率 * dodgeMultiplier);
const actualHitRate = Math.max(0, Math.min(100, player.命中率 - effectiveMonsterDodge));
// 计算总攻击次数 = 战斗时间(秒) × 攻速
const maxHits = Math.floor(battleTime * player.攻速);
let killTime = 0; // 击杀所需时间(秒)
for (let i = 0; i < maxHits && monsterHP > 0; i++) {
const attackNumber = i + 1;
const didHit = Math.random() * 100 < actualHitRate;
const splitResult = getSplitResult(player);
const segmentCount = Math.max(1, splitResult.segments || 1);
const baseDamageScale = 1 / segmentCount;
if (!didHit) {
missCount++;
const missDescriptor = formatSplitDescriptor(splitResult, segmentCount, 1);
const missPrefix = missDescriptor ? `${missDescriptor},` : '';
battleLog.push(`<p>${missPrefix}攻击未命中</p>`);
continue;
}
hitCount++;
for (let segmentIndex = 0; segmentIndex < segmentCount && monsterHP > 0; segmentIndex++) {
let segmentIsCrit = Math.random() * 100 < actualCritRate;
const explosionTags = [];
if (!segmentIsCrit && Array.isArray(player.爆发词条) && player.爆发词条.length > 0) {
for (const affix of player.爆发词条) {
const triggerChance = Math.max(0, Math.min(100, affix.triggerChance ?? 100));
const extraChance = Math.max(0, Math.min(100, affix.extraCritChance ?? 0));
if (extraChance <= 0 || triggerChance <= 0) {
continue;
}
if (Math.random() * 100 < triggerChance) {
if (Math.random() * 100 < extraChance) {
segmentIsCrit = true;
explosionTags.push(affix.name || '爆发');
break;
}
}
}
}
if (segmentIsCrit) {
critCount++;
}
const damageResult = calculateDamage(player, monster, segmentIsCrit, {
baseDamageScale,
currentMonsterHP: monsterHP,
maxMonsterHP: monster.血量
});
const damage = damageResult.damage;
monsterHP = Math.max(0, monsterHP - damage);
totalDamage += damage;
// 记录击杀时间
if (monsterHP <= 0 && killTime === 0) {
killTime = attackNumber / player.攻速;
}
const effectTags = (player.常驻显示词条 || []).map(name => name);
if (segmentIsCrit) {
effectTags.push('暴击');
}
if (damageResult.trueDamageDetails.length > 0) {
damageResult.trueDamageDetails.forEach(detail => {
effectTags.push(detail.name);
});
}
if (damageResult.extraTags && damageResult.extraTags.length > 0) {
damageResult.extraTags.forEach(tag => {
effectTags.push(tag);
});
}
if (explosionTags.length > 0) {
explosionTags.forEach(tag => effectTags.push(tag));
}
const descriptor = formatSplitDescriptor(splitResult, segmentCount, segmentIndex + 1, effectTags);
const { ratio, tags } = parseDescriptorParts(descriptor);
const ratioHtml = ratio ? `<span class="split-ratio">${ratio}</span>` : '';
const tagHtml = tags.length > 0 ? tags.map(tag => `<b>${tag}</b>`).join(' ') : '';
const labelHtml = [ratioHtml, tagHtml].filter(Boolean).join(' ').trim();
const prefix = labelHtml ? `${labelHtml},` : '';
const elementIcon = getElementIcon(player.攻击属性);
const damageDisplay = elementIcon ? `${elementIcon}${damage}` : `${damage}`;
const damageColor = '#e74c3c';
battleLog.push(
`<p>${prefix}造成 <span class="hp" style="color: ${damageColor}; font-weight: normal;">${damageDisplay}</span> 点伤害</p>`
);
// 附加伤害会在描述中以标签形式展示,无需重复记录
}
}
// 计算实际战斗时间和DPS
const actualBattleTime = killTime > 0 ? killTime : battleTime;
const dps = actualBattleTime > 0 ? Math.round(totalDamage / actualBattleTime) : 0;
return {
battleLog,
totalDamage,
hitCount,
critCount,
missCount,
avgDamage: hitCount > 0 ? Math.round(totalDamage / hitCount) : 0,
critRate: hitCount > 0 ? Math.round((critCount / hitCount) * 100 * 100) / 100 : 0,
dps: dps,
killTime: killTime > 0 ? killTime : null,
remainingHP: monsterHP,
isKilled: monsterHP <= 0
};
}
// 重复战斗10次
function simulateMultipleBattles(player, monster, battleTime, times = 10) {
const results = [];
let successCount = 0;
let totalKillTime = 0;
let killTimeCount = 0;
for (let i = 0; i < times; i++) {
const result = simulateBattle(player, monster, battleTime);
results.push(result);
if (result.isKilled) {
successCount++;
totalKillTime += result.killTime;
killTimeCount++;
}
}
const lastBattle = results[results.length - 1];
return {
winRate: Math.round((successCount / times) * 100 * 100) / 100,
currentDPS: lastBattle.dps,
avgKillTime: killTimeCount > 0 ? totalKillTime / killTimeCount : null,
lastBattleLog: lastBattle.battleLog,
lastRemainingHP: lastBattle.remainingHP,
isKilled: lastBattle.isKilled
};
}
function setPanelStatus(text, canReload = false) {
statusHint.textContent = text;
statusHint.style.display = text ? 'block' : 'none';
statusHint.dataset.reload = canReload ? 'true' : 'false';
statusHint.style.cursor = canReload ? 'pointer' : 'default';
}
statusHint.dataset.reload = 'false';
statusHint.onclick = () => {
if (!panelState.isLoading && statusHint.dataset.reload === 'true') {
loadBattleData();
}
};
function resetEquipmentCollapse() {
equipmentExpanded = false;
toggleIcon.textContent = '▼';
equipmentContent.style.maxHeight = '0px';
equipmentContent.style.opacity = '0';
}
async function loadBattleData() {
if (panelState.isLoading) {
return;
}
panelState.isLoading = true;
mainActionBtn.disabled = true;
mainActionBtn.textContent = '读取中...';
setPanelStatus('读取装备中...', false);
try {
const userAttrs = parseUserAttrs();
panelState.userAttrs = userAttrs;
playerStats.攻击属性 = '无';
const relicMonitor = getRelicMonitor();
const relicResult = relicMonitor.captureAttackElement();
const attackElementKey = relicResult.element || null;
playerStats.攻击属性 = relicResult.elementName;
playerStats.元素伤害加成 = attackElementKey ? (playerStats.元素伤害Map[attackElementKey] || 0) : 0;
userAttrs['攻击属性'] = relicResult.elementName;
if (!relicMonitor.isMonitoring) {
relicMonitor.startMonitoring();
}
const equipButtons = document.querySelectorAll('.item-btn-wrap .common-btn-wrap button');
const equipmentData = [];
for (let i = 0; i < Math.min(equipButtons.length, 5); i++) {
try {
equipButtons[i].click();
await new Promise(resolve => setTimeout(resolve, 300));
const equipInfo = document.querySelector('.item-info-wrap .equip-info.affix');
if (equipInfo) {
const equipment = parseEquipment(equipInfo);
equipmentData.push(equipment);
}
await new Promise(resolve => setTimeout(resolve, 200));
} catch (error) {
console.warn('装备读取失败', error);
}
}
if (equipmentData.length > 0) {
applyEquipmentEffects(equipmentData);
} else {
playerStats.追击伤害 = 0;
playerStats.追击词条 = [];
playerStats.影刃词条 = [];
playerStats.虚无词条 = [];
playerStats.重击词条 = [];
playerStats.裂创词条 = [];
playerStats.重创词条 = [];
playerStats.分裂词条 = [];
playerStats.爆发词条 = [];
playerStats.回响词条 = [];
playerStats.增幅词条 = [];
playerStats.灼烧词条 = [];
playerStats.引爆词条 = [];
playerStats.穿刺词条 = [];
playerStats.协同词条 = [];
}
panelState.equipmentData = equipmentData;
personalContent.innerHTML = buildPersonalAttrHTML(userAttrs);
personalSection.style.display = 'block';
equipmentContent.innerHTML = buildEquipmentTraitsHTML(equipmentData);
equipmentSection.style.display = 'block';
resetEquipmentCollapse();
helperPanelState.hasData = true;
updateHelperPanelVisibility();
panelState.isReady = Object.keys(userAttrs).length > 0;
mainActionBtn.textContent = panelState.isReady ? '打开战斗模拟' : '重新加载';
setPanelStatus(panelState.isReady ? '读取完成 · 点击重新读取' : '属性缺失 · 点击重新读取', true);
} catch (error) {
console.error('读取装备失败', error);
panelState.isReady = false;
mainActionBtn.textContent = '重新加载';
setPanelStatus('读取失败 · 点击重试', true);
} finally {
panelState.isLoading = false;
mainActionBtn.disabled = false;
}
}
mainActionBtn.onclick = () => {
if (panelState.isLoading) {
return;
}
if (!panelState.isReady) {
loadBattleData();
return;
}
openSimulationPanel();
};
function openSimulationPanel() {
const html = `
<div style="position: relative; text-align: center; margin-bottom: 12px;">
<div style="position: absolute; left: 0; top: 50%; transform: translateY(-50%);">
<button id="monsterSettingsToggle" style="padding: 8px 10px; border-radius: 18px; border: 1px solid rgba(255,255,255,0.1); background: rgba(255,255,255,0.05); color: #d6ddff; font-size: 12px; cursor: pointer;">👾 怪物属性</button>
</div>
<h2 style="margin: 0; color: #f8fafc; font-size: 16px;">⚔️ 战斗模拟器</h2>
<div style="position: absolute; right: 0; top: 50%; transform: translateY(-50%);">
<button id="closeSimulate" style="width: 32px; height: 32px; border-radius: 50%; border: 1px solid rgba(255,255,255,0.2); background: rgba(255,255,255,0.06); color: #d1d8ff; font-size: 18px; line-height: 1; cursor: pointer;">×</button>
</div>
</div>
<div style="margin-bottom: 12px;">
<button id="startBattle" style="width: 100%; padding: 10px; border-radius: 8px; border: none; background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); color: #fff; font-size: 12px; font-weight: bold; cursor: pointer;">开始模拟</button>
</div>
<div id="monsterSettingsPanel" style="display: none; background: rgba(255,255,255,0.02); padding: 12px; border-radius: 10px; border: 1px solid rgba(255,255,255,0.08); margin-bottom: 14px;">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 8px;">
<label style="display: flex; flex-direction: column; font-size: 11px; color: #9ea8d5;">血量<input type="number" id="monsterHP" value="${monsterSettings.血量}" style="margin-top: 4px; padding: 6px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.1); background: rgba(0,0,0,0.2); color: #fff;"></label>
<label style="display: flex; flex-direction: column; font-size: 11px; color: #9ea8d5;">防御<input type="number" id="monsterDefense" value="${monsterSettings.防御}" style="margin-top: 4px; padding: 6px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.1); background: rgba(0,0,0,0.2); color: #fff;"></label>
<label style="display: flex; flex-direction: column; font-size: 11px; color: #9ea8d5;">闪避(%)<input type="number" id="monsterDodge" value="${monsterSettings.闪避率}" style="margin-top: 4px; padding: 6px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.1); background: rgba(0,0,0,0.2); color: #fff;"></label>
<label style="display: flex; flex-direction: column; font-size: 11px; color: #9ea8d5;">抗暴(%)<input type="number" id="monsterAntiCrit" value="${monsterSettings.抗暴率}" style="margin-top: 4px; padding: 6px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.1); background: rgba(0,0,0,0.2); color: #fff;"></label>
<label style="display: flex; flex-direction: column; font-size: 11px; color: #9ea8d5;">承伤系数
<select id="damageCurveConstant" style="margin-top: 4px; padding: 6px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.1); background: rgba(0,0,0,0.2); color: #fff;">
<option value="150" ${monsterSettings.承伤系数 === 150 ? 'selected' : ''}>150(单人挑战)</option>
<option value="200" ${monsterSettings.承伤系数 === 200 ? 'selected' : ''}>200(可组队挑战怪物)</option>
</select>
</label>
<label style="display: flex; flex-direction: column; font-size: 11px; color: #9ea8d5;">战斗时间(秒)<input type="number" id="battleTime" value="${monsterSettings.战斗时间}" style="margin-top: 4px; padding: 6px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.1); background: rgba(0,0,0,0.2); color: #fff;"></label>
</div>
</div>
<div id="battleResult" style="margin-top: 10px; display: none;">
<h3 style="color: #f093fb; font-size: 13px; border-bottom: 1px solid rgba(255,255,255,0.08); padding-bottom: 6px; margin-bottom: 2px;">模拟结果</h3>
<div id="battleStats" style="background: rgba(255,255,255,0.02); padding: 12px; border-radius: 10px; margin: 2px 0;"></div>
<div style="background: rgba(255,255,255,0.02); border: 1px solid rgba(255,255,255,0.08); border-radius: 10px; padding: 10px;">
<button id="battleLogToggle" style="width: 100%; background: none; border: none; color: #f5f6ff; font-size: 12px; font-weight: bold; display: flex; justify-content: space-between; align-items: center; cursor: pointer; position: relative; padding: 0 0;">
<span style="visibility: hidden;">▼</span>
<span style="position: absolute; left: 50%; transform: translateX(-50%);">战斗日志</span>
<span id="battleLogIcon">▼</span>
</button>
<div id="battleLogWrapper" style="max-height: 0; overflow: hidden; opacity: 0; transition: max-height 0.25s ease, opacity 0.25s ease;">
<div id="battleLog" style="max-height: 260px; overflow-y: auto; font-size: 11px; line-height: 1.35;"></div>
</div>
</div>
</div>
`;
simulatePanel.innerHTML = html;
simulatePanel.style.display = 'block';
const settingsPanel = document.getElementById('monsterSettingsPanel');
const monsterToggle = document.getElementById('monsterSettingsToggle');
monsterToggle.onclick = () => {
const isOpen = settingsPanel.style.display === 'block';
settingsPanel.style.display = isOpen ? 'none' : 'block';
};
document.getElementById('closeSimulate').onclick = () => {
simulatePanel.style.display = 'none';
};
let logExpanded = false;
const logToggle = document.getElementById('battleLogToggle');
const logWrapper = document.getElementById('battleLogWrapper');
const logIcon = document.getElementById('battleLogIcon');
logToggle.onclick = () => {
logExpanded = !logExpanded;
logIcon.textContent = logExpanded ? '▲' : '▼';
if (logExpanded) {
logWrapper.style.maxHeight = logWrapper.scrollHeight + 'px';
logWrapper.style.opacity = '1';
} else {
logWrapper.style.maxHeight = '0px';
logWrapper.style.opacity = '0';
}
};
document.getElementById('startBattle').onclick = () => {
monsterSettings.血量 = parseInt(document.getElementById('monsterHP').value) || 0;
monsterSettings.防御 = parseInt(document.getElementById('monsterDefense').value) || 0;
monsterSettings.闪避率 = parseFloat(document.getElementById('monsterDodge').value) || 0;
monsterSettings.抗暴率 = parseFloat(document.getElementById('monsterAntiCrit').value) || 0;
monsterSettings.承伤系数 = parseInt(document.getElementById('damageCurveConstant').value) || 150;
const monster = {
血量: monsterSettings.血量,
防御: monsterSettings.防御,
闪避率: monsterSettings.闪避率,
抗暴率: monsterSettings.抗暴率,
承伤系数: monsterSettings.承伤系数
};
const battleTime = parseInt(document.getElementById('battleTime').value) || 180;
monsterSettings.战斗时间 = battleTime;
if (playerStats.攻击 === 0) {
alert('请先通过“加载战斗模拟”读取人物属性');
return;
}
if (playerStats.攻速 === 0) {
alert('攻速不能为空!');
return;
}
const result = simulateMultipleBattles(playerStats, monster, battleTime, 10);
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = Math.round(seconds % 60);
return `${mins}分${secs}秒`;
};
const killTimeDisplay = result.avgKillTime !== null
? `<div style="color: #4ade80; font-size: 13px; font-weight: bold;">${formatTime(result.avgKillTime)}</div>`
: `<div style="color: #f87171; font-size: 13px; font-weight: bold;">未击杀</div>`;
const remainingHPDisplay = result.isKilled
? `<div style="color: #4ade80; font-size: 13px; font-weight: bold;">已击杀</div>`
: `<div style="color: #f87171; font-size: 13px; font-weight: bold;">${result.lastRemainingHP}</div>`;
const statsHTML = `
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(60px, 1fr)); gap: 6px; text-align: center;">
<div style="background: rgba(0,0,0,0.25); padding: 6px; border-radius: 8px;">
<div style="color: #9ea8d5; font-size: 11px; margin-bottom: 6px;">DPS</div>
<div style="color: #c084fc; font-size: 13px; font-weight: bold;">${result.currentDPS}</div>
</div>
<div style="background: rgba(0,0,0,0.25); padding: 6px; border-radius: 8px;">
<div style="color: #9ea8d5; font-size: 11px; margin-bottom: 6px;">击杀时间</div>
${killTimeDisplay}
</div>
<div style="background: rgba(0,0,0,0.25); padding: 6px; border-radius: 8px;">
<div style="color: #9ea8d5; font-size: 11px; margin-bottom: 6px;">剩余血量</div>
${remainingHPDisplay}
</div>
<div style="background: rgba(0,0,0,0.25); padding: 6px; border-radius: 8px;">
<div style="color: #9ea8d5; font-size: 11px; margin-bottom: 6px;">胜率</div>
<div style="color: #fcd34d; font-size: 13px; font-weight: bold;">${result.winRate}%</div>
</div>
</div>
`;
document.getElementById('battleStats').innerHTML = statsHTML;
let logHTML = '<h4 style="margin: 0 0 8px 0; color: #d1d8ff; font-size: 12px;">战斗日志</h4>';
logHTML += result.lastBattleLog.join('');
const logContainer = document.getElementById('battleLog');
logContainer.innerHTML = logHTML;
document.getElementById('battleResult').style.display = 'block';
logContainer.scrollTop = logContainer.scrollHeight;
};
}
/**
* 圣物监控模块
*/
class RelicMonitor {
constructor() {
this.elementMap = {
'风灵球': 'wind',
'风暴之核': 'wind',
'火灵球': 'fire',
'熔岩之核': 'fire',
'水灵球': 'water',
'极冰之核': 'water',
'土灵球': 'earth',
'撼地之核': 'earth'
};
this.currentRelics = [];
this.currentElement = null;
this.observer = null;
this.debug = true;
this.isMonitoring = false;
}
log() {
// 控制台输出已禁用,保留钩子方便扩展
}
readRelics() {
const panels = document.querySelectorAll('.btn-wrap.item-btn-wrap');
if (panels.length < 3) {
return [];
}
const relicPanel = panels[2];
const buttons = relicPanel.querySelectorAll('.common-btn');
const relics = [];
buttons.forEach((button) => {
const span = button.querySelector('span[data-v-f49ac02d]');
if (span) {
const text = span.textContent.trim();
if (text && text !== '(未携带)') {
let relicName = text.replace(/[🌪️🔥💧⛰️]/g, '').trim();
relicName = relicName.replace(/\[\d+\]$/, '').trim();
relics.push(relicName);
}
}
});
return relics;
}
determineElement(relics) {
const elementCount = {
wind: 0,
fire: 0,
water: 0,
earth: 0
};
const elementRelics = {
wind: [],
fire: [],
water: [],
earth: []
};
relics.forEach((relic) => {
const element = this.elementMap[relic];
if (element) {
elementCount[element] += 1;
elementRelics[element].push(relic);
}
});
let maxCount = 0;
let candidates = [];
for (const [element, count] of Object.entries(elementCount)) {
if (count > maxCount) {
maxCount = count;
candidates = [element];
} else if (count === maxCount && count > 0) {
candidates.push(element);
}
}
if (maxCount === 0) {
return null;
}
if (candidates.length === 1) {
return candidates[0];
}
return this.compareElementBonus(candidates, elementRelics);
}
compareElementBonus(candidates) {
const bonusData = this.getElementBonus();
let maxBonus = -1;
let bestElement = candidates[0];
for (const element of candidates) {
const bonus = bonusData[element] || 0;
if (bonus > maxBonus) {
maxBonus = bonus;
bestElement = element;
}
}
return bestElement;
}
getElementBonus() {
const bonus = {
wind: 0,
fire: 0,
water: 0,
earth: 0
};
try {
const userAttrs = document.querySelector('.user-attrs');
const textWrap = userAttrs ? userAttrs.querySelector('.text-wrap') : null;
if (!textWrap) {
return bonus;
}
const paragraphs = textWrap.querySelectorAll('p');
paragraphs.forEach((p) => {
const text = p.textContent.trim();
if (text.includes('风伤害加成:')) {
const match = text.match(/风伤害加成:([\d.]+)%/);
if (match) {
bonus.wind = parseFloat(match[1]);
}
} else if (text.includes('火伤害加成:')) {
const match = text.match(/火伤害加成:([\d.]+)%/);
if (match) {
bonus.fire = parseFloat(match[1]);
}
} else if (text.includes('水伤害加成:')) {
const match = text.match(/水伤害加成:([\d.]+)%/);
if (match) {
bonus.water = parseFloat(match[1]);
}
} else if (text.includes('土伤害加成:')) {
const match = text.match(/土伤害加成:([\d.]+)%/);
if (match) {
bonus.earth = parseFloat(match[1]);
}
}
});
} catch (error) {
// 静默失败,确保主逻辑不中断
}
return bonus;
}
checkRelicChanges(newRelics) {
const added = newRelics.filter((r) => !this.currentRelics.includes(r));
const removed = this.currentRelics.filter((r) => !newRelics.includes(r));
return {
hasChanged: added.length > 0 || removed.length > 0,
added,
removed,
current: newRelics
};
}
update() {
const newRelics = this.readRelics();
const changes = this.checkRelicChanges(newRelics);
if (!changes.hasChanged) {
return;
}
this.currentRelics = newRelics;
const newElement = this.determineElement(newRelics);
if (newElement !== this.currentElement) {
this.currentElement = newElement;
this.onElementChange(newElement);
}
this.onRelicChange(changes);
}
onRelicChange() {
// 供外部覆盖
}
onElementChange() {
// 供外部覆盖
}
startMonitoring() {
this.currentRelics = this.readRelics();
this.currentElement = this.determineElement(this.currentRelics);
const targetNode = document.querySelector('.equip-list');
if (!targetNode) {
return;
}
const config = {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['class', 'style']
};
this.observer = new MutationObserver(() => {
this.update();
});
this.observer.observe(targetNode, config);
this.isMonitoring = true;
}
stopMonitoring() {
if (this.observer) {
this.observer.disconnect();
this.observer = null;
}
this.isMonitoring = false;
}
getStatus() {
return {
relics: this.currentRelics,
element: this.currentElement,
elementName: this.getElementName(this.currentElement)
};
}
getElementName(element) {
const names = {
wind: '风属性',
fire: '火属性',
water: '水属性',
earth: '土属性'
};
return element ? names[element] : '无';
}
test() {
return this.captureAttackElement();
}
captureAttackElement() {
const relics = this.readRelics();
const element = this.determineElement(relics);
return { relics, element, elementName: this.getElementName(element) };
}
}
function getRelicMonitor() {
if (!window.relicMonitor || typeof window.relicMonitor.captureAttackElement !== 'function') {
window.relicMonitor = new RelicMonitor();
}
return window.relicMonitor;
}
})();