import type {Generation, AbilityName} from '../data/interface'; import {toID} from '../util'; import { getBerryResistType, getFlingPower, getItemBoostType, } from '../items'; import type {RawDesc} from '../desc'; import type {Field} from '../field'; import type {Move} from '../move'; import type {Pokemon} from '../pokemon'; import {Result} from '../result'; import { chainMods, checkForecast, checkInfiltrator, checkIntimidate, checkItem, checkMultihitBoost, checkRawStatChanges, computeFinalStats, countBoosts, getBaseDamage, getStatDescriptionText, getFinalDamage, getModifiedStat, getMoveEffectiveness, getShellSideArmCategory, getWeight, handleFixedDamageMoves, isGrounded, OF16, OF32, pokeRound, getStabMod, } from './util'; export function calculateChampions( gen: Generation, attacker: Pokemon, defender: Pokemon, move: Move, field: Field ) { // #region Initial checkForecast(attacker, field.weather); checkForecast(defender, field.weather); checkItem(attacker, field.isMagicRoom); checkItem(defender, field.isMagicRoom); checkRawStatChanges(attacker, field.attackerSide.isPowerTrick, field.isWonderRoom); checkRawStatChanges(defender, field.defenderSide.isPowerTrick, field.isWonderRoom); computeFinalStats(gen, attacker, defender, field, 'def', 'spd', 'spe'); checkIntimidate(gen, attacker, defender); checkIntimidate(gen, defender, attacker); if (move.named('Meteor Beam', 'Electro Shot')) { attacker.boosts.spa += attacker.hasAbility('Contrary') ? -1 : 1; // restrict to +- 6 attacker.boosts.spa = Math.min(6, Math.max(-6, attacker.boosts.spa)); } computeFinalStats(gen, attacker, defender, field, 'atk', 'spa'); checkInfiltrator(attacker, field.defenderSide); checkInfiltrator(defender, field.attackerSide); const desc: RawDesc = { attackerName: attacker.name, moveName: move.name, defenderName: defender.name, isWonderRoom: field.isWonderRoom, }; const result = new Result(gen, attacker, defender, move, field, 0, desc); if (move.category === 'Status') { return result; } if (move.named('Shell Side Arm') && getShellSideArmCategory(attacker, defender, field.isWonderRoom) === 'Physical') { move.category = 'Physical'; move.flags.contact = 1; } const breaksProtect = move.breaksProtect || (attacker.hasAbility('Unseen Fist', 'Piercing Drill') && move.flags.contact); if (field.defenderSide.isProtected && !breaksProtect) { desc.isProtected = true; return result; } if (move.name === 'Pain Split') { const average = Math.floor((attacker.curHP() + defender.curHP()) / 2); const damage = Math.max(0, defender.curHP() - average); result.damage = damage; return result; } const defenderAbilityIgnored = defender.hasAbility( 'Armor Tail', 'Aroma Veil', 'Battle Armor', 'Big Pecks', 'Bulletproof', 'Clear Body', 'Contrary', 'Damp', 'Disguise', 'Dry Skin', 'Earth Eater', 'Eelevate', 'Filter', 'Flash Fire', 'Flower Veil', 'Friend Guard', 'Fur Coat', 'Heatproof', 'Heavy Metal', 'Hyper Cutter', 'Illuminate', 'Immunity', 'Inner Focus', 'Insomnia', 'Keen Eye', 'Leaf Guard', 'Levitate', 'Light Metal', 'Lightning Rod', 'Limber', 'Magic Bounce', 'Magma Armor', 'Marvel Scale', 'Mirror Armor', 'Motor Drive', 'Multiscale', 'Oblivious', 'Overcoat', 'Own Tempo', 'Purifying Salt', 'Queenly Majesty', 'Sand Veil', 'Sap Sipper', 'Shell Armor', 'Shield Dust', 'Snow Cloak', 'Solid Rock', 'Soundproof', 'Sticky Hold', 'Storm Drain', 'Sturdy', 'Sweet Veil', 'Tangled Feet', 'Telepathy', 'Thick Fat', 'Unaware', 'Vital Spirit', 'Volt Absorb', 'Water Absorb', 'Water Bubble', 'Water Veil', 'White Smoke' ); const attackerIgnoresAbility = attacker.hasAbility('Mold Breaker'); if (defenderAbilityIgnored && attackerIgnoresAbility) { if (attackerIgnoresAbility) desc.attackerAbility = attacker.ability; defender.ability = '' as AbilityName; } // Merciless does not ignore Shell Armor, damage dealt to a poisoned Pokemon with Shell Armor // will not be a critical hit (UltiMario) const isCritical = !defender.hasAbility('Shell Armor') && (move.isCrit || (attacker.hasAbility('Merciless') && defender.hasStatus('psn', 'tox'))) && move.timesUsed === 1; let type = move.type; if (move.originalName === 'Weather Ball') { const isMegaSol = attacker.hasAbility('Mega Sol'); type = field.hasWeather('Sun', 'Harsh Sunshine') || isMegaSol ? 'Fire' : field.hasWeather('Rain', 'Heavy Rain') ? 'Water' : field.hasWeather('Sand') ? 'Rock' : field.hasWeather('Hail', 'Snow') ? 'Ice' : 'Normal'; isMegaSol ? desc.attackerAbility = attacker.ability : desc.weather = field.weather; desc.moveType = type; } else if ( move.originalName === 'Terrain Pulse' && isGrounded(attacker, field) ) { type = field.hasTerrain('Electric') ? 'Electric' : field.hasTerrain('Grassy') ? 'Grass' : field.hasTerrain('Misty') ? 'Fairy' : field.hasTerrain('Psychic') ? 'Psychic' : 'Normal'; desc.terrain = field.terrain; // If the Nature Power user has the ability Prankster, it cannot affect // Dark-types or grounded foes if Psychic Terrain is active if (!(move.named('Nature Power') && attacker.hasAbility('Prankster')) && ((defender.types.includes('Dark') || (field.hasTerrain('Psychic') && isGrounded(defender, field))))) { desc.moveType = type; } } else if (move.named('Aura Wheel')) { if (attacker.named('Morpeko')) { type = 'Electric'; } else if (attacker.named('Morpeko-Hangry')) { type = 'Dark'; } } else if (move.named('Raging Bull')) { if (attacker.named('Tauros-Paldea-Combat')) { type = 'Fighting'; } else if (attacker.named('Tauros-Paldea-Blaze')) { type = 'Fire'; } else if (attacker.named('Tauros-Paldea-Aqua')) { type = 'Water'; } field.defenderSide.isReflect = false; field.defenderSide.isLightScreen = false; field.defenderSide.isAuroraVeil = false; } else if (move.named('Brick Break', 'Psychic Fangs')) { field.defenderSide.isReflect = false; field.defenderSide.isLightScreen = false; field.defenderSide.isAuroraVeil = false; } let hasAteAbilityTypeChange = false; let isAerilate = false; let isDragonize = false; let isPixilate = false; let isRefrigerate = false; let isLiquidVoice = false; const noTypeChange = move.named( 'Weather Ball', 'Terrain Pulse', 'Struggle', ); if (!noTypeChange) { const normal = type === 'Normal'; if ((isAerilate = attacker.hasAbility('Aerilate') && normal)) { type = 'Flying'; } else if ((isDragonize = attacker.hasAbility('Dragonize') && normal)) { type = 'Dragon'; } else if ((isLiquidVoice = attacker.hasAbility('Liquid Voice') && !!move.flags.sound)) { type = 'Water'; } else if ((isPixilate = attacker.hasAbility('Pixilate') && normal)) { type = 'Fairy'; } else if ((isRefrigerate = attacker.hasAbility('Refrigerate') && normal)) { type = 'Ice'; } if (isAerilate || isDragonize || isPixilate || isRefrigerate) { desc.attackerAbility = attacker.ability; hasAteAbilityTypeChange = true; } else if (isLiquidVoice) { desc.attackerAbility = attacker.ability; } } move.type = type; const isGhostRevealed = attacker.hasAbility('Scrappy'); const type1Effectiveness = getMoveEffectiveness( gen, move, defender.types[0], isGhostRevealed, field.isGravity, false ); const type2Effectiveness = defender.types[1] ? getMoveEffectiveness( gen, move, defender.types[1], isGhostRevealed, field.isGravity, false ) : 1; let typeEffectiveness = type1Effectiveness * type2Effectiveness; if (typeEffectiveness === 0 && move.hasType('Ground') && defender.hasItem('Iron Ball') && !defender.hasAbility('Klutz')) { typeEffectiveness = 1; } if (typeEffectiveness === 0) { return result; } if ( (move.named('Steel Roller') && !field.terrain) || (move.named('Poltergeist') && !defender.item) ) { return result; } if ((move.hasType('Grass') && defender.hasAbility('Sap Sipper')) || (move.hasType('Fire') && defender.hasAbility('Flash Fire')) || (move.hasType('Water') && defender.hasAbility('Dry Skin', 'Water Absorb')) || (move.hasType('Electric') && defender.hasAbility('Lightning Rod', 'Motor Drive', 'Volt Absorb')) || (move.hasType('Ground') && !field.isGravity && defender.hasAbility('Levitate', 'Eelevate')) || (move.flags.bullet && defender.hasAbility('Bulletproof')) || (move.flags.sound && !move.named('Clangorous Soul') && defender.hasAbility('Soundproof')) || (move.priority > 0 && defender.hasAbility('Queenly Majesty', 'Armor Tail')) || (move.hasType('Ground') && defender.hasAbility('Earth Eater')) ) { desc.defenderAbility = defender.ability; return result; } if (move.priority > 0 && field.hasTerrain('Psychic') && isGrounded(defender, field)) { desc.terrain = field.terrain; return result; } desc.HPEVs = getStatDescriptionText(gen, defender, 'hp'); const fixedDamage = handleFixedDamageMoves(attacker, move); if (fixedDamage) { if (attacker.hasAbility('Parental Bond')) { result.damage = [fixedDamage, fixedDamage]; desc.attackerAbility = attacker.ability; } else { result.damage = fixedDamage; } return result; } if (move.named('Final Gambit')) { result.damage = attacker.curHP(); return result; } if (move.hits > 1) { desc.hits = move.hits; } const turnOrder = attacker.stats.spe > defender.stats.spe ? 'first' : 'last'; // #endregion // #region Base Power const basePower = calculateBasePowerChampions( gen, attacker, defender, move, field, hasAteAbilityTypeChange, desc ); if (basePower === 0) { return result; } // #endregion // #region (Special) Attack const attack = calculateAttackChampions(gen, attacker, defender, move, field, desc, isCritical); // #endregion // #region (Special) Defense const defense = calculateDefenseChampions(gen, attacker, defender, move, field, desc, isCritical); const hitsPhysical = move.overrideDefensiveStat === 'def' || move.category === 'Physical'; const defenseStat = hitsPhysical ? 'def' : 'spd'; // #endregion // #region Damage const baseDamage = calculateBaseDamageChampions( gen, attacker, defender, basePower, attack, defense, move, field, desc, isCritical ); // FIXME: this is incorrect, should be move.flags.heal, not move.drain if ((attacker.hasAbility('Gale Wings') && move.hasType('Flying') && attacker.curHP() === attacker.maxHP())) { move.priority = 1; desc.attackerAbility = attacker.ability; } // the random factor is applied between the crit mod and the stab mod, so don't apply anything // below this until we're inside the loop let stabMod = getStabMod(attacker, move, desc); const applyBurn = attacker.hasStatus('brn') && move.category === 'Physical' && !attacker.hasAbility('Guts') && !move.named('Facade'); desc.isBurned = applyBurn; const finalMods = calculateFinalModsChampions( gen, attacker, defender, move, field, desc, isCritical, typeEffectiveness ); let protect = false; if (field.defenderSide.isProtected && (attacker.hasAbility('Unseen Fist', 'Piercing Drill') && move.flags.contact)) { protect = true; desc.isProtected = true; } const finalMod = chainMods(finalMods, 41, 131072); const isSpread = field.gameType !== 'Singles' && ['allAdjacent', 'allAdjacentFoes'].includes(move.target); let childDamage: number[] | undefined; if (attacker.hasAbility('Parental Bond') && move.hits === 1 && !isSpread) { const child = attacker.clone(); child.ability = 'Parental Bond (Child)' as AbilityName; checkMultihitBoost(gen, child, defender, move, field, desc); childDamage = calculateChampions(gen, child, defender, move, field).damage as number[]; desc.attackerAbility = attacker.ability; } const damage = []; for (let i = 0; i < 16; i++) { damage[i] = getFinalDamage(baseDamage, i, typeEffectiveness, applyBurn, stabMod, finalMod, protect); } result.damage = childDamage ? [damage, childDamage] : damage; if (move.timesUsed! > 1 || move.hits > 1) { // store boosts so intermediate boosts don't show. const origDefBoost = desc.defenseBoost; const origAtkBoost = desc.attackBoost; let numAttacks = 1; if (move.timesUsed! > 1) { desc.moveTurns = `over ${move.timesUsed} turns`; numAttacks = move.timesUsed!; } else { numAttacks = move.hits; } let usedItems = [false, false]; const damageMatrix = [damage]; for (let times = 1; times < numAttacks; times++) { usedItems = checkMultihitBoost(gen, attacker, defender, move, field, desc, usedItems[0], usedItems[1]); const newAttack = calculateAttackChampions(gen, attacker, defender, move, field, desc, isCritical); const newDefense = calculateDefenseChampions(gen, attacker, defender, move, field, desc, isCritical); // Check if lost -ate ability. Typing stays the same, only boost is lost // Cannot be regained during multihit move and no Normal moves with stat drawbacks hasAteAbilityTypeChange = hasAteAbilityTypeChange && attacker.hasAbility('Aerilate', 'Dragonize', 'Pixilate', 'Refrigerate'); if (move.timesUsed! > 1) { stabMod = getStabMod(attacker, move, desc); } const newBasePower = calculateBasePowerChampions( gen, attacker, defender, move, field, hasAteAbilityTypeChange, desc, times + 1 ); const newBaseDamage = calculateBaseDamageChampions( gen, attacker, defender, newBasePower, newAttack, newDefense, move, field, desc, isCritical ); const newFinalMods = calculateFinalModsChampions( gen, attacker, defender, move, field, desc, isCritical, typeEffectiveness, times ); const newFinalMod = chainMods(newFinalMods, 41, 131072); const damageArray = []; for (let i = 0; i < 16; i++) { const newFinalDamage = getFinalDamage( newBaseDamage, i, typeEffectiveness, applyBurn, stabMod, newFinalMod, protect ); damageArray[i] = newFinalDamage; } damageMatrix[times] = damageArray; } result.damage = damageMatrix; desc.defenseBoost = origDefBoost; desc.attackBoost = origAtkBoost; } // #endregion return result; } export function calculateBasePowerChampions( gen: Generation, attacker: Pokemon, defender: Pokemon, move: Move, field: Field, hasAteAbilityTypeChange: boolean, desc: RawDesc, hit = 1, ) { const turnOrder = attacker.stats.spe > defender.stats.spe ? 'first' : 'last'; let basePower: number; switch (move.name) { case 'Payback': basePower = move.bp * (turnOrder === 'last' ? 2 : 1); desc.moveBP = basePower; break; case 'Electro Ball': const r = Math.floor(attacker.stats.spe / defender.stats.spe); basePower = r >= 4 ? 150 : r >= 3 ? 120 : r >= 2 ? 80 : r >= 1 ? 60 : 40; if (defender.stats.spe === 0) basePower = 40; desc.moveBP = basePower; break; case 'Gyro Ball': basePower = Math.min(150, Math.floor((25 * defender.stats.spe) / attacker.stats.spe) + 1); if (attacker.stats.spe === 0) basePower = 1; desc.moveBP = basePower; break; case 'Punishment': basePower = Math.min(200, 60 + 20 * countBoosts(gen, defender.boosts)); desc.moveBP = basePower; break; case 'Low Kick': case 'Grass Knot': const w = getWeight(defender, desc, 'defender'); basePower = w >= 200 ? 120 : w >= 100 ? 100 : w >= 50 ? 80 : w >= 25 ? 60 : w >= 10 ? 40 : 20; desc.moveBP = basePower; break; case 'Hex': case 'Infernal Parade': basePower = move.bp * (defender.status ? 2 : 1); desc.moveBP = basePower; break; case 'Heavy Slam': case 'Heat Crash': const wr = getWeight(attacker, desc, 'attacker') / getWeight(defender, desc, 'defender'); basePower = wr >= 5 ? 120 : wr >= 4 ? 100 : wr >= 3 ? 80 : wr >= 2 ? 60 : 40; desc.moveBP = basePower; break; case 'Stored Power': case 'Power Trip': basePower = 20 + 20 * countBoosts(gen, attacker.boosts); desc.moveBP = basePower; break; case 'Acrobatics': basePower = move.bp * (!attacker.item ? 2 : 1); desc.moveBP = basePower; break; case 'Assurance': basePower = move.bp * (defender.hasAbility('Parental Bond (Child)') ? 2 : 1); // NOTE: desc.attackerAbility = 'Parental Bond' will already reflect this boost break; case 'Smelling Salts': basePower = move.bp * (defender.hasStatus('par') ? 2 : 1); desc.moveBP = basePower; break; case 'Weather Ball': basePower = move.bp * (field.weather || attacker.hasAbility('Mega Sol') ? 2 : 1); desc.moveBP = basePower; break; case 'Terrain Pulse': basePower = move.bp * (isGrounded(attacker, field) && field.terrain ? 2 : 1); desc.moveBP = basePower; break; case 'Rising Voltage': basePower = move.bp * ((isGrounded(defender, field) && field.hasTerrain('Electric')) ? 2 : 1); desc.moveBP = basePower; break; case 'Fling': basePower = getFlingPower(attacker.item, gen.num); desc.moveBP = basePower; desc.attackerItem = attacker.item; break; case 'Eruption': case 'Water Spout': basePower = Math.max(1, Math.floor((150 * attacker.curHP()) / attacker.maxHP())); desc.moveBP = basePower; break; case 'Flail': case 'Reversal': const p = Math.floor((48 * attacker.curHP()) / attacker.maxHP()); basePower = p <= 1 ? 200 : p <= 4 ? 150 : p <= 9 ? 100 : p <= 16 ? 80 : p <= 32 ? 40 : 20; desc.moveBP = basePower; break; // Triple Axel's damage increases after each consecutive hit (20, 40, 60) case 'Triple Axel': basePower = hit * 20; desc.moveBP = move.hits === 2 ? 60 : move.hits === 3 ? 120 : 20; break; case 'Hard Press': basePower = 100 * Math.floor((defender.curHP() * 4096) / defender.maxHP()); basePower = Math.floor(Math.floor((100 * basePower + 2048 - 1) / 4096) / 100) || 1; desc.moveBP = basePower; break; default: basePower = move.bp; } if (basePower === 0) { return 0; } const bpMods = calculateBPModsChampions( gen, attacker, defender, move, field, desc, basePower, hasAteAbilityTypeChange, turnOrder, hit ); basePower = OF16(Math.max(1, pokeRound((basePower * chainMods(bpMods, 41, 2097152)) / 4096))); return basePower; } export function calculateBPModsChampions( gen: Generation, attacker: Pokemon, defender: Pokemon, move: Move, field: Field, desc: RawDesc, basePower: number, hasAteAbilityTypeChange: boolean, turnOrder: string, hit: number ) { const bpMods = []; // Move effects const defenderItem = (defender.item && defender.item !== '') ? defender.item : defender.disabledItem; let resistedKnockOffDamage = !defenderItem; // The last case only applies when the Pokemon has the Mega Stone that matches its species // (or when it's already a Mega-Evolution) if (!resistedKnockOffDamage && defenderItem) { const item = gen.items.get(toID(defenderItem))!; resistedKnockOffDamage = !!(item.megaStone && (item.megaStone[defender.name] || Object.values(item.megaStone).includes(defender.name))); } // Resist knock off damage if your item was already knocked off if (!resistedKnockOffDamage && hit > 1 && !defender.hasAbility('Sticky Hold')) { resistedKnockOffDamage = true; } // Move effects if ((move.named('Facade') && attacker.hasStatus('brn', 'par', 'psn', 'tox')) || (move.named('Venoshock') && defender.hasStatus('psn', 'tox')) || (move.named('Lash Out') && (countBoosts(gen, attacker.boosts) < 0)) ) { bpMods.push(8192); desc.moveBP = basePower * 2; } else if ( move.named('Expanding Force') && isGrounded(attacker, field) && field.hasTerrain('Psychic') ) { move.target = 'allAdjacentFoes'; bpMods.push(6144); desc.moveBP = basePower * 1.5; } else if ((move.named('Knock Off') && !resistedKnockOffDamage) || (move.named('Misty Explosion') && isGrounded(attacker, field) && field.hasTerrain('Misty')) || (move.named('Grav Apple') && field.isGravity) ) { bpMods.push(6144); desc.moveBP = basePower * 1.5; } else if (move.named('Solar Beam', 'Solar Blade') && field.hasWeather('Rain', 'Sand', 'Hail', 'Snow') && !attacker.hasAbility('Mega Sol')) { bpMods.push(2048); desc.moveBP = basePower / 2; desc.weather = field.weather; } if (field.attackerSide.isHelpingHand) { bpMods.push(6144); desc.isHelpingHand = true; } // Field effects const terrainMultiplier = 5325; if (isGrounded(attacker, field)) { if ((field.hasTerrain('Electric') && move.hasType('Electric')) || (field.hasTerrain('Grassy') && move.hasType('Grass')) || (field.hasTerrain('Psychic') && move.hasType('Psychic')) ) { bpMods.push(terrainMultiplier); desc.terrain = field.terrain; } } if (isGrounded(defender, field)) { if ((field.hasTerrain('Misty') && move.hasType('Dragon')) || (field.hasTerrain('Grassy') && move.named('Bulldoze', 'Earthquake')) ) { bpMods.push(2048); desc.terrain = field.terrain; } } // Abilities // Use BasePower after moves with custom BP to determine if Technician should boost if ((attacker.hasAbility('Technician') && basePower <= 60) || (attacker.hasAbility('Mega Launcher') && move.flags.pulse) || (attacker.hasAbility('Strong Jaw') && move.flags.bite) || (attacker.hasAbility('Sharpness') && move.flags.slicing) ) { bpMods.push(6144); desc.attackerAbility = attacker.ability; } const aura = `${move.type} Aura`; const isAttackerAura = attacker.hasAbility(aura); const isDefenderAura = defender.hasAbility(aura); const isFieldFairyAura = field.isFairyAura && move.type === 'Fairy'; const isFieldDarkAura = field.isDarkAura && move.type === 'Dark'; const auraActive = isAttackerAura || isDefenderAura || isFieldFairyAura || isFieldDarkAura; if (auraActive) { bpMods.push(5448); if (isAttackerAura) desc.attackerAbility = attacker.ability; if (isDefenderAura) desc.defenderAbility = defender.ability; } if ( (attacker.hasAbility('Sheer Force') && (move.secondaries || move.named('Electro Shot')) || (attacker.hasAbility('Sand Force') && field.hasWeather('Sand') && move.hasType('Rock', 'Ground', 'Steel')) || (attacker.hasAbility('Analytic') && (turnOrder !== 'first' || field.defenderSide.isSwitching === 'out' || attacker.abilityOn)) || (attacker.hasAbility('Tough Claws') && move.flags.contact)) ) { bpMods.push(5325); desc.attackerAbility = attacker.ability; } if (attacker.hasAbility('Rivalry') && ![attacker.gender, defender.gender].includes('N')) { if (attacker.gender === defender.gender) { bpMods.push(5120); desc.rivalry = 'buffed'; } else { bpMods.push(3072); desc.rivalry = 'nerfed'; } desc.attackerAbility = attacker.ability; } // The -ate abilities already changed move typing earlier, so most checks are done and desc is set if (hasAteAbilityTypeChange) { bpMods.push(4915); } if ((attacker.hasAbility('Reckless') && (move.recoil || move.hasCrashDamage)) || (attacker.hasAbility('Iron Fist') && move.flags.punch) ) { bpMods.push(4915); desc.attackerAbility = attacker.ability; } if (defender.hasAbility('Dry Skin') && move.hasType('Fire')) { bpMods.push(5120); desc.defenderAbility = defender.ability; } if (attacker.hasAbility('Supreme Overlord') && attacker.alliesFainted) { const powMod = [4096, 4506, 4915, 5325, 5734, 6144]; bpMods.push(powMod[Math.min(5, attacker.alliesFainted)]); desc.attackerAbility = attacker.ability; desc.alliesFainted = attacker.alliesFainted; } // Items if ( attacker.item && move.hasType(getItemBoostType(attacker.item)) ) { bpMods.push(4915); desc.attackerItem = attacker.item; } else if ( (attacker.hasItem('Muscle Band') && move.category === 'Physical') || (attacker.hasItem('Wise Glasses') && move.category === 'Special') ) { bpMods.push(4505); desc.attackerItem = attacker.item; } return bpMods; } export function calculateAttackChampions( gen: Generation, attacker: Pokemon, defender: Pokemon, move: Move, field: Field, desc: RawDesc, isCritical = false ) { let attack: number; const attackSource = move.named('Foul Play') ? defender : attacker; const attackStat = move.named('Body Press') ? (field.isWonderRoom ? 'spd' : 'def') : (move.category === 'Special' ? 'spa' : 'atk'); // Body Press in Wonder Room uses normal Def, which checkRawStatChanges has moved to SpD desc.attackEVs = move.named('Foul Play') ? getStatDescriptionText( gen, attackSource, attackStat, field.defenderSide.isPowerTrick ) : getStatDescriptionText( gen, attackSource, attackStat, field.attackerSide.isPowerTrick, field.isWonderRoom ); if (field.attackerSide.isPowerTrick) { if ((move.category === 'Physical' && !move.named('Foul Play')) || move.named('Body Press')) { desc.isPowerTrickAttacker = true; } } const boosts = attackSource.boosts[attackStat]; if (boosts === 0 || (isCritical && boosts < 0)) { attack = attackSource.rawStats[attackStat]; } else if (defender.hasAbility('Unaware')) { attack = attackSource.rawStats[attackStat]; desc.defenderAbility = defender.ability; } else { attack = getModifiedStat(attackSource.rawStats[attackStat]!, boosts); desc.attackBoost = boosts; } // unlike all other attack modifiers, Hustle gets applied directly if (attacker.hasAbility('Hustle') && move.category === 'Physical') { attack = pokeRound((attack * 3) / 2); desc.attackerAbility = attacker.ability; } const atMods = calculateAtModsChampions(gen, attacker, defender, move, field, desc); attack = OF16(Math.max(1, pokeRound((attack * chainMods(atMods, 410, 131072)) / 4096))); return attack; } export function calculateAtModsChampions( gen: Generation, attacker: Pokemon, defender: Pokemon, move: Move, field: Field, desc: RawDesc ) { const atMods = []; if ( (attacker.hasAbility('Solar Power') && field.hasWeather('Sun') && move.category === 'Special')) { atMods.push(6144); desc.attackerAbility = attacker.ability; desc.weather = field.weather; } else if ( (attacker.hasAbility('Guts') && attacker.status && move.category === 'Physical') || (attacker.curHP() <= attacker.maxHP() / 3 && ((attacker.hasAbility('Overgrow') && move.hasType('Grass')) || (attacker.hasAbility('Blaze') && move.hasType('Fire')) || (attacker.hasAbility('Torrent') && move.hasType('Water')) || (attacker.hasAbility('Swarm') && move.hasType('Bug')))) || (move.category === 'Special' && attacker.abilityOn && attacker.hasAbility('Plus', 'Minus')) ) { atMods.push(6144); desc.attackerAbility = attacker.ability; } else if (attacker.hasAbility('Flash Fire') && attacker.abilityOn && move.hasType('Fire')) { atMods.push(6144); desc.attackerAbility = 'Flash Fire'; } else if ( attacker.hasAbility('Fire Mane') && move.hasType('Fire') ) { atMods.push(6144); desc.attackerAbility = attacker.ability; } else if ( (attacker.hasAbility('Water Bubble') && move.hasType('Water')) || (attacker.hasAbility('Huge Power', 'Pure Power') && move.category === 'Physical') ) { atMods.push(8192); desc.attackerAbility = attacker.ability; } if ((defender.hasAbility('Thick Fat') && move.hasType('Fire', 'Ice')) || (defender.hasAbility('Water Bubble') && move.hasType('Fire')) || (defender.hasAbility('Purifying Salt') && move.hasType('Ghost'))) { atMods.push(2048); desc.defenderAbility = defender.ability; } if (defender.hasAbility('Heatproof') && move.hasType('Fire')) { atMods.push(2048); desc.defenderAbility = defender.ability; } if (attacker.hasItem('Light Ball') && attacker.name.includes('Pikachu')) { atMods.push(8192); desc.attackerItem = attacker.item; } return atMods; } export function calculateDefenseChampions( gen: Generation, attacker: Pokemon, defender: Pokemon, move: Move, field: Field, desc: RawDesc, isCritical = false ) { let defense: number; const hitsPhysical = move.overrideDefensiveStat === 'def' || move.category === 'Physical'; const defenseStat = hitsPhysical ? 'def' : 'spd'; desc.defenseEVs = getStatDescriptionText( gen, defender, defenseStat, field.defenderSide.isPowerTrick, field.isWonderRoom ); if (field.defenderSide.isPowerTrick && (field.isWonderRoom !== hitsPhysical)) { desc.isPowerTrickDefender = true; } const boosts = defender.boosts[defenseStat]; if (boosts === 0 || (isCritical && boosts > 0) || move.ignoreDefensive) { defense = defender.rawStats[defenseStat]; } else if (attacker.hasAbility('Unaware')) { defense = defender.rawStats[defenseStat]; desc.attackerAbility = attacker.ability; } else { defense = getModifiedStat(defender.rawStats[defenseStat]!, boosts); desc.defenseBoost = boosts; } // unlike all other defense modifiers, Sandstorm SpD boost gets applied directly if (!attacker.hasAbility('Mega Sol')) { if (field.hasWeather('Sand') && defender.hasType('Rock') && !hitsPhysical) { defense = pokeRound((defense * 3) / 2); desc.weather = field.weather; } if (field.hasWeather('Snow') && defender.hasType('Ice') && hitsPhysical) { defense = pokeRound((defense * 3) / 2); desc.weather = field.weather; } } const dfMods = calculateDfModsChampions( gen, attacker, defender, move, field, desc, isCritical, hitsPhysical ); return OF16(Math.max(1, pokeRound((defense * chainMods(dfMods, 410, 131072)) / 4096))); } export function calculateDfModsChampions( gen: Generation, attacker: Pokemon, defender: Pokemon, move: Move, field: Field, desc: RawDesc, isCritical = false, hitsPhysical = false ) { const dfMods = []; if (defender.hasAbility('Marvel Scale') && defender.status && hitsPhysical) { dfMods.push(6144); desc.defenderAbility = defender.ability; } else if (defender.hasAbility('Fur Coat') && hitsPhysical) { dfMods.push(8192); desc.defenderAbility = defender.ability; } return dfMods; } function calculateBaseDamageChampions( gen: Generation, attacker: Pokemon, defender: Pokemon, basePower: number, attack: number, defense: number, move: Move, field: Field, desc: RawDesc, isCritical = false, ) { let baseDamage = getBaseDamage(attacker.level, basePower, attack, defense); const isSpread = field.gameType !== 'Singles' && ['allAdjacent', 'allAdjacentFoes'].includes(move.target); if (isSpread) { baseDamage = pokeRound(OF32(baseDamage * 3072) / 4096); } if (attacker.hasAbility('Parental Bond (Child)')) { baseDamage = pokeRound(OF32(baseDamage * 1024) / 4096); } const isMegaSol = attacker.hasAbility('Mega Sol'); if ( ((field.hasWeather('Sun') || isMegaSol) && move.hasType('Fire')) || ((field.hasWeather('Rain') && !isMegaSol) && move.hasType('Water')) ) { baseDamage = pokeRound(OF32(baseDamage * 6144) / 4096); isMegaSol ? desc.attackerAbility = attacker.ability : desc.weather = field.weather; } else if ( ((field.hasWeather('Sun') || isMegaSol) && move.hasType('Water')) || (field.hasWeather('Rain') && move.hasType('Fire')) ) { baseDamage = pokeRound(OF32(baseDamage * 2048) / 4096); isMegaSol ? desc.attackerAbility = attacker.ability : desc.weather = field.weather; } if (isCritical) { baseDamage = Math.floor(OF32(baseDamage * 1.5)); desc.isCritical = isCritical; } return baseDamage; } export function calculateFinalModsChampions( gen: Generation, attacker: Pokemon, defender: Pokemon, move: Move, field: Field, desc: RawDesc, isCritical = false, typeEffectiveness: number, hitCount = 0 ) { const finalMods = []; if (field.defenderSide.isReflect && move.category === 'Physical' && !isCritical && !field.defenderSide.isAuroraVeil) { // doesn't stack with Aurora Veil finalMods.push(field.gameType !== 'Singles' ? 2732 : 2048); desc.isReflect = true; } else if ( field.defenderSide.isLightScreen && move.category === 'Special' && !isCritical && !field.defenderSide.isAuroraVeil ) { // doesn't stack with Aurora Veil finalMods.push(field.gameType !== 'Singles' ? 2732 : 2048); desc.isLightScreen = true; } if (field.defenderSide.isAuroraVeil && !isCritical) { finalMods.push(field.gameType !== 'Singles' ? 2732 : 2048); desc.isAuroraVeil = true; } if (attacker.hasAbility('Sniper') && isCritical) { finalMods.push(6144); desc.attackerAbility = attacker.ability; } if (defender.hasAbility('Multiscale') && defender.curHP() === defender.maxHP() && hitCount === 0 && (!field.defenderSide.isSR && (!field.defenderSide.spikes || defender.hasType('Flying'))) && !attacker.hasAbility('Parental Bond (Child)') ) { finalMods.push(2048); desc.defenderAbility = defender.ability; } if (defender.hasAbility('Solid Rock', 'Filter') && typeEffectiveness > 1) { finalMods.push(3072); desc.defenderAbility = defender.ability; } if (field.defenderSide.isFriendGuard) { finalMods.push(3072); desc.isFriendGuard = true; } if (attacker.hasItem('Expert Belt') && typeEffectiveness > 1) { finalMods.push(4915); desc.attackerItem = attacker.item; } else if (attacker.hasItem('Life Orb')) { finalMods.push(5324); desc.attackerItem = attacker.item; } else if (attacker.hasItem('Metronome') && move.timesUsedWithMetronome! >= 1) { const timesUsedWithMetronome = Math.floor(move.timesUsedWithMetronome!); if (timesUsedWithMetronome <= 4) { finalMods.push(4096 + timesUsedWithMetronome * 819); } else { finalMods.push(8192); } desc.attackerItem = attacker.item; } if (move.hasType(getBerryResistType(defender.item)) && (typeEffectiveness > 1 || move.hasType('Normal')) && hitCount === 0 && !attacker.hasAbility('Unnerve')) { if (defender.hasAbility('Ripen')) { finalMods.push(1024); } else { finalMods.push(2048); } desc.defenderItem = defender.item; } return finalMods; }