// ==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 // @downloadURL none // ==/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 '
暂未读取到属性,请在角色界面触发
'; } return entries.map(([key, value]) => `
${key} ${value}
`).join(''); } function buildEquipmentTraitsHTML(equipmentData) { if (!equipmentData || equipmentData.length === 0) { return '
暂无装备词条
'; } 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 '
暂未检测到可展示的词条
'; } return entries.map(entry => { if (entry.type === 'special') { return `
${entry.description}
`; } const triggerRate = entry.chance ? entry.chance : '100%'; return `
${entry.name} ${triggerRate}
${entry.description}
`; }).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(`

${missPrefix}攻击未命中

`); 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 ? `${ratio}` : ''; const tagHtml = tags.length > 0 ? tags.map(tag => `${tag}`).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( `

${prefix}造成 ${damageDisplay} 点伤害

` ); // 附加伤害会在描述中以标签形式展示,无需重复记录 } } // 计算实际战斗时间和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 = `

⚔️ 战斗模拟器

`; 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 ? `
${formatTime(result.avgKillTime)}
` : `
未击杀
`; const remainingHPDisplay = result.isKilled ? `
已击杀
` : `
${result.lastRemainingHP}
`; const statsHTML = `
DPS
${result.currentDPS}
击杀时间
${killTimeDisplay}
剩余血量
${remainingHPDisplay}
胜率
${result.winRate}%
`; document.getElementById('battleStats').innerHTML = statsHTML; let logHTML = '

战斗日志

'; 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; } })();