// ==UserScript==
// @name [Pokechill] 伤害计算器
// @namespace https://play-pokechill.github.io/
// @version 1.4
// @description 不好用来骂我
// @author 人民当家做主
// @license MIT
// @icon https://play-pokechill.github.io/img/icons/icon.png
// @match https://play-pokechill.github.io/*
// @match https://g1tyx.github.io/play-pokechill/*
// @grant none
// ==/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);
// 默认出招时间(来自游戏)
const defaultPlayerMoveTimer = typeof window.defaultPlayerMoveTimer !== 'undefined' ? window.defaultPlayerMoveTimer : 2;
function initCalculator() {
console.log('[伤害计算器] 初始化...');
// 汉化字典
const DICT = window.EN_CN_DICT || {};
// 获取中文名称
function getChinese(englishId) {
if (!englishId) return '';
let formatted = format(englishId);
return DICT[englishId] || DICT[formatted] || formatted;
}
// 根据显示名称查找ID
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;
}
// 创建带中文选项的datalist
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', ''), 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 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;
}
// 道具倍率直接调用 power 方法
function getItemMultiplier(itemId, level) {
const it = item[itemId];
if (!it || typeof it.power !== 'function') return 1;
return it.power(level);
}
// 获取隐藏特性ID(无论是否解锁)
function getHiddenAbilityId(pkmnId) {
const pk = pkmn[pkmnId];
if (pk && pk.hiddenAbility) {
return pk.hiddenAbility.id;
}
return null;
}
// 检查隐藏特性是否已解锁
function isHiddenAbilityUnlocked(pkmnId) {
const pk = pkmn[pkmnId];
return !!(pk && pk.hiddenAbilityUnlocked);
}
// 天气倍率(与游戏一致)
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;
}
// 属性克制表(严格按照Pokechill:克制1.5倍,抵抗0.5倍,免疫0)
const typeChartMap = {
normal: { rock: 0.5, steel: 0.5, ghost: 0 },
fire: { fire: 0.5, water: 0.5, grass: 1.5, ice: 1.5, bug: 1.5, rock: 0.5, dragon: 0.5, steel: 1.5 },
water: { fire: 1.5, water: 0.5, grass: 0.5, ground: 1.5, rock: 1.5, dragon: 0.5 },
electric: { water: 1.5, electric: 0.5, grass: 0.5, ground: 0, flying: 1.5, dragon: 0.5 },
grass: { fire: 0.5, water: 1.5, grass: 0.5, poison: 0.5, ground: 1.5, flying: 0.5, bug: 0.5, rock: 1.5, dragon: 0.5, steel: 0.5 },
ice: { fire: 0.5, water: 0.5, grass: 1.5, ice: 0.5, ground: 1.5, flying: 1.5, dragon: 1.5, steel: 0.5 },
fighting: { normal: 1.5, ice: 1.5, poison: 0.5, flying: 0.5, psychic: 0.5, bug: 0.5, rock: 1.5, ghost: 0, dark: 1.5, fairy: 0.5, steel: 1.5 },
poison: { grass: 1.5, poison: 0.5, ground: 0.5, rock: 0.5, ghost: 0.5, steel: 0, fairy: 1.5 },
ground: { fire: 1.5, electric: 1.5, grass: 0.5, poison: 1.5, flying: 0, bug: 0.5, rock: 1.5, steel: 1.5 },
flying: { electric: 0.5, grass: 1.5, fighting: 1.5, bug: 1.5, rock: 0.5, steel: 0.5 },
psychic: { fighting: 1.5, poison: 1.5, psychic: 0.5, dark: 0, steel: 0.5, fairy: 0.5 },
bug: { fire: 0.5, grass: 1.5, fighting: 0.5, poison: 0.5, flying: 0.5, psychic: 1.5, ghost: 0.5, dark: 1.5, steel: 0.5, fairy: 0.5 },
rock: { fire: 1.5, ice: 1.5, fighting: 0.5, ground: 0.5, flying: 1.5, bug: 1.5, steel: 0.5 },
ghost: { normal: 0, psychic: 1.5, ghost: 1.5, dark: 0.5 },
dragon: { dragon: 1.5, steel: 0.5, fairy: 0 },
dark: { fighting: 0.5, psychic: 1.5, ghost: 1.5, dark: 0.5, fairy: 0.5 },
steel: { fire: 0.5, water: 0.5, electric: 0.5, ice: 1.5, rock: 1.5, steel: 0.5, fairy: 1.5 },
fairy: { fire: 0.5, fighting: 1.5, poison: 0.5, dragon: 1.5, dark: 1.5, steel: 0.5 },
stellar: { stellar: 0 }
};
// 属性克制计算(加入免疫覆盖、冰冻干燥、特性影响)
function calculateTypeEffectiveness(moveType, targetTypes, defenderAbility, defenderHiddenAbility, context) {
let effectiveness = 1;
for (const targetType of targetTypes) {
const typeChart = typeChartMap[moveType];
if (typeChart && typeChart[targetType] !== undefined) {
effectiveness *= typeChart[targetType];
}
}
if (moveType === 'ice' && targetTypes.includes('water')) {
effectiveness *= 1.5;
}
const noImmunities = context.area === 'training' || context.area === 'spiralingTower';
const fieldNoMercy = context.fieldEffect?.includes('noMercy');
if (effectiveness === 0 && (noImmunities || fieldNoMercy)) {
effectiveness = 1;
for (const targetType of targetTypes) {
let eff = typeChartMap[moveType]?.[targetType] ?? 1;
if (eff === 0) eff = 0.5;
effectiveness *= eff;
}
}
function applyAbilityToEffectiveness(abObj) {
if (!abObj) return;
if (abObj.id === 'scrappy' && targetTypes.includes('ghost') && (moveType === 'fighting' || moveType === 'normal')) {
effectiveness = 1;
}
if (abObj.id === 'tintedLens' && effectiveness < 1 && effectiveness > 0) {
effectiveness = 1;
}
if (abObj.id === 'noGuard' && effectiveness === 0) {
effectiveness = 1;
}
if (abObj.id === 'thousandArms') {
effectiveness = 1.5;
}
}
if (defenderAbility) applyAbilityToEffectiveness(defenderAbility);
if (defenderHiddenAbility) applyAbilityToEffectiveness(defenderHiddenAbility);
if (context.fieldEffect?.includes('reverseField')) {
const map = {
0.25: 2.25, 0.5: 1.5, 0.75: 1.25,
1.25: 0.75, 1.5: 0.5, 2.25: 0.25
};
if (map[effectiveness] !== undefined) effectiveness = map[effectiveness];
}
return effectiveness;
}
// 特性应用函数(区分威力加成和最终倍率)
function applyAbility(abilityObj, context) {
if (!abilityObj) return { finalMult: 1, note: '' };
const { isPhysical, moveObj, attacker, defender,
attackerHPPercent, defenderHPPercent,
defenderHasStatus, faintedAllies, weather } = context;
const abId = abilityObj.id;
let finalMult = 1;
let note = '';
// ----- 威力加成类特性(直接修改 context.movePower) -----
const powerMultAbilities = {
technician: { condition: context.movePower <= 60, mult: 1.5, note: '技术高手' },
ironFist: { condition: moveObj.affectedBy?.includes('ironFist'), mult: 1.5, note: '铁拳' },
strongJaw: { condition: moveObj.affectedBy?.includes('strongJaw'), mult: 2, note: '强壮之颚' },
toughClaws: { condition: moveObj.affectedBy?.includes('toughClaws'), mult: 2, note: '硬爪' },
sharpness: { condition: moveObj.affectedBy?.includes('sharpness'), mult: 1.5, note: '锋锐' },
megaLauncher: { condition: moveObj.affectedBy?.includes('megaLauncher'), mult: 1.5, note: 'Mega发射器' },
reckless: { condition: moveObj.timer > defaultPlayerMoveTimer, mult: 1.5, note: '舍身' },
libero: { condition: moveObj.timer < defaultPlayerMoveTimer, mult: 2, note: '自由者' }
};
if (powerMultAbilities[abId] && powerMultAbilities[abId].condition) {
context.movePower *= powerMultAbilities[abId].mult;
note = powerMultAbilities[abId].note;
return { finalMult: 1, note }; // 威力已修改,不产生最终倍率
}
// 皮肤类特性(修改威力并改变属性)
const ateAbilities = {
ferrilate:'steel', refrigerate:'ice', terralate:'ground',
toxilate:'poison', hydrolate:'water', pyrolate:'fire',
chrysilate:'bug', galvanize:'electric', gloomilate:'dark',
espilate:'psychic', aerilate:'flying', pixilate:'fairy',
dragonMaw:'dragon', verdify:'grass'
};
if (ateAbilities[abId] && moveObj.type === 'normal') {
context.changedType = ateAbilities[abId];
context.movePower *= 1.3;
note = '皮肤';
return { finalMult: 1, note };
}
if (abId === 'normalize' && moveObj.type !== 'normal') {
context.changedType = 'normal';
context.movePower *= 1.3;
note = '一般皮肤';
return { finalMult: 1, note };
}
// ----- 最终倍率类特性 -----
if (abId === 'hugePower' || abId === 'purePower') {
if (isPhysical) { finalMult = 2; note = '大力士/全力'; }
}
else if (abId === 'adaptability' && attacker.type.includes(moveObj.type)) {
context.adaptabilityActive = true; // 使用新标志
return { powerMult: 1, finalMult: 1, note: '适应力' };
}
else if (['overgrow','blaze','swarm','torrent','bastion','average',
'resolve','mistify','hexerei','glimmer','skyward',
'draconic','noxious','solid','rime','voltage'].includes(abId)) {
const typeMap = {
overgrow:'grass', blaze:'fire', swarm:'bug', torrent:'water',
bastion:'steel', average:'normal', resolve:'fighting',
mistify:'psychic', hexerei:'ghost', glimmer:'fairy',
skyward:'flying', draconic:'dragon', noxious:'poison',
solid:'rock', rime:'ice', voltage:'electric'
};
if (moveObj.type === typeMap[abId] && attackerHPPercent < 0.5) {
finalMult = 1.3; note = '激流/猛火等';
}
}
else if (abId === 'rivalry') {
if (attacker.type.some(t => defender.type.includes(t))) {
finalMult = 1.5; note = '斗争心';
}
}
else if (abId === 'toxicBoost' && attacker.status === 'poison' && isPhysical) {
finalMult = 1.2; note = '剧毒boost';
}
else if (abId === 'flareBoost' && attacker.status === 'burn' && !isPhysical) {
finalMult = 1.2; note = '热暴走';
}
else if (abId === 'merciless') {
const isNerf = (typeof window.testAbility === 'function' && window.testAbility('active', abId) === 'nerf');
if (defenderHasStatus) {
finalMult = isNerf ? 1.35 : 1.5; note = '不仁不义';
}
}
else if (abId === 'supremeOverlord') {
finalMult = 1 + 0.15 * faintedAllies; note = '至尊将领';
}
else if (abId === 'gorillaTactics') {
const isNerf = (typeof window.testAbility === 'function' && window.testAbility('active', abId) === 'nerf');
finalMult = isNerf ? 1.35 : 1.5; note = '大力士/强壮之颚?';
}
else if (abId === 'parentalBond') {
finalMult = 1.5; note = '亲子爱';
}
else if (abId === 'soulAsterism') {
finalMult = 1 + 0.1 * (faintedAllies === 0 ? 5 : 6 - faintedAllies);
note = '魂星群';
}
// ----- 防御方减伤 -----
else if (['filter','prismArmor','solidRock'].includes(abId)) {
const typeMult = context.typeEffectiveness;
if (typeMult > 1) {
finalMult = 0.75; note = '过滤/棱镜/坚石';
}
}
else if (abId === 'multiscale' && defenderHPPercent >= 0.99) {
finalMult = 0.5; note = '多重鳞片';
}
else if (abId === 'wonderGuard') {
if (context.typeEffectiveness <= 1) {
finalMult = 0.2; note = '神奇守护';
}
}
else if (abId === 'levitate' && moveObj.type === 'ground') {
finalMult = 0; note = '浮游';
}
else if (abId === 'thickFat' && (moveObj.type === 'fire' || moveObj.type === 'ice')) {
finalMult = 0.5; note = '厚脂肪';
}
else if (abId === 'flashFire' && moveObj.type === 'fire') {
finalMult = 0; note = '引火';
}
else if (abId === 'waterAbsorb' && moveObj.type === 'water') {
finalMult = 0; note = '储水';
}
else if (abId === 'voltAbsorb' && moveObj.type === 'electric') {
finalMult = 0; note = '蓄电';
}
else if (abId === 'lightningRod' && moveObj.type === 'electric') {
finalMult = 0; note = '避雷针';
}
else if (abId === 'motorDrive' && moveObj.type === 'electric') {
finalMult = 0; note = '电气引擎';
}
else if (abId === 'sapSipper' && moveObj.type === 'grass') {
finalMult = 0; note = '食草';
}
else if (abId === 'stormDrain' && moveObj.type === 'water') {
finalMult = 0; note = '引水';
}
return { finalMult, note };
}
// ---------- 创建UI(攻击/防御分区)----------
const panel = document.createElement('div');
panel.id = 'pokechill-damage-calc';
panel.innerHTML = `
`;
document.body.appendChild(panel);
// 样式
const style = document.createElement('style');
style.textContent = `
#pokechill-damage-calc {
position: fixed;
top: 80px;
right: 20px;
width: 560px;
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: 12px;
border: 1px solid #f8d030;
padding-bottom: 6px;
}
.section-title {
background: #204080;
color: #f8d030;
padding: 4px 10px;
border-radius: 8px 8px 0 0;
font-weight: bold;
border-bottom: 1px solid #f8d030;
margin-bottom: 8px;
}
.calc-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
padding: 0 10px 10px 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;
}
input[type="text"], select, input[type="number"] {
width: 100%;
padding: 4px 6px;
background: #f0f0f0;
border: 2px solid #f8d030;
color: #204080;
border-radius: 4px;
font-size: 12px;
outline: none;
font-weight: bold;
box-sizing: border-box;
}
input[type="text"]:focus, select:focus, input[type="number"]:focus {
border-color: #ffb0b0;
box-shadow: 0 0 0 2px rgba(255,176,176,0.5);
}
.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;
}
.hidden-ability-box {
background: #404040;
color: #f8d030;
padding: 4px 8px;
border-radius: 4px;
border: 1px solid #f8d030;
font-size: 12px;
}
.item-level-badge {
background: #f8d030;
color: #204080;
padding: 4px 8px;
border-radius: 4px;
font-weight: bold;
font-size: 12px;
}
.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: 100%; }
}
`;
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', pkmnIds, attackerInput);
createDataListWithChinese('pkmn-list', pkmnIds, defenderInput);
createDataListWithChinese('move-list', moveIds, moveInput);
createDataListWithChinese('ability-list', abilityIds, abilityInput);
createDataListWithChinese('item-list', itemIds, itemInput);
// 攻击方输入变化时自动更新隐藏特性显示
attackerInput.addEventListener('change', function() {
const id = this.dataset.id;
if (id) {
const hiddenId = getHiddenAbilityId(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 = '';
document.getElementById('calc-attacker-hp').value = 100;
document.getElementById('calc-attacker-hp-val').innerText = '100';
document.getElementById('calc-defender-hp').value = 100;
document.getElementById('calc-defender-hp-val').innerText = '100';
document.getElementById('calc-defender-status').checked = false;
document.getElementById('calc-fainted-allies').value = 0;
hiddenAbilityDisplay.textContent = '无';
document.getElementById('calc-result').style.display = 'none';
window.__lastDamageReport = null;
});
// 计算按钮
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;
const atkIv = parseInt(atkIvSlider.value, 10);
const defIv = parseInt(defIvSlider.value, 10);
const isShiny = shinyCheck.checked;
const isCross = crossCheck.checked;
const attackerHPPercent = parseFloat(document.getElementById('calc-attacker-hp').value) / 100;
const defenderHPPercent = parseFloat(document.getElementById('calc-defender-hp').value) / 100;
const defenderHasStatus = document.getElementById('calc-defender-status').checked;
const faintedAllies = parseInt(document.getElementById('calc-fainted-allies').value, 10) || 0;
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 inputAbilityObj = abilityId ? ability[abilityId] : null;
const hiddenAbilityId = getHiddenAbilityId(attackerId);
const hiddenAbilityObj = (hiddenAbilityId && isHiddenAbilityUnlocked(attackerId)) ? ability[hiddenAbilityId] : null;
// 防御方特性
const defenderAbilityObj = defender.ability ? ability[defender.ability] : null;
const defenderHiddenId = getHiddenAbilityId(defenderId);
const defenderHiddenObj = (defenderHiddenId && isHiddenAbilityUnlocked(defenderId)) ? ability[defenderHiddenId] : 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;
const atkFactor = Math.pow(1.1, atkIv);
const defFactor = Math.pow(1.1, defIv);
const attackerLevel = 100;
const levelMult = 1 + 100 * 0.1;
// 初始化上下文,包含可修改的 movePower
const context = {
isPhysical, moveObj, attacker, defender,
movePower: moveObj.power, // 可修改的威力值
attackerHPPercent, defenderHPPercent,
defenderHasStatus, faintedAllies, weather: weatherVal,
typeEffectiveness: 1,
area: '', fieldEffect: null,
adaptabilitySTAB: false,
changedType: null
};
let breakdown = [];
let abilityFinalMult = 1; // 用于累积最终倍率
let abilityEffects = []; // 记录最终倍率特性
let powerEffectNotes = []; // 记录威力加成特性
// 应用攻击方手动特性
if (inputAbilityObj) {
const res = applyAbility(inputAbilityObj, context);
if (res.note) {
if (res.finalMult !== 1) {
abilityFinalMult *= res.finalMult;
abilityEffects.push({ name: getChinese(inputAbilityObj.id), note: res.note, mult: res.finalMult });
} else {
powerEffectNotes.push({ name: getChinese(inputAbilityObj.id), note: res.note });
}
}
if (context.changedType) moveObj.type = context.changedType;
}
// 应用攻击方隐藏特性
if (hiddenAbilityObj) {
const res = applyAbility(hiddenAbilityObj, context);
if (res.note) {
if (res.finalMult !== 1) {
abilityFinalMult *= res.finalMult;
abilityEffects.push({ name: getChinese(hiddenAbilityObj.id), note: res.note, mult: res.finalMult });
} else {
powerEffectNotes.push({ name: getChinese(hiddenAbilityObj.id), note: res.note });
}
}
if (context.changedType) moveObj.type = context.changedType;
}
// 基础伤害计算(使用修改后的 movePower)
let attackContribution = attackerBaseStar * 30 * atkFactor;
let defenseContribution = defenderBaseStar * 30 * defFactor;
let baseDamage = context.movePower + Math.max(0, attackContribution - defenseContribution);
let damage = baseDamage * levelMult;
// 记录威力加成
breakdown.push(`招式威力(最终): ${context.movePower.toFixed(1)}`);
breakdown.push(`攻击基础星级: ${attackerBaseStar.toFixed(1)}`);
breakdown.push(`攻击贡献: ${attackerBaseStar.toFixed(1)} x30 x${atkFactor.toFixed(2)} = ${attackContribution.toFixed(0)}`);
breakdown.push(`防御基础星级: ${defenderBaseStar.toFixed(1)}`);
breakdown.push(`防御减免: ${defenderBaseStar.toFixed(1)} x30 x${defFactor.toFixed(2)} = ${defenseContribution.toFixed(0)}`);
breakdown.push(`差值: ${(attackContribution - defenseContribution).toFixed(0)}`);
breakdown.push(`基础伤害: ${baseDamage.toFixed(0)}`);
breakdown.push(`等级系数 x${levelMult.toFixed(2)}`);
let totalMult = 1;
let multLog = '1';
// 属性克制
let typeMult = calculateTypeEffectiveness(moveObj.type, defender.type, defenderAbilityObj, defenderHiddenObj, context);
context.typeEffectiveness = typeMult;
damage *= typeMult;
totalMult *= typeMult;
multLog += ` x${typeMult.toFixed(2)} (克制)`;
breakdown.push(`属性克制 x${typeMult.toFixed(2)}`);
// STAB
let stab = 1;
if (attacker.type.includes(moveObj.type)) {
stab = 1.5;
if (attacker.type.length === 1) stab += 0.2;
if (context.adaptabilityActive) stab += 0.2; // 适应力额外+0.2
damage *= stab;
totalMult *= stab;
multLog += ` x${stab.toFixed(2)} (STAB)`;
breakdown.push(`STAB x${stab.toFixed(2)}`);
} else {
breakdown.push(`STAB x1 (无本系)`);
}
// 天气
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) {
itemMult = getItemMultiplier(itemId, itemLevel);
if (itemMult > 1) {
damage *= itemMult;
totalMult *= itemMult;
multLog += ` x${itemMult.toFixed(2)} (道具)`;
breakdown.push(`道具 x${itemMult.toFixed(2)} (星级5)`);
} else {
breakdown.push(`道具: 无伤害加成`);
}
}
// 应用最终倍率特性
damage *= abilityFinalMult;
totalMult *= abilityFinalMult;
abilityEffects.forEach(ae => {
breakdown.push(`特性 ${ae.name} ${ae.note} x${ae.mult.toFixed(2)}`);
multLog += ` x${ae.mult.toFixed(2)} (${ae.note})`;
});
// ========== 能力等级独立乘区(与游戏一致) ==========
// 获取攻击方和防御方是否有 Unaware 特性(需要检查普通和隐藏)
const attackerHasUnaware = (inputAbilityObj && inputAbilityObj.id === 'unaware') || (hiddenAbilityObj && hiddenAbilityObj.id === 'unaware');
const defenderHasUnaware = (defenderAbilityObj && defenderAbilityObj.id === 'unaware') || (defenderHiddenObj && defenderHiddenObj.id === 'unaware');
let atkBuffMult = 1;
let defBuffMult = 1;
if (!defenderHasUnaware) {
const stageVal = isPhysical ? atkStageVal : satkStageVal;
atkBuffMult = getStageMult(stageVal, false);
} else {
breakdown.push(`防御方具有纯朴,无视攻击方能力等级`);
}
if (!attackerHasUnaware) {
const stageVal = isPhysical ? defStageVal : sdefStageVal;
defBuffMult = getStageMult(stageVal, true);
} else {
breakdown.push(`攻击方具有纯朴,无视防御方能力等级`);
}
let stageMult = atkBuffMult * defBuffMult;
damage *= stageMult;
totalMult *= stageMult;
if (atkBuffMult !== 1) {
breakdown.push(`攻击能力等级 x${atkBuffMult.toFixed(2)}`);
}
if (defBuffMult !== 1) {
breakdown.push(`防御能力等级 x${defBuffMult.toFixed(2)}`);
}
if (atkBuffMult !== 1 || defBuffMult !== 1) {
multLog += ` x${stageMult.toFixed(2)} (能力等级)`;
}
// ========== 能力等级乘区结束 ==========
const finalDamage = Math.round(damage);
// 保存详细报告到全局变量
window.__lastDamageReport = {
attacker: {
name: getChinese(attackerId),
level: 100,
split: isPhysical ? '物理' : '特殊',
baseStar: attackerBaseStar,
stage: isPhysical ? atkStageVal : satkStageVal,
stageMult: atkBuffMult,
iv: isPhysical ? atkIv : atkIv,
ivFactor: atkFactor,
finalStat: attackerBaseStar,
finalStatValue: attackContribution.toFixed(0),
status: attackerStatusVal,
shiny: isShiny,
powerEffects: powerEffectNotes, // 新增这一行
cross: isCross,
item: itemId ? { name: getChinese(itemId), level: itemLevel, mult: itemMult } : null,
abilityEffects: abilityEffects,
powerEffects: powerEffectNotes // 新增这一行
},
defender: {
name: getChinese(defenderId),
baseStar: defenderBaseStar,
stage: isPhysical ? defStageVal : sdefStageVal,
stageMult: defBuffMult,
iv: isPhysical ? defIv : defIv,
ivFactor: defFactor,
finalStat: defenderBaseStar,
finalStatValue: defenseContribution.toFixed(0)
},
move: {
name: getChinese(moveId),
power: moveObj.power,
type: moveObj.type,
finalPower: context.movePower // 新增这一行
},
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.${r.attacker.level}(${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)} × 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} 星级 ${r.attacker.item.level} → 伤害 x${r.attacker.item.mult.toFixed(2)}`);
}
if (r.attacker.abilityEffects.length) {
r.attacker.abilityEffects.forEach(ae => {
lines.push(` ├─ 特性:${ae.name}(${ae.note}) → 伤害 x${ae.mult.toFixed(2)}`);
});
}
lines.push('');
// 输出威力特性
if (r.attacker.powerEffects && r.attacker.powerEffects.length) {
r.attacker.powerEffects.forEach(pe => {
lines.push(` ├─ 特性:${pe.name}(${pe.note}) → 威力提升`);
});
}
// 防御方
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)} × 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}`);
let powerDisplay = r.move.power;
let powerNote = '';
if (r.move.finalPower && r.move.finalPower !== r.move.power) {
powerDisplay = r.move.finalPower.toFixed(1);
// 获取威力特性名称
let abilityNames = [];
if (r.attacker.powerEffects && r.attacker.powerEffects.length) {
abilityNames = r.attacker.powerEffects.map(pe => pe.name);
}
const abilityText = abilityNames.length ? `经 ${abilityNames.join('、')} 特性修正` : '经特性修正';
powerNote = ` (原始威力 ${r.move.power} ${abilityText})`;
}
lines.push(` ② 基础伤害 = 招式威力${powerNote} + 威力差值 = ${powerDisplay} + ${diff} = ${r.baseDamage}`);
lines.push(` ③ 等级系数 = 1 + 等级 × 0.1 = 1 + ${r.attacker.level} × 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}(${ae.note}):x${ae.mult.toFixed(2)}`);
});
lines.push(` • 能力等级综合:x${(r.attacker.stageMult * r.defender.stageMult).toFixed(2)}(攻击 ${r.attacker.stageMult.toFixed(2)} × 防御 ${r.defender.stageMult.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.7] 初始化完成,能力等级独立乘区,支持双特性,汉化已启用');
}
})();