// ==UserScript== // @name [Pokechill] 伤害计算器 // @namespace https://play-pokechill.github.io/ // @version 1.0 // @description 计算伤害,支持复制到粘贴板 // @author 人民当家做主 // @icon https://play-pokechill.github.io/img/icons/icon.png // @match https://play-pokechill.github.io/* // @match https://g1tyx.github.io/play-pokechill/* // ==/UserScript== (function () { 'use strict'; const MAX_WAIT = 15000; const CHECK_INTERVAL = 300; let waited = 0; const interval = setInterval(() => { if (document.readyState === 'complete' && typeof pkmn !== 'undefined' && typeof move !== 'undefined' && typeof ability !== 'undefined' && typeof item !== 'undefined' && typeof format !== 'undefined' && typeof openMenu !== 'undefined') { clearInterval(interval); initCalculator(); } else { waited += CHECK_INTERVAL; if (waited >= MAX_WAIT) { clearInterval(interval); console.warn('[伤害计算器] 游戏数据加载超时'); initCalculator(); } } }, CHECK_INTERVAL); function initCalculator() { const DICT = window.EN_CN_DICT || {}; function getChinese(englishId) { if (!englishId) return ''; let formatted = format(englishId); return DICT[englishId] || DICT[formatted] || formatted; } function calculateTypeEffectiveness(moveType, targetTypes) { let effectiveness = 1; for (const targetType of targetTypes) { const typeChart = typeChartMap[moveType]; if (typeChart && typeChart[targetType] !== undefined) { effectiveness *= typeChart[targetType]; } else { effectiveness *= 1; } } return effectiveness; } const typeChartMap = { normal: { rock: 0.5, steel: 0.5, ghost: 0 }, fire: { fire: 0.5, water: 0.5, grass: 2, ice: 2, bug: 2, rock: 0.5, dragon: 0.5, steel: 2 }, water: { fire: 2, water: 0.5, grass: 0.5, ground: 2, rock: 2, dragon: 0.5 }, electric: { water: 2, electric: 0.5, grass: 0.5, ground: 0, flying: 2, dragon: 0.5 }, grass: { fire: 0.5, water: 2, grass: 0.5, poison: 0.5, ground: 2, flying: 0.5, bug: 0.5, rock: 2, dragon: 0.5, steel: 0.5 }, ice: { fire: 0.5, water: 0.5, grass: 2, ice: 0.5, ground: 2, flying: 2, dragon: 2, steel: 0.5 }, fighting: { normal: 2, ice: 2, poison: 0.5, flying: 0.5, psychic: 0.5, bug: 0.5, rock: 2, ghost: 0, dark: 2, fairy: 0.5, steel: 2 }, poison: { grass: 2, poison: 0.5, ground: 0.5, rock: 0.5, ghost: 0.5, steel: 0, fairy: 2 }, ground: { fire: 2, electric: 2, grass: 0.5, poison: 2, flying: 0, bug: 0.5, rock: 2, steel: 2 }, flying: { electric: 0.5, grass: 2, fighting: 2, bug: 2, rock: 0.5, steel: 0.5 }, psychic: { fighting: 2, poison: 2, psychic: 0.5, dark: 0, steel: 0.5, fairy: 0.5 }, bug: { fire: 0.5, grass: 2, fighting: 0.5, poison: 0.5, flying: 0.5, psychic: 2, ghost: 0.5, dark: 2, steel: 0.5, fairy: 0.5 }, rock: { fire: 2, ice: 2, fighting: 0.5, ground: 0.5, flying: 2, bug: 2, steel: 0.5 }, ghost: { normal: 0, psychic: 2, ghost: 2, dark: 0.5 }, dragon: { dragon: 2, steel: 0.5, fairy: 0 }, dark: { fighting: 0.5, psychic: 2, ghost: 2, dark: 0.5, fairy: 0.5 }, steel: { fire: 0.5, water: 0.5, electric: 0.5, ice: 2, rock: 2, steel: 0.5, fairy: 2 }, fairy: { fire: 0.5, fighting: 2, poison: 0.5, dragon: 2, dark: 2, steel: 0.5 }, stellar: { stellar: 0 } }; function findIdByDisplayName(category, displayName) { if (!displayName) return null; let list; if (category === 'pkmn') list = Object.keys(pkmn).filter(id => !pkmn[id].hidden); else if (category === 'move') list = Object.keys(move); else if (category === 'ability') list = Object.keys(ability); else if (category === 'item') list = Object.keys(item).filter(id => item[id].type === 'held' && !item[id].hidden); else return null; const lowerDisplay = displayName.toLowerCase(); for (const id of list) { const chinese = getChinese(id).toLowerCase(); if (chinese === lowerDisplay || id.toLowerCase() === lowerDisplay) return id; } return null; } function createDataListWithChinese(id, items, inputElement) { const datalist = document.createElement('datalist'); datalist.id = id; for (const key of items) { const option = document.createElement('option'); option.value = getChinese(key); option.dataset.id = key; datalist.appendChild(option); } document.body.appendChild(datalist); inputElement.addEventListener('input', function () { const val = this.value; const options = datalist.options; for (let opt of options) { if (opt.value === val) { this.dataset.id = opt.dataset.id; return; } } const foundId = findIdByDisplayName(id.replace('-list-cn', ''), val); if (foundId) { this.dataset.id = foundId; this.value = getChinese(foundId); } else { this.dataset.id = ''; } }); inputElement.addEventListener('change', function () { if (this.dataset.id) return; const val = this.value; const options = datalist.options; for (let opt of options) { if (opt.value === val) { this.dataset.id = opt.dataset.id; return; } } }); } function getItemMultiplierFormula(itemId) { const it = item[itemId]; if (!it || typeof it.power !== 'function') return null; const fnStr = it.power.toString(); const match = fnStr.match(/return\s+1\+\((\d+(?:\.\d+)?)\s*\*\s*returnItemLevel\(/); if (match) { const coeff = parseFloat(match[1]); return (level) => 1 + coeff * level; } const directMatch = fnStr.match(/return\s+([\d.]+);?$/); if (directMatch) { const fixed = parseFloat(directMatch[1]); return () => fixed; } return () => 1; } function getHiddenAbility(pkmnId) { const pk = pkmn[pkmnId]; if (pk && pk.hiddenAbility) { return pk.hiddenAbility.id; } return null; } function getWeatherTypeMult(weather, moveType) { if (!weather) return 1; switch (weather) { case 'sunny': if (moveType === 'fire') return 1.5; if (moveType === 'water') return 0.5; break; case 'rainy': if (moveType === 'water') return 1.5; if (moveType === 'fire') return 0.5; break; case 'sandstorm': if (moveType === 'rock' || moveType === 'ground') return 1.5; break; case 'hail': if (moveType === 'ice') return 1.5; break; case 'electricTerrain': if (moveType === 'electric' || moveType === 'steel') return 1.5; break; case 'grassyTerrain': if (moveType === 'grass' || moveType === 'bug') return 1.5; break; case 'mistyTerrain': if (moveType === 'fairy' || moveType === 'psychic') return 1.5; break; case 'foggy': if (moveType === 'ghost' || moveType === 'dark') return 1.5; break; default: return 1; } return 1; } // ---------- 创建UI ---------- const panel = document.createElement('div'); panel.id = 'pokechill-damage-calc'; panel.innerHTML = `
伤害计算器
6
星级5(固定)
6
`; document.body.appendChild(panel); // 样式调整:输入框/下拉框宽度减少1/3,统一样式 const style = document.createElement('style'); style.textContent = ` #pokechill-damage-calc { position: fixed; top: 80px; right: 20px; width: 540px; max-width: 95%; background: #3068b0; border: 4px solid #f8d030; border-radius: 16px; box-shadow: 0 12px 30px rgba(0,0,0,0.8); z-index: 10000; color: #f8f8f8; font-family: 'Winky Sans', 'Segoe UI', sans-serif; font-size: 13px; user-select: none; backdrop-filter: blur(2px); } #pokechill-damage-calc .calc-header { display: flex; justify-content: space-between; align-items: center; padding: 10px 14px; background: #204080; border-radius: 12px 12px 0 0; cursor: move; border-bottom: 2px solid #f8d030; } #pokechill-damage-calc .calc-title { font-weight: bold; color: #f8d030; display: flex; align-items: center; gap: 6px; font-size: 15px; text-shadow: 2px 2px 0 #183060; } #pokechill-damage-calc .calc-header-buttons { display: flex; gap: 6px; } #pokechill-damage-calc .calc-toggle-btn { background: #f8d030; border: 2px solid #c0a020; color: #204080; font-size: 16px; cursor: pointer; padding: 4px 8px; border-radius: 6px; transition: 0.1s; line-height: 1; font-weight: bold; } #pokechill-damage-calc .calc-toggle-btn:hover { background: #ffdf70; border-color: #e0b020; } #pokechill-damage-calc .calc-body { padding: 12px; max-height: 70vh; overflow-y: auto; scrollbar-width: thin; scrollbar-color: #f8d030 #204080; } .calc-section { background: rgba(32, 64, 128, 0.7); border-radius: 10px; margin-bottom: 10px; border: 1px solid #f8d030; } .calc-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; padding: 10px; } .calc-field { display: flex; flex-direction: column; gap: 2px; } .calc-field label { display: flex; align-items: center; gap: 4px; color: #f8e0a0; font-size: 12px; text-shadow: 1px 1px 0 #183060; } .field-icon { font-size: 14px; } /* 统一输入框和下拉框样式,宽度减少1/3 */ input[type="text"] .slider-group { display: flex; align-items: center; gap: 6px; width: 100%; } input[type="range"] { flex: 1; height: 6px; background: #f8d030; border-radius: 3px; -webkit-appearance: none; } input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 16px; height: 16px; background: #f8f8f8; border: 2px solid #204080; border-radius: 50%; cursor: pointer; } .slider-group span { min-width: 20px; text-align: center; background: #f8d030; padding: 1px 0; border-radius: 3px; color: #204080; font-size: 12px; font-weight: bold; } .checkbox-field { justify-content: center; flex-direction: row; align-items: center; } .checkbox-field label { display: flex; align-items: center; gap: 4px; cursor: pointer; } input[type="checkbox"] { width: 14px; height: 14px; accent-color: #f8d030; } .calc-actions { display: flex; gap: 8px; justify-content: center; margin: 10px 0 6px; } .calc-btn { border: none; padding: 6px 14px; border-radius: 20px; font-weight: bold; font-size: 13px; cursor: pointer; transition: 0.15s; display: inline-flex; align-items: center; gap: 4px; border: 2px solid; } .calc-btn.primary { background: #f8d030; color: #204080; border-color: #c0a020; box-shadow: 0 3px 0 #a08010; } .calc-btn.primary:hover { background: #ffdf70; transform: translateY(-1px); box-shadow: 0 4px 0 #a08010; } .calc-btn.primary:active { transform: translateY(1px); box-shadow: 0 2px 0 #a08010; } .calc-btn.secondary { background: #c0c0c0; color: #204080; border-color: #a0a0a0; box-shadow: 0 3px 0 #808080; } .calc-btn.secondary:hover { background: #d0d0d0; transform: translateY(-1px); box-shadow: 0 4px 0 #808080; } .calc-result { background: rgba(32, 64, 128, 0.8); border-radius: 8px; padding: 10px; margin-top: 10px; border: 2px solid #f8d030; animation: fadeIn 0.3s; } .calc-result-title { font-size: 13px; font-weight: bold; color: #f8d030; margin-bottom: 8px; text-align: center; text-shadow: 1px 1px 0 #183060; } .calc-total { font-size: 18px; font-weight: bold; text-align: center; margin-top: 8px; color: #f8f8f8; background: #204080; padding: 4px; border-radius: 20px; border: 1px solid #f8d030; } #calc-breakdown { font-size: 11px; line-height: 1.5; color: #f0f0f0; max-height: 150px; overflow-y: auto; padding-right: 4px; } #calc-breakdown div { border-bottom: 1px dotted #f8d030; padding: 2px 0; } #calc-breakdown span { color: #f8d030; font-weight: bold; } @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } @media (max-width: 600px) { #pokechill-damage-calc { width: 95%; right: 2.5%; top: 60px; } .calc-grid { grid-template-columns: 1fr; } input[type="text"], select { width: 60%; } /* 移动端稍宽 */ } `; document.head.appendChild(style); // 获取元素 const attackerInput = document.getElementById('calc-attacker'); const defenderInput = document.getElementById('calc-defender'); const moveInput = document.getElementById('calc-move'); const abilityInput = document.getElementById('calc-ability'); const itemInput = document.getElementById('calc-item'); const atkIvSlider = document.getElementById('calc-atk-iv'); const defIvSlider = document.getElementById('calc-def-iv'); const atkIvVal = document.getElementById('calc-atk-iv-val'); const defIvVal = document.getElementById('calc-def-iv-val'); const shinyCheck = document.getElementById('calc-shiny'); const crossCheck = document.getElementById('calc-cross'); const atkStage = document.getElementById('calc-atk-stage'); const satkStage = document.getElementById('calc-satk-stage'); const defStage = document.getElementById('calc-def-stage'); const sdefStage = document.getElementById('calc-sdef-stage'); const attackerStatus = document.getElementById('calc-attacker-status'); const weatherSelect = document.getElementById('calc-weather'); const toggleBtn = panel.querySelector('.calc-toggle-btn'); const copyBtn = document.getElementById('calc-copy-result'); const hiddenAbilityDisplay = document.getElementById('calc-hidden-ability-display'); // 滑块联动 atkIvSlider.addEventListener('input', () => { atkIvVal.innerText = atkIvSlider.value; }); defIvSlider.addEventListener('input', () => { defIvVal.innerText = defIvSlider.value; }); // 构建数据列表 const pkmnIds = Object.keys(pkmn).filter(id => !pkmn[id].hidden); const moveIds = Object.keys(move); const abilityIds = Object.keys(ability); const itemIds = Object.keys(item).filter(id => item[id].type === 'held' && !item[id].hidden); createDataListWithChinese('pkmn-list-cn', pkmnIds, attackerInput); createDataListWithChinese('pkmn-list-cn', pkmnIds, defenderInput); createDataListWithChinese('move-list-cn', moveIds, moveInput); createDataListWithChinese('ability-list-cn', abilityIds, abilityInput); createDataListWithChinese('item-list-cn', itemIds, itemInput); // 攻击方输入变化时自动更新隐藏特性 attackerInput.addEventListener('change', function() { const id = this.dataset.id; if (id) { const hiddenId = getHiddenAbility(id); hiddenAbilityDisplay.textContent = hiddenId ? getChinese(hiddenId) : '无'; } else { hiddenAbilityDisplay.textContent = '无'; } }); // 拖动 let isDragging = false, offsetX, offsetY; const header = panel.querySelector('.calc-header'); header.addEventListener('mousedown', (e) => { if (e.target.tagName === 'BUTTON') return; isDragging = true; offsetX = e.clientX - panel.offsetLeft; offsetY = e.clientY - panel.offsetTop; panel.style.cursor = 'grabbing'; }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; panel.style.left = (e.clientX - offsetX) + 'px'; panel.style.top = (e.clientY - offsetY) + 'px'; panel.style.right = 'auto'; }); document.addEventListener('mouseup', () => { isDragging = false; panel.style.cursor = 'default'; }); // 折叠/展开 let isCollapsed = false; toggleBtn.addEventListener('click', () => { isCollapsed = !isCollapsed; const body = panel.querySelector('.calc-body'); const result = panel.querySelector('#calc-result'); if (isCollapsed) { toggleBtn.textContent = '︾'; body.style.display = 'none'; if (result.style.display !== 'none') { result.style.display = 'none'; result.__wasVisibleBeforeCollapse = true; } } else { toggleBtn.textContent = '︽'; body.style.display = 'block'; if (result.__wasVisibleBeforeCollapse) { result.style.display = 'block'; delete result.__wasVisibleBeforeCollapse; } } }); // 重置 document.getElementById('calc-reset').addEventListener('click', () => { attackerInput.value = ''; attackerInput.dataset.id = ''; defenderInput.value = ''; defenderInput.dataset.id = ''; moveInput.value = ''; moveInput.dataset.id = ''; abilityInput.value = ''; abilityInput.dataset.id = ''; itemInput.value = ''; itemInput.dataset.id = ''; atkIvSlider.value = 6; atkIvVal.innerText = '6'; defIvSlider.value = 6; defIvVal.innerText = '6'; shinyCheck.checked = false; crossCheck.checked = false; atkStage.value = '0'; satkStage.value = '0'; defStage.value = '0'; sdefStage.value = '0'; attackerStatus.value = 'none'; weatherSelect.value = ''; hiddenAbilityDisplay.textContent = '无'; document.getElementById('calc-result').style.display = 'none'; }); // 计算按钮 document.getElementById('calc-btn').addEventListener('click', () => { const attackerId = attackerInput.dataset.id; const defenderId = defenderInput.dataset.id; const moveId = moveInput.dataset.id; const abilityId = abilityInput.dataset.id; const itemId = itemInput.dataset.id; const itemLevel = 5; // 固定5级 const atkIv = parseInt(atkIvSlider.value, 10); const defIv = parseInt(defIvSlider.value, 10); const isShiny = shinyCheck.checked; const isCross = crossCheck.checked; const hiddenAbilityId = getHiddenAbility(attackerId); // 更新隐藏特性显示(确保即使不触发change也能更新) if (hiddenAbilityId) { hiddenAbilityDisplay.textContent = getChinese(hiddenAbilityId); } else { hiddenAbilityDisplay.textContent = '无'; } const atkStageVal = parseFloat(atkStage.value); const satkStageVal = parseFloat(satkStage.value); const defStageVal = parseFloat(defStage.value); const sdefStageVal = parseFloat(sdefStage.value); const attackerStatusVal = attackerStatus.value; const weatherVal = weatherSelect.value; const attacker = pkmn[attackerId]; const defender = pkmn[defenderId]; const moveObj = move[moveId]; if (!attacker || !defender || !moveObj) { alert('请正确填写宝可梦和招式'); return; } if (moveObj.power === 0) { alert('变化招式没有伤害'); return; } const abilityObj = ability[abilityId]; const hiddenAbilityObj = hiddenAbilityId ? ability[hiddenAbilityId] : null; const split = moveObj.split; const isPhysical = split === 'physical'; let attackerBaseStar = isPhysical ? attacker.bst.atk : attacker.bst.satk; let defenderBaseStar = isPhysical ? defender.bst.def : defender.bst.sdef; function getStageMult(val, isDef = false) { if (val === 0) return 1; if (val === 1) return isDef ? 2/3 : 1.5; if (val === 2) return isDef ? 0.5 : 2; if (val === -1) return isDef ? 1.5 : 2/3; if (val === -2) return isDef ? 2 : 0.5; return 1; } const atkStageMult = isPhysical ? getStageMult(atkStageVal) : 1; const satkStageMult = !isPhysical ? getStageMult(satkStageVal) : 1; const defStageMult = isPhysical ? getStageMult(defStageVal, true) : 1; const sdefStageMult = !isPhysical ? getStageMult(sdefStageVal, true) : 1; let attackerStar = attackerBaseStar * (isPhysical ? atkStageMult : satkStageMult); let defenderStar = defenderBaseStar * (isPhysical ? defStageMult : sdefStageMult); const atkFactor = Math.pow(1.1, atkIv); const defFactor = Math.pow(1.1, defIv); const attackerLevel = 100; const levelMult = 1 + 100 * 0.1; let baseDamage = moveObj.power + Math.max(0, (attackerStar * 30 * atkFactor) - (defenderStar * 30 * defFactor) ); let damage = baseDamage * levelMult; let breakdown = []; breakdown.push(`基础威力: ${moveObj.power}`); breakdown.push(`攻击星: ${attackerBaseStar.toFixed(1)} x能力${(isPhysical ? atkStageVal : satkStageVal)} = ${attackerStar.toFixed(1)} (x30 x${atkFactor.toFixed(2)}) = ${(attackerStar * 30 * atkFactor).toFixed(0)}`); breakdown.push(`防御星: ${defenderBaseStar.toFixed(1)} x能力${(isPhysical ? defStageVal : sdefStageVal)} = ${defenderStar.toFixed(1)} (x30 x${defFactor.toFixed(2)}) = ${(defenderStar * 30 * defFactor).toFixed(0)}`); breakdown.push(`差值: ${(attackerStar * 30 * atkFactor - defenderStar * 30 * defFactor).toFixed(0)}`); breakdown.push(`基础伤害: ${baseDamage.toFixed(0)}`); breakdown.push(`等级系数 x${levelMult.toFixed(2)}`); let totalMult = 1; let multLog = '1'; // STAB let stab = 1; if (attacker.type.includes(moveObj.type)) { stab = 1.5; if (attacker.type.length === 1) stab += 0.2; if (abilityObj && abilityObj.id === 'adaptability') stab = 2; if (hiddenAbilityObj && hiddenAbilityObj.id === 'adaptability') stab = 2; damage *= stab; totalMult *= stab; multLog += ` x${stab.toFixed(2)} (STAB)`; breakdown.push(`STAB x${stab.toFixed(2)}`); } else { breakdown.push(`STAB x1 (无本系)`); } // 属性克制 let typeMult = calculateTypeEffectiveness(moveObj.type, defender.type); damage *= typeMult; totalMult *= typeMult; multLog += ` x${typeMult.toFixed(2)} (克制)`; breakdown.push(`属性克制 x${typeMult.toFixed(2)}`); // 天气 const weatherMult = getWeatherTypeMult(weatherVal, moveObj.type); if (weatherMult !== 1) { damage *= weatherMult; totalMult *= weatherMult; multLog += ` x${weatherMult.toFixed(2)} (天气)`; breakdown.push(`天气 x${weatherMult.toFixed(2)}`); } // 闪光 if (isShiny) { damage *= 1.15; totalMult *= 1.15; multLog += ` x1.15 (闪光)`; breakdown.push(`闪光 x1.15`); } // 交叉之力 if (isCross) { damage *= 1.3; totalMult *= 1.3; multLog += ` x1.3 (交叉)`; breakdown.push(`交叉之力 x1.3`); } // 攻击方状态 if (attackerStatusVal === 'burn' && isPhysical) { damage /= 1.5; totalMult /= 1.5; multLog += ` ÷1.5 (烧伤)`; breakdown.push(`烧伤 ÷1.5`); } if (attackerStatusVal === 'poison' && !isPhysical) { damage /= 1.5; totalMult /= 1.5; multLog += ` ÷1.5 (中毒)`; breakdown.push(`中毒 ÷1.5`); } // 道具 let itemMult = 1; if (itemId) { const formula = getItemMultiplierFormula(itemId); if (formula) { itemMult = formula(itemLevel); if (itemMult > 1) { damage *= itemMult; totalMult *= itemMult; multLog += ` x${itemMult.toFixed(2)} (道具)`; breakdown.push(`道具 x${itemMult.toFixed(2)} (星级5)`); } else { breakdown.push(`道具: 无伤害加成`); } } } function applyAbility(abObj, label) { if (!abObj) return 1; let mult = 1; let note = ''; switch (abObj.id) { case 'hugePower': if (isPhysical) { mult = 2; note = '大力士'; } break; case 'purePower': if (isPhysical) { mult = 2; note = '全力'; } break; case 'technician': if (moveObj.power <= 60) { mult = 1.5; note = '技术高手'; } break; case 'ironFist': if (moveObj.affectedBy && moveObj.affectedBy.includes('ironFist')) { mult = 1.5; note = '铁拳'; } break; case 'strongJaw': if (moveObj.affectedBy && moveObj.affectedBy.includes('strongJaw')) { mult = 2; note = '强壮之颚'; } break; case 'toughClaws': if (moveObj.affectedBy && moveObj.affectedBy.includes('toughClaws')) { mult = 2; note = '硬爪'; } break; case 'sheerForce': if (moveObj.hitEffect) { mult = 1.25; note = '强行'; } break; case 'reckless': if (moveObj.timer > 10) { mult = 1.5; note = '舍身'; } break; case 'libero': if (moveObj.timer < 10) { mult = 2; note = '自由者'; } break; case 'overgrow': if (moveObj.type === 'grass' && attacker.hp / attacker.hpMax < 0.5) { mult = 1.5; note = '茂盛'; } break; case 'blaze': if (moveObj.type === 'fire' && attacker.hp / attacker.hpMax < 0.5) { mult = 1.5; note = '猛火'; } break; case 'swarm': if (moveObj.type === 'bug' && attacker.hp / attacker.hpMax < 0.5) { mult = 1.5; note = '虫之预感'; } break; case 'torrent': if (moveObj.type === 'water' && attacker.hp / attacker.hpMax < 0.5) { mult = 1.5; note = '激流'; } break; default: break; } if (mult > 1) { damage *= mult; totalMult *= mult; multLog += ` x${mult.toFixed(2)} (${label})`; breakdown.push(`特性 ${label} ${note} x${mult.toFixed(2)}`); } else { breakdown.push(`特性 ${label}: 无伤害加成`); } return mult; } if (abilityObj) applyAbility(abilityObj, '可选特性'); if (hiddenAbilityObj) applyAbility(hiddenAbilityObj, '隐藏特性'); const finalDamage = Math.round(damage); // 保存详细报告 window.__lastDamageReport = { attacker: { name: getChinese(attackerId), level: 100, split: isPhysical ? '物理' : '特殊', baseStar: isPhysical ? attackerBaseStar : attackerBaseStar, stage: isPhysical ? atkStageVal : satkStageVal, stageMult: isPhysical ? atkStageMult : satkStageMult, iv: isPhysical ? atkIv : atkIv, ivFactor: isPhysical ? atkFactor : Math.pow(1.1, atkIv), finalStat: attackerStar, finalStatValue: (attackerStar * 30 * (isPhysical ? atkFactor : Math.pow(1.1, atkIv))).toFixed(0), status: attackerStatusVal, shiny: isShiny, cross: isCross, item: itemId ? { name: getChinese(itemId), level: itemLevel, mult: itemMult } : null, abilityEffects: [] }, defender: { name: getChinese(defenderId), baseStar: isPhysical ? defenderBaseStar : defenderBaseStar, stage: isPhysical ? defStageVal : sdefStageVal, stageMult: isPhysical ? defStageMult : sdefStageMult, iv: isPhysical ? defIv : defIv, ivFactor: defFactor, finalStat: defenderStar, finalStatValue: (defenderStar * 30 * defFactor).toFixed(0) }, move: { name: getChinese(moveId), power: moveObj.power, type: moveObj.type }, weather: weatherVal, typeMult: typeMult, stab: stab, levelMult: levelMult, baseDamage: baseDamage.toFixed(0), damageBeforeMult: (damage / totalMult).toFixed(0), totalMult: totalMult, finalDamage: finalDamage }; // 显示结果 if (isCollapsed) { isCollapsed = false; panel.querySelector('.calc-body').style.display = 'block'; const result = panel.querySelector('#calc-result'); if (result.__wasVisibleBeforeCollapse) { result.style.display = 'block'; delete result.__wasVisibleBeforeCollapse; } toggleBtn.textContent = '︽'; } document.getElementById('calc-result').style.display = 'block'; document.getElementById('calc-total').innerText = finalDamage.toLocaleString(); const breakdownDiv = document.getElementById('calc-breakdown'); breakdownDiv.innerHTML = ''; breakdown.forEach(line => { const p = document.createElement('div'); p.innerHTML = line.replace(/x([\d.]+)/g, 'x$1'); breakdownDiv.appendChild(p); }); const multLine = document.createElement('div'); multLine.style.marginTop = '6px'; multLine.style.borderTop = '1px dashed #f8d030'; multLine.style.paddingTop = '4px'; multLine.innerHTML = `总倍率: ${multLog} = ${totalMult.toFixed(3)}`; breakdownDiv.appendChild(multLine); }); // 复制详细报告 copyBtn.addEventListener('click', () => { const r = window.__lastDamageReport; if (!r || r.finalDamage === undefined) { alert('请先计算伤害'); return; } const lines = []; lines.push('══════════════════════════════════════════'); lines.push(' 宝可梦伤害计算详细报告'); lines.push('══════════════════════════════════════════'); lines.push(''); // 攻击方 lines.push('【攻击方】'); lines.push(` 宝可梦:${r.attacker.name} Lv.100(${r.attacker.split})`); lines.push(` ├─ 基础星级:${r.attacker.baseStar.toFixed(1)} ★(种族值转化)`); lines.push(` ├─ 能力等级:${r.attacker.stage} → 倍率 x${r.attacker.stageMult.toFixed(2)}`); lines.push(` ├─ 个体值:${r.attacker.iv} 星 → 因子 x${r.attacker.ivFactor.toFixed(2)}`); lines.push(` ├─ 最终攻击值 = 基础星级 × 能力倍率 × 30 × 个体因子`); lines.push(` = ${r.attacker.baseStar.toFixed(1)} × ${r.attacker.stageMult.toFixed(2)} × 30 × ${r.attacker.ivFactor.toFixed(2)} = ${r.attacker.finalStatValue}`); if (r.attacker.status !== 'none') { const statusText = r.attacker.status === 'burn' ? '烧伤' : '中毒'; lines.push(` ├─ 状态:${statusText}(${r.attacker.split === '物理' ? '物理' : '特殊'}攻击伤害 ÷1.5)`); } if (r.attacker.shiny) lines.push(` ├─ 闪光:伤害 x1.15`); if (r.attacker.cross) lines.push(` ├─ 交叉之力:伤害 x1.3`); if (r.attacker.item) { lines.push(` ├─ 携带道具:${r.attacker.item.name} 星级 5 → 伤害 x${r.attacker.item.mult.toFixed(2)}`); } if (r.attacker.abilityEffects.length) { r.attacker.abilityEffects.forEach(ae => { lines.push(` ├─ 特性:${ae.name}${ae.note ? '(' + ae.note + ')' : ''} → 伤害 x${ae.mult.toFixed(2)}`); }); } lines.push(''); // 防御方 lines.push('【防御方】'); lines.push(` 宝可梦:${r.defender.name}`); lines.push(` ├─ 基础星级:${r.defender.baseStar.toFixed(1)} ★(种族值转化)`); lines.push(` ├─ 能力等级:${r.defender.stage} → 倍率 x${r.defender.stageMult.toFixed(2)}`); lines.push(` ├─ 个体值:${r.defender.iv} 星 → 因子 x${r.defender.ivFactor.toFixed(2)}`); lines.push(` ├─ 最终防御值 = 基础星级 × 能力倍率 × 30 × 个体因子`); lines.push(` = ${r.defender.baseStar.toFixed(1)} × ${r.defender.stageMult.toFixed(2)} × 30 × ${r.defender.ivFactor.toFixed(2)} = ${r.defender.finalStatValue}`); lines.push(''); // 招式 lines.push('【招式】'); lines.push(` 名称:${r.move.name}`); lines.push(` 威力:${r.move.power}`); lines.push(` 属性:${r.move.type}`); lines.push(''); // 基础伤害计算 lines.push('【基础伤害计算】'); const attackVal = parseInt(r.attacker.finalStatValue); const defenseVal = parseInt(r.defender.finalStatValue); const diff = Math.max(0, attackVal - defenseVal); lines.push(` ① 威力差值 = max(0, 攻击方最终值 - 防御方最终值) = max(0, ${attackVal} - ${defenseVal}) = ${diff}`); lines.push(` ② 基础伤害 = 招式威力 + 威力差值 = ${r.move.power} + ${diff} = ${r.baseDamage}`); lines.push(` ③ 等级系数 = 1 + 等级 × 0.1 = 1 + 100 × 0.1 = ${r.levelMult.toFixed(2)}`); lines.push(` ④ 基础值 = 基础伤害 × 等级系数 = ${r.baseDamage} × ${r.levelMult.toFixed(2)} = ${r.damageBeforeMult}`); lines.push(''); // 倍率明细 lines.push('【伤害倍率明细】'); lines.push(` • STAB(本系加成):x${r.stab.toFixed(2)}${r.stab > 1 ? '(同属性威力提升)' : '(无本系)'}`); lines.push(` • 属性克制:x${r.typeMult.toFixed(2)}`); if (r.weather) { const weatherNames = { sunny: '大晴天', rainy: '下雨', sandstorm: '沙暴', hail: '冰雹', electricTerrain: '电气场地', grassyTerrain: '青草场地', mistyTerrain: '薄雾场地', foggy: '起雾' }; const wMult = getWeatherTypeMult(r.weather, r.move.type); if (wMult !== 1) lines.push(` • 天气(${weatherNames[r.weather] || r.weather}):x${wMult.toFixed(2)}`); } if (r.attacker.status === 'burn' && r.attacker.split === '物理') lines.push(` • 烧伤(物理攻击减伤):÷1.5`); if (r.attacker.status === 'poison' && r.attacker.split === '特殊') lines.push(` • 中毒(特殊攻击减伤):÷1.5`); if (r.attacker.shiny) lines.push(` • 闪光:x1.15`); if (r.attacker.cross) lines.push(` • 交叉之力:x1.3`); if (r.attacker.item) lines.push(` • 道具(${r.attacker.item.name}):x${r.attacker.item.mult.toFixed(2)}`); r.attacker.abilityEffects.forEach(ae => { lines.push(` • 特性 ${ae.name}:x${ae.mult.toFixed(2)}`); }); lines.push(''); // 总倍率与最终伤害 lines.push('【计算结果】'); lines.push(` 总倍率 = 上述所有倍率连乘 = ${r.totalMult.toFixed(3)}`); lines.push(` 最终伤害 = 基础值 × 总倍率 = ${r.damageBeforeMult} × ${r.totalMult.toFixed(3)} = ${r.finalDamage.toLocaleString()}`); lines.push(''); lines.push('══════════════════════════════════════════'); navigator.clipboard.writeText(lines.join('\n')).then(() => { alert('详细报告已复制到剪贴板'); }).catch(() => { alert('复制失败,请手动复制'); }); }); console.log('[伤害计算器v2.6] 已加载'); } })();