diff --git a/config/formats.ts b/config/formats.ts index 45fe35e3e3..a5bfea4a5a 100644 --- a/config/formats.ts +++ b/config/formats.ts @@ -1539,7 +1539,7 @@ export const Formats: FormatList = [ name: move.name, onStart(this: Battle, pokemon: Pokemon) { this.add('-activate', pokemon, 'ability: ' + move.name); - this.useMove(move, pokemon); + this.actions.useMove(move, pokemon); }, toString() { return ""; diff --git a/data/abilities.ts b/data/abilities.ts index 7c4ce1fe0d..d862156c65 100644 --- a/data/abilities.ts +++ b/data/abilities.ts @@ -148,14 +148,14 @@ export const Abilities: {[abilityid: string]: AbilityData} = { }, arenatrap: { onFoeTrapPokemon(pokemon) { - if (!this.isAdjacent(pokemon, this.effectData.target)) return; + if (!pokemon.isAdjacent(this.effectData.target)) return; if (pokemon.isGrounded()) { pokemon.tryTrap(true); } }, onFoeMaybeTrapPokemon(pokemon, source) { if (!source) source = this.effectData.target; - if (!source || !this.isAdjacent(pokemon, source)) return; + if (!source || !pokemon.isAdjacent(source)) return; if (pokemon.isGrounded(!pokemon.knownType)) { // Negate immunity if the type is unknown pokemon.maybeTrapped = true; } @@ -1362,11 +1362,8 @@ export const Abilities: {[abilityid: string]: AbilityData} = { if (pokemon.side.active.length === 1) { return; } - for (const allyActive of pokemon.side.active) { - if ( - allyActive && - (allyActive.hp && this.isAdjacent(pokemon, allyActive) && allyActive.status) && this.randomChance(3, 10) - ) { + for (const allyActive of pokemon.adjacentAllies()) { + if (allyActive.status && this.randomChance(3, 10)) { this.add('-activate', pokemon, 'ability: Healer'); allyActive.cureStatus(); } @@ -1672,8 +1669,7 @@ export const Abilities: {[abilityid: string]: AbilityData} = { intimidate: { onStart(pokemon) { let activated = false; - for (const target of pokemon.side.foe.active) { - if (!target || !this.isAdjacent(target, pokemon)) continue; + for (const target of pokemon.adjacentFoes()) { if (!activated) { this.add('-ability', pokemon, 'Intimidate', 'boost'); activated = true; @@ -1883,7 +1879,7 @@ export const Abilities: {[abilityid: string]: AbilityData} = { const newMove = this.dex.getActiveMove(move.id); newMove.hasBounced = true; newMove.pranksterBoosted = false; - this.useMove(newMove, target, source); + this.actions.useMove(newMove, target, source); return null; }, onAllyTryHitSide(target, source, move) { @@ -1893,7 +1889,7 @@ export const Abilities: {[abilityid: string]: AbilityData} = { const newMove = this.dex.getActiveMove(move.id); newMove.hasBounced = true; newMove.pranksterBoosted = false; - this.useMove(newMove, this.effectData.target, source); + this.actions.useMove(newMove, this.effectData.target, source); return null; }, condition: { @@ -1947,13 +1943,13 @@ export const Abilities: {[abilityid: string]: AbilityData} = { }, magnetpull: { onFoeTrapPokemon(pokemon) { - if (pokemon.hasType('Steel') && this.isAdjacent(pokemon, this.effectData.target)) { + if (pokemon.hasType('Steel') && pokemon.isAdjacent(this.effectData.target)) { pokemon.tryTrap(true); } }, onFoeMaybeTrapPokemon(pokemon, source) { if (!source) source = this.effectData.target; - if (!source || !this.isAdjacent(pokemon, source)) return; + if (!source || !pokemon.isAdjacent(source)) return; if (!pokemon.knownType || pokemon.hasType('Steel')) { pokemon.maybeTrapped = true; } @@ -2561,12 +2557,9 @@ export const Abilities: {[abilityid: string]: AbilityData} = { onResidualSubOrder: 1, onResidual(pokemon) { if (pokemon.item) return; - const pickupTargets = []; - for (const target of this.getAllActive()) { - if (target.lastItem && target.usedItemThisTurn && this.isAdjacent(pokemon, target)) { - pickupTargets.push(target); - } - } + const pickupTargets = this.getAllActive().filter(target => ( + target.lastItem && target.usedItemThisTurn && pokemon.isAdjacent(target) + )); if (!pickupTargets.length) return; const randomTarget = this.sample(pickupTargets); const item = randomTarget.lastItem; @@ -3218,13 +3211,13 @@ export const Abilities: {[abilityid: string]: AbilityData} = { }, shadowtag: { onFoeTrapPokemon(pokemon) { - if (!pokemon.hasAbility('shadowtag') && this.isAdjacent(pokemon, this.effectData.target)) { + if (!pokemon.hasAbility('shadowtag') && pokemon.isAdjacent(this.effectData.target)) { pokemon.tryTrap(true); } }, onFoeMaybeTrapPokemon(pokemon, source) { if (!source) source = this.effectData.target; - if (!source || !this.isAdjacent(pokemon, source)) return; + if (!source || !pokemon.isAdjacent(source)) return; if (!pokemon.hasAbility('shadowtag')) { pokemon.maybeTrapped = true; } @@ -3935,15 +3928,13 @@ export const Abilities: {[abilityid: string]: AbilityData} = { }, trace: { onStart(pokemon) { - if (pokemon.side.foe.active.some( - foeActive => foeActive && this.isAdjacent(pokemon, foeActive) && foeActive.ability === 'noability' - )) { + if (pokemon.adjacentFoes().some(foeActive => foeActive.ability === 'noability')) { this.effectData.gaveUp = true; } }, onUpdate(pokemon) { if (!pokemon.isStarted || this.effectData.gaveUp) return; - const possibleTargets = pokemon.side.foe.active.filter(foeActive => foeActive && this.isAdjacent(pokemon, foeActive)); + const possibleTargets = pokemon.adjacentFoes(); while (possibleTargets.length) { let rand = 0; if (possibleTargets.length > 1) rand = this.random(possibleTargets.length); @@ -4381,7 +4372,7 @@ export const Abilities: {[abilityid: string]: AbilityData} = { } const newMove = this.dex.getActiveMove(move.id); newMove.hasBounced = true; - this.useMove(newMove, target, source); + this.actions.useMove(newMove, target, source); return null; }, onAllyTryHitSide(target, source, move) { @@ -4392,7 +4383,7 @@ export const Abilities: {[abilityid: string]: AbilityData} = { } const newMove = this.dex.getActiveMove(move.id); newMove.hasBounced = true; - this.useMove(newMove, this.effectData.target, source); + this.actions.useMove(newMove, this.effectData.target, source); return null; }, condition: { diff --git a/data/conditions.ts b/data/conditions.ts index 060b3979eb..84ff3f1450 100644 --- a/data/conditions.ts +++ b/data/conditions.ts @@ -172,7 +172,7 @@ export const Conditions: {[k: string]: ConditionData} = { return; } this.activeTarget = pokemon; - const damage = this.getDamage(pokemon, pokemon, 40); + const damage = this.actions.getDamage(pokemon, pokemon, 40); if (typeof damage !== 'number') throw new Error("Confusion damage not dealt"); const activeMove = {id: this.toID('confused'), effectType: 'Move', type: '???'}; this.damage(damage, pokemon, pokemon, activeMove as ActiveMove); @@ -376,7 +376,7 @@ export const Conditions: {[k: string]: ConditionData} = { } const hitMove = new this.dex.Move(data.moveData) as ActiveMove; - this.trySpreadMoveHit([target], data.source, hitMove, true); + this.actions.trySpreadMoveHit([target], data.source, hitMove, true); }, }, healreplacement: { diff --git a/data/mods/gen1/moves.ts b/data/mods/gen1/moves.ts index db2023c0b6..dc2ab397c0 100644 --- a/data/mods/gen1/moves.ts +++ b/data/mods/gen1/moves.ts @@ -95,7 +95,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { } this.add('-end', pokemon, 'Bide'); const target = this.effectData.sourceSide.active[this.effectData.sourcePosition]; - this.moveHit(target, pokemon, move, {damage: this.effectData.totalDamage * 2} as ActiveMove); + this.actions.moveHit(target, pokemon, move, {damage: this.effectData.totalDamage * 2} as ActiveMove); return false; } this.add('-activate', pokemon, 'Bide'); @@ -584,7 +584,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { if (!foe?.lastMove || foe.lastMove.id === 'mirrormove') { return false; } - this.useMove(foe.lastMove.id, pokemon); + this.actions.useMove(foe.lastMove.id, pokemon); }, }, mist: { @@ -861,7 +861,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { // NOTE: In future generations the damage is capped to the remaining HP of the // Substitute, here we deliberately use the uncapped damage when tracking lastDamage etc. // Also, multi-hit moves must always deal the same damage as the first hit for any subsequent hits - let uncappedDamage = move.hit > 1 ? source.lastDamage : this.getDamage(source, target, move); + let uncappedDamage = move.hit > 1 ? source.lastDamage : this.actions.getDamage(source, target, move); if (!uncappedDamage) return null; uncappedDamage = this.runEvent('SubDamage', target, source, move, uncappedDamage); if (!uncappedDamage) return uncappedDamage; diff --git a/data/mods/gen1/scripts.ts b/data/mods/gen1/scripts.ts index fb8f926476..d3cd4a38bb 100644 --- a/data/mods/gen1/scripts.ts +++ b/data/mods/gen1/scripts.ts @@ -13,16 +13,6 @@ export const Scripts: ModdedBattleScriptsData = { (this.data.Pokedex[i] as any).eggGroups = null; } }, - // Gen 1 stores the last damage dealt by a move in the battle. - // This is used for the move Counter. - lastDamage: 0, - // BattleSide scripts. - // In gen 1, last move information is stored on the side rather than on the active Pokémon. - // This is because there was actually no side, just Battle and active Pokémon effects. - // Side's lastMove is used for Counter and Mirror Move. - side: { - lastMove: null, - }, // BattlePokemon scripts. pokemon: { getStat(statName, unmodified) { @@ -68,531 +58,742 @@ export const Scripts: ModdedBattleScriptsData = { return changed; }, }, - // Battle scripts. - // runMove can be found in scripts.js. This function is the main one when running a move. - // It deals with the beforeMove and AfterMoveSelf events. - // This leads with partial trapping moves shennanigans after the move has been used. - // It also deals with how PP reduction works on gen 1. - runMove(moveOrMoveName, pokemon, targetLoc, sourceEffect) { - const target = this.getTarget(pokemon, moveOrMoveName, targetLoc); - const move = this.dex.getActiveMove(moveOrMoveName); - if (target?.subFainted) target.subFainted = null; + actions: { + // This function is the main one when running a move. + // It deals with the beforeMove and AfterMoveSelf events. + // This leads with partial trapping moves shennanigans after the move has been used. + // It also deals with how PP reduction works on gen 1. + runMove(moveOrMoveName, pokemon, targetLoc, sourceEffect) { + const target = this.battle.getTarget(pokemon, moveOrMoveName, targetLoc); + const move = this.battle.dex.getActiveMove(moveOrMoveName); + if (target?.subFainted) target.subFainted = null; - this.setActiveMove(move, pokemon, target); + this.battle.setActiveMove(move, pokemon, target); - if (pokemon.moveThisTurn || !this.runEvent('BeforeMove', pokemon, target, move)) { - // Prevent invulnerability from persisting until the turn ends. - pokemon.removeVolatile('twoturnmove'); - // Rampage moves end without causing confusion - delete pokemon.volatiles['lockedmove']; - this.clearActiveMove(true); - // This is only run for sleep. - this.runEvent('AfterMoveSelf', pokemon, target, move); - return; - } - if (move.beforeMoveCallback) { - if (move.beforeMoveCallback.call(this, pokemon, target, move)) { - this.clearActiveMove(true); + if (pokemon.moveThisTurn || !this.battle.runEvent('BeforeMove', pokemon, target, move)) { + // Prevent invulnerability from persisting until the turn ends. + pokemon.removeVolatile('twoturnmove'); + // Rampage moves end without causing confusion + delete pokemon.volatiles['lockedmove']; + this.battle.clearActiveMove(true); + // This is only run for sleep. + this.battle.runEvent('AfterMoveSelf', pokemon, target, move); return; } - } - pokemon.lastDamage = 0; - let lockedMove = this.runEvent('LockMove', pokemon); - if (lockedMove === true) lockedMove = false; - if ( - !lockedMove && - (!pokemon.volatiles['partialtrappinglock'] || pokemon.volatiles['partialtrappinglock'].locked !== target) - ) { - pokemon.deductPP(move, null, target); - // On gen 1 moves are stored when they are chosen and a PP is deducted. - pokemon.side.lastMove = move; - pokemon.lastMove = move; - } else { - sourceEffect = move; - } - if (pokemon.volatiles['partialtrappinglock'] && target !== pokemon.volatiles['partialtrappinglock'].locked) { - const moveSlot = pokemon.moveSlots.find(ms => ms.id === move.id); - if (moveSlot && moveSlot.pp < 0) { - moveSlot.pp = 63; - this.hint("In Gen 1, if a player is forced to use a move with 0 PP, the move will underflow to have 63 PP."); - } - } - this.useMove(move, pokemon, target, sourceEffect); - this.singleEvent('AfterMove', move, null, pokemon, target, move); - - // If target fainted - if (target && target.hp <= 0) { - // We remove recharge - if (pokemon.volatiles['mustrecharge']) pokemon.removeVolatile('mustrecharge'); - delete pokemon.volatiles['partialtrappinglock']; - // We remove screens - target.side.removeSideCondition('reflect'); - target.side.removeSideCondition('lightscreen'); - pokemon.removeVolatile('twoturnmove'); - } else if (pokemon.hp) { - this.runEvent('AfterMoveSelf', pokemon, target, move); - } - if (pokemon.volatiles['mustrecharge']) this.add('-mustrecharge', pokemon); - - // For partial trapping moves, we are saving the target - if (move.volatileStatus === 'partiallytrapped' && target && target.hp > 0) { - // Let's check if the lock exists - if (pokemon.volatiles['partialtrappinglock'] && target.volatiles['partiallytrapped']) { - // Here the partialtrappinglock volatile has been already applied - const sourceVolatile = pokemon.volatiles['partialtrappinglock']; - const targetVolatile = target.volatiles['partiallytrapped']; - if (!sourceVolatile.locked) { - // If it's the first hit, we save the target - sourceVolatile.locked = target; - } else if (target !== pokemon && target !== sourceVolatile.locked) { - // Our target switched out! Re-roll the duration, damage, and accuracy. - const duration = this.sample([2, 2, 2, 3, 3, 3, 4, 5]); - sourceVolatile.duration = duration; - sourceVolatile.locked = target; - // Duration reset thus partially trapped at 2 always. - targetVolatile.duration = 2; + if (move.beforeMoveCallback) { + if (move.beforeMoveCallback.call(this.battle, pokemon, target, move)) { + this.battle.clearActiveMove(true); + return; } - } // If we move to here, the move failed and there's no partial trapping lock. - } - }, - // useMove can be found on scripts.js - // It is the function that actually uses the move, running ModifyMove events. - // It uses the move and then deals with the effects after the move. - useMove(moveOrMoveName, pokemon, target, sourceEffect) { - if (!sourceEffect && this.effect.id) sourceEffect = this.effect; - const baseMove = this.dex.getMove(moveOrMoveName); - let move = this.dex.getActiveMove(baseMove); - if (target === undefined) target = this.getRandomTarget(pokemon, move); - if (move.target === 'self') { - target = pokemon; - } - if (sourceEffect) move.sourceEffect = sourceEffect.id; - - this.setActiveMove(move, pokemon, target); - - this.singleEvent('ModifyMove', move, null, pokemon, target, move, move); - if (baseMove.target !== move.target) { - // Target changed in ModifyMove, so we must adjust it here - target = this.getRandomTarget(pokemon, move); - } - move = this.runEvent('ModifyMove', pokemon, target, move, move); - if (baseMove.target !== move.target) { - // Check again, this shouldn't ever happen on Gen 1. - target = this.getRandomTarget(pokemon, move); - this.debug('not a gen 1 mechanic'); - } - if (!move) return false; - - let attrs = ''; - if (pokemon.fainted) { - // Removing screens upon faint. - pokemon.side.removeSideCondition('reflect'); - pokemon.side.removeSideCondition('lightscreen'); - return false; - } - - if (sourceEffect) attrs += '|[from]' + this.dex.getEffect(sourceEffect); - this.addMove('move', pokemon, move.name, target + attrs); - - if (!this.singleEvent('Try', move, null, pokemon, target, move)) { - return true; - } - if (!this.singleEvent('TryMove', move, null, pokemon, target, move) || - !this.runEvent('TryMove', pokemon, target, move)) { - return true; - } - - if (move.ignoreImmunity === undefined) { - move.ignoreImmunity = (move.category === 'Status'); - } - - let damage: number | undefined | false | '' = false; - if (!target || target.fainted) { - this.attrLastMove('[notarget]'); - this.add('-notarget'); - return true; - } - damage = this.tryMoveHit(target, pokemon, move); - - // Store 0 damage for last damage if move failed or dealt 0 damage. - // This only happens on moves that don't deal damage but call GetDamageVarsForPlayerAttack (disassembly). - const neverDamageMoves = [ - 'conversion', 'haze', 'mist', 'focusenergy', 'confuseray', 'supersonic', 'transform', 'lightscreen', 'reflect', 'substitute', 'mimic', 'leechseed', 'splash', 'softboiled', 'recover', 'rest', - ]; - if ( - !damage && - (move.category !== 'Status' || (move.status && !['psn', 'tox', 'par'].includes(move.status))) && - !neverDamageMoves.includes(move.id) - ) { - this.lastDamage = 0; - } - - // Go ahead with results of the used move. - if (damage === false) { - this.singleEvent('MoveFail', move, null, target, pokemon, move); - return true; - } - - if (!move.negateSecondary) { - this.singleEvent('AfterMoveSecondarySelf', move, null, pokemon, target, move); - this.runEvent('AfterMoveSecondarySelf', pokemon, target, move); - } - return true; - }, - // tryMoveHit can be found on scripts.js - // This function attempts a move hit and returns the attempt result before the actual hit happens. - // It deals with partial trapping weirdness and accuracy bugs as well. - tryMoveHit(target, pokemon, move) { - let damage: number | false | undefined = 0; - - // First, check if the target is semi-invulnerable - let hitResult = this.runEvent('Invulnerability', target, pokemon, move); - if (hitResult === false) { - if (!move.spreadHit) this.attrLastMove('[miss]'); - this.add('-miss', pokemon); - return false; - } - - // Then, check if the Pokémon is immune to this move. - if ( - (!move.ignoreImmunity || (move.ignoreImmunity !== true && !move.ignoreImmunity[move.type])) && - !target.runImmunity(move.type, true) - ) { - if (move.selfdestruct) { - this.faint(pokemon, pokemon, move); } - return false; - } - hitResult = this.singleEvent('TryImmunity', move, null, target, pokemon, move); - if (hitResult === false) { - this.add('-immune', target); - return false; - } - - // Now, let's calculate the accuracy. - let accuracy = move.accuracy; - - // Partial trapping moves: true accuracy while it lasts - if (move.volatileStatus === 'partiallytrapped' && target === pokemon.volatiles['partialtrappinglock']?.locked) { - accuracy = true; - } - - // If a sleep inducing move is used while the user is recharging, the accuracy is true. - if (move.status === 'slp' && target && target.volatiles['mustrecharge']) { - accuracy = true; - } - - // OHKO moves only have a chance to hit if the user is at least as fast as the target - if (move.ohko) { - if (target.speed > pokemon.speed) { - this.add('-immune', target, '[ohko]'); - return false; - } - } - - // Calculate true accuracy for gen 1, which uses 0-255. - // Gen 1 uses the same boost table for accuracy and evasiveness as every other stat - const boostTable = [25, 28, 33, 40, 50, 66, 100, 150, 200, 250, 300, 350, 400]; - if (accuracy !== true) { - accuracy = Math.floor(accuracy * 255 / 100); - // Check also for accuracy modifiers. - if (!move.ignoreAccuracy) { - accuracy = Math.floor(accuracy * (boostTable[pokemon.boosts.accuracy + 6] / 100)); - } - if (!move.ignoreEvasion) { - accuracy = Math.floor(accuracy * (boostTable[-target.boosts.evasion + 6] / 100)); - } - accuracy = Math.min(accuracy, 255); - } - accuracy = this.runEvent('Accuracy', target, pokemon, move, accuracy); - // Moves that target the user do not suffer from the 1/256 miss chance. - if (move.target === 'self' && accuracy !== true) accuracy++; - - // 1/256 chance of missing always, no matter what. Besides the aforementioned exceptions. - if (accuracy !== true && !this.randomChance(accuracy, 256)) { - this.attrLastMove('[miss]'); - this.add('-miss', pokemon); - if (accuracy === 255) this.hint("In Gen 1, moves with 100% accuracy can still miss 1/256 of the time."); - damage = false; - } - - // If damage is 0 and not false it means it didn't miss, let's calc. - if (damage !== false) { pokemon.lastDamage = 0; - if (move.multihit) { - let hits = move.multihit; - if (Array.isArray(hits)) { - // Yes, it's hardcoded... meh - if (hits[0] === 2 && hits[1] === 5) { - hits = this.sample([2, 2, 3, 3, 4, 5]); - } else { - hits = this.random(hits[0], hits[1] + 1); - } - } - hits = Math.floor(hits); - // In gen 1, all the hits have the same damage for multihits move - let moveDamage: number | undefined | false = 0; - let i: number; - for (i = 0; i < hits && target.hp && pokemon.hp; i++) { - move.hit = i + 1; - moveDamage = this.moveHit(target, pokemon, move); - if (moveDamage === false) break; - damage = (moveDamage || 0); - // Move damage is fixed to be the first move's damage - if (i === 0) move.damage = damage; - if (target.subFainted) { - i++; - break; - } - } - move.damage = null; - if (i === 0) return 1; - this.add('-hitcount', target, i); + let lockedMove = this.battle.runEvent('LockMove', pokemon); + if (lockedMove === true) lockedMove = false; + if ( + !lockedMove && + (!pokemon.volatiles['partialtrappinglock'] || pokemon.volatiles['partialtrappinglock'].locked !== target) + ) { + pokemon.deductPP(move, null, target); + // On gen 1 moves are stored when they are chosen and a PP is deducted. + pokemon.side.lastMove = move; + pokemon.lastMove = move; } else { - damage = this.moveHit(target, pokemon, move); + sourceEffect = move; } - } - - if (move.category !== 'Status') { - target.gotAttacked(move, damage, pokemon); - } - - if (move.selfdestruct) { - if (!target.subFainted) { - this.faint(pokemon, pokemon, move); - } else { - this.hint(`In Gen 1, the user of ${move.name} will not take damage if it breaks a Substitute.`); + if (pokemon.volatiles['partialtrappinglock'] && target !== pokemon.volatiles['partialtrappinglock'].locked) { + const moveSlot = pokemon.moveSlots.find(ms => ms.id === move.id); + if (moveSlot && moveSlot.pp < 0) { + moveSlot.pp = 63; + this.battle.hint("In Gen 1, if a player is forced to use a move with 0 PP, the move will underflow to have 63 PP."); + } } - } + this.useMove(move, pokemon, target, sourceEffect); + this.battle.singleEvent('AfterMove', move, null, pokemon, target, move); - // The move missed. - if (!damage && damage !== 0) { - // Delete the partial trap lock if necessary. - delete pokemon.volatiles['partialtrappinglock']; - return false; - } - - if (move.ohko) this.add('-ohko'); - - if (!move.negateSecondary) { - this.singleEvent('AfterMoveSecondary', move, null, target, pokemon, move); - this.runEvent('AfterMoveSecondary', target, pokemon, move); - } - - return damage; - }, - // move Hit can be found on scripts.js - // It deals with the actual move hit, as the name indicates, dealing damage and/or effects. - // This function also deals with the Gen 1 Substitute behaviour on the hitting process. - moveHit(target, pokemon, move, moveData, isSecondary, isSelf) { - let damage: number | false | null | undefined = 0; - - if (!isSecondary && !isSelf) this.setActiveMove(move, pokemon, target); - let hitResult: number | boolean = true; - if (!moveData) moveData = move; - - if (move.ignoreImmunity === undefined) { - move.ignoreImmunity = (move.category === 'Status'); - } - - // We get the sub to the target to see if it existed - const targetSub = (target) ? target.volatiles['substitute'] : false; - const targetHadSub = (targetSub !== null && targetSub !== false && (typeof targetSub !== 'undefined')); - - if (target) { - hitResult = this.singleEvent('TryHit', moveData, {}, target, pokemon, move); - - // Handle here the applying of partial trapping moves to Pokémon with Substitute - if (targetSub && moveData.volatileStatus && moveData.volatileStatus === 'partiallytrapped') { - target.addVolatile(moveData.volatileStatus, pokemon, move); + // If target fainted + if (target && target.hp <= 0) { + // We remove recharge + if (pokemon.volatiles['mustrecharge']) pokemon.removeVolatile('mustrecharge'); + delete pokemon.volatiles['partialtrappinglock']; + // We remove screens + target.side.removeSideCondition('reflect'); + target.side.removeSideCondition('lightscreen'); + pokemon.removeVolatile('twoturnmove'); + } else if (pokemon.hp) { + this.battle.runEvent('AfterMoveSelf', pokemon, target, move); } + if (pokemon.volatiles['mustrecharge']) this.battle.add('-mustrecharge', pokemon); - if (!hitResult) { - if (hitResult === false) this.add('-fail', target); + // For partial trapping moves, we are saving the target + if (move.volatileStatus === 'partiallytrapped' && target && target.hp > 0) { + // Let's check if the lock exists + if (pokemon.volatiles['partialtrappinglock'] && target.volatiles['partiallytrapped']) { + // Here the partialtrappinglock volatile has been already applied + const sourceVolatile = pokemon.volatiles['partialtrappinglock']; + const targetVolatile = target.volatiles['partiallytrapped']; + if (!sourceVolatile.locked) { + // If it's the first hit, we save the target + sourceVolatile.locked = target; + } else if (target !== pokemon && target !== sourceVolatile.locked) { + // Our target switched out! Re-roll the duration, damage, and accuracy. + const duration = this.battle.sample([2, 2, 2, 3, 3, 3, 4, 5]); + sourceVolatile.duration = duration; + sourceVolatile.locked = target; + // Duration reset thus partially trapped at 2 always. + targetVolatile.duration = 2; + } + } // If we move to here, the move failed and there's no partial trapping lock. + } + }, + // This is the function that actually uses the move, running ModifyMove events. + // It uses the move and then deals with the effects after the move. + useMove(moveOrMoveName, pokemon, target, sourceEffect) { + if (!sourceEffect && this.battle.effect.id) sourceEffect = this.battle.effect; + const baseMove = this.battle.dex.getMove(moveOrMoveName); + let move = this.battle.dex.getActiveMove(baseMove); + if (target === undefined) target = this.battle.getRandomTarget(pokemon, move); + if (move.target === 'self') { + target = pokemon; + } + if (sourceEffect) move.sourceEffect = sourceEffect.id; + + this.battle.setActiveMove(move, pokemon, target); + + this.battle.singleEvent('ModifyMove', move, null, pokemon, target, move, move); + if (baseMove.target !== move.target) { + // Target changed in ModifyMove, so we must adjust it here + target = this.battle.getRandomTarget(pokemon, move); + } + move = this.battle.runEvent('ModifyMove', pokemon, target, move, move); + if (baseMove.target !== move.target) { + // Check again, this shouldn't ever happen on Gen 1. + target = this.battle.getRandomTarget(pokemon, move); + this.battle.debug('not a gen 1 mechanic'); + } + if (!move) return false; + + let attrs = ''; + if (pokemon.fainted) { + // Removing screens upon faint. + pokemon.side.removeSideCondition('reflect'); + pokemon.side.removeSideCondition('lightscreen'); return false; } - // Only run the hit events for the hit itself, not the secondary or self hits - if (!isSelf && !isSecondary) { - hitResult = this.runEvent('TryHit', target, pokemon, move); + if (sourceEffect) attrs += '|[from]' + this.battle.dex.getEffect(sourceEffect); + this.battle.addMove('move', pokemon, move.name, target + attrs); + + if (!this.battle.singleEvent('Try', move, null, pokemon, target, move)) { + return true; + } + if (!this.battle.singleEvent('TryMove', move, null, pokemon, target, move) || + !this.battle.runEvent('TryMove', pokemon, target, move)) { + return true; + } + + if (move.ignoreImmunity === undefined) { + move.ignoreImmunity = (move.category === 'Status'); + } + + let damage: number | undefined | false | '' = false; + if (!target || target.fainted) { + this.battle.attrLastMove('[notarget]'); + this.battle.add('-notarget'); + return true; + } + damage = this.tryMoveHit(target, pokemon, move); + + // Store 0 damage for last damage if move failed or dealt 0 damage. + // This only happens on moves that don't deal damage but call GetDamageVarsForPlayerAttack (disassembly). + const neverDamageMoves = [ + 'conversion', 'haze', 'mist', 'focusenergy', 'confuseray', 'supersonic', 'transform', 'lightscreen', 'reflect', 'substitute', 'mimic', 'leechseed', 'splash', 'softboiled', 'recover', 'rest', + ]; + if ( + !damage && + (move.category !== 'Status' || (move.status && !['psn', 'tox', 'par'].includes(move.status))) && + !neverDamageMoves.includes(move.id) + ) { + this.battle.lastDamage = 0; + } + + // Go ahead with results of the used move. + if (damage === false) { + this.battle.singleEvent('MoveFail', move, null, target, pokemon, move); + return true; + } + + if (!move.negateSecondary) { + this.battle.singleEvent('AfterMoveSecondarySelf', move, null, pokemon, target, move); + this.battle.runEvent('AfterMoveSecondarySelf', pokemon, target, move); + } + return true; + }, + // This function attempts a move hit and returns the attempt result before the actual hit happens. + // It deals with partial trapping weirdness and accuracy bugs as well. + tryMoveHit(target, pokemon, move) { + let damage: number | false | undefined = 0; + + // First, check if the target is semi-invulnerable + let hitResult = this.battle.runEvent('Invulnerability', target, pokemon, move); + if (hitResult === false) { + if (!move.spreadHit) this.battle.attrLastMove('[miss]'); + this.battle.add('-miss', pokemon); + return false; + } + + // Then, check if the Pokémon is immune to this move. + if ( + (!move.ignoreImmunity || (move.ignoreImmunity !== true && !move.ignoreImmunity[move.type])) && + !target.runImmunity(move.type, true) + ) { + if (move.selfdestruct) { + this.battle.faint(pokemon, pokemon, move); + } + return false; + } + hitResult = this.battle.singleEvent('TryImmunity', move, null, target, pokemon, move); + if (hitResult === false) { + this.battle.add('-immune', target); + return false; + } + + // Now, let's calculate the accuracy. + let accuracy = move.accuracy; + + // Partial trapping moves: true accuracy while it lasts + if (move.volatileStatus === 'partiallytrapped' && target === pokemon.volatiles['partialtrappinglock']?.locked) { + accuracy = true; + } + + // If a sleep inducing move is used while the user is recharging, the accuracy is true. + if (move.status === 'slp' && target && target.volatiles['mustrecharge']) { + accuracy = true; + } + + // OHKO moves only have a chance to hit if the user is at least as fast as the target + if (move.ohko) { + if (target.speed > pokemon.speed) { + this.battle.add('-immune', target, '[ohko]'); + return false; + } + } + + // Calculate true accuracy for gen 1, which uses 0-255. + // Gen 1 uses the same boost table for accuracy and evasiveness as every other stat + const boostTable = [25, 28, 33, 40, 50, 66, 100, 150, 200, 250, 300, 350, 400]; + if (accuracy !== true) { + accuracy = Math.floor(accuracy * 255 / 100); + // Check also for accuracy modifiers. + if (!move.ignoreAccuracy) { + accuracy = Math.floor(accuracy * (boostTable[pokemon.boosts.accuracy + 6] / 100)); + } + if (!move.ignoreEvasion) { + accuracy = Math.floor(accuracy * (boostTable[-target.boosts.evasion + 6] / 100)); + } + accuracy = Math.min(accuracy, 255); + } + accuracy = this.battle.runEvent('Accuracy', target, pokemon, move, accuracy); + // Moves that target the user do not suffer from the 1/256 miss chance. + if (move.target === 'self' && accuracy !== true) accuracy++; + + // 1/256 chance of missing always, no matter what. Besides the aforementioned exceptions. + if (accuracy !== true && !this.battle.randomChance(accuracy, 256)) { + this.battle.attrLastMove('[miss]'); + this.battle.add('-miss', pokemon); + if (accuracy === 255) this.battle.hint("In Gen 1, moves with 100% accuracy can still miss 1/256 of the time."); + damage = false; + } + + // If damage is 0 and not false it means it didn't miss, let's calc. + if (damage !== false) { + pokemon.lastDamage = 0; + if (move.multihit) { + let hits = move.multihit; + if (Array.isArray(hits)) { + // Yes, it's hardcoded... meh + if (hits[0] === 2 && hits[1] === 5) { + hits = this.battle.sample([2, 2, 3, 3, 4, 5]); + } else { + hits = this.battle.random(hits[0], hits[1] + 1); + } + } + hits = Math.floor(hits); + // In gen 1, all the hits have the same damage for multihits move + let moveDamage: number | undefined | false = 0; + let i: number; + for (i = 0; i < hits && target.hp && pokemon.hp; i++) { + move.hit = i + 1; + moveDamage = this.moveHit(target, pokemon, move); + if (moveDamage === false) break; + damage = (moveDamage || 0); + // Move damage is fixed to be the first move's damage + if (i === 0) move.damage = damage; + if (target.subFainted) { + i++; + break; + } + } + move.damage = null; + if (i === 0) return 1; + this.battle.add('-hitcount', target, i); + } else { + damage = this.moveHit(target, pokemon, move); + } + } + + if (move.category !== 'Status') { + target.gotAttacked(move, damage, pokemon); + } + + if (move.selfdestruct) { + if (!target.subFainted) { + this.battle.faint(pokemon, pokemon, move); + } else { + this.battle.hint(`In Gen 1, the user of ${move.name} will not take damage if it breaks a Substitute.`); + } + } + + // The move missed. + if (!damage && damage !== 0) { + // Delete the partial trap lock if necessary. + delete pokemon.volatiles['partialtrappinglock']; + return false; + } + + if (move.ohko) this.battle.add('-ohko'); + + if (!move.negateSecondary) { + this.battle.singleEvent('AfterMoveSecondary', move, null, target, pokemon, move); + this.battle.runEvent('AfterMoveSecondary', target, pokemon, move); + } + + return damage; + }, + // It deals with the actual move hit, as the name indicates, dealing damage and/or effects. + // This function also deals with the Gen 1 Substitute behaviour on the hitting process. + moveHit(target, pokemon, move, moveData, isSecondary, isSelf) { + let damage: number | false | null | undefined = 0; + + if (!isSecondary && !isSelf) this.battle.setActiveMove(move, pokemon, target); + let hitResult: number | boolean = true; + if (!moveData) moveData = move; + + if (move.ignoreImmunity === undefined) { + move.ignoreImmunity = (move.category === 'Status'); + } + + // We get the sub to the target to see if it existed + const targetSub = (target) ? target.volatiles['substitute'] : false; + const targetHadSub = (targetSub !== null && targetSub !== false && (typeof targetSub !== 'undefined')); + + if (target) { + hitResult = this.battle.singleEvent('TryHit', moveData, {}, target, pokemon, move); + + // Handle here the applying of partial trapping moves to Pokémon with Substitute + if (targetSub && moveData.volatileStatus && moveData.volatileStatus === 'partiallytrapped') { + target.addVolatile(moveData.volatileStatus, pokemon, move); + } + if (!hitResult) { - if (hitResult === false) this.add('-fail', target); - // Special Substitute hit flag - if (hitResult !== 0) { + if (hitResult === false) this.battle.add('-fail', target); + return false; + } + + // Only run the hit events for the hit itself, not the secondary or self hits + if (!isSelf && !isSecondary) { + hitResult = this.battle.runEvent('TryHit', target, pokemon, move); + if (!hitResult) { + if (hitResult === false) this.battle.add('-fail', target); + // Special Substitute hit flag + if (hitResult !== 0) { + return false; + } + } + if (!this.battle.runEvent('TryFieldHit', target, pokemon, move)) { return false; } + } else if (isSecondary && !moveData.self) { + hitResult = this.battle.runEvent('TrySecondaryHit', target, pokemon, moveData); } - if (!this.runEvent('TryFieldHit', target, pokemon, move)) { + + if (hitResult === 0) { + target = null; + } else if (!hitResult) { + if (hitResult === false) this.battle.add('-fail', target); return false; } - } else if (isSecondary && !moveData.self) { - hitResult = this.runEvent('TrySecondaryHit', target, pokemon, moveData); } - if (hitResult === 0) { - target = null; - } else if (!hitResult) { - if (hitResult === false) this.add('-fail', target); - return false; - } - } + if (target) { + let didSomething = false; - if (target) { - let didSomething = false; + damage = this.getDamage(pokemon, target, moveData); - damage = this.getDamage(pokemon, target, moveData); + // getDamage has several possible return values: + // + // a number: + // means that much damage is dealt (0 damage still counts as dealing + // damage for the purposes of things like Static) + // false: + // gives error message: "But it failed!" and move ends + // null: + // the move ends, with no message (usually, a custom fail message + // was already output by an event handler) + // undefined: + // means no damage is dealt and the move continues + // + // basically, these values have the same meanings as they do for event + // handlers. - // getDamage has several possible return values: - // - // a number: - // means that much damage is dealt (0 damage still counts as dealing - // damage for the purposes of things like Static) - // false: - // gives error message: "But it failed!" and move ends - // null: - // the move ends, with no message (usually, a custom fail message - // was already output by an event handler) - // undefined: - // means no damage is dealt and the move continues - // - // basically, these values have the same meanings as they do for event - // handlers. - - if ((damage || damage === 0) && !target.fainted) { - damage = this.damage(damage, target, pokemon, move); - if (!(damage || damage === 0)) return false; - didSomething = true; - } else if (damage === false && typeof hitResult === 'undefined') { - this.add('-fail', target); - } - if (damage === false || damage === null) { - return false; - } - if (moveData.boosts && target.hp) { - if (!this.boost(moveData.boosts, target, pokemon, move)) { - this.add('-fail', target); + if ((damage || damage === 0) && !target.fainted) { + damage = this.battle.damage(damage, target, pokemon, move); + if (!(damage || damage === 0)) return false; + didSomething = true; + } else if (damage === false && typeof hitResult === 'undefined') { + this.battle.add('-fail', target); + } + if (damage === false || damage === null) { return false; } - didSomething = true; - // Check the status of the Pokémon whose turn is not. - // When a move that affects stat levels is used, if the Pokémon whose turn it is not right now is paralyzed or - // burned, the correspoding stat penalties will be applied again to that Pokémon. - if (pokemon.side.foe.active[0].status) { - // If it's paralysed, quarter its speed. - if (pokemon.side.foe.active[0].status === 'par') { - pokemon.side.foe.active[0].modifyStat!('spe', 0.25); + if (moveData.boosts && target.hp) { + if (!this.battle.boost(moveData.boosts, target, pokemon, move)) { + this.battle.add('-fail', target); + return false; } - // If it's burned, halve its attack. - if (pokemon.side.foe.active[0].status === 'brn') { - pokemon.side.foe.active[0].modifyStat!('atk', 0.5); + didSomething = true; + // Check the status of the Pokémon whose turn is not. + // When a move that affects stat levels is used, if the Pokémon whose turn it is not right now is paralyzed or + // burned, the correspoding stat penalties will be applied again to that Pokémon. + if (pokemon.side.foe.active[0].status) { + // If it's paralysed, quarter its speed. + if (pokemon.side.foe.active[0].status === 'par') { + pokemon.side.foe.active[0].modifyStat!('spe', 0.25); + } + // If it's burned, halve its attack. + if (pokemon.side.foe.active[0].status === 'brn') { + pokemon.side.foe.active[0].modifyStat!('atk', 0.5); + } } } - } - if (moveData.heal && !target.fainted) { - const d = target.heal(Math.floor(target.maxhp * moveData.heal[0] / moveData.heal[1])); - if (!d) { - this.add('-fail', target); + if (moveData.heal && !target.fainted) { + const d = target.heal(Math.floor(target.maxhp * moveData.heal[0] / moveData.heal[1])); + if (!d) { + this.battle.add('-fail', target); + return false; + } + this.battle.add('-heal', target, target.getHealth); + didSomething = true; + } + if (moveData.status) { + // Gen 1 bug: If the target has just used hyperbeam and must recharge, its status will be ignored and put to sleep. + // This does NOT revert the paralyse speed drop or the burn attack drop. + // Also, being put to sleep clears the recharge condition. + if (moveData.status === 'slp' && target.volatiles['mustrecharge']) { + // The sleep move is guaranteed to hit in this situation, unless Sleep Clause activates. + // Do not clear recharge in that case. + if (target.setStatus(moveData.status, pokemon, move)) { + target.removeVolatile('mustrecharge'); + this.battle.hint( + "In Gen 1, if a Pokémon that has just used Hyper Beam and has yet to recharge is targeted with a sleep inducing move, " + + "any other status it may already have will be ignored and sleep will be induced regardless." + ); + } + } else if (!target.status) { + if (target.setStatus(moveData.status, pokemon, move)) { + // Gen 1 mechanics: The burn attack drop and the paralyse speed drop are applied here directly on stat modifiers. + if (moveData.status === 'brn') target.modifyStat!('atk', 0.5); + if (moveData.status === 'par') target.modifyStat!('spe', 0.25); + } + } else if (!isSecondary) { + if (target.status === moveData.status) { + this.battle.add('-fail', target, target.status); + } else { + this.battle.add('-fail', target); + } + } + didSomething = true; + } + if (moveData.forceStatus) { + if (target.setStatus(moveData.forceStatus, pokemon, move)) { + if (moveData.forceStatus === 'brn') target.modifyStat!('atk', 0.5); + if (moveData.forceStatus === 'par') target.modifyStat!('spe', 0.25); + didSomething = true; + } + } + if (moveData.volatileStatus) { + if (target.addVolatile(moveData.volatileStatus, pokemon, move)) { + didSomething = true; + } + } + if (moveData.sideCondition) { + if (target.side.addSideCondition(moveData.sideCondition, pokemon, move)) { + didSomething = true; + } + } + if (moveData.pseudoWeather) { + if (this.battle.field.addPseudoWeather(moveData.pseudoWeather, pokemon, move)) { + didSomething = true; + } + } + // Hit events + hitResult = this.battle.singleEvent('Hit', moveData, {}, target, pokemon, move); + if (!isSelf && !isSecondary) { + this.battle.runEvent('Hit', target, pokemon, move); + } + if (!hitResult && !didSomething) { + if (hitResult === false) this.battle.add('-fail', target); return false; } - this.add('-heal', target, target.getHealth); - didSomething = true; } - if (moveData.status) { - // Gen 1 bug: If the target has just used hyperbeam and must recharge, its status will be ignored and put to sleep. - // This does NOT revert the paralyse speed drop or the burn attack drop. - // Also, being put to sleep clears the recharge condition. - if (moveData.status === 'slp' && target.volatiles['mustrecharge']) { - // The sleep move is guaranteed to hit in this situation, unless Sleep Clause activates. - // Do not clear recharge in that case. - if (target.setStatus(moveData.status, pokemon, move)) { - target.removeVolatile('mustrecharge'); - this.hint( - "In Gen 1, if a Pokémon that has just used Hyper Beam and has yet to recharge is targeted with a sleep inducing move, " + - "any other status it may already have will be ignored and sleep will be induced regardless." - ); - } - } else if (!target.status) { - if (target.setStatus(moveData.status, pokemon, move)) { - // Gen 1 mechanics: The burn attack drop and the paralyse speed drop are applied here directly on stat modifiers. - if (moveData.status === 'brn') target.modifyStat!('atk', 0.5); - if (moveData.status === 'par') target.modifyStat!('spe', 0.25); - } - } else if (!isSecondary) { - if (target.status === moveData.status) { - this.add('-fail', target, target.status); - } else { - this.add('-fail', target); - } - } - didSomething = true; - } - if (moveData.forceStatus) { - if (target.setStatus(moveData.forceStatus, pokemon, move)) { - if (moveData.forceStatus === 'brn') target.modifyStat!('atk', 0.5); - if (moveData.forceStatus === 'par') target.modifyStat!('spe', 0.25); - didSomething = true; - } - } - if (moveData.volatileStatus) { - if (target.addVolatile(moveData.volatileStatus, pokemon, move)) { - didSomething = true; - } - } - if (moveData.sideCondition) { - if (target.side.addSideCondition(moveData.sideCondition, pokemon, move)) { - didSomething = true; - } - } - if (moveData.pseudoWeather) { - if (this.field.addPseudoWeather(moveData.pseudoWeather, pokemon, move)) { - didSomething = true; - } - } - // Hit events - hitResult = this.singleEvent('Hit', moveData, {}, target, pokemon, move); - if (!isSelf && !isSecondary) { - this.runEvent('Hit', target, pokemon, move); - } - if (!hitResult && !didSomething) { - if (hitResult === false) this.add('-fail', target); - return false; - } - } - const targetHasSub = !!(target?.volatiles['substitute']); + const targetHasSub = !!(target?.volatiles['substitute']); - // Here's where self effects are applied. - const doSelf = (targetHadSub && targetHasSub) || !targetHadSub; - if (moveData.self && (doSelf || (moveData.self !== true && moveData.self.volatileStatus === 'partialtrappinglock'))) { - this.moveHit(pokemon, pokemon, move, moveData.self, isSecondary, true); - } + // Here's where self effects are applied. + const doSelf = (targetHadSub && targetHasSub) || !targetHadSub; + if (moveData.self && (doSelf || (moveData.self !== true && moveData.self.volatileStatus === 'partialtrappinglock'))) { + this.moveHit(pokemon, pokemon, move, moveData.self, isSecondary, true); + } - // Now we can save the partial trapping damage. - if (pokemon.volatiles['partialtrappinglock']) { - pokemon.volatiles['partialtrappinglock'].damage = pokemon.lastDamage; - } + // Now we can save the partial trapping damage. + if (pokemon.volatiles['partialtrappinglock']) { + pokemon.volatiles['partialtrappinglock'].damage = pokemon.lastDamage; + } - // Apply move secondaries. - if (moveData.secondaries) { - for (const secondary of moveData.secondaries) { - // We check here whether to negate the probable secondary status if it's para, burn, or freeze. - // In the game, this is checked and if true, the random number generator is not called. - // That means that a move that does not share the type of the target can status it. - // If a move that was not fire-type would exist on Gen 1, it could burn a Pokémon. - if (!(secondary.status && ['par', 'brn', 'frz'].includes(secondary.status) && target && target.hasType(move.type))) { - if (secondary.chance === undefined || this.randomChance(Math.ceil(secondary.chance * 256 / 100), 256)) { - this.moveHit(target, pokemon, move, secondary, true, isSelf); + // Apply move secondaries. + if (moveData.secondaries) { + for (const secondary of moveData.secondaries) { + // We check here whether to negate the probable secondary status if it's para, burn, or freeze. + // In the game, this is checked and if true, the random number generator is not called. + // That means that a move that does not share the type of the target can status it. + // If a move that was not fire-type would exist on Gen 1, it could burn a Pokémon. + if (!(secondary.status && ['par', 'brn', 'frz'].includes(secondary.status) && target && target.hasType(move.type))) { + if (secondary.chance === undefined || this.battle.randomChance(Math.ceil(secondary.chance * 256 / 100), 256)) { + this.moveHit(target, pokemon, move, secondary, true, isSelf); + } } } } - } - if (move.selfSwitch && pokemon.hp) { - pokemon.switchFlag = move.selfSwitch; - } + if (move.selfSwitch && pokemon.hp) { + pokemon.switchFlag = move.selfSwitch; + } - return damage; + return damage; + }, + // This calculates the damage pokemon does to target with move. + getDamage(pokemon, target, move, suppressMessages) { + // First of all, we get the move. + if (typeof move === 'string') { + move = this.battle.dex.getActiveMove(move); + } else if (typeof move === 'number') { + move = { + basePower: move, + type: '???', + category: 'Physical', + willCrit: false, + flags: {}, + } as ActiveMove; + } + + // Let's see if the target is immune to the move. + if (!move.ignoreImmunity || (move.ignoreImmunity !== true && !move.ignoreImmunity[move.type])) { + if (!target.runImmunity(move.type, true)) { + return false; + } + } + + // Is it an OHKO move? + if (move.ohko) { + return target.maxhp; + } + + // We edit the damage through move's damage callback if necessary. + if (move.damageCallback) { + return move.damageCallback.call(this.battle, pokemon, target); + } + + // We take damage from damage=level moves (seismic toss). + if (move.damage === 'level') { + return pokemon.level; + } + + // If there's a fix move damage, we return that. + if (move.damage) { + return move.damage; + } + + // If it's the first hit on a Normal-type partially trap move, it hits Ghosts anyways but damage is 0. + if (move.volatileStatus === 'partiallytrapped' && move.type === 'Normal' && target.hasType('Ghost')) { + return 0; + } + + // Let's check if we are in middle of a partial trap sequence to return the previous damage. + if (pokemon.volatiles['partialtrappinglock'] && (target === pokemon.volatiles['partialtrappinglock'].locked)) { + return pokemon.volatiles['partialtrappinglock'].damage; + } + + // We check the category and typing to calculate later on the damage. + if (!move.category) move.category = 'Physical'; + if (!move.defensiveCategory) move.defensiveCategory = move.category; + // '???' is typeless damage: used for Struggle and Confusion etc + if (!move.type) move.type = '???'; + const type = move.type; + + // We get the base power and apply basePowerCallback if necessary. + let basePower: number | false | null = move.basePower; + if (move.basePowerCallback) { + basePower = move.basePowerCallback.call(this.battle, pokemon, target, move); + } + if (!basePower) { + return basePower === 0 ? undefined : basePower; + } + basePower = this.battle.clampIntRange(basePower, 1); + + // Checking for the move's Critical Hit possibility. We check if it's a 100% crit move, otherwise we calculate the chance. + let isCrit = move.willCrit || false; + if (!isCrit) { + // In gen 1, the critical chance is based on speed. + // First, we get the base speed, divide it by 2 and floor it. This is our current crit chance. + let critChance = Math.floor(pokemon.species.baseStats['spe'] / 2); + + // Now we check for focus energy volatile. + if (pokemon.volatiles['focusenergy']) { + // If it exists, crit chance is divided by 2 again and floored. + critChance = Math.floor(critChance / 2); + } else { + // Normally, without focus energy, crit chance is multiplied by 2 and capped at 255 here. + critChance = this.battle.clampIntRange(critChance * 2, 1, 255); + } + + // Now we check for the move's critical hit ratio. + if (move.critRatio === 1) { + // Normal hit ratio, we divide the crit chance by 2 and floor the result again. + critChance = Math.floor(critChance / 2); + } else if (move.critRatio === 2) { + // High crit ratio, we multiply the result so far by 4 and cap it at 255. + critChance = this.battle.clampIntRange(critChance * 4, 1, 255); + } + + // Last, we check deppending on ratio if the move critical hits or not. + // We compare our critical hit chance against a random number between 0 and 255. + // If the random number is lower, we get a critical hit. This means there is always a 1/255 chance of not hitting critically. + if (critChance > 0) { + isCrit = this.battle.randomChance(critChance, 256); + } + } + if (isCrit) target.getMoveHitData(move).crit = true; + + // Happens after crit calculation. + if (basePower) { + basePower = this.battle.runEvent('BasePower', pokemon, target, move, basePower); + if (basePower && move.basePowerModifier) { + basePower *= move.basePowerModifier; + } + } + if (!basePower) return 0; + basePower = this.battle.clampIntRange(basePower, 1); + + // We now check attacker's and defender's stats. + let level = pokemon.level; + let attacker = pokemon; + const defender = target; + if (move.useTargetOffensive) attacker = target; + const atkType: StatNameExceptHP = (move.category === 'Physical') ? 'atk' : 'spa'; + const defType: StatNameExceptHP = (move.defensiveCategory === 'Physical') ? 'def' : 'spd'; + let attack = attacker.getStat(move.useSourceDefensiveAsOffensive ? defType : atkType); + let defense = defender.getStat(defType); + // In gen 1, screen effect is applied here. + if ((defType === 'def' && defender.volatiles['reflect']) || (defType === 'spd' && defender.volatiles['lightscreen'])) { + this.battle.debug('Screen doubling (Sp)Def'); + defense *= 2; + defense = this.battle.clampIntRange(defense, 1, 1998); + } + + // In the event of a critical hit, the offense and defense changes are ignored. + // This includes both boosts and screens. + // Also, level is doubled in damage calculation. + if (isCrit) { + move.ignoreOffensive = true; + move.ignoreDefensive = true; + level *= 2; + if (!suppressMessages) this.battle.add('-crit', target); + } + if (move.ignoreOffensive) { + this.battle.debug('Negating (sp)atk boost/penalty.'); + attack = attacker.getStat(atkType, true); + } + if (move.ignoreDefensive) { + this.battle.debug('Negating (sp)def boost/penalty.'); + // No screens + defense = target.getStat(defType, true); + } + + // When either attack or defense are higher than 256, both are divided by 4. + // If that's still over 256, it rolls over (%256), which is what causes rollover bugs. + if (attack >= 256 || defense >= 256) { + attack = this.battle.clampIntRange(Math.floor(attack / 4) % 256, 1); + // Defense isn't checked on the cartridge, but we don't want those / 0 bugs on the sim. + defense = this.battle.clampIntRange(Math.floor(defense / 4) % 256, 1); + } + + // Self destruct moves halve defense at this point. + if (move.selfdestruct && defType === 'def') { + defense = this.battle.clampIntRange(Math.floor(defense / 2), 1); + } + + // Let's go with the calculation now that we have what we need. + // We do it step by step just like the game does. + let damage = level * 2; + damage = Math.floor(damage / 5); + damage += 2; + damage *= basePower; + damage *= attack; + damage = Math.floor(damage / defense); + damage = this.battle.clampIntRange(Math.floor(damage / 50), 1, 997); + damage += 2; + + // STAB damage bonus, the "???" type never gets STAB + if (type !== '???' && pokemon.hasType(type)) { + damage += Math.floor(damage / 2); + } + + // Type effectiveness. + // The order here is not correct, must change to check the move versus each type. + const totalTypeMod = target.runEffectiveness(move); + // Super effective attack + if (totalTypeMod > 0) { + if (!suppressMessages) this.battle.add('-supereffective', target); + damage *= 20; + damage = Math.floor(damage / 10); + if (totalTypeMod >= 2) { + damage *= 20; + damage = Math.floor(damage / 10); + } + } + if (totalTypeMod < 0) { + if (!suppressMessages) this.battle.add('-resisted', target); + damage *= 5; + damage = Math.floor(damage / 10); + if (totalTypeMod <= -2) { + damage *= 5; + damage = Math.floor(damage / 10); + } + } + + // If damage becomes 0, the move is made to miss. + // This occurs when damage was either 2 or 3 prior to applying STAB/Type matchup, and target is 4x resistant to the move. + if (damage === 0) return damage; + + // Apply random factor is damage is greater than 1 + if (damage > 1) { + damage *= this.battle.random(217, 256); + damage = Math.floor(damage / 255); + if (damage > target.hp && !target.volatiles['substitute']) damage = target.hp; + } + + // And we are done. + return Math.floor(damage); + }, }, - // boost can be found on sim/battle.js on Battle object. - // It deals with Pokémon stat boosting, including Gen 1 buggy behaviour with burn and paralyse. + // deals with Pokémon stat boosting, including Gen 1 buggy behaviour with burn and paralyse. boost(boost, target, source = null, effect = null) { if (this.event) { if (!target) target = this.event.target; @@ -640,219 +841,4 @@ export const Scripts: ModdedBattleScriptsData = { this.runEvent('AfterBoost', target, source, effect, boost); return success; }, - // getDamage can be found on sim/battle.js on the Battle object. - // It calculates the damage pokemon does to target with move. - getDamage(pokemon, target, move, suppressMessages) { - // First of all, we get the move. - if (typeof move === 'string') { - move = this.dex.getActiveMove(move); - } else if (typeof move === 'number') { - move = { - basePower: move, - type: '???', - category: 'Physical', - willCrit: false, - flags: {}, - } as ActiveMove; - } - - // Let's see if the target is immune to the move. - if (!move.ignoreImmunity || (move.ignoreImmunity !== true && !move.ignoreImmunity[move.type])) { - if (!target.runImmunity(move.type, true)) { - return false; - } - } - - // Is it an OHKO move? - if (move.ohko) { - return target.maxhp; - } - - // We edit the damage through move's damage callback if necessary. - if (move.damageCallback) { - return move.damageCallback.call(this, pokemon, target); - } - - // We take damage from damage=level moves (seismic toss). - if (move.damage === 'level') { - return pokemon.level; - } - - // If there's a fix move damage, we return that. - if (move.damage) { - return move.damage; - } - - // If it's the first hit on a Normal-type partially trap move, it hits Ghosts anyways but damage is 0. - if (move.volatileStatus === 'partiallytrapped' && move.type === 'Normal' && target.hasType('Ghost')) { - return 0; - } - - // Let's check if we are in middle of a partial trap sequence to return the previous damage. - if (pokemon.volatiles['partialtrappinglock'] && (target === pokemon.volatiles['partialtrappinglock'].locked)) { - return pokemon.volatiles['partialtrappinglock'].damage; - } - - // We check the category and typing to calculate later on the damage. - if (!move.category) move.category = 'Physical'; - if (!move.defensiveCategory) move.defensiveCategory = move.category; - // '???' is typeless damage: used for Struggle and Confusion etc - if (!move.type) move.type = '???'; - const type = move.type; - - // We get the base power and apply basePowerCallback if necessary. - let basePower: number | false | null = move.basePower; - if (move.basePowerCallback) { - basePower = move.basePowerCallback.call(this, pokemon, target, move); - } - if (!basePower) { - return basePower === 0 ? undefined : basePower; - } - basePower = this.clampIntRange(basePower, 1); - - // Checking for the move's Critical Hit possibility. We check if it's a 100% crit move, otherwise we calculate the chance. - let isCrit = move.willCrit || false; - if (!isCrit) { - // In gen 1, the critical chance is based on speed. - // First, we get the base speed, divide it by 2 and floor it. This is our current crit chance. - let critChance = Math.floor(pokemon.species.baseStats['spe'] / 2); - - // Now we check for focus energy volatile. - if (pokemon.volatiles['focusenergy']) { - // If it exists, crit chance is divided by 2 again and floored. - critChance = Math.floor(critChance / 2); - } else { - // Normally, without focus energy, crit chance is multiplied by 2 and capped at 255 here. - critChance = this.clampIntRange(critChance * 2, 1, 255); - } - - // Now we check for the move's critical hit ratio. - if (move.critRatio === 1) { - // Normal hit ratio, we divide the crit chance by 2 and floor the result again. - critChance = Math.floor(critChance / 2); - } else if (move.critRatio === 2) { - // High crit ratio, we multiply the result so far by 4 and cap it at 255. - critChance = this.clampIntRange(critChance * 4, 1, 255); - } - - // Last, we check deppending on ratio if the move critical hits or not. - // We compare our critical hit chance against a random number between 0 and 255. - // If the random number is lower, we get a critical hit. This means there is always a 1/255 chance of not hitting critically. - if (critChance > 0) { - isCrit = this.randomChance(critChance, 256); - } - } - if (isCrit) target.getMoveHitData(move).crit = true; - - // Happens after crit calculation. - if (basePower) { - basePower = this.runEvent('BasePower', pokemon, target, move, basePower); - if (basePower && move.basePowerModifier) { - basePower *= move.basePowerModifier; - } - } - if (!basePower) return 0; - basePower = this.clampIntRange(basePower, 1); - - // We now check attacker's and defender's stats. - let level = pokemon.level; - let attacker = pokemon; - const defender = target; - if (move.useTargetOffensive) attacker = target; - const atkType: StatNameExceptHP = (move.category === 'Physical') ? 'atk' : 'spa'; - const defType: StatNameExceptHP = (move.defensiveCategory === 'Physical') ? 'def' : 'spd'; - let attack = attacker.getStat(move.useSourceDefensiveAsOffensive ? defType : atkType); - let defense = defender.getStat(defType); - // In gen 1, screen effect is applied here. - if ((defType === 'def' && defender.volatiles['reflect']) || (defType === 'spd' && defender.volatiles['lightscreen'])) { - this.debug('Screen doubling (Sp)Def'); - defense *= 2; - defense = this.clampIntRange(defense, 1, 1998); - } - - // In the event of a critical hit, the offense and defense changes are ignored. - // This includes both boosts and screens. - // Also, level is doubled in damage calculation. - if (isCrit) { - move.ignoreOffensive = true; - move.ignoreDefensive = true; - level *= 2; - if (!suppressMessages) this.add('-crit', target); - } - if (move.ignoreOffensive) { - this.debug('Negating (sp)atk boost/penalty.'); - attack = attacker.getStat(atkType, true); - } - if (move.ignoreDefensive) { - this.debug('Negating (sp)def boost/penalty.'); - // No screens - defense = target.getStat(defType, true); - } - - // When either attack or defense are higher than 256, they are both divided by 4 and moded by 256. - // This is what cuases the roll over bugs. - if (attack >= 256 || defense >= 256) { - attack = this.clampIntRange(Math.floor(attack / 4) % 256, 1); - // Defense isn't checked on the cartridge, but we don't want those / 0 bugs on the sim. - defense = this.clampIntRange(Math.floor(defense / 4) % 256, 1); - } - - // Self destruct moves halve defense at this point. - if (move.selfdestruct && defType === 'def') { - defense = this.clampIntRange(Math.floor(defense / 2), 1); - } - - // Let's go with the calculation now that we have what we need. - // We do it step by step just like the game does. - let damage = level * 2; - damage = Math.floor(damage / 5); - damage += 2; - damage *= basePower; - damage *= attack; - damage = Math.floor(damage / defense); - damage = this.clampIntRange(Math.floor(damage / 50), 1, 997); - damage += 2; - - // STAB damage bonus, the "???" type never gets STAB - if (type !== '???' && pokemon.hasType(type)) { - damage += Math.floor(damage / 2); - } - - // Type effectiveness. - // The order here is not correct, must change to check the move versus each type. - const totalTypeMod = target.runEffectiveness(move); - // Super effective attack - if (totalTypeMod > 0) { - if (!suppressMessages) this.add('-supereffective', target); - damage *= 20; - damage = Math.floor(damage / 10); - if (totalTypeMod >= 2) { - damage *= 20; - damage = Math.floor(damage / 10); - } - } - if (totalTypeMod < 0) { - if (!suppressMessages) this.add('-resisted', target); - damage *= 5; - damage = Math.floor(damage / 10); - if (totalTypeMod <= -2) { - damage *= 5; - damage = Math.floor(damage / 10); - } - } - - // If damage becomes 0, the move is made to miss. - // This occurs when damage was either 2 or 3 prior to applying STAB/Type matchup, and target is 4x resistant to the move. - if (damage === 0) return damage; - - // Apply random factor is damage is greater than 1 - if (damage > 1) { - damage *= this.random(217, 256); - damage = Math.floor(damage / 255); - if (damage > target.hp && !target.volatiles['substitute']) damage = target.hp; - } - - // And we are done. - return Math.floor(damage); - }, }; diff --git a/data/mods/gen1stadium/moves.ts b/data/mods/gen1stadium/moves.ts index ad70cec780..24b1025ea3 100644 --- a/data/mods/gen1stadium/moves.ts +++ b/data/mods/gen1stadium/moves.ts @@ -197,7 +197,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { return; } if (move.volatileStatus && target === source) return; - let damage = this.getDamage(source, target, move); + let damage = this.actions.getDamage(source, target, move); if (!damage) return null; damage = this.runEvent('SubDamage', target, source, move, damage); if (!damage) return damage; diff --git a/data/mods/gen1stadium/scripts.ts b/data/mods/gen1stadium/scripts.ts index f8c87d3b93..0ea1c7e1e7 100644 --- a/data/mods/gen1stadium/scripts.ts +++ b/data/mods/gen1stadium/scripts.ts @@ -59,576 +59,578 @@ export const Scripts: ModdedBattleScriptsData = { return changed; }, }, - // Battle scripts. - runMove(moveOrMoveName, pokemon, targetLoc, sourceEffect) { - const move = this.dex.getActiveMove(moveOrMoveName); - const target = this.getTarget(pokemon, move, targetLoc); - if (target?.subFainted) target.subFainted = null; + actions: { + inherit: true, + runMove(moveOrMoveName, pokemon, targetLoc, sourceEffect) { + const move = this.dex.getActiveMove(moveOrMoveName); + const target = this.battle.getTarget(pokemon, move, targetLoc); + if (target?.subFainted) target.subFainted = null; - this.setActiveMove(move, pokemon, target); + this.battle.setActiveMove(move, pokemon, target); - if (pokemon.moveThisTurn || !this.runEvent('BeforeMove', pokemon, target, move)) { - this.debug('' + pokemon.fullname + ' move interrupted; movedThisTurn: ' + pokemon.moveThisTurn); - this.clearActiveMove(true); - // This is only run for sleep - this.runEvent('AfterMoveSelf', pokemon, target, move); - return; - } - if (move.beforeMoveCallback) { - if (move.beforeMoveCallback.call(this, pokemon, target, move)) { - this.clearActiveMove(true); + if (pokemon.moveThisTurn || !this.battle.runEvent('BeforeMove', pokemon, target, move)) { + this.battle.debug('' + pokemon.fullname + ' move interrupted; movedThisTurn: ' + pokemon.moveThisTurn); + this.battle.clearActiveMove(true); + // This is only run for sleep + this.battle.runEvent('AfterMoveSelf', pokemon, target, move); return; } - } - pokemon.lastDamage = 0; - let lockedMove = this.runEvent('LockMove', pokemon); - if (lockedMove === true) lockedMove = false; - if ( - !lockedMove && - (!pokemon.volatiles['partialtrappinglock'] || pokemon.volatiles['partialtrappinglock'].locked !== target) - ) { - pokemon.deductPP(move, null, target); - pokemon.side.lastMove = move; - pokemon.lastMove = move; - } else { - sourceEffect = move; - } - this.useMove(move, pokemon, target, sourceEffect); - this.singleEvent('AfterMove', move, null, pokemon, target, move); - - // If target fainted - if (target && target.hp <= 0) { - // We remove screens - target.side.removeSideCondition('reflect'); - target.side.removeSideCondition('lightscreen'); - } else { - this.runEvent('AfterMoveSelf', pokemon, target, move); - } - if (pokemon.volatiles['mustrecharge']) this.add('-mustrecharge', pokemon); - - // For partial trapping moves, we are saving the target. - if (move.volatileStatus === 'partiallytrapped' && target && target.hp > 0) { - // It hit, so let's remove must recharge volatile. Yup, this happens on Stadium. - target.removeVolatile('mustrecharge'); - // Let's check if the lock exists - if (pokemon.volatiles['partialtrappinglock'] && target.volatiles['partiallytrapped']) { - // Here the partialtrappinglock volatile has been already applied - if (!pokemon.volatiles['partialtrappinglock'].locked) { - // If it's the first hit, we save the target - pokemon.volatiles['partialtrappinglock'].locked = target; + if (move.beforeMoveCallback) { + if (move.beforeMoveCallback.call(this.battle, pokemon, target, move)) { + this.battle.clearActiveMove(true); + return; } - } // If we move to here, the move failed and there's no partial trapping lock - } - }, - tryMoveHit(target, pokemon, move) { - let damage: number | false | undefined = 0; - - // First, check if the target is semi-invulnerable - let hitResult = this.runEvent('Invulnerability', target, pokemon, move); - if (hitResult === false) { - if (!move.spreadHit) this.attrLastMove('[miss]'); - this.add('-miss', pokemon); - return false; - } - - // Then, check if the Pokemon is immune to this move. - if ( - (!move.ignoreImmunity || (move.ignoreImmunity !== true && !move.ignoreImmunity[move.type])) && - !target.runImmunity(move.type, true) - ) { - if (move.selfdestruct) { - this.faint(pokemon, pokemon, move); } - return false; - } - hitResult = this.singleEvent('TryImmunity', move, null, target, pokemon, move); - if (hitResult === false) { - this.add('-immune', target); - return false; - } + pokemon.lastDamage = 0; + let lockedMove = this.battle.runEvent('LockMove', pokemon); + if (lockedMove === true) lockedMove = false; + if ( + !lockedMove && + (!pokemon.volatiles['partialtrappinglock'] || pokemon.volatiles['partialtrappinglock'].locked !== target) + ) { + pokemon.deductPP(move, null, target); + pokemon.side.lastMove = move; + pokemon.lastMove = move; + } else { + sourceEffect = move; + } + this.battle.actions.useMove(move, pokemon, target, sourceEffect); + this.battle.singleEvent('AfterMove', move, null, pokemon, target, move); - // Now, let's calculate the accuracy. - let accuracy = move.accuracy; + // If target fainted + if (target && target.hp <= 0) { + // We remove screens + target.side.removeSideCondition('reflect'); + target.side.removeSideCondition('lightscreen'); + } else { + this.battle.runEvent('AfterMoveSelf', pokemon, target, move); + } + if (pokemon.volatiles['mustrecharge']) this.battle.add('-mustrecharge', pokemon); - // Partial trapping moves: true accuracy while it lasts - if (pokemon.volatiles['partialtrappinglock']) { - if (move.volatileStatus === 'partiallytrapped' && target === pokemon.volatiles['partialtrappinglock'].locked) { - accuracy = true; - } else if (pokemon.volatiles['partialtrappinglock'].locked !== target) { - // The target switched, therefor, you fail using wrap. + // For partial trapping moves, we are saving the target. + if (move.volatileStatus === 'partiallytrapped' && target && target.hp > 0) { + // It hit, so let's remove must recharge volatile. Yup, this happens on Stadium. + target.removeVolatile('mustrecharge'); + // Let's check if the lock exists + if (pokemon.volatiles['partialtrappinglock'] && target.volatiles['partiallytrapped']) { + // Here the partialtrappinglock volatile has been already applied + if (!pokemon.volatiles['partialtrappinglock'].locked) { + // If it's the first hit, we save the target + pokemon.volatiles['partialtrappinglock'].locked = target; + } + } // If we move to here, the move failed and there's no partial trapping lock + } + }, + tryMoveHit(target, pokemon, move) { + let damage: number | false | undefined = 0; + + // First, check if the target is semi-invulnerable + let hitResult = this.battle.runEvent('Invulnerability', target, pokemon, move); + if (hitResult === false) { + if (!move.spreadHit) this.battle.attrLastMove('[miss]'); + this.battle.add('-miss', pokemon); + return false; + } + + // Then, check if the Pokemon is immune to this move. + if ( + (!move.ignoreImmunity || (move.ignoreImmunity !== true && !move.ignoreImmunity[move.type])) && + !target.runImmunity(move.type, true) + ) { + if (move.selfdestruct) { + this.battle.faint(pokemon, pokemon, move); + } + return false; + } + hitResult = this.battle.singleEvent('TryImmunity', move, null, target, pokemon, move); + if (hitResult === false) { + this.battle.add('-immune', target); + return false; + } + + // Now, let's calculate the accuracy. + let accuracy = move.accuracy; + + // Partial trapping moves: true accuracy while it lasts + if (pokemon.volatiles['partialtrappinglock']) { + if (move.volatileStatus === 'partiallytrapped' && target === pokemon.volatiles['partialtrappinglock'].locked) { + accuracy = true; + } else if (pokemon.volatiles['partialtrappinglock'].locked !== target) { + // The target switched, therefor, you fail using wrap. + delete pokemon.volatiles['partialtrappinglock']; + return false; + } + } + + // OHKO moves only have a chance to hit if the user is at least as fast as the target + if (move.ohko) { + if (target.speed > pokemon.speed) { + this.battle.add('-immune', target, '[ohko]'); + return false; + } + } + + // Calculate true accuracy for gen 1, which uses 0-255. + // Stadium uses the Gen 2 boost table for accuracy and evasiveness, except for 1/3 instead of 0.33 + const boostTable = [1 / 3, 0.36, 0.43, 0.5, 0.66, 0.75, 1, 1.33, 1.66, 2, 2.33, 2.66, 3]; + if (accuracy !== true) { + accuracy = Math.floor(accuracy * 255 / 100); + // Check also for accuracy modifiers. + if (!move.ignoreAccuracy) { + accuracy = Math.floor(accuracy * boostTable[pokemon.boosts.accuracy + 6]); + } + if (!move.ignoreEvasion) { + accuracy = Math.floor(accuracy * boostTable[-target.boosts.evasion + 6]); + } + accuracy = Math.min(accuracy, 255); + } + accuracy = this.battle.runEvent('Accuracy', target, pokemon, move, accuracy); + + // Stadium fixes the 1/256 accuracy bug. + if (accuracy !== true && !this.battle.randomChance(accuracy + 1, 256)) { + this.battle.attrLastMove('[miss]'); + this.battle.add('-miss', pokemon); + damage = false; + } + + // If damage is 0 and not false it means it didn't miss, let's calc. + if (damage !== false) { + pokemon.lastDamage = 0; + if (move.multihit) { + let hits = move.multihit; + if (Array.isArray(hits)) { + // Yes, it's hardcoded... meh + if (hits[0] === 2 && hits[1] === 5) { + hits = this.battle.sample([2, 2, 3, 3, 4, 5]); + } else { + hits = this.battle.random(hits[0], hits[1] + 1); + } + } + hits = Math.floor(hits); + // In gen 1, all the hits have the same damage for multihits move + let moveDamage: number | false | undefined = 0; + let i: number; + for (i = 0; i < hits && target.hp && pokemon.hp; i++) { + move.hit = i + 1; + moveDamage = this.moveHit(target, pokemon, move); + if (moveDamage === false) break; + damage = (moveDamage || 0); + // Move damage is fixed to be the first move's damage + if (i === 0) move.damage = damage; + if (target.subFainted) { + i++; + break; + } + } + move.damage = null; + if (i === 0) return 1; + this.battle.add('-hitcount', target, i); + } else { + damage = this.moveHit(target, pokemon, move); + } + } + + if (move.category !== 'Status') target.gotAttacked(move, damage, pokemon); + + if (move.selfdestruct) { + this.battle.faint(pokemon, pokemon, move); + } + + // The move missed. + if (damage === false) { + // Delete the partial trap lock if necessary. delete pokemon.volatiles['partialtrappinglock']; return false; } - } - // OHKO moves only have a chance to hit if the user is at least as fast as the target - if (move.ohko) { - if (target.speed > pokemon.speed) { - this.add('-immune', target, '[ohko]'); - return false; + if (move.ohko) this.battle.add('-ohko'); + + if (!move.negateSecondary) { + this.battle.singleEvent('AfterMoveSecondary', move, null, target, pokemon, move); + this.battle.runEvent('AfterMoveSecondary', target, pokemon, move); } - } - // Calculate true accuracy for gen 1, which uses 0-255. - // Stadium uses the Gen 2 boost table for accuracy and evasiveness, except for 1/3 instead of 0.33 - const boostTable = [1 / 3, 0.36, 0.43, 0.5, 0.66, 0.75, 1, 1.33, 1.66, 2, 2.33, 2.66, 3]; - if (accuracy !== true) { - accuracy = Math.floor(accuracy * 255 / 100); - // Check also for accuracy modifiers. - if (!move.ignoreAccuracy) { - accuracy = Math.floor(accuracy * boostTable[pokemon.boosts.accuracy + 6]); + return damage; + }, + moveHit(target, pokemon, moveOrMoveName, moveData, isSecondary, isSelf) { + let damage: number | false | null | undefined = 0; + const move = this.dex.getActiveMove(moveOrMoveName); + + if (!isSecondary && !isSelf) this.battle.setActiveMove(move, pokemon, target); + let hitResult: number | boolean = true; + if (!moveData) moveData = move; + + if (move.ignoreImmunity === undefined) { + move.ignoreImmunity = (move.category === 'Status'); } - if (!move.ignoreEvasion) { - accuracy = Math.floor(accuracy * boostTable[-target.boosts.evasion + 6]); - } - accuracy = Math.min(accuracy, 255); - } - accuracy = this.runEvent('Accuracy', target, pokemon, move, accuracy); - // Stadium fixes the 1/256 accuracy bug. - if (accuracy !== true && !this.randomChance(accuracy + 1, 256)) { - this.attrLastMove('[miss]'); - this.add('-miss', pokemon); - damage = false; - } + if (target) { + hitResult = this.battle.singleEvent('TryHit', moveData, {}, target, pokemon, move); - // If damage is 0 and not false it means it didn't miss, let's calc. - if (damage !== false) { - pokemon.lastDamage = 0; - if (move.multihit) { - let hits = move.multihit; - if (Array.isArray(hits)) { - // Yes, it's hardcoded... meh - if (hits[0] === 2 && hits[1] === 5) { - hits = this.sample([2, 2, 3, 3, 4, 5]); - } else { - hits = this.random(hits[0], hits[1] + 1); - } + // Partial trapping moves still apply their volatile to Pokémon behind a Sub + const targetHadSub = !!target.volatiles['substitute']; + if (targetHadSub && moveData.volatileStatus && moveData.volatileStatus === 'partiallytrapped') { + target.addVolatile(moveData.volatileStatus, pokemon, move); } - hits = Math.floor(hits); - // In gen 1, all the hits have the same damage for multihits move - let moveDamage: number | false | undefined = 0; - let i: number; - for (i = 0; i < hits && target.hp && pokemon.hp; i++) { - move.hit = i + 1; - moveDamage = this.moveHit(target, pokemon, move); - if (moveDamage === false) break; - damage = (moveDamage || 0); - // Move damage is fixed to be the first move's damage - if (i === 0) move.damage = damage; - if (target.subFainted) { - i++; - break; - } - } - move.damage = null; - if (i === 0) return 1; - this.add('-hitcount', target, i); - } else { - damage = this.moveHit(target, pokemon, move); - } - } - if (move.category !== 'Status') target.gotAttacked(move, damage, pokemon); - - if (move.selfdestruct) { - this.faint(pokemon, pokemon, move); - } - - // The move missed. - if (damage === false) { - // Delete the partial trap lock if necessary. - delete pokemon.volatiles['partialtrappinglock']; - return false; - } - - if (move.ohko) this.add('-ohko'); - - if (!move.negateSecondary) { - this.singleEvent('AfterMoveSecondary', move, null, target, pokemon, move); - this.runEvent('AfterMoveSecondary', target, pokemon, move); - } - - return damage; - }, - moveHit(target, pokemon, moveOrMoveName, moveData, isSecondary, isSelf) { - let damage: number | false | null | undefined = 0; - const move = this.dex.getActiveMove(moveOrMoveName); - - if (!isSecondary && !isSelf) this.setActiveMove(move, pokemon, target); - let hitResult: number | boolean = true; - if (!moveData) moveData = move; - - if (move.ignoreImmunity === undefined) { - move.ignoreImmunity = (move.category === 'Status'); - } - - if (target) { - hitResult = this.singleEvent('TryHit', moveData, {}, target, pokemon, move); - - // Partial trapping moves still apply their volatile to Pokémon behind a Sub - const targetHadSub = !!target.volatiles['substitute']; - if (targetHadSub && moveData.volatileStatus && moveData.volatileStatus === 'partiallytrapped') { - target.addVolatile(moveData.volatileStatus, pokemon, move); - } - - if (!hitResult) { - if (hitResult === false) this.add('-fail', target); - return false; - } - - // Only run the hit events for the hit itself, not the secondary or self hits - if (!isSelf && !isSecondary) { - hitResult = this.runEvent('TryHit', target, pokemon, move); if (!hitResult) { - if (hitResult === false) this.add('-fail', target); - // Special Substitute hit flag - if (hitResult !== 0) { + if (hitResult === false) this.battle.add('-fail', target); + return false; + } + + // Only run the hit events for the hit itself, not the secondary or self hits + if (!isSelf && !isSecondary) { + hitResult = this.battle.runEvent('TryHit', target, pokemon, move); + if (!hitResult) { + if (hitResult === false) this.battle.add('-fail', target); + // Special Substitute hit flag + if (hitResult !== 0) { + return false; + } + } + if (!this.battle.runEvent('TryFieldHit', target, pokemon, move)) { return false; } + } else if (isSecondary && !moveData.self) { + hitResult = this.battle.runEvent('TrySecondaryHit', target, pokemon, moveData); } - if (!this.runEvent('TryFieldHit', target, pokemon, move)) { + + if (hitResult === 0) { + target = null; + } else if (!hitResult) { + if (hitResult === false) this.battle.add('-fail', target); return false; } - } else if (isSecondary && !moveData.self) { - hitResult = this.runEvent('TrySecondaryHit', target, pokemon, moveData); } - if (hitResult === 0) { - target = null; - } else if (!hitResult) { - if (hitResult === false) this.add('-fail', target); - return false; - } - } + if (target) { + let didSomething = false; - if (target) { - let didSomething = false; - - damage = this.getDamage(pokemon, target, moveData); - if ((damage || damage === 0) && !target.fainted) { - damage = this.damage(damage, target, pokemon, move); - if (!(damage || damage === 0)) return false; - didSomething = true; - } else if (damage === false && typeof hitResult === 'undefined') { - this.add('-fail', target); - } - if (damage === false || damage === null) { - return false; - } - if (moveData.boosts && !target.fainted) { - this.boost(moveData.boosts, target, pokemon, move); - } - if (moveData.heal && !target.fainted) { - const d = target.heal(Math.floor(target.maxhp * moveData.heal[0] / moveData.heal[1])); - if (!d) { - this.add('-fail', target); + damage = this.getDamage(pokemon, target, moveData); + if ((damage || damage === 0) && !target.fainted) { + damage = this.battle.damage(damage, target, pokemon, move); + if (!(damage || damage === 0)) return false; + didSomething = true; + } else if (damage === false && typeof hitResult === 'undefined') { + this.battle.add('-fail', target); + } + if (damage === false || damage === null) { return false; } - this.add('-heal', target, target.getHealth); - didSomething = true; - } - if (moveData.status) { - if (!target.status) { - target.setStatus(moveData.status, pokemon, move); - target.recalculateStats!(); - } else if (!isSecondary) { - if (target.status === moveData.status) { - this.add('-fail', target, target.status); - } else { - this.add('-fail', target); + if (moveData.boosts && !target.fainted) { + this.battle.boost(moveData.boosts, target, pokemon, move); + } + if (moveData.heal && !target.fainted) { + const d = target.heal(Math.floor(target.maxhp * moveData.heal[0] / moveData.heal[1])); + if (!d) { + this.battle.add('-fail', target); + return false; + } + this.battle.add('-heal', target, target.getHealth); + didSomething = true; + } + if (moveData.status) { + if (!target.status) { + target.setStatus(moveData.status, pokemon, move); + target.recalculateStats!(); + } else if (!isSecondary) { + if (target.status === moveData.status) { + this.battle.add('-fail', target, target.status); + } else { + this.battle.add('-fail', target); + } + } + didSomething = true; + } + if (moveData.forceStatus) { + if (target.setStatus(moveData.forceStatus, pokemon, move)) { + target.recalculateStats!(); + didSomething = true; } } - didSomething = true; - } - if (moveData.forceStatus) { - if (target.setStatus(moveData.forceStatus, pokemon, move)) { - target.recalculateStats!(); - didSomething = true; + if (moveData.volatileStatus) { + if (target.addVolatile(moveData.volatileStatus, pokemon, move)) { + didSomething = true; + } + } + if (moveData.sideCondition) { + if (target.side.addSideCondition(moveData.sideCondition, pokemon, move)) { + didSomething = true; + } + } + if (moveData.pseudoWeather) { + if (this.battle.field.addPseudoWeather(moveData.pseudoWeather, pokemon, move)) { + didSomething = true; + } + } + // Hit events + hitResult = this.battle.singleEvent('Hit', moveData, {}, target, pokemon, move); + if (!isSelf && !isSecondary) { + this.battle.runEvent('Hit', target, pokemon, move); + } + if (!hitResult && !didSomething) { + if (hitResult === false) this.battle.add('-fail', target); + return false; } } - if (moveData.volatileStatus) { - if (target.addVolatile(moveData.volatileStatus, pokemon, move)) { - didSomething = true; - } - } - if (moveData.sideCondition) { - if (target.side.addSideCondition(moveData.sideCondition, pokemon, move)) { - didSomething = true; - } - } - if (moveData.pseudoWeather) { - if (this.field.addPseudoWeather(moveData.pseudoWeather, pokemon, move)) { - didSomething = true; - } - } - // Hit events - hitResult = this.singleEvent('Hit', moveData, {}, target, pokemon, move); - if (!isSelf && !isSecondary) { - this.runEvent('Hit', target, pokemon, move); - } - if (!hitResult && !didSomething) { - if (hitResult === false) this.add('-fail', target); - return false; - } - } - // Here's where self effects are applied. - if (moveData.self) { - this.moveHit(pokemon, pokemon, move, moveData.self, isSecondary, true); - } + // Here's where self effects are applied. + if (moveData.self) { + this.moveHit(pokemon, pokemon, move, moveData.self, isSecondary, true); + } - // Now we can save the partial trapping damage. - if (pokemon.volatiles['partialtrappinglock']) { - pokemon.volatiles['partialtrappinglock'].damage = pokemon.lastDamage; - } + // Now we can save the partial trapping damage. + if (pokemon.volatiles['partialtrappinglock']) { + pokemon.volatiles['partialtrappinglock'].damage = pokemon.lastDamage; + } - // Apply move secondaries. - if (moveData.secondaries) { - for (const secondary of moveData.secondaries) { - // We check here whether to negate the probable secondary status if it's para, burn, or freeze. - // In the game, this is checked and if true, the random number generator is not called. - // That means that a move that does not share the type of the target can status it. - // If a move that was not fire-type would exist on Gen 1, it could burn a Pokémon. - if (!(secondary.status && ['par', 'brn', 'frz'].includes(secondary.status) && target && target.hasType(move.type))) { - const effectChance = Math.floor((secondary.chance || 100) * 255 / 100); - if (typeof secondary.chance === 'undefined' || this.randomChance(effectChance + 1, 256)) { - this.moveHit(target, pokemon, move, secondary, true, isSelf); + // Apply move secondaries. + if (moveData.secondaries) { + for (const secondary of moveData.secondaries) { + // We check here whether to negate the probable secondary status if it's para, burn, or freeze. + // In the game, this is checked and if true, the random number generator is not called. + // That means that a move that does not share the type of the target can status it. + // If a move that was not fire-type would exist on Gen 1, it could burn a Pokémon. + if (!(secondary.status && ['par', 'brn', 'frz'].includes(secondary.status) && target && target.hasType(move.type))) { + const effectChance = Math.floor((secondary.chance || 100) * 255 / 100); + if (typeof secondary.chance === 'undefined' || this.battle.randomChance(effectChance + 1, 256)) { + this.moveHit(target, pokemon, move, secondary, true, isSelf); + } } } } - } - if (move.selfSwitch && pokemon.hp) { - pokemon.switchFlag = move.selfSwitch; - } - - return damage; - }, - getDamage(pokemon, target, move, suppressMessages) { - // First of all, we get the move. - if (typeof move === 'string') { - move = this.dex.getActiveMove(move); - } else if (typeof move === 'number') { - move = { - basePower: move, - type: '???', - category: 'Physical', - willCrit: false, - flags: {}, - } as ActiveMove; - } - - // Let's see if the target is immune to the move. - if (!move.ignoreImmunity || (move.ignoreImmunity !== true && !move.ignoreImmunity[move.type])) { - if (!target.runImmunity(move.type, true)) { - return false; - } - } - - // Is it an OHKO move? - if (move.ohko) { - return target.maxhp; - } - - // We edit the damage through move's damage callback if necessary. - if (move.damageCallback) { - return move.damageCallback.call(this, pokemon, target); - } - - // We take damage from damage=level moves (seismic toss). - if (move.damage === 'level') { - return pokemon.level; - } - - // If there's a fix move damage, we return that. - if (move.damage) { - return move.damage; - } - - // If it's the first hit on a Normal-type partially trap move, it hits Ghosts anyways but damage is 0. - if (move.volatileStatus === 'partiallytrapped' && move.type === 'Normal' && target.hasType('Ghost')) { - return 0; - } - - // Let's check if we are in middle of a partial trap sequence to return the previous damage. - if (pokemon.volatiles['partialtrappinglock'] && (target === pokemon.volatiles['partialtrappinglock'].locked)) { - return pokemon.volatiles['partialtrappinglock'].damage; - } - - // We check the category and typing to calculate later on the damage. - if (!move.category) move.category = 'Physical'; - if (!move.defensiveCategory) move.defensiveCategory = move.category; - // '???' is typeless damage: used for Struggle and Confusion etc - if (!move.type) move.type = '???'; - const type = move.type; - - // We get the base power and apply basePowerCallback if necessary. - let basePower: number | false | null = move.basePower; - if (move.basePowerCallback) { - basePower = move.basePowerCallback.call(this, pokemon, target, move); - } - if (!basePower) { - return basePower === 0 ? undefined : basePower; - } - basePower = this.clampIntRange(basePower, 1); - - // Checking for the move's Critical Hit possibility. We check if it's a 100% crit move, otherwise we calculate the chance. - let isCrit = move.willCrit || false; - if (!isCrit) { - // In Stadium, the critical chance is based on speed. - // First, we get the base speed and store it. Then we add 76. This is our current crit chance. - let critChance = pokemon.species.baseStats['spe'] + 76; - - // Now we right logical shift it two places, essentially dividing by 4 and flooring it. - critChance = critChance >> 2; - - // Now we check for focus energy volatile. - if (pokemon.volatiles['focusenergy']) { - // If it exists, crit chance is multiplied by 4 and floored with a logical left shift. - critChance = critChance << 2; - // Then we add 160. - critChance += 160; - } else { - // If it is not active, we left shift it by 1. - critChance = critChance << 1; + if (move.selfSwitch && pokemon.hp) { + pokemon.switchFlag = move.selfSwitch; } - // Now we check for the move's critical hit ratio. - if (move.critRatio === 2) { - // High crit ratio, we multiply the result so far by 4. - critChance = critChance << 2; - } else if (move.critRatio === 1) { - // Normal hit ratio, we divide the crit chance by 2 and floor the result again. - critChance = critChance >> 1; + return damage; + }, + getDamage(pokemon, target, move, suppressMessages) { + // First of all, we get the move. + if (typeof move === 'string') { + move = this.dex.getActiveMove(move); + } else if (typeof move === 'number') { + move = { + basePower: move, + type: '???', + category: 'Physical', + willCrit: false, + flags: {}, + } as ActiveMove; } - // Now we make sure it's a number between 1 and 255. - critChance = this.clampIntRange(critChance, 1, 255); - - // Last, we check deppending on ratio if the move critical hits or not. - // We compare our critical hit chance against a random number between 0 and 255. - // If the random number is lower, we get a critical hit. This means there is always a 1/255 chance of not hitting critically. - if (critChance > 0) { - isCrit = this.randomChance(critChance, 256); + // Let's see if the target is immune to the move. + if (!move.ignoreImmunity || (move.ignoreImmunity !== true && !move.ignoreImmunity[move.type])) { + if (!target.runImmunity(move.type, true)) { + return false; + } } - } - // There is a critical hit. - if (isCrit && this.runEvent('CriticalHit', target, null, move)) { - target.getMoveHitData(move).crit = true; - } - // Happens after crit calculation. - if (basePower) { - basePower = this.runEvent('BasePower', pokemon, target, move, basePower); - if (basePower && move.basePowerModifier) { - basePower *= move.basePowerModifier; + // Is it an OHKO move? + if (move.ohko) { + return target.maxhp; } - } - if (!basePower) return 0; - basePower = this.clampIntRange(basePower, 1); - // We now check attacker's and defender's stats. - let level = pokemon.level; - let attacker = pokemon; - const defender = target; - if (move.useTargetOffensive) attacker = target; - let atkType: StatNameExceptHP = (move.category === 'Physical') ? 'atk' : 'spa'; - const defType: StatNameExceptHP = (move.defensiveCategory === 'Physical') ? 'def' : 'spd'; - if (move.useSourceDefensiveAsOffensive) atkType = defType; - let attack = attacker.getStat(atkType); - let defense = defender.getStat(defType); - // In gen 1, screen effect is applied here. - if ((defType === 'def' && defender.volatiles['reflect']) || (defType === 'spd' && defender.volatiles['lightscreen'])) { - this.debug('Screen doubling (Sp)Def'); - defense *= 2; - defense = this.clampIntRange(defense, 1, 1998); - } + // We edit the damage through move's damage callback if necessary. + if (move.damageCallback) { + return move.damageCallback.call(this.battle, pokemon, target); + } - // In the event of a critical hit, the offense and defense changes are ignored. - // This includes both boosts and screens. - // Also, level is doubled in damage calculation. - if (isCrit) { - move.ignoreOffensive = true; - move.ignoreDefensive = true; - level *= 2; - if (!suppressMessages) this.add('-crit', target); - } - if (move.ignoreOffensive) { - this.debug('Negating (sp)atk boost/penalty.'); - attack = attacker.getStat(atkType, true); - } - if (move.ignoreDefensive) { - this.debug('Negating (sp)def boost/penalty.'); - defense = target.getStat(defType, true); - } + // We take damage from damage=level moves (seismic toss). + if (move.damage === 'level') { + return pokemon.level; + } - // When either attack or defense are higher than 256, they are both divided by 4 and moded by 256. - // This is what cuases the roll over bugs. - if (attack >= 256 || defense >= 256) { - attack = this.clampIntRange(Math.floor(attack / 4) % 256, 1); - // Defense isn't checked on the cartridge, but we don't want those / 0 bugs on the sim. - defense = this.clampIntRange(Math.floor(defense / 4) % 256, 1); - } + // If there's a fix move damage, we return that. + if (move.damage) { + return move.damage; + } - // Self destruct moves halve defense at this point. - if (move.selfdestruct && defType === 'def') { - defense = this.clampIntRange(Math.floor(defense / 2), 1); - } + // If it's the first hit on a Normal-type partially trap move, it hits Ghosts anyways but damage is 0. + if (move.volatileStatus === 'partiallytrapped' && move.type === 'Normal' && target.hasType('Ghost')) { + return 0; + } - // Let's go with the calculation now that we have what we need. - // We do it step by step just like the game does. - let damage = level * 2; - damage = Math.floor(damage / 5); - damage += 2; - damage *= basePower; - damage *= attack; - damage = Math.floor(damage / defense); - damage = this.clampIntRange(Math.floor(damage / 50), 1, 997); - damage += 2; + // Let's check if we are in middle of a partial trap sequence to return the previous damage. + if (pokemon.volatiles['partialtrappinglock'] && (target === pokemon.volatiles['partialtrappinglock'].locked)) { + return pokemon.volatiles['partialtrappinglock'].damage; + } - // STAB damage bonus, the "???" type never gets STAB - if (type !== '???' && pokemon.hasType(type)) { - damage += Math.floor(damage / 2); - } + // We check the category and typing to calculate later on the damage. + if (!move.category) move.category = 'Physical'; + if (!move.defensiveCategory) move.defensiveCategory = move.category; + // '???' is typeless damage: used for Struggle and Confusion etc + if (!move.type) move.type = '???'; + const type = move.type; - // Type effectiveness. - // The order here is not correct, must change to check the move versus each type. - const totalTypeMod = this.dex.getEffectiveness(type, target); - // Super effective attack - if (totalTypeMod > 0) { - if (!suppressMessages) this.add('-supereffective', target); - damage *= 20; - damage = Math.floor(damage / 10); - if (totalTypeMod >= 2) { + // We get the base power and apply basePowerCallback if necessary. + let basePower: number | false | null = move.basePower; + if (move.basePowerCallback) { + basePower = move.basePowerCallback.call(this.battle, pokemon, target, move); + } + if (!basePower) { + return basePower === 0 ? undefined : basePower; + } + basePower = this.battle.clampIntRange(basePower, 1); + + // Checking for the move's Critical Hit possibility. We check if it's a 100% crit move, otherwise we calculate the chance. + let isCrit = move.willCrit || false; + if (!isCrit) { + // In Stadium, the critical chance is based on speed. + // First, we get the base speed and store it. Then we add 76. This is our current crit chance. + let critChance = pokemon.species.baseStats['spe'] + 76; + + // Now we right logical shift it two places, essentially dividing by 4 and flooring it. + critChance = critChance >> 2; + + // Now we check for focus energy volatile. + if (pokemon.volatiles['focusenergy']) { + // If it exists, crit chance is multiplied by 4 and floored with a logical left shift. + critChance = critChance << 2; + // Then we add 160. + critChance += 160; + } else { + // If it is not active, we left shift it by 1. + critChance = critChance << 1; + } + + // Now we check for the move's critical hit ratio. + if (move.critRatio === 2) { + // High crit ratio, we multiply the result so far by 4. + critChance = critChance << 2; + } else if (move.critRatio === 1) { + // Normal hit ratio, we divide the crit chance by 2 and floor the result again. + critChance = critChance >> 1; + } + + // Now we make sure it's a number between 1 and 255. + critChance = this.battle.clampIntRange(critChance, 1, 255); + + // Last, we check deppending on ratio if the move critical hits or not. + // We compare our critical hit chance against a random number between 0 and 255. + // If the random number is lower, we get a critical hit. This means there is always a 1/255 chance of not hitting critically. + if (critChance > 0) { + isCrit = this.battle.randomChance(critChance, 256); + } + } + // There is a critical hit. + if (isCrit && this.battle.runEvent('CriticalHit', target, null, move)) { + target.getMoveHitData(move).crit = true; + } + + // Happens after crit calculation. + if (basePower) { + basePower = this.battle.runEvent('BasePower', pokemon, target, move, basePower); + if (basePower && move.basePowerModifier) { + basePower *= move.basePowerModifier; + } + } + if (!basePower) return 0; + basePower = this.battle.clampIntRange(basePower, 1); + + // We now check attacker's and defender's stats. + let level = pokemon.level; + let attacker = pokemon; + const defender = target; + if (move.useTargetOffensive) attacker = target; + let atkType: StatNameExceptHP = (move.category === 'Physical') ? 'atk' : 'spa'; + const defType: StatNameExceptHP = (move.defensiveCategory === 'Physical') ? 'def' : 'spd'; + if (move.useSourceDefensiveAsOffensive) atkType = defType; + let attack = attacker.getStat(atkType); + let defense = defender.getStat(defType); + // In gen 1, screen effect is applied here. + if ((defType === 'def' && defender.volatiles['reflect']) || (defType === 'spd' && defender.volatiles['lightscreen'])) { + this.battle.debug('Screen doubling (Sp)Def'); + defense *= 2; + defense = this.battle.clampIntRange(defense, 1, 1998); + } + + // In the event of a critical hit, the offense and defense changes are ignored. + // This includes both boosts and screens. + // Also, level is doubled in damage calculation. + if (isCrit) { + move.ignoreOffensive = true; + move.ignoreDefensive = true; + level *= 2; + if (!suppressMessages) this.battle.add('-crit', target); + } + if (move.ignoreOffensive) { + this.battle.debug('Negating (sp)atk boost/penalty.'); + attack = attacker.getStat(atkType, true); + } + if (move.ignoreDefensive) { + this.battle.debug('Negating (sp)def boost/penalty.'); + defense = target.getStat(defType, true); + } + + // When either attack or defense are higher than 256, they are both divided by 4 and moded by 256. + // This is what cuases the roll over bugs. + if (attack >= 256 || defense >= 256) { + attack = this.battle.clampIntRange(Math.floor(attack / 4) % 256, 1); + // Defense isn't checked on the cartridge, but we don't want those / 0 bugs on the sim. + defense = this.battle.clampIntRange(Math.floor(defense / 4) % 256, 1); + } + + // Self destruct moves halve defense at this point. + if (move.selfdestruct && defType === 'def') { + defense = this.battle.clampIntRange(Math.floor(defense / 2), 1); + } + + // Let's go with the calculation now that we have what we need. + // We do it step by step just like the game does. + let damage = level * 2; + damage = Math.floor(damage / 5); + damage += 2; + damage *= basePower; + damage *= attack; + damage = Math.floor(damage / defense); + damage = this.battle.clampIntRange(Math.floor(damage / 50), 1, 997); + damage += 2; + + // STAB damage bonus, the "???" type never gets STAB + if (type !== '???' && pokemon.hasType(type)) { + damage += Math.floor(damage / 2); + } + + // Type effectiveness. + // The order here is not correct, must change to check the move versus each type. + const totalTypeMod = this.dex.getEffectiveness(type, target); + // Super effective attack + if (totalTypeMod > 0) { + if (!suppressMessages) this.battle.add('-supereffective', target); damage *= 20; damage = Math.floor(damage / 10); + if (totalTypeMod >= 2) { + damage *= 20; + damage = Math.floor(damage / 10); + } } - } - if (totalTypeMod < 0) { - if (!suppressMessages) this.add('-resisted', target); - damage *= 5; - damage = Math.floor(damage / 10); - if (totalTypeMod <= -2) { + if (totalTypeMod < 0) { + if (!suppressMessages) this.battle.add('-resisted', target); damage *= 5; damage = Math.floor(damage / 10); + if (totalTypeMod <= -2) { + damage *= 5; + damage = Math.floor(damage / 10); + } } - } - // If damage becomes 0, the move is made to miss. - // This occurs when damage was either 2 or 3 prior to applying STAB/Type matchup, and target is 4x resistant to the move. - if (damage === 0) return damage; + // If damage becomes 0, the move is made to miss. + // This occurs when damage was either 2 or 3 prior to applying STAB/Type matchup, and target is 4x resistant to the move. + if (damage === 0) return damage; - // Apply random factor is damage is greater than 1 - if (damage > 1) { - damage *= this.random(217, 256); - damage = Math.floor(damage / 255); - if (damage > target.hp && !target.volatiles['substitute']) damage = target.hp; - if (target.volatiles['substitute'] && damage > target.volatiles['substitute'].hp) { - damage = target.volatiles['substitute'].hp; + // Apply random factor is damage is greater than 1 + if (damage > 1) { + damage *= this.battle.random(217, 256); + damage = Math.floor(damage / 255); + if (damage > target.hp && !target.volatiles['substitute']) damage = target.hp; + if (target.volatiles['substitute'] && damage > target.volatiles['substitute'].hp) { + damage = target.volatiles['substitute'].hp; + } } - } - // We are done, this is the final damage. - return Math.floor(damage); + // We are done, this is the final damage. + return Math.floor(damage); + }, }, }; diff --git a/data/mods/gen2/conditions.ts b/data/mods/gen2/conditions.ts index ff5bf2a901..159427ea0e 100644 --- a/data/mods/gen2/conditions.ts +++ b/data/mods/gen2/conditions.ts @@ -143,7 +143,7 @@ export const Conditions: {[k: string]: ModdedConditionData} = { flags: {}, selfdestruct: move.selfdestruct, } as unknown as ActiveMove; - const damage = this.getDamage(pokemon, pokemon, move); + const damage = this.actions.getDamage(pokemon, pokemon, move); if (typeof damage !== 'number') throw new Error("Confusion damage not dealt"); this.directDamage(damage); return false; diff --git a/data/mods/gen2/moves.ts b/data/mods/gen2/moves.ts index e4a289fb12..d2ea8994b8 100644 --- a/data/mods/gen2/moves.ts +++ b/data/mods/gen2/moves.ts @@ -97,7 +97,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { effectType: 'Move', type: 'Normal', } as unknown as ActiveMove; - this.tryMoveHit(target, pokemon, moveData); + this.actions.tryMoveHit(target, pokemon, moveData); return false; } this.add('-activate', pokemon, 'move: Bide'); @@ -319,7 +319,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { inherit: true, onMoveFail(target, source, move) { if (target.runImmunity('Fighting')) { - const damage = this.getDamage(source, target, move, true); + const damage = this.actions.getDamage(source, target, move, true); if (typeof damage !== 'number') throw new Error("Couldn't get High Jump Kick recoil"); this.damage(this.clampIntRange(damage / 8, 1), source, source, move); } @@ -329,7 +329,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { inherit: true, onMoveFail(target, source, move) { if (target.runImmunity('Fighting')) { - const damage = this.getDamage(source, target, move, true); + const damage = this.actions.getDamage(source, target, move, true); if (typeof damage !== 'number') throw new Error("Couldn't get Jump Kick recoil"); this.damage(this.clampIntRange(damage / 8, 1), source, source, move); } @@ -455,7 +455,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { if (noMirror.includes(lastMove) || pokemon.moves.includes(lastMove)) { return false; } - this.useMove(lastMove, pokemon); + this.actions.useMove(lastMove, pokemon); }, noSketch: true, }, @@ -648,7 +648,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { let randomMove = ''; if (moves.length) randomMove = this.sample(moves); if (!randomMove) return false; - this.useMove(randomMove, pokemon); + this.actions.useMove(randomMove, pokemon); }, noSketch: true, }, @@ -726,7 +726,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { } return; } - let damage = this.getDamage(source, target, move); + let damage = this.actions.getDamage(source, target, move); if (!damage) { return null; } diff --git a/data/mods/gen2/scripts.ts b/data/mods/gen2/scripts.ts index 6f81f42c46..a90d4bbfc0 100644 --- a/data/mods/gen2/scripts.ts +++ b/data/mods/gen2/scripts.ts @@ -5,7 +5,6 @@ export const Scripts: ModdedBattleScriptsData = { inherit: 'gen3', gen: 2, - // BattlePokemon scripts. pokemon: { getStat(statName, unboosted, unmodified, fastReturn) { // @ts-ignore - type checking prevents 'hp' from being passed, but we're paranoid @@ -87,616 +86,621 @@ export const Scripts: ModdedBattleScriptsData = { return delta; }, }, - // Battle scripts. - runMove(moveOrMoveName, pokemon, targetLoc, sourceEffect) { - let move = this.dex.getActiveMove(moveOrMoveName); - let target = this.getTarget(pokemon, move, targetLoc); - if (!sourceEffect && move.id !== 'struggle') { - const changedMove = this.runEvent('OverrideAction', pokemon, target, move); - if (changedMove && changedMove !== true) { - move = this.dex.getActiveMove(changedMove); - target = this.getRandomTarget(pokemon, move); + actions: { + inherit: true, + runMove(moveOrMoveName, pokemon, targetLoc, sourceEffect) { + let move = this.dex.getActiveMove(moveOrMoveName); + let target = this.battle.getTarget(pokemon, move, targetLoc); + if (!sourceEffect && move.id !== 'struggle') { + const changedMove = this.battle.runEvent('OverrideAction', pokemon, target, move); + if (changedMove && changedMove !== true) { + move = this.dex.getActiveMove(changedMove); + target = this.battle.getRandomTarget(pokemon, move); + } } - } - if (!target && target !== false) target = this.getRandomTarget(pokemon, move); + if (!target && target !== false) target = this.battle.getRandomTarget(pokemon, move); - this.setActiveMove(move, pokemon, target); + this.battle.setActiveMove(move, pokemon, target); - if (pokemon.moveThisTurn) { - // THIS IS PURELY A SANITY CHECK - // DO NOT TAKE ADVANTAGE OF THIS TO PREVENT A POKEMON FROM MOVING; - // USE this.queue.cancelMove INSTEAD - this.debug('' + pokemon.fullname + ' INCONSISTENT STATE, ALREADY MOVED: ' + pokemon.moveThisTurn); - this.clearActiveMove(true); - return; - } - if (!this.runEvent('BeforeMove', pokemon, target, move)) { - this.runEvent('MoveAborted', pokemon, target, move); - this.clearActiveMove(true); - // This is only run for sleep and fully paralysed. - this.runEvent('AfterMoveSelf', pokemon, target, move); - return; - } - if (move.beforeMoveCallback) { - if (move.beforeMoveCallback.call(this, pokemon, target, move)) { - this.clearActiveMove(true); + if (pokemon.moveThisTurn) { + // THIS IS PURELY A SANITY CHECK + // DO NOT TAKE ADVANTAGE OF THIS TO PREVENT A POKEMON FROM MOVING; + // USE this.battle.queue.cancelMove INSTEAD + this.battle.debug('' + pokemon.fullname + ' INCONSISTENT STATE, ALREADY MOVED: ' + pokemon.moveThisTurn); + this.battle.clearActiveMove(true); return; } - } - pokemon.lastDamage = 0; - let lockedMove = this.runEvent('LockMove', pokemon); - if (lockedMove === true) lockedMove = false; - if (!lockedMove) { - if (!pokemon.deductPP(move, null, target) && (move.id !== 'struggle')) { - this.add('cant', pokemon, 'nopp', move); - this.clearActiveMove(true); + if (!this.battle.runEvent('BeforeMove', pokemon, target, move)) { + this.battle.runEvent('MoveAborted', pokemon, target, move); + this.battle.clearActiveMove(true); + // This is only run for sleep and fully paralysed. + this.battle.runEvent('AfterMoveSelf', pokemon, target, move); return; } - } - pokemon.moveUsed(move); - this.useMove(move, pokemon, target, sourceEffect); - this.singleEvent('AfterMove', move, null, pokemon, target, move); - if (!move.selfSwitch && pokemon.side.foe.active[0].hp) this.runEvent('AfterMoveSelf', pokemon, target, move); - }, - tryMoveHit(target, pokemon, move) { - const positiveBoostTable = [1, 1.33, 1.66, 2, 2.33, 2.66, 3]; - const negativeBoostTable = [1, 0.75, 0.6, 0.5, 0.43, 0.36, 0.33]; - const doSelfDestruct = true; - let damage: number | false | undefined = 0; - - if (move.selfdestruct && doSelfDestruct) { - this.faint(pokemon, pokemon, move); - } - - let hitResult = this.singleEvent('PrepareHit', move, {}, target, pokemon, move); - if (!hitResult) { - if (hitResult === false) this.add('-fail', target); - return false; - } - this.runEvent('PrepareHit', pokemon, target, move); - - if (!this.singleEvent('Try', move, null, pokemon, target, move)) { - return false; - } - - hitResult = this.runEvent('Invulnerability', target, pokemon, move); - if (hitResult === false) { - if (!move.spreadHit) this.attrLastMove('[miss]'); - this.add('-miss', pokemon); - return false; - } - - if (move.ignoreImmunity === undefined) { - move.ignoreImmunity = (move.category === 'Status'); - } - - if ( - (!move.ignoreImmunity || (move.ignoreImmunity !== true && !move.ignoreImmunity[move.type])) && - !target.runImmunity(move.type, true) - ) { - return false; - } - - hitResult = this.singleEvent('TryImmunity', move, {}, target, pokemon, move); - if (hitResult === false) { - this.add('-immune', target); - return false; - } - - hitResult = this.runEvent('TryHit', target, pokemon, move); - if (!hitResult) { - if (hitResult === false) this.add('-fail', target); - return false; - } - - let accuracy = move.accuracy; - if (move.alwaysHit) { - accuracy = true; - } else { - accuracy = this.runEvent('Accuracy', target, pokemon, move, accuracy); - } - // Now, let's calculate the accuracy. - if (accuracy !== true) { - accuracy = Math.floor(accuracy * 255 / 100); - if (move.ohko) { - if (pokemon.level >= target.level) { - accuracy += (pokemon.level - target.level) * 2; - accuracy = Math.min(accuracy, 255); - } else { - this.add('-immune', target, '[ohko]'); - return false; + if (move.beforeMoveCallback) { + if (move.beforeMoveCallback.call(this.battle, pokemon, target, move)) { + this.battle.clearActiveMove(true); + return; } } - if (!move.ignoreAccuracy) { - if (pokemon.boosts.accuracy > 0) { - accuracy *= positiveBoostTable[pokemon.boosts.accuracy]; - } else { - accuracy *= negativeBoostTable[-pokemon.boosts.accuracy]; + pokemon.lastDamage = 0; + let lockedMove = this.battle.runEvent('LockMove', pokemon); + if (lockedMove === true) lockedMove = false; + if (!lockedMove) { + if (!pokemon.deductPP(move, null, target) && (move.id !== 'struggle')) { + this.battle.add('cant', pokemon, 'nopp', move); + this.battle.clearActiveMove(true); + return; } } - if (!move.ignoreEvasion) { - if (target.boosts.evasion > 0 && !move.ignorePositiveEvasion) { - accuracy *= negativeBoostTable[target.boosts.evasion]; - } else if (target.boosts.evasion < 0) { - accuracy *= positiveBoostTable[-target.boosts.evasion]; - } + pokemon.moveUsed(move); + this.battle.actions.useMove(move, pokemon, target, sourceEffect); + this.battle.singleEvent('AfterMove', move, null, pokemon, target, move); + if (!move.selfSwitch && pokemon.side.foe.active[0].hp) this.battle.runEvent('AfterMoveSelf', pokemon, target, move); + }, + tryMoveHit(target, pokemon, move) { + const positiveBoostTable = [1, 1.33, 1.66, 2, 2.33, 2.66, 3]; + const negativeBoostTable = [1, 0.75, 0.6, 0.5, 0.43, 0.36, 0.33]; + const doSelfDestruct = true; + let damage: number | false | undefined = 0; + + if (move.selfdestruct && doSelfDestruct) { + this.battle.faint(pokemon, pokemon, move); } - accuracy = Math.min(Math.floor(accuracy), 255); - accuracy = Math.max(accuracy, 1); - } else { - accuracy = this.runEvent('Accuracy', target, pokemon, move, accuracy); - } - accuracy = this.runEvent('ModifyAccuracy', target, pokemon, move, accuracy); - if (accuracy !== true) accuracy = Math.max(accuracy, 0); - if (move.alwaysHit) { - accuracy = true; - } else { - accuracy = this.runEvent('Accuracy', target, pokemon, move, accuracy); - } - if (accuracy !== true && accuracy !== 255 && !this.randomChance(accuracy, 256)) { - this.attrLastMove('[miss]'); - this.add('-miss', pokemon); - damage = false; - return damage; - } - move.totalDamage = 0; - pokemon.lastDamage = 0; - if (move.multihit) { - let hits = move.multihit; - if (Array.isArray(hits)) { - if (hits[0] === 2 && hits[1] === 5) { - hits = this.sample([2, 2, 2, 3, 3, 3, 4, 5]); - } else { - hits = this.random(hits[0], hits[1] + 1); - } + + let hitResult = this.battle.singleEvent('PrepareHit', move, {}, target, pokemon, move); + if (!hitResult) { + if (hitResult === false) this.battle.add('-fail', target); + return false; } - hits = Math.floor(hits); - let nullDamage = true; - let moveDamage: number | undefined | false; + this.battle.runEvent('PrepareHit', pokemon, target, move); - const isSleepUsable = move.sleepUsable || this.dex.getMove(move.sourceEffect).sleepUsable; - let i: number; - for (i = 0; i < hits && target.hp && pokemon.hp; i++) { - if (pokemon.status === 'slp' && !isSleepUsable) break; - move.hit = i + 1; - if (move.hit === hits) move.lastHit = true; - moveDamage = this.moveHit(target, pokemon, move); - if (moveDamage === false) break; - if (nullDamage && (moveDamage || moveDamage === 0 || moveDamage === undefined)) nullDamage = false; - damage = (moveDamage || 0); - move.totalDamage += damage; - this.eachEvent('Update'); - } - if (i === 0) return 1; - if (nullDamage) damage = false; - this.add('-hitcount', target, i); - } else { - damage = this.moveHit(target, pokemon, move); - move.totalDamage = damage; - } - if (move.category !== 'Status') { - target.gotAttacked(move, damage, pokemon); - } - if (move.ohko) this.add('-ohko'); - - if (!move.negateSecondary) { - this.singleEvent('AfterMoveSecondary', move, null, target, pokemon, move); - this.runEvent('AfterMoveSecondary', target, pokemon, move); - } - - if (move.recoil && move.totalDamage) { - this.damage(this.calcRecoilDamage(move.totalDamage, move), pokemon, target, 'recoil'); - } - return damage; - }, - moveHit(target, pokemon, move, moveData, isSecondary, isSelf) { - let damage: number | false | null | undefined = undefined; - move = this.dex.getActiveMove(move); - - if (!moveData) moveData = move; - let hitResult: boolean | number | null = true; - - if (move.target === 'all' && !isSelf) { - hitResult = this.singleEvent('TryHitField', moveData, {}, target, pokemon, move); - } else if ((move.target === 'foeSide' || move.target === 'allySide') && !isSelf) { - hitResult = this.singleEvent('TryHitSide', moveData, {}, (target ? target.side : null), pokemon, move); - } else if (target) { - hitResult = this.singleEvent('TryHit', moveData, {}, target, pokemon, move); - } - if (!hitResult) { - if (hitResult === false) this.add('-fail', target); - return false; - } - - if (target && !isSecondary && !isSelf) { - hitResult = this.runEvent('TryPrimaryHit', target, pokemon, moveData); - if (hitResult === 0) { - // special Substitute flag - hitResult = true; - target = null; - } - } - if (target && isSecondary && !moveData.self) { - hitResult = true; - } - if (!hitResult) { - return false; - } - - if (target) { - let didSomething: boolean | number | null = false; - damage = this.getDamage(pokemon, target, moveData); - - if ((damage || damage === 0) && !target.fainted) { - damage = this.damage(damage, target, pokemon, move); - if (!(damage || damage === 0)) { - this.debug('damage interrupted'); - return false; - } - didSomething = true; - } - if (damage === false || damage === null) { - if (damage === false && !isSecondary && !isSelf) { - this.add('-fail', target); - } - this.debug('damage calculation interrupted'); + if (!this.battle.singleEvent('Try', move, null, pokemon, target, move)) { return false; } - if (moveData.boosts && !target.fainted) { - if ( - pokemon.volatiles['lockon'] && target === pokemon.volatiles['lockon'].source && - target.isSemiInvulnerable() && !isSelf - ) { - if (!isSecondary) this.add('-fail', target); - return false; - } - hitResult = this.boost(moveData.boosts, target, pokemon, move); - didSomething = didSomething || hitResult; + hitResult = this.battle.runEvent('Invulnerability', target, pokemon, move); + if (hitResult === false) { + if (!move.spreadHit) this.battle.attrLastMove('[miss]'); + this.battle.add('-miss', pokemon); + return false; } - if (moveData.heal && !target.fainted) { - const d = target.heal(Math.round(target.maxhp * moveData.heal[0] / moveData.heal[1])); - if (!d && d !== 0) { - this.add('-fail', target); - this.debug('heal interrupted'); - return false; - } - this.add('-heal', target, target.getHealth); - didSomething = true; + + if (move.ignoreImmunity === undefined) { + move.ignoreImmunity = (move.category === 'Status'); } - if (moveData.status) { - hitResult = target.trySetStatus(moveData.status, pokemon, move); - if (!hitResult && move.status) return hitResult; - didSomething = didSomething || hitResult; + + if ( + (!move.ignoreImmunity || (move.ignoreImmunity !== true && !move.ignoreImmunity[move.type])) && + !target.runImmunity(move.type, true) + ) { + return false; } - if (moveData.forceStatus) { - hitResult = target.setStatus(moveData.forceStatus, pokemon, move); - didSomething = didSomething || hitResult; + + hitResult = this.battle.singleEvent('TryImmunity', move, {}, target, pokemon, move); + if (hitResult === false) { + this.battle.add('-immune', target); + return false; } - if (moveData.volatileStatus) { - hitResult = target.addVolatile(moveData.volatileStatus, pokemon, move); - didSomething = didSomething || hitResult; + + hitResult = this.battle.runEvent('TryHit', target, pokemon, move); + if (!hitResult) { + if (hitResult === false) this.battle.add('-fail', target); + return false; } - if (moveData.sideCondition) { - hitResult = target.side.addSideCondition(moveData.sideCondition, pokemon, move); - didSomething = didSomething || hitResult; - } - if (moveData.weather) { - hitResult = this.field.setWeather(moveData.weather, pokemon, move); - didSomething = didSomething || hitResult; - } - if (moveData.pseudoWeather) { - hitResult = this.field.addPseudoWeather(moveData.pseudoWeather, pokemon, move); - didSomething = didSomething || hitResult; - } - if (moveData.forceSwitch) { - if (this.canSwitch(target.side)) didSomething = true; // at least defer the fail message to later - } - if (moveData.selfSwitch) { - if (this.canSwitch(pokemon.side)) didSomething = true; // at least defer the fail message to later - } - // Hit events - // These are like the TryHit events, except we don't need a FieldHit event. - // Scroll up for the TryHit event documentation, and just ignore the "Try" part. ;) - hitResult = null; - if (move.target === 'all' && !isSelf) { - if (moveData.onHitField) hitResult = this.singleEvent('HitField', moveData, {}, target, pokemon, move); - } else if ((move.target === 'foeSide' || move.target === 'allySide') && !isSelf) { - if (moveData.onHitSide) hitResult = this.singleEvent('HitSide', moveData, {}, target.side, pokemon, move); + + let accuracy = move.accuracy; + if (move.alwaysHit) { + accuracy = true; } else { - if (moveData.onHit) hitResult = this.singleEvent('Hit', moveData, {}, target, pokemon, move); - if (!isSelf && !isSecondary) { - this.runEvent('Hit', target, pokemon, move); + accuracy = this.battle.runEvent('Accuracy', target, pokemon, move, accuracy); + } + // Now, let's calculate the accuracy. + if (accuracy !== true) { + accuracy = Math.floor(accuracy * 255 / 100); + if (move.ohko) { + if (pokemon.level >= target.level) { + accuracy += (pokemon.level - target.level) * 2; + accuracy = Math.min(accuracy, 255); + } else { + this.battle.add('-immune', target, '[ohko]'); + return false; + } } - if (moveData.onAfterHit) hitResult = this.singleEvent('AfterHit', moveData, {}, target, pokemon, move); + if (!move.ignoreAccuracy) { + if (pokemon.boosts.accuracy > 0) { + accuracy *= positiveBoostTable[pokemon.boosts.accuracy]; + } else { + accuracy *= negativeBoostTable[-pokemon.boosts.accuracy]; + } + } + if (!move.ignoreEvasion) { + if (target.boosts.evasion > 0 && !move.ignorePositiveEvasion) { + accuracy *= negativeBoostTable[target.boosts.evasion]; + } else if (target.boosts.evasion < 0) { + accuracy *= positiveBoostTable[-target.boosts.evasion]; + } + } + accuracy = Math.min(Math.floor(accuracy), 255); + accuracy = Math.max(accuracy, 1); + } else { + accuracy = this.battle.runEvent('Accuracy', target, pokemon, move, accuracy); + } + accuracy = this.battle.runEvent('ModifyAccuracy', target, pokemon, move, accuracy); + if (accuracy !== true) accuracy = Math.max(accuracy, 0); + if (move.alwaysHit) { + accuracy = true; + } else { + accuracy = this.battle.runEvent('Accuracy', target, pokemon, move, accuracy); + } + if (accuracy !== true && accuracy !== 255 && !this.battle.randomChance(accuracy, 256)) { + this.battle.attrLastMove('[miss]'); + this.battle.add('-miss', pokemon); + damage = false; + return damage; + } + move.totalDamage = 0; + pokemon.lastDamage = 0; + if (move.multihit) { + let hits = move.multihit; + if (Array.isArray(hits)) { + if (hits[0] === 2 && hits[1] === 5) { + hits = this.battle.sample([2, 2, 2, 3, 3, 3, 4, 5]); + } else { + hits = this.battle.random(hits[0], hits[1] + 1); + } + } + hits = Math.floor(hits); + let nullDamage = true; + let moveDamage: number | undefined | false; + + const isSleepUsable = move.sleepUsable || this.dex.getMove(move.sourceEffect).sleepUsable; + let i: number; + for (i = 0; i < hits && target.hp && pokemon.hp; i++) { + if (pokemon.status === 'slp' && !isSleepUsable) break; + move.hit = i + 1; + if (move.hit === hits) move.lastHit = true; + moveDamage = this.moveHit(target, pokemon, move); + if (moveDamage === false) break; + if (nullDamage && (moveDamage || moveDamage === 0 || moveDamage === undefined)) nullDamage = false; + damage = (moveDamage || 0); + move.totalDamage += damage; + this.battle.eachEvent('Update'); + } + if (i === 0) return 1; + if (nullDamage) damage = false; + this.battle.add('-hitcount', target, i); + } else { + damage = this.moveHit(target, pokemon, move); + move.totalDamage = damage; + } + if (move.category !== 'Status') { + target.gotAttacked(move, damage, pokemon); + } + if (move.ohko) this.battle.add('-ohko'); + + if (!move.negateSecondary) { + this.battle.singleEvent('AfterMoveSecondary', move, null, target, pokemon, move); + this.battle.runEvent('AfterMoveSecondary', target, pokemon, move); } - if (!hitResult && !didSomething && !moveData.self && !moveData.selfdestruct) { - if (!isSelf && !isSecondary) { - if (hitResult === false || didSomething === false) this.add('-fail', target); - } - this.debug('move failed because it did nothing'); + if (move.recoil && move.totalDamage) { + this.battle.damage(this.calcRecoilDamage(move.totalDamage, move), pokemon, target, 'recoil'); + } + return damage; + }, + moveHit(target, pokemon, move, moveData, isSecondary, isSelf) { + let damage: number | false | null | undefined = undefined; + move = this.dex.getActiveMove(move); + + if (!moveData) moveData = move; + let hitResult: boolean | number | null = true; + + if (move.target === 'all' && !isSelf) { + hitResult = this.battle.singleEvent('TryHitField', moveData, {}, target, pokemon, move); + } else if ((move.target === 'foeSide' || move.target === 'allySide') && !isSelf) { + hitResult = this.battle.singleEvent('TryHitSide', moveData, {}, (target ? target.side : null), pokemon, move); + } else if (target) { + hitResult = this.battle.singleEvent('TryHit', moveData, {}, target, pokemon, move); + } + if (!hitResult) { + if (hitResult === false) this.battle.add('-fail', target); return false; } - } - if (moveData.self) { - // This is done solely to mimic in-game RNG behaviour. All self drops have a 100% chance of happening but still grab a random number. - if (!isSecondary && moveData.self.boosts) this.random(100); - this.moveHit(pokemon, pokemon, move, moveData.self, isSecondary, true); - } - // Secondary effects don't happen if the target faints from the attack - if (target?.hp && moveData.secondaries && this.runEvent('TrySecondaryHit', target, pokemon, moveData)) { - for (const secondary of moveData.secondaries) { - // We check here whether to negate the probable secondary status if it's burn or freeze. - // In the game, this is checked and if true, the random number generator is not called. - // That means that a move that does not share the type of the target can status it. - // This means tri-attack can burn fire-types and freeze ice-types. - // Unlike gen 1, though, paralysis works for all unless the target is immune to direct move (ie. ground-types and t-wave). - if (secondary.status && ['brn', 'frz'].includes(secondary.status) && target.hasType(move.type)) { - this.debug('Target immune to [' + secondary.status + ']'); - continue; + + if (target && !isSecondary && !isSelf) { + hitResult = this.battle.runEvent('TryPrimaryHit', target, pokemon, moveData); + if (hitResult === 0) { + // special Substitute flag + hitResult = true; + target = null; } - // A sleeping or frozen target cannot be flinched in Gen 2; King's Rock is exempt - if (secondary.volatileStatus === 'flinch' && ['slp', 'frz'].includes(target.status) && !secondary.kingsrock) { - this.debug('Cannot flinch a sleeping or frozen target'); - continue; + } + if (target && isSecondary && !moveData.self) { + hitResult = true; + } + if (!hitResult) { + return false; + } + + if (target) { + let didSomething: boolean | number | null = false; + damage = this.getDamage(pokemon, target, moveData); + + if ((damage || damage === 0) && !target.fainted) { + damage = this.battle.damage(damage, target, pokemon, move); + if (!(damage || damage === 0)) { + this.battle.debug('damage interrupted'); + return false; + } + didSomething = true; } - // Multi-hit moves only roll for status once - if (!move.multihit || move.lastHit) { - const effectChance = Math.floor((secondary.chance || 100) * 255 / 100); - if (typeof secondary.chance === 'undefined' || this.randomChance(effectChance, 256)) { - this.moveHit(target, pokemon, move, secondary, true, isSelf); - } else if (effectChance === 255) { - this.hint("In Gen 2, moves with a 100% secondary effect chance will not trigger in 1/256 uses."); + if (damage === false || damage === null) { + if (damage === false && !isSecondary && !isSelf) { + this.battle.add('-fail', target); + } + this.battle.debug('damage calculation interrupted'); + return false; + } + + if (moveData.boosts && !target.fainted) { + if ( + pokemon.volatiles['lockon'] && target === pokemon.volatiles['lockon'].source && + target.isSemiInvulnerable() && !isSelf + ) { + if (!isSecondary) this.battle.add('-fail', target); + return false; + } + hitResult = this.battle.boost(moveData.boosts, target, pokemon, move); + didSomething = didSomething || hitResult; + } + if (moveData.heal && !target.fainted) { + const d = target.heal(Math.round(target.maxhp * moveData.heal[0] / moveData.heal[1])); + if (!d && d !== 0) { + this.battle.add('-fail', target); + this.battle.debug('heal interrupted'); + return false; + } + this.battle.add('-heal', target, target.getHealth); + didSomething = true; + } + if (moveData.status) { + hitResult = target.trySetStatus(moveData.status, pokemon, move); + if (!hitResult && move.status) return hitResult; + didSomething = didSomething || hitResult; + } + if (moveData.forceStatus) { + hitResult = target.setStatus(moveData.forceStatus, pokemon, move); + didSomething = didSomething || hitResult; + } + if (moveData.volatileStatus) { + hitResult = target.addVolatile(moveData.volatileStatus, pokemon, move); + didSomething = didSomething || hitResult; + } + if (moveData.sideCondition) { + hitResult = target.side.addSideCondition(moveData.sideCondition, pokemon, move); + didSomething = didSomething || hitResult; + } + if (moveData.weather) { + hitResult = this.battle.field.setWeather(moveData.weather, pokemon, move); + didSomething = didSomething || hitResult; + } + if (moveData.pseudoWeather) { + hitResult = this.battle.field.addPseudoWeather(moveData.pseudoWeather, pokemon, move); + didSomething = didSomething || hitResult; + } + if (moveData.forceSwitch) { + if (this.battle.canSwitch(target.side)) didSomething = true; // at least defer the fail message to later + } + if (moveData.selfSwitch) { + if (this.battle.canSwitch(pokemon.side)) didSomething = true; // at least defer the fail message to later + } + // Hit events + // These are like the TryHit events, except we don't need a FieldHit event. + // Scroll up for the TryHit event documentation, and just ignore the "Try" part. ;) + hitResult = null; + if (move.target === 'all' && !isSelf) { + if (moveData.onHitField) hitResult = this.battle.singleEvent('HitField', moveData, {}, target, pokemon, move); + } else if ((move.target === 'foeSide' || move.target === 'allySide') && !isSelf) { + if (moveData.onHitSide) hitResult = this.battle.singleEvent('HitSide', moveData, {}, target.side, pokemon, move); + } else { + if (moveData.onHit) hitResult = this.battle.singleEvent('Hit', moveData, {}, target, pokemon, move); + if (!isSelf && !isSecondary) { + this.battle.runEvent('Hit', target, pokemon, move); + } + if (moveData.onAfterHit) hitResult = this.battle.singleEvent('AfterHit', moveData, {}, target, pokemon, move); + } + + if (!hitResult && !didSomething && !moveData.self && !moveData.selfdestruct) { + if (!isSelf && !isSecondary) { + if (hitResult === false || didSomething === false) this.battle.add('-fail', target); + } + this.battle.debug('move failed because it did nothing'); + return false; + } + } + if (moveData.self) { + // This is done solely to mimic in-game RNG behaviour. All self drops have a 100% chance of happening but still grab a random number. + if (!isSecondary && moveData.self.boosts) this.battle.random(100); + this.moveHit(pokemon, pokemon, move, moveData.self, isSecondary, true); + } + // Secondary effects don't happen if the target faints from the attack + if (target?.hp && moveData.secondaries && this.battle.runEvent('TrySecondaryHit', target, pokemon, moveData)) { + for (const secondary of moveData.secondaries) { + // We check here whether to negate the probable secondary status if it's burn or freeze. + // In the game, this is checked and if true, the random number generator is not called. + // That means that a move that does not share the type of the target can status it. + // This means tri-attack can burn fire-types and freeze ice-types. + // Unlike gen 1, though, paralysis works for all unless the target is immune to direct move (ie. ground-types and t-wave). + if (secondary.status && ['brn', 'frz'].includes(secondary.status) && target.hasType(move.type)) { + this.battle.debug('Target immune to [' + secondary.status + ']'); + continue; + } + // A sleeping or frozen target cannot be flinched in Gen 2; King's Rock is exempt + if (secondary.volatileStatus === 'flinch' && ['slp', 'frz'].includes(target.status) && !secondary.kingsrock) { + this.battle.debug('Cannot flinch a sleeping or frozen target'); + continue; + } + // Multi-hit moves only roll for status once + if (!move.multihit || move.lastHit) { + const effectChance = Math.floor((secondary.chance || 100) * 255 / 100); + if (typeof secondary.chance === 'undefined' || this.battle.randomChance(effectChance, 256)) { + this.moveHit(target, pokemon, move, secondary, true, isSelf); + } else if (effectChance === 255) { + this.battle.hint("In Gen 2, moves with a 100% secondary effect chance will not trigger in 1/256 uses."); + } } } } - } - if (target && target.hp > 0 && pokemon.hp > 0 && moveData.forceSwitch && this.canSwitch(target.side)) { - hitResult = this.runEvent('DragOut', target, pokemon, move); - if (hitResult) { - this.dragIn(target.side, target.position); - } else if (hitResult === false) { - this.add('-fail', target); + if (target && target.hp > 0 && pokemon.hp > 0 && moveData.forceSwitch && this.battle.canSwitch(target.side)) { + hitResult = this.battle.runEvent('DragOut', target, pokemon, move); + if (hitResult) { + this.dragIn(target.side, target.position); + } else if (hitResult === false) { + this.battle.add('-fail', target); + } } - } - if (move.selfSwitch && pokemon.hp) { - pokemon.switchFlag = move.id; - } - return damage; - }, - getDamage(pokemon, target, move, suppressMessages) { - // First of all, we get the move. - if (typeof move === 'string') { - move = this.dex.getActiveMove(move); - } else if (typeof move === 'number') { - move = { - basePower: move, - type: '???', - category: 'Physical', - willCrit: false, - flags: {}, - } as unknown as ActiveMove; - } - - // Let's test for immunities. - if (!move.ignoreImmunity || (move.ignoreImmunity !== true && !move.ignoreImmunity[move.type])) { - if (!target.runImmunity(move.type, true)) { - return false; + if (move.selfSwitch && pokemon.hp) { + pokemon.switchFlag = move.id; } - } - - // Is it an OHKO move? - if (move.ohko) { - return target.maxhp; - } - - // We edit the damage through move's damage callback - if (move.damageCallback) { - return move.damageCallback.call(this, pokemon, target); - } - - // We take damage from damage=level moves - if (move.damage === 'level') { - return pokemon.level; - } - - // If there's a fix move damage, we run it - if (move.damage) { - return move.damage; - } - - // We check the category and typing to calculate later on the damage - move.category = this.getCategory(move); - if (!move.defensiveCategory) move.defensiveCategory = move.category; - // '???' is typeless damage: used for Struggle and Confusion etc - if (!move.type) move.type = '???'; - const type = move.type; - - // We get the base power and apply basePowerCallback if necessary - let basePower: number | false | null | undefined = move.basePower; - if (move.basePowerCallback) { - basePower = move.basePowerCallback.call(this, pokemon, target, move); - } - - // We check for Base Power - if (!basePower) { - if (basePower === 0) return; // Returning undefined means not dealing damage - return basePower; - } - basePower = this.clampIntRange(basePower, 1); - - // Checking for the move's Critical Hit ratio - let critRatio = this.runEvent('ModifyCritRatio', pokemon, target, move, move.critRatio || 0); - critRatio = this.clampIntRange(critRatio, 0, 5); - const critMult = [0, 16, 8, 4, 3, 2]; - let isCrit = move.willCrit || false; - if (typeof move.willCrit === 'undefined') { - if (critRatio) { - isCrit = this.randomChance(1, critMult[critRatio]); + return damage; + }, + getDamage(pokemon, target, move, suppressMessages) { + // First of all, we get the move. + if (typeof move === 'string') { + move = this.dex.getActiveMove(move); + } else if (typeof move === 'number') { + move = { + basePower: move, + type: '???', + category: 'Physical', + willCrit: false, + flags: {}, + } as unknown as ActiveMove; } - } - if (isCrit && this.runEvent('CriticalHit', target, null, move)) { - target.getMoveHitData(move).crit = true; - } - - // Happens after crit calculation - if (basePower) { - // confusion damage - if (move.isConfusionSelfHit) { - move.type = move.baseMoveType!; - basePower = this.runEvent('BasePower', pokemon, target, move, basePower, true); - move.type = '???'; - } else { - basePower = this.runEvent('BasePower', pokemon, target, move, basePower, true); + // Let's test for immunities. + if (!move.ignoreImmunity || (move.ignoreImmunity !== true && !move.ignoreImmunity[move.type])) { + if (!target.runImmunity(move.type, true)) { + return false; + } } - if (basePower && move.basePowerModifier) { - basePower *= move.basePowerModifier; + + // Is it an OHKO move? + if (move.ohko) { + return target.maxhp; } - } - if (!basePower) return 0; - basePower = this.clampIntRange(basePower, 1); - // We now check for attacker and defender - let level = pokemon.level; - - // Using Beat Up - if (move.allies) { - this.add('-activate', pokemon, 'move: Beat Up', '[of] ' + move.allies[0].name); - level = move.allies[0].level; - } - - let attacker = pokemon; - const defender = target; - if (move.useTargetOffensive) attacker = target; - let atkType: StatNameExceptHP = (move.category === 'Physical') ? 'atk' : 'spa'; - const defType: StatNameExceptHP = (move.defensiveCategory === 'Physical') ? 'def' : 'spd'; - if (move.useSourceDefensiveAsOffensive) atkType = defType; - let unboosted = false; - let noburndrop = false; - - if (isCrit) { - if (!suppressMessages) this.add('-crit', target); - // Stat level modifications are ignored if they are neutral to or favour the defender. - // Reflect and Light Screen defensive boosts are only ignored if stat level modifications were also ignored as a result of that. - if (attacker.boosts[atkType] <= defender.boosts[defType]) { - unboosted = true; - noburndrop = true; + // We edit the damage through move's damage callback + if (move.damageCallback) { + return move.damageCallback.call(this.battle, pokemon, target); } - } - // Get stats now. - let attack = attacker.getStat(atkType, unboosted, noburndrop); - let defense = defender.getStat(defType, unboosted); - // Using Beat Up - if (move.allies) { - attack = move.allies[0].species.baseStats.atk; - move.allies.shift(); - defense = defender.species.baseStats.def; - } - - // Moves that ignore offense and defense respectively. - if (move.ignoreOffensive) { - this.debug('Negating (sp)atk boost/penalty.'); - // The attack drop from the burn is only applied when attacker's attack level is higher than defender's defense level. - attack = attacker.getStat(atkType, true, true); - } - if (move.ignoreDefensive) { - this.debug('Negating (sp)def boost/penalty.'); - defense = target.getStat(defType, true, true); - } - - if (move.id === 'present') { - const typeIndexes: {[k: string]: number} = { - Normal: 0, Fighting: 1, Flying: 2, Poison: 3, Ground: 4, Rock: 5, Bug: 7, Ghost: 8, Steel: 9, - Fire: 20, Water: 21, Grass: 22, Electric: 23, Psychic: 24, Ice: 25, Dragon: 26, Dark: 27, - }; - attack = 10; - - const attackerLastType = attacker.getTypes().slice(-1)[0]; - const defenderLastType = defender.getTypes().slice(-1)[0]; - - defense = typeIndexes[attackerLastType] || 1; - level = typeIndexes[defenderLastType] || 1; - this.hint("Gen 2 Present has a glitched damage calculation using the secondary types of the Pokemon for the Attacker's Level and Defender's Defense.", true); - } - - // When either attack or defense are higher than 256, they are both divided by 4 and modded by 256. - // This is what causes the rollover bugs. - if (attack >= 256 || defense >= 256) { - if (attack >= 1024 || defense >= 1024) { - this.hint("In Gen 2, a stat will roll over to a small number if it is larger than 1024."); + // We take damage from damage=level moves + if (move.damage === 'level') { + return pokemon.level; } - attack = this.clampIntRange(Math.floor(attack / 4) % 256, 1); - defense = this.clampIntRange(Math.floor(defense / 4) % 256, 1); - } - // Self destruct moves halve defense at this point. - if (move.selfdestruct && defType === 'def') { - defense = this.clampIntRange(Math.floor(defense / 2), 1); - } - - // Let's go with the calculation now that we have what we need. - // We do it step by step just like the game does. - let damage = level * 2; - damage = Math.floor(damage / 5); - damage += 2; - damage *= basePower; - damage *= attack; - damage = Math.floor(damage / defense); - damage = Math.floor(damage / 50); - if (isCrit) damage *= 2; - damage = Math.floor(this.runEvent('ModifyDamage', attacker, defender, move, damage)); - damage = this.clampIntRange(damage, 1, 997); - damage += 2; - - // Weather modifiers - if ((this.field.isWeather('raindance') && type === 'Water') || (this.field.isWeather('sunnyday') && type === 'Fire')) { - damage = Math.floor(damage * 1.5); - } else if ( - (this.field.isWeather('raindance') && (type === 'Fire' || move.id === 'solarbeam')) || - (this.field.isWeather('sunnyday') && type === 'Water') - ) { - damage = Math.floor(damage / 2); - } - - // STAB damage bonus, the "???" type never gets STAB - if (type !== '???' && pokemon.hasType(type)) { - damage += Math.floor(damage / 2); - } - - // Type effectiveness - const totalTypeMod = target.runEffectiveness(move); - // Super effective attack - if (totalTypeMod > 0) { - if (!suppressMessages) this.add('-supereffective', target); - damage *= 2; - if (totalTypeMod >= 2) { - damage *= 2; + // If there's a fix move damage, we run it + if (move.damage) { + return move.damage; } - } - // Resisted attack - if (totalTypeMod < 0) { - if (!suppressMessages) this.add('-resisted', target); - damage = Math.floor(damage / 2); - if (totalTypeMod <= -2) { + + // We check the category and typing to calculate later on the damage + move.category = this.battle.getCategory(move); + if (!move.defensiveCategory) move.defensiveCategory = move.category; + // '???' is typeless damage: used for Struggle and Confusion etc + if (!move.type) move.type = '???'; + const type = move.type; + + // We get the base power and apply basePowerCallback if necessary + let basePower: number | false | null | undefined = move.basePower; + if (move.basePowerCallback) { + basePower = move.basePowerCallback.call(this.battle, pokemon, target, move); + } + + // We check for Base Power + if (!basePower) { + if (basePower === 0) return; // Returning undefined means not dealing damage + return basePower; + } + basePower = this.battle.clampIntRange(basePower, 1); + + // Checking for the move's Critical Hit ratio + let critRatio = this.battle.runEvent('ModifyCritRatio', pokemon, target, move, move.critRatio || 0); + critRatio = this.battle.clampIntRange(critRatio, 0, 5); + const critMult = [0, 16, 8, 4, 3, 2]; + let isCrit = move.willCrit || false; + if (typeof move.willCrit === 'undefined') { + if (critRatio) { + isCrit = this.battle.randomChance(1, critMult[critRatio]); + } + } + + if (isCrit && this.battle.runEvent('CriticalHit', target, null, move)) { + target.getMoveHitData(move).crit = true; + } + + // Happens after crit calculation + if (basePower) { + // confusion damage + if (move.isConfusionSelfHit) { + move.type = move.baseMoveType!; + basePower = this.battle.runEvent('BasePower', pokemon, target, move, basePower, true); + move.type = '???'; + } else { + basePower = this.battle.runEvent('BasePower', pokemon, target, move, basePower, true); + } + if (basePower && move.basePowerModifier) { + basePower *= move.basePowerModifier; + } + } + if (!basePower) return 0; + basePower = this.battle.clampIntRange(basePower, 1); + + // We now check for attacker and defender + let level = pokemon.level; + + // Using Beat Up + if (move.allies) { + this.battle.add('-activate', pokemon, 'move: Beat Up', '[of] ' + move.allies[0].name); + level = move.allies[0].level; + } + + let attacker = pokemon; + const defender = target; + if (move.useTargetOffensive) attacker = target; + let atkType: StatNameExceptHP = (move.category === 'Physical') ? 'atk' : 'spa'; + const defType: StatNameExceptHP = (move.defensiveCategory === 'Physical') ? 'def' : 'spd'; + if (move.useSourceDefensiveAsOffensive) atkType = defType; + let unboosted = false; + let noburndrop = false; + + if (isCrit) { + if (!suppressMessages) this.battle.add('-crit', target); + // Stat level modifications are ignored if they are neutral to or favour the defender. + // Reflect and Light Screen defensive boosts are only ignored if stat level modifications were also ignored as a result of that. + if (attacker.boosts[atkType] <= defender.boosts[defType]) { + unboosted = true; + noburndrop = true; + } + } + // Get stats now. + let attack = attacker.getStat(atkType, unboosted, noburndrop); + let defense = defender.getStat(defType, unboosted); + + // Using Beat Up + if (move.allies) { + attack = move.allies[0].species.baseStats.atk; + move.allies.shift(); + defense = defender.species.baseStats.def; + } + + // Moves that ignore offense and defense respectively. + if (move.ignoreOffensive) { + this.battle.debug('Negating (sp)atk boost/penalty.'); + // The attack drop from the burn is only applied when attacker's attack level is higher than defender's defense level. + attack = attacker.getStat(atkType, true, true); + } + if (move.ignoreDefensive) { + this.battle.debug('Negating (sp)def boost/penalty.'); + defense = target.getStat(defType, true, true); + } + + if (move.id === 'present') { + const typeIndexes: {[k: string]: number} = { + Normal: 0, Fighting: 1, Flying: 2, Poison: 3, Ground: 4, Rock: 5, Bug: 7, Ghost: 8, Steel: 9, + Fire: 20, Water: 21, Grass: 22, Electric: 23, Psychic: 24, Ice: 25, Dragon: 26, Dark: 27, + }; + attack = 10; + + const attackerLastType = attacker.getTypes().slice(-1)[0]; + const defenderLastType = defender.getTypes().slice(-1)[0]; + + defense = typeIndexes[attackerLastType] || 1; + level = typeIndexes[defenderLastType] || 1; + this.battle.hint("Gen 2 Present has a glitched damage calculation using the secondary types of the Pokemon for the Attacker's Level and Defender's Defense.", true); + } + + // When either attack or defense are higher than 256, they are both divided by 4 and modded by 256. + // This is what causes the rollover bugs. + if (attack >= 256 || defense >= 256) { + if (attack >= 1024 || defense >= 1024) { + this.battle.hint("In Gen 2, a stat will roll over to a small number if it is larger than 1024."); + } + attack = this.battle.clampIntRange(Math.floor(attack / 4) % 256, 1); + defense = this.battle.clampIntRange(Math.floor(defense / 4) % 256, 1); + } + + // Self destruct moves halve defense at this point. + if (move.selfdestruct && defType === 'def') { + defense = this.battle.clampIntRange(Math.floor(defense / 2), 1); + } + + // Let's go with the calculation now that we have what we need. + // We do it step by step just like the game does. + let damage = level * 2; + damage = Math.floor(damage / 5); + damage += 2; + damage *= basePower; + damage *= attack; + damage = Math.floor(damage / defense); + damage = Math.floor(damage / 50); + if (isCrit) damage *= 2; + damage = Math.floor(this.battle.runEvent('ModifyDamage', attacker, defender, move, damage)); + damage = this.battle.clampIntRange(damage, 1, 997); + damage += 2; + + // Weather modifiers + if ( + (type === 'Water' && this.battle.field.isWeather('raindance')) || + (type === 'Fire' && this.battle.field.isWeather('sunnyday')) + ) { + damage = Math.floor(damage * 1.5); + } else if ( + ((type === 'Fire' || move.id === 'solarbeam') && this.battle.field.isWeather('raindance')) || + (type === 'Water' && this.battle.field.isWeather('sunnyday')) + ) { damage = Math.floor(damage / 2); } - } - // Apply random factor is damage is greater than 1, except for Flail and Reversal - if (!move.noDamageVariance && damage > 1) { - damage *= this.random(217, 256); - damage = Math.floor(damage / 255); - } + // STAB damage bonus, the "???" type never gets STAB + if (type !== '???' && pokemon.hasType(type)) { + damage += Math.floor(damage / 2); + } - // If damage is less than 1, we return 1 - if (basePower && !Math.floor(damage)) { - return 1; - } + // Type effectiveness + const totalTypeMod = target.runEffectiveness(move); + // Super effective attack + if (totalTypeMod > 0) { + if (!suppressMessages) this.battle.add('-supereffective', target); + damage *= 2; + if (totalTypeMod >= 2) { + damage *= 2; + } + } + // Resisted attack + if (totalTypeMod < 0) { + if (!suppressMessages) this.battle.add('-resisted', target); + damage = Math.floor(damage / 2); + if (totalTypeMod <= -2) { + damage = Math.floor(damage / 2); + } + } - // We are done, this is the final damage - return damage; + // Apply random factor is damage is greater than 1, except for Flail and Reversal + if (!move.noDamageVariance && damage > 1) { + damage *= this.battle.random(217, 256); + damage = Math.floor(damage / 255); + } + + // If damage is less than 1, we return 1 + if (basePower && !Math.floor(damage)) { + return 1; + } + + // We are done, this is the final damage + return damage; + }, }, }; diff --git a/data/mods/gen3/abilities.ts b/data/mods/gen3/abilities.ts index ec052b2fd0..f9faaeee20 100644 --- a/data/mods/gen3/abilities.ts +++ b/data/mods/gen3/abilities.ts @@ -55,8 +55,8 @@ export const Abilities: {[k: string]: ModdedAbilityData} = { inherit: true, onStart(pokemon) { let activated = false; - for (const target of pokemon.side.foe.active) { - if (target && this.isAdjacent(target, pokemon) && !target.volatiles['substitute']) { + for (const target of pokemon.adjacentFoes()) { + if (!target.volatiles['substitute']) { activated = true; break; } @@ -68,9 +68,7 @@ export const Abilities: {[k: string]: ModdedAbilityData} = { } this.add('-ability', pokemon, 'Intimidate', 'boost'); - for (const target of pokemon.side.foe.active) { - if (!target || !this.isAdjacent(target, pokemon)) continue; - + for (const target of pokemon.adjacentFoes()) { if (target.volatiles['substitute']) { this.add('-immune', target); } else { diff --git a/data/mods/gen3/moves.ts b/data/mods/gen3/moves.ts index a1970c626f..299c46fcfa 100644 --- a/data/mods/gen3/moves.ts +++ b/data/mods/gen3/moves.ts @@ -75,7 +75,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { effectType: 'Move', type: 'Normal', } as unknown as ActiveMove; - this.tryMoveHit(target, pokemon, moveData); + this.actions.tryMoveHit(target, pokemon, moveData); return false; } this.add('-activate', pokemon, 'move: Bide'); @@ -224,7 +224,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { willCrit: false, type: '???', } as unknown as ActiveMove; - const damage = this.getDamage(source, target, moveData, true); + const damage = this.actions.getDamage(source, target, moveData, true); Object.assign(target.side.slotConditions[target.position]['futuremove'], { duration: 3, move: 'doomdesire', @@ -353,7 +353,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { basePower: 85, onMoveFail(target, source, move) { if (target.runImmunity('Fighting')) { - const damage = this.getDamage(source, target, move, true); + const damage = this.actions.getDamage(source, target, move, true); if (typeof damage !== 'number') throw new Error("HJK recoil failed"); this.damage(this.clampIntRange(damage / 2, 1, Math.floor(target.maxhp / 2)), source, source, move); } @@ -368,7 +368,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { basePower: 70, onMoveFail(target, source, move) { if (target.runImmunity('Fighting')) { - const damage = this.getDamage(source, target, move, true); + const damage = this.actions.getDamage(source, target, move, true); if (typeof damage !== 'number') throw new Error("Jump Kick didn't recoil"); this.damage(this.clampIntRange(damage / 2, 1, Math.floor(target.maxhp / 2)), source, source, move); } @@ -433,7 +433,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { if (noMirror.includes(lastAttackedBy.move) || !lastAttackedBy.source.hasMove(lastAttackedBy.move)) { return false; } - this.useMove(lastAttackedBy.move, pokemon); + this.actions.useMove(lastAttackedBy.move, pokemon); }, target: "self", }, @@ -441,7 +441,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { inherit: true, accuracy: 95, onHit(target) { - this.useMove('swift', target); + this.actions.useMove('swift', target); }, }, needlearm: { @@ -513,7 +513,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { this.add('cant', pokemon, 'nopp', randomMove.move); return; } - this.useMove(randomMove.move, pokemon); + this.actions.useMove(randomMove.move, pokemon); }, }, spiderweb: { diff --git a/data/mods/gen3/scripts.ts b/data/mods/gen3/scripts.ts index 6c87756c4a..3ddb0786b3 100644 --- a/data/mods/gen3/scripts.ts +++ b/data/mods/gen3/scripts.ts @@ -13,449 +13,452 @@ export const Scripts: ModdedBattleScriptsData = { } } }, - modifyDamage(baseDamage, pokemon, target, move, suppressMessages = false) { - // RSE divides modifiers into several mathematically important stages - // The modifiers run earlier than other generations are called with ModifyDamagePhase1 and ModifyDamagePhase2 + actions: { + inherit: true, + modifyDamage(baseDamage, pokemon, target, move, suppressMessages = false) { + // RSE divides modifiers into several mathematically important stages + // The modifiers run earlier than other generations are called with ModifyDamagePhase1 and ModifyDamagePhase2 - if (!move.type) move.type = '???'; - const type = move.type; + if (!move.type) move.type = '???'; + const type = move.type; - // Burn - if (pokemon.status === 'brn' && baseDamage && move.category === 'Physical' && !pokemon.hasAbility('guts')) { - baseDamage = this.modify(baseDamage, 0.5); - } - - // Other modifiers (Reflect/Light Screen/etc) - baseDamage = this.runEvent('ModifyDamagePhase1', pokemon, target, move, baseDamage); - - // Double battle multi-hit - // In Generation 3, the spread move modifier is 0.5x instead of 0.75x. Moves that hit both foes - // and the user's ally, like Earthquake and Explosion, don't get affected by spread modifiers - if (move.spreadHit && move.target === 'allAdjacentFoes') { - const spreadModifier = move.spreadModifier || 0.5; - this.debug('Spread modifier: ' + spreadModifier); - baseDamage = this.modify(baseDamage, spreadModifier); - } - - // Weather - baseDamage = this.runEvent('WeatherModifyDamage', pokemon, target, move, baseDamage); - - if (move.category === 'Physical' && !Math.floor(baseDamage)) { - baseDamage = 1; - } - - baseDamage += 2; - - const isCrit = target.getMoveHitData(move).crit; - if (isCrit) { - baseDamage = this.modify(baseDamage, move.critModifier || 2); - } - - // Mod 2 (Damage is floored after all multipliers are in) - baseDamage = Math.floor(this.runEvent('ModifyDamagePhase2', pokemon, target, move, baseDamage)); - - // this is not a modifier - baseDamage = this.randomizer(baseDamage); - - // STAB - if (move.forceSTAB || type !== '???' && pokemon.hasType(type)) { - // The "???" type never gets STAB - // Not even if you Roost in Gen 4 and somehow manage to use - // Struggle in the same turn. - // (On second thought, it might be easier to get a MissingNo.) - baseDamage = this.modify(baseDamage, move.stab || 1.5); - } - // types - let typeMod = target.runEffectiveness(move); - typeMod = this.clampIntRange(typeMod, -6, 6); - target.getMoveHitData(move).typeMod = typeMod; - if (typeMod > 0) { - if (!suppressMessages) this.add('-supereffective', target); - - for (let i = 0; i < typeMod; i++) { - baseDamage *= 2; + // Burn + if (pokemon.status === 'brn' && baseDamage && move.category === 'Physical' && !pokemon.hasAbility('guts')) { + baseDamage = this.battle.modify(baseDamage, 0.5); } - } - if (typeMod < 0) { - if (!suppressMessages) this.add('-resisted', target); - for (let i = 0; i > typeMod; i--) { - baseDamage = Math.floor(baseDamage / 2); + // Other modifiers (Reflect/Light Screen/etc) + baseDamage = this.battle.runEvent('ModifyDamagePhase1', pokemon, target, move, baseDamage); + + // Double battle multi-hit + // In Generation 3, the spread move modifier is 0.5x instead of 0.75x. Moves that hit both foes + // and the user's ally, like Earthquake and Explosion, don't get affected by spread modifiers + if (move.spreadHit && move.target === 'allAdjacentFoes') { + const spreadModifier = move.spreadModifier || 0.5; + this.battle.debug('Spread modifier: ' + spreadModifier); + baseDamage = this.battle.modify(baseDamage, spreadModifier); } - } - if (isCrit && !suppressMessages) this.add('-crit', target); + // Weather + baseDamage = this.battle.runEvent('WeatherModifyDamage', pokemon, target, move, baseDamage); - // Final modifier. - baseDamage = this.runEvent('ModifyDamage', pokemon, target, move, baseDamage); + if (move.category === 'Physical' && !Math.floor(baseDamage)) { + baseDamage = 1; + } - if (!Math.floor(baseDamage)) { - return 1; - } + baseDamage += 2; - return Math.floor(baseDamage); - }, - useMoveInner(moveOrMoveName, pokemon, target, sourceEffect, zMove) { - if (!sourceEffect && this.effect.id) sourceEffect = this.effect; - if (sourceEffect && sourceEffect.id === 'instruct') sourceEffect = null; + const isCrit = target.getMoveHitData(move).crit; + if (isCrit) { + baseDamage = this.battle.modify(baseDamage, move.critModifier || 2); + } - let move = this.dex.getActiveMove(moveOrMoveName); - pokemon.lastMoveUsed = move; + // Mod 2 (Damage is floored after all multipliers are in) + baseDamage = Math.floor(this.battle.runEvent('ModifyDamagePhase2', pokemon, target, move, baseDamage)); - if (this.activeMove) { - move.priority = this.activeMove.priority; - } - const baseTarget = move.target; - if (target === undefined) target = this.getRandomTarget(pokemon, move); - if (move.target === 'self' || move.target === 'allies') { - target = pokemon; - } - if (sourceEffect) { - move.sourceEffect = sourceEffect.id; - move.ignoreAbility = false; - } - let moveResult = false; + // this is not a modifier + baseDamage = this.battle.randomizer(baseDamage); - this.setActiveMove(move, pokemon, target); + // STAB + if (move.forceSTAB || type !== '???' && pokemon.hasType(type)) { + // The "???" type never gets STAB + // Not even if you Roost in Gen 4 and somehow manage to use + // Struggle in the same turn. + // (On second thought, it might be easier to get a MissingNo.) + baseDamage = this.battle.modify(baseDamage, move.stab || 1.5); + } + // types + let typeMod = target.runEffectiveness(move); + typeMod = this.battle.clampIntRange(typeMod, -6, 6); + target.getMoveHitData(move).typeMod = typeMod; + if (typeMod > 0) { + if (!suppressMessages) this.battle.add('-supereffective', target); - this.singleEvent('ModifyMove', move, null, pokemon, target, move, move); - if (baseTarget !== move.target) { - // Target changed in ModifyMove, so we must adjust it here - // Adjust before the next event so the correct target is passed to the - // event - target = this.getRandomTarget(pokemon, move); - } - move = this.runEvent('ModifyMove', pokemon, target, move, move); - if (baseTarget !== move.target) { - // Adjust again - target = this.getRandomTarget(pokemon, move); - } - if (!move || pokemon.fainted) { - return false; - } - - let attrs = ''; - - let movename = move.name; - if (move.id === 'hiddenpower') movename = 'Hidden Power'; - if (sourceEffect) attrs += `|[from]${this.dex.getEffect(sourceEffect)}`; - this.addMove('move', pokemon, movename, target + attrs); - - if (!target) { - this.attrLastMove('[notarget]'); - this.add('-notarget', pokemon); - return false; - } - - const {targets, pressureTargets} = pokemon.getMoveTargets(move, target); - - if (!sourceEffect || sourceEffect.id === 'pursuit') { - let extraPP = 0; - for (const source of pressureTargets) { - const ppDrop = this.runEvent('DeductPP', source, pokemon, move); - if (ppDrop !== true) { - extraPP += ppDrop || 0; + for (let i = 0; i < typeMod; i++) { + baseDamage *= 2; } } - if (extraPP > 0) { - pokemon.deductPP(move, extraPP); + if (typeMod < 0) { + if (!suppressMessages) this.battle.add('-resisted', target); + + for (let i = 0; i > typeMod; i--) { + baseDamage = Math.floor(baseDamage / 2); + } } - } - if (!this.singleEvent('TryMove', move, null, pokemon, target, move) || - !this.runEvent('TryMove', pokemon, target, move)) { - move.mindBlownRecoil = false; - return false; - } + if (isCrit && !suppressMessages) this.battle.add('-crit', target); - this.singleEvent('UseMoveMessage', move, null, pokemon, target, move); + // Final modifier. + baseDamage = this.battle.runEvent('ModifyDamage', pokemon, target, move, baseDamage); - if (move.ignoreImmunity === undefined) { - move.ignoreImmunity = (move.category === 'Status'); - } + if (!Math.floor(baseDamage)) { + return 1; + } - if (move.selfdestruct === 'always') { - this.faint(pokemon, pokemon, move); - } + return Math.floor(baseDamage); + }, + useMoveInner(moveOrMoveName, pokemon, target, sourceEffect, zMove) { + if (!sourceEffect && this.battle.effect.id) sourceEffect = this.battle.effect; + if (sourceEffect && sourceEffect.id === 'instruct') sourceEffect = null; - let damage: number | false | undefined | '' = false; - if (move.target === 'all' || move.target === 'foeSide' || move.target === 'allySide' || move.target === 'allyTeam') { - damage = this.tryMoveHit(target, pokemon, move); - if (damage === this.NOT_FAIL) pokemon.moveThisTurnResult = null; - if (damage || damage === 0 || damage === undefined) moveResult = true; - } else if (move.target === 'allAdjacent' || move.target === 'allAdjacentFoes') { - if (!targets.length) { - this.attrLastMove('[notarget]'); - this.add('-notarget', pokemon); + let move = this.dex.getActiveMove(moveOrMoveName); + pokemon.lastMoveUsed = move; + + if (this.battle.activeMove) { + move.priority = this.battle.activeMove.priority; + } + const baseTarget = move.target; + if (target === undefined) target = this.battle.getRandomTarget(pokemon, move); + if (move.target === 'self' || move.target === 'allies') { + target = pokemon; + } + if (sourceEffect) { + move.sourceEffect = sourceEffect.id; + move.ignoreAbility = false; + } + let moveResult = false; + + this.battle.setActiveMove(move, pokemon, target); + + this.battle.singleEvent('ModifyMove', move, null, pokemon, target, move, move); + if (baseTarget !== move.target) { + // Target changed in ModifyMove, so we must adjust it here + // Adjust before the next event so the correct target is passed to the + // event + target = this.battle.getRandomTarget(pokemon, move); + } + move = this.battle.runEvent('ModifyMove', pokemon, target, move, move); + if (baseTarget !== move.target) { + // Adjust again + target = this.battle.getRandomTarget(pokemon, move); + } + if (!move || pokemon.fainted) { return false; } - if (targets.length > 1) move.spreadHit = true; - const hitSlots = []; - for (const source of targets) { - const hitResult = this.tryMoveHit(source, pokemon, move); - if (hitResult || hitResult === 0 || hitResult === undefined) { - moveResult = true; - hitSlots.push(source.getSlot()); - } - if (damage) { - damage += hitResult || 0; - } else { - if (damage !== false || hitResult !== this.NOT_FAIL) damage = hitResult; - } - if (damage === this.NOT_FAIL) pokemon.moveThisTurnResult = null; - } - if (move.spreadHit) this.attrLastMove('[spread] ' + hitSlots.join(',')); - } else { - target = targets[0]; - let lacksTarget = !target || target.fainted; - if (!lacksTarget) { - if (['adjacentFoe', 'adjacentAlly', 'normal', 'randomNormal'].includes(move.target)) { - lacksTarget = !this.isAdjacent(target, pokemon); - } - } - if (lacksTarget && !move.isFutureMove) { - this.attrLastMove('[notarget]'); - this.add('-notarget', pokemon); + + let attrs = ''; + + let movename = move.name; + if (move.id === 'hiddenpower') movename = 'Hidden Power'; + if (sourceEffect) attrs += `|[from]${this.dex.getEffect(sourceEffect)}`; + this.battle.addMove('move', pokemon, movename, target + attrs); + + if (!target) { + this.battle.attrLastMove('[notarget]'); + this.battle.add('-notarget', pokemon); return false; } - damage = this.tryMoveHit(target, pokemon, move); - if (damage === this.NOT_FAIL) pokemon.moveThisTurnResult = null; - if (damage || damage === 0 || damage === undefined) moveResult = true; - } - if (move.selfBoost && moveResult) this.moveHit(pokemon, pokemon, move, move.selfBoost, false, true); - if (!pokemon.hp) { - this.faint(pokemon, pokemon, move); - } - if (!moveResult) { - this.singleEvent('MoveFail', move, null, target, pokemon, move); - return false; - } + const {targets, pressureTargets} = pokemon.getMoveTargets(move, target); - if (!move.negateSecondary && !(move.hasSheerForce && pokemon.hasAbility('sheerforce'))) { - this.singleEvent('AfterMoveSecondarySelf', move, null, pokemon, target, move); - this.runEvent('AfterMoveSecondarySelf', pokemon, target, move); - } - return true; - }, - tryMoveHit(target, pokemon, move) { - this.setActiveMove(move, pokemon, target); - let naturalImmunity = false; - let accPass = true; - - let hitResult = this.singleEvent('PrepareHit', move, {}, target, pokemon, move) && - this.runEvent('PrepareHit', pokemon, target, move); - if (!hitResult) { - if (hitResult === false) { - this.add('-fail', pokemon); - this.attrLastMove('[still]'); - } - return false; - } - - if (!this.singleEvent('Try', move, null, pokemon, target, move)) { - return false; - } - - if (move.target === 'all' || move.target === 'foeSide' || move.target === 'allySide' || move.target === 'allyTeam') { - if (move.target === 'all') { - hitResult = this.runEvent('TryHitField', target, pokemon, move); - } else { - hitResult = this.runEvent('TryHitSide', target, pokemon, move); - } - if (!hitResult) { - if (hitResult === false) { - this.add('-fail', pokemon); - this.attrLastMove('[still]'); + if (!sourceEffect || sourceEffect.id === 'pursuit') { + let extraPP = 0; + for (const source of pressureTargets) { + const ppDrop = this.battle.runEvent('DeductPP', source, pokemon, move); + if (ppDrop !== true) { + extraPP += ppDrop || 0; + } } + if (extraPP > 0) { + pokemon.deductPP(move, extraPP); + } + } + + if (!this.battle.singleEvent('TryMove', move, null, pokemon, target, move) || + !this.battle.runEvent('TryMove', pokemon, target, move)) { + move.mindBlownRecoil = false; return false; } - return this.moveHit(target, pokemon, move); - } - hitResult = this.runEvent('Invulnerability', target, pokemon, move); - if (hitResult === false) { - if (!move.spreadHit) this.attrLastMove('[miss]'); - this.add('-miss', pokemon, target); - return false; - } + this.battle.singleEvent('UseMoveMessage', move, null, pokemon, target, move); - if (move.ignoreImmunity === undefined) { - move.ignoreImmunity = (move.category === 'Status'); - } - - if ( - (!move.ignoreImmunity || (move.ignoreImmunity !== true && !move.ignoreImmunity[move.type])) && - !target.runImmunity(move.type) - ) { - naturalImmunity = true; - } else { - hitResult = this.singleEvent('TryImmunity', move, {}, target, pokemon, move); - if (hitResult === false) { - naturalImmunity = true; + if (move.ignoreImmunity === undefined) { + move.ignoreImmunity = (move.category === 'Status'); } - } - const boostTable = [1, 4 / 3, 5 / 3, 2, 7 / 3, 8 / 3, 3]; + if (move.selfdestruct === 'always') { + this.battle.faint(pokemon, pokemon, move); + } - // calculate true accuracy - let accuracy = move.accuracy; - let boosts: SparseBoostsTable = {}; - let boost: number; - if (accuracy !== true) { - if (!move.ignoreAccuracy) { - boosts = this.runEvent('ModifyBoost', pokemon, null, null, {...pokemon.boosts}); - boost = this.clampIntRange(boosts['accuracy'], -6, 6); - if (boost > 0) { - accuracy *= boostTable[boost]; - } else { - accuracy /= boostTable[-boost]; - } - } - if (!move.ignoreEvasion) { - boosts = this.runEvent('ModifyBoost', target, null, null, {...target.boosts}); - boost = this.clampIntRange(boosts['evasion'], -6, 6); - if (boost > 0) { - accuracy /= boostTable[boost]; - } else if (boost < 0) { - accuracy *= boostTable[-boost]; - } - } - } - if (move.ohko) { // bypasses accuracy modifiers - if (!target.isSemiInvulnerable()) { - accuracy = 30; - if (pokemon.level >= target.level && (move.ohko === true || !target.hasType(move.ohko))) { - accuracy += (pokemon.level - target.level); - } else { - this.add('-immune', target, '[ohko]'); + let damage: number | false | undefined | '' = false; + if (move.target === 'all' || move.target === 'foeSide' || move.target === 'allySide' || move.target === 'allyTeam') { + damage = this.tryMoveHit(target, pokemon, move); + if (damage === this.battle.NOT_FAIL) pokemon.moveThisTurnResult = null; + if (damage || damage === 0 || damage === undefined) moveResult = true; + } else if (move.target === 'allAdjacent' || move.target === 'allAdjacentFoes') { + if (!targets.length) { + this.battle.attrLastMove('[notarget]'); + this.battle.add('-notarget', pokemon); return false; } + if (targets.length > 1) move.spreadHit = true; + const hitSlots = []; + for (const source of targets) { + const hitResult = this.tryMoveHit(source, pokemon, move); + if (hitResult || hitResult === 0 || hitResult === undefined) { + moveResult = true; + hitSlots.push(source.getSlot()); + } + if (damage) { + damage += hitResult || 0; + } else { + if (damage !== false || hitResult !== this.battle.NOT_FAIL) damage = hitResult; + } + if (damage === this.battle.NOT_FAIL) pokemon.moveThisTurnResult = null; + } + if (move.spreadHit) this.battle.attrLastMove('[spread] ' + hitSlots.join(',')); + } else { + target = targets[0]; + let lacksTarget = !target || target.fainted; + if (!lacksTarget) { + if (['adjacentFoe', 'adjacentAlly', 'normal', 'randomNormal'].includes(move.target)) { + lacksTarget = !target.isAdjacent(pokemon); + } + } + if (lacksTarget && !move.isFutureMove) { + this.battle.attrLastMove('[notarget]'); + this.battle.add('-notarget', pokemon); + return false; + } + damage = this.tryMoveHit(target, pokemon, move); + if (damage === this.battle.NOT_FAIL) pokemon.moveThisTurnResult = null; + if (damage || damage === 0 || damage === undefined) moveResult = true; + } + if (move.selfBoost && moveResult) this.moveHit(pokemon, pokemon, move, move.selfBoost, false, true); + if (!pokemon.hp) { + this.battle.faint(pokemon, pokemon, move); } - } else { - accuracy = this.runEvent('ModifyAccuracy', target, pokemon, move, accuracy); - } - if (move.alwaysHit) { - accuracy = true; // bypasses ohko accuracy modifiers - } else { - accuracy = this.runEvent('Accuracy', target, pokemon, move, accuracy); - } - if (accuracy !== true && !this.randomChance(accuracy, 100)) { - accPass = false; - } - if (accPass) { - hitResult = this.runEvent('TryHit', target, pokemon, move); + if (!moveResult) { + this.battle.singleEvent('MoveFail', move, null, target, pokemon, move); + return false; + } + + if (!move.negateSecondary && !(move.hasSheerForce && pokemon.hasAbility('sheerforce'))) { + this.battle.singleEvent('AfterMoveSecondarySelf', move, null, pokemon, target, move); + this.battle.runEvent('AfterMoveSecondarySelf', pokemon, target, move); + } + return true; + }, + tryMoveHit(target, pokemon, move) { + this.battle.setActiveMove(move, pokemon, target); + let naturalImmunity = false; + let accPass = true; + + let hitResult = this.battle.singleEvent('PrepareHit', move, {}, target, pokemon, move) && + this.battle.runEvent('PrepareHit', pokemon, target, move); if (!hitResult) { if (hitResult === false) { - this.add('-fail', pokemon); - this.attrLastMove('[still]'); + this.battle.add('-fail', pokemon); + this.battle.attrLastMove('[still]'); } return false; - } else if (naturalImmunity) { - this.add('-immune', target); + } + + if (!this.battle.singleEvent('Try', move, null, pokemon, target, move)) { return false; } - } else { - if (naturalImmunity) { - this.add('-immune', target); - } else { - if (!move.spreadHit) this.attrLastMove('[miss]'); - this.add('-miss', pokemon, target); - } - return false; - } - move.totalDamage = 0; - let damage: number | undefined | false = 0; - pokemon.lastDamage = 0; - if (move.multihit) { - let hits = move.multihit; - if (Array.isArray(hits)) { - // yes, it's hardcoded... meh - if (hits[0] === 2 && hits[1] === 5) { - hits = this.sample([2, 2, 2, 3, 3, 3, 4, 5]); + if (move.target === 'all' || move.target === 'foeSide' || move.target === 'allySide' || move.target === 'allyTeam') { + if (move.target === 'all') { + hitResult = this.battle.runEvent('TryHitField', target, pokemon, move); } else { - hits = this.random(hits[0], hits[1] + 1); + hitResult = this.battle.runEvent('TryHitSide', target, pokemon, move); + } + if (!hitResult) { + if (hitResult === false) { + this.battle.add('-fail', pokemon); + this.battle.attrLastMove('[still]'); + } + return false; + } + return this.moveHit(target, pokemon, move); + } + + hitResult = this.battle.runEvent('Invulnerability', target, pokemon, move); + if (hitResult === false) { + if (!move.spreadHit) this.battle.attrLastMove('[miss]'); + this.battle.add('-miss', pokemon, target); + return false; + } + + if (move.ignoreImmunity === undefined) { + move.ignoreImmunity = (move.category === 'Status'); + } + + if ( + (!move.ignoreImmunity || (move.ignoreImmunity !== true && !move.ignoreImmunity[move.type])) && + !target.runImmunity(move.type) + ) { + naturalImmunity = true; + } else { + hitResult = this.battle.singleEvent('TryImmunity', move, {}, target, pokemon, move); + if (hitResult === false) { + naturalImmunity = true; } } - hits = Math.floor(hits); - let nullDamage = true; - let moveDamage: number | undefined | false; - // There is no need to recursively check the ´sleepUsable´ flag as Sleep Talk can only be used while asleep. - const isSleepUsable = move.sleepUsable || this.dex.getMove(move.sourceEffect).sleepUsable; - let i: number; - for (i = 0; i < hits && target.hp && pokemon.hp; i++) { - if (pokemon.status === 'slp' && !isSleepUsable) break; - move.hit = i + 1; - if (move.multiaccuracy && i > 0) { - accuracy = move.accuracy; - if (accuracy !== true) { - if (!move.ignoreAccuracy) { - boosts = this.runEvent('ModifyBoost', pokemon, null, null, {...pokemon.boosts}); - boost = this.clampIntRange(boosts['accuracy'], -6, 6); - if (boost > 0) { - accuracy *= boostTable[boost]; - } else { - accuracy /= boostTable[-boost]; - } - } - if (!move.ignoreEvasion) { - boosts = this.runEvent('ModifyBoost', target, null, null, {...target.boosts}); - boost = this.clampIntRange(boosts['evasion'], -6, 6); - if (boost > 0) { - accuracy /= boostTable[boost]; - } else if (boost < 0) { - accuracy *= boostTable[-boost]; - } - } - } - accuracy = this.runEvent('ModifyAccuracy', target, pokemon, move, accuracy); - if (!move.alwaysHit) { - accuracy = this.runEvent('Accuracy', target, pokemon, move, accuracy); - if (accuracy !== true && !this.randomChance(accuracy, 100)) break; + const boostTable = [1, 4 / 3, 5 / 3, 2, 7 / 3, 8 / 3, 3]; + + // calculate true accuracy + let accuracy = move.accuracy; + let boosts: SparseBoostsTable = {}; + let boost: number; + if (accuracy !== true) { + if (!move.ignoreAccuracy) { + boosts = this.battle.runEvent('ModifyBoost', pokemon, null, null, {...pokemon.boosts}); + boost = this.battle.clampIntRange(boosts['accuracy'], -6, 6); + if (boost > 0) { + accuracy *= boostTable[boost]; + } else { + accuracy /= boostTable[-boost]; + } + } + if (!move.ignoreEvasion) { + boosts = this.battle.runEvent('ModifyBoost', target, null, null, {...target.boosts}); + boost = this.battle.clampIntRange(boosts['evasion'], -6, 6); + if (boost > 0) { + accuracy /= boostTable[boost]; + } else if (boost < 0) { + accuracy *= boostTable[-boost]; } } - - moveDamage = this.moveHit(target, pokemon, move); - if (moveDamage === false) break; - if (nullDamage && (moveDamage || moveDamage === 0 || moveDamage === undefined)) nullDamage = false; - // Damage from each hit is individually counted for the - // purposes of Counter, Metal Burst, and Mirror Coat. - damage = (moveDamage || 0); - move.totalDamage += damage; - this.eachEvent('Update'); } - if (i === 0) return false; - if (nullDamage) damage = false; - this.add('-hitcount', target, i); - } else { - damage = this.moveHit(target, pokemon, move); - move.totalDamage = damage; - } + if (move.ohko) { // bypasses accuracy modifiers + if (!target.isSemiInvulnerable()) { + accuracy = 30; + if (pokemon.level >= target.level && (move.ohko === true || !target.hasType(move.ohko))) { + accuracy += (pokemon.level - target.level); + } else { + this.battle.add('-immune', target, '[ohko]'); + return false; + } + } + } else { + accuracy = this.battle.runEvent('ModifyAccuracy', target, pokemon, move, accuracy); + } + if (move.alwaysHit) { + accuracy = true; // bypasses ohko accuracy modifiers + } else { + accuracy = this.battle.runEvent('Accuracy', target, pokemon, move, accuracy); + } + if (accuracy !== true && !this.battle.randomChance(accuracy, 100)) { + accPass = false; + } - if (move.recoil && move.totalDamage) { - this.damage(this.calcRecoilDamage(move.totalDamage, move), pokemon, target, 'recoil'); - } + if (accPass) { + hitResult = this.battle.runEvent('TryHit', target, pokemon, move); + if (!hitResult) { + if (hitResult === false) { + this.battle.add('-fail', pokemon); + this.battle.attrLastMove('[still]'); + } + return false; + } else if (naturalImmunity) { + this.battle.add('-immune', target); + return false; + } + } else { + if (naturalImmunity) { + this.battle.add('-immune', target); + } else { + if (!move.spreadHit) this.battle.attrLastMove('[miss]'); + this.battle.add('-miss', pokemon, target); + } + return false; + } - if (target && pokemon !== target) target.gotAttacked(move, damage, pokemon); + move.totalDamage = 0; + let damage: number | undefined | false = 0; + pokemon.lastDamage = 0; + if (move.multihit) { + let hits = move.multihit; + if (Array.isArray(hits)) { + // yes, it's hardcoded... meh + if (hits[0] === 2 && hits[1] === 5) { + hits = this.battle.sample([2, 2, 2, 3, 3, 3, 4, 5]); + } else { + hits = this.battle.random(hits[0], hits[1] + 1); + } + } + hits = Math.floor(hits); + let nullDamage = true; + let moveDamage: number | undefined | false; + // There is no need to recursively check the ´sleepUsable´ flag as Sleep Talk can only be used while asleep. + const isSleepUsable = move.sleepUsable || this.dex.getMove(move.sourceEffect).sleepUsable; + let i: number; + for (i = 0; i < hits && target.hp && pokemon.hp; i++) { + if (pokemon.status === 'slp' && !isSleepUsable) break; + move.hit = i + 1; - if (move.ohko && !target.hp) this.add('-ohko'); + if (move.multiaccuracy && i > 0) { + accuracy = move.accuracy; + if (accuracy !== true) { + if (!move.ignoreAccuracy) { + boosts = this.battle.runEvent('ModifyBoost', pokemon, null, null, {...pokemon.boosts}); + boost = this.battle.clampIntRange(boosts['accuracy'], -6, 6); + if (boost > 0) { + accuracy *= boostTable[boost]; + } else { + accuracy /= boostTable[-boost]; + } + } + if (!move.ignoreEvasion) { + boosts = this.battle.runEvent('ModifyBoost', target, null, null, {...target.boosts}); + boost = this.battle.clampIntRange(boosts['evasion'], -6, 6); + if (boost > 0) { + accuracy /= boostTable[boost]; + } else if (boost < 0) { + accuracy *= boostTable[-boost]; + } + } + } + accuracy = this.battle.runEvent('ModifyAccuracy', target, pokemon, move, accuracy); + if (!move.alwaysHit) { + accuracy = this.battle.runEvent('Accuracy', target, pokemon, move, accuracy); + if (accuracy !== true && !this.battle.randomChance(accuracy, 100)) break; + } + } - if (!damage && damage !== 0) return damage; + moveDamage = this.moveHit(target, pokemon, move); + if (moveDamage === false) break; + if (nullDamage && (moveDamage || moveDamage === 0 || moveDamage === undefined)) nullDamage = false; + // Damage from each hit is individually counted for the + // purposes of Counter, Metal Burst, and Mirror Coat. + damage = (moveDamage || 0); + move.totalDamage += damage; + this.battle.eachEvent('Update'); + } + if (i === 0) return false; + if (nullDamage) damage = false; + this.battle.add('-hitcount', target, i); + } else { + damage = this.moveHit(target, pokemon, move); + move.totalDamage = damage; + } - this.eachEvent('Update'); + if (move.recoil && move.totalDamage) { + this.battle.damage(this.calcRecoilDamage(move.totalDamage, move), pokemon, target, 'recoil'); + } - if (target && !move.negateSecondary) { - this.singleEvent('AfterMoveSecondary', move, null, target, pokemon, move); - this.runEvent('AfterMoveSecondary', target, pokemon, move); - } + if (target && pokemon !== target) target.gotAttacked(move, damage, pokemon); - return damage; - }, + if (move.ohko && !target.hp) this.battle.add('-ohko'); - calcRecoilDamage(damageDealt, move) { - return this.clampIntRange(Math.floor(damageDealt * move.recoil![0] / move.recoil![1]), 1); + if (!damage && damage !== 0) return damage; + + this.battle.eachEvent('Update'); + + if (target && !move.negateSecondary) { + this.battle.singleEvent('AfterMoveSecondary', move, null, target, pokemon, move); + this.battle.runEvent('AfterMoveSecondary', target, pokemon, move); + } + + return damage; + }, + + calcRecoilDamage(damageDealt, move) { + return this.battle.clampIntRange(Math.floor(damageDealt * move.recoil![0] / move.recoil![1]), 1); + }, }, }; diff --git a/data/mods/gen4/abilities.ts b/data/mods/gen4/abilities.ts index b02482f843..b880c12b66 100644 --- a/data/mods/gen4/abilities.ts +++ b/data/mods/gen4/abilities.ts @@ -172,15 +172,9 @@ export const Abilities: {[k: string]: ModdedAbilityData} = { intimidate: { inherit: true, onStart(pokemon) { - let activated = false; - for (const target of pokemon.side.foe.active) { - if (target && this.isAdjacent(target, pokemon) && - !(target.volatiles['substitute'] || - target.volatiles['substitutebroken'] && target.volatiles['substitutebroken'].move === 'uturn')) { - activated = true; - break; - } - } + const activated = pokemon.adjacentFoes().some(target => ( + !(target.volatiles['substitute'] || target.volatiles['substitutebroken']?.move === 'uturn') + )); if (!activated) { this.hint("In Gen 4, Intimidate does not activate if every target has a Substitute (or the Substitute was just broken by U-turn).", false, pokemon.side); @@ -188,12 +182,10 @@ export const Abilities: {[k: string]: ModdedAbilityData} = { } this.add('-ability', pokemon, 'Intimidate', 'boost'); - for (const target of pokemon.side.foe.active) { - if (!target || !this.isAdjacent(target, pokemon)) continue; - + for (const target of pokemon.adjacentFoes()) { if (target.volatiles['substitute']) { this.add('-immune', target); - } else if (target.volatiles['substitutebroken'] && target.volatiles['substitutebroken'].move === 'uturn') { + } else if (target.volatiles['substitutebroken']?.move === 'uturn') { this.hint("In Gen 4, if U-turn breaks Substitute the incoming Intimidate does nothing."); } else { this.boost({atk: -1}, target, pokemon, null, true); diff --git a/data/mods/gen4/moves.ts b/data/mods/gen4/moves.ts index d10734f0e9..96c51c6fd0 100644 --- a/data/mods/gen4/moves.ts +++ b/data/mods/gen4/moves.ts @@ -56,7 +56,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { if (!randomMove) { return false; } - this.useMove(randomMove, target); + this.actions.useMove(randomMove, target); }, }, beatup: { @@ -140,7 +140,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { effectType: 'Move', type: 'Normal', } as unknown as ActiveMove; - this.tryMoveHit(target, pokemon, moveData); + this.actions.tryMoveHit(target, pokemon, moveData); return false; } this.add('-activate', pokemon, 'move: Bide'); @@ -217,7 +217,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { if (!this.lastMove || noCopycat.includes(this.lastMove.id)) { return false; } - this.useMove(this.lastMove.id, pokemon); + this.actions.useMove(this.lastMove.id, pokemon); }, }, cottonspore: { @@ -344,7 +344,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { willCrit: false, type: '???', } as unknown as ActiveMove; - const damage = this.getDamage(source, target, moveData, true); + const damage = this.actions.getDamage(source, target, moveData, true); Object.assign(target.side.slotConditions[target.position]['futuremove'], { duration: 3, move: 'doomdesire', @@ -546,7 +546,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { willCrit: false, type: '???', } as unknown as ActiveMove; - const damage = this.getDamage(source, target, moveData, true); + const damage = this.actions.getDamage(source, target, moveData, true); Object.assign(target.side.slotConditions[target.position]['futuremove'], { duration: 3, move: 'futuresight', @@ -660,7 +660,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { pp: 20, onMoveFail(target, source, move) { move.causedCrashDamage = true; - let damage = this.getDamage(source, target, move, true); + let damage = this.actions.getDamage(source, target, move, true); if (!damage) damage = target.maxhp; this.damage(this.clampIntRange(damage / 2, 1, Math.floor(target.maxhp / 2)), source, source, move); }, @@ -688,7 +688,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { pp: 25, onMoveFail(target, source, move) { move.causedCrashDamage = true; - let damage = this.getDamage(source, target, move, true); + let damage = this.actions.getDamage(source, target, move, true); if (!damage) damage = target.maxhp; this.damage(this.clampIntRange(damage / 2, 1, Math.floor(target.maxhp / 2)), source, source, move); }, @@ -794,7 +794,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { target.removeVolatile('magiccoat'); const newMove = this.dex.getActiveMove(move.id); newMove.hasBounced = true; - this.useMove(newMove, target, source); + this.actions.useMove(newMove, target, source); return null; }, }, @@ -887,7 +887,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { if (noMirror.includes(lastAttackedBy.move) || !lastAttackedBy.source.hasMove(lastAttackedBy.move)) { return false; } - this.useMove(lastAttackedBy.move, pokemon); + this.actions.useMove(lastAttackedBy.move, pokemon); }, target: "self", }, @@ -931,7 +931,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { naturepower: { inherit: true, onHit(pokemon) { - this.useMove('triattack', pokemon); + this.actions.useMove('triattack', pokemon); }, }, odorsleuth: { @@ -1174,7 +1174,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { if (target === source || move.flags['authentic']) { return; } - let damage = this.getDamage(source, target, move); + let damage = this.actions.getDamage(source, target, move); if (!damage && damage !== 0) { this.add('-fail', source); this.attrLastMove('[still]'); @@ -1198,7 +1198,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { this.add('-activate', target, 'Substitute', '[damage]'); } if (move.recoil && damage) { - this.damage(this.calcRecoilDamage(damage, move), source, target, 'recoil'); + this.damage(this.actions.calcRecoilDamage(damage, move), source, target, 'recoil'); } if (move.drain) { this.heal(Math.ceil(damage * move.drain[0] / move.drain[1]), source, target, 'drain'); diff --git a/data/mods/gen4/scripts.ts b/data/mods/gen4/scripts.ts index 787036a046..be608c4d29 100644 --- a/data/mods/gen4/scripts.ts +++ b/data/mods/gen4/scripts.ts @@ -1,155 +1,158 @@ export const Scripts: ModdedBattleScriptsData = { inherit: 'gen5', gen: 4, - modifyDamage(baseDamage, pokemon, target, move, suppressMessages = false) { - // DPP divides modifiers into several mathematically important stages - // The modifiers run earlier than other generations are called with ModifyDamagePhase1 and ModifyDamagePhase2 - if (!move.type) move.type = '???'; - const type = move.type; + actions: { + inherit: true, + modifyDamage(baseDamage, pokemon, target, move, suppressMessages = false) { + // DPP divides modifiers into several mathematically important stages + // The modifiers run earlier than other generations are called with ModifyDamagePhase1 and ModifyDamagePhase2 - // Burn - if (pokemon.status === 'brn' && baseDamage && move.category === 'Physical' && !pokemon.hasAbility('guts')) { - baseDamage = this.modify(baseDamage, 0.5); - } + if (!move.type) move.type = '???'; + const type = move.type; - // Other modifiers (Reflect/Light Screen/etc) - baseDamage = this.runEvent('ModifyDamagePhase1', pokemon, target, move, baseDamage); - - // Double battle multi-hit - if (move.spreadHit) { - const spreadModifier = move.spreadModifier || (this.gameType === 'free-for-all' ? 0.5 : 0.75); - this.debug('Spread modifier: ' + spreadModifier); - baseDamage = this.modify(baseDamage, spreadModifier); - } - - // Weather - baseDamage = this.runEvent('WeatherModifyDamage', pokemon, target, move, baseDamage); - - if (this.gen === 3 && move.category === 'Physical' && !Math.floor(baseDamage)) { - baseDamage = 1; - } - - baseDamage += 2; - - const isCrit = target.getMoveHitData(move).crit; - if (isCrit) { - baseDamage = this.modify(baseDamage, move.critModifier || 2); - } - - // Mod 2 (Damage is floored after all multipliers are in) - baseDamage = Math.floor(this.runEvent('ModifyDamagePhase2', pokemon, target, move, baseDamage)); - - // this is not a modifier - baseDamage = this.randomizer(baseDamage); - - // STAB - if (move.forceSTAB || type !== '???' && pokemon.hasType(type)) { - // The "???" type never gets STAB - // Not even if you Roost in Gen 4 and somehow manage to use - // Struggle in the same turn. - // (On second thought, it might be easier to get a MissingNo.) - baseDamage = this.modify(baseDamage, move.stab || 1.5); - } - // types - let typeMod = target.runEffectiveness(move); - typeMod = this.clampIntRange(typeMod, -6, 6); - target.getMoveHitData(move).typeMod = typeMod; - if (typeMod > 0) { - if (!suppressMessages) this.add('-supereffective', target); - - for (let i = 0; i < typeMod; i++) { - baseDamage *= 2; + // Burn + if (pokemon.status === 'brn' && baseDamage && move.category === 'Physical' && !pokemon.hasAbility('guts')) { + baseDamage = this.battle.modify(baseDamage, 0.5); } - } - if (typeMod < 0) { - if (!suppressMessages) this.add('-resisted', target); - for (let i = 0; i > typeMod; i--) { - baseDamage = Math.floor(baseDamage / 2); + // Other modifiers (Reflect/Light Screen/etc) + baseDamage = this.battle.runEvent('ModifyDamagePhase1', pokemon, target, move, baseDamage); + + // Double battle multi-hit + if (move.spreadHit) { + const spreadModifier = move.spreadModifier || (this.battle.gameType === 'free-for-all' ? 0.5 : 0.75); + this.battle.debug('Spread modifier: ' + spreadModifier); + baseDamage = this.battle.modify(baseDamage, spreadModifier); } - } - if (isCrit && !suppressMessages) this.add('-crit', target); + // Weather + baseDamage = this.battle.runEvent('WeatherModifyDamage', pokemon, target, move, baseDamage); - // Final modifier. - baseDamage = this.runEvent('ModifyDamage', pokemon, target, move, baseDamage); - - if (!Math.floor(baseDamage)) { - return 1; - } - - return Math.floor(baseDamage); - }, - hitStepInvulnerabilityEvent(targets, pokemon, move) { - const hitResults = this.runEvent('Invulnerability', targets, pokemon, move); - for (const [i, target] of targets.entries()) { - if (hitResults[i] === false) { - this.attrLastMove('[miss]'); - this.add('-miss', pokemon, target); + if (this.battle.gen === 3 && move.category === 'Physical' && !Math.floor(baseDamage)) { + baseDamage = 1; } - } - return hitResults; - }, - hitStepAccuracy(targets, pokemon, move) { - const hitResults = []; - for (const [i, target] of targets.entries()) { - this.activeTarget = target; - // calculate true accuracy - let accuracy = move.accuracy; - if (move.ohko) { // bypasses accuracy modifiers - if (!target.isSemiInvulnerable()) { - if (pokemon.level < target.level) { - this.add('-immune', target, '[ohko]'); - hitResults[i] = false; - continue; - } - accuracy = 30 + pokemon.level - target.level; + + baseDamage += 2; + + const isCrit = target.getMoveHitData(move).crit; + if (isCrit) { + baseDamage = this.battle.modify(baseDamage, move.critModifier || 2); + } + + // Mod 2 (Damage is floored after all multipliers are in) + baseDamage = Math.floor(this.battle.runEvent('ModifyDamagePhase2', pokemon, target, move, baseDamage)); + + // this is not a modifier + baseDamage = this.battle.randomizer(baseDamage); + + // STAB + if (move.forceSTAB || type !== '???' && pokemon.hasType(type)) { + // The "???" type never gets STAB + // Not even if you Roost in Gen 4 and somehow manage to use + // Struggle in the same turn. + // (On second thought, it might be easier to get a MissingNo.) + baseDamage = this.battle.modify(baseDamage, move.stab || 1.5); + } + // types + let typeMod = target.runEffectiveness(move); + typeMod = this.battle.clampIntRange(typeMod, -6, 6); + target.getMoveHitData(move).typeMod = typeMod; + if (typeMod > 0) { + if (!suppressMessages) this.battle.add('-supereffective', target); + + for (let i = 0; i < typeMod; i++) { + baseDamage *= 2; } - } else { - const boostTable = [1, 4 / 3, 5 / 3, 2, 7 / 3, 8 / 3, 3]; + } + if (typeMod < 0) { + if (!suppressMessages) this.battle.add('-resisted', target); - let boosts; - let boost!: number; - if (accuracy !== true) { - if (!move.ignoreAccuracy) { - boosts = this.runEvent('ModifyBoost', pokemon, null, null, {...pokemon.boosts}); - boost = this.clampIntRange(boosts['accuracy'], -6, 6); - if (boost > 0) { - accuracy *= boostTable[boost]; - } else { - accuracy /= boostTable[-boost]; - } - } - if (!move.ignoreEvasion) { - boosts = this.runEvent('ModifyBoost', target, null, null, {...target.boosts}); - boost = this.clampIntRange(boosts['evasion'], -6, 6); - if (boost > 0) { - accuracy /= boostTable[boost]; - } else if (boost < 0) { - accuracy *= boostTable[-boost]; + for (let i = 0; i > typeMod; i--) { + baseDamage = Math.floor(baseDamage / 2); + } + } + + if (isCrit && !suppressMessages) this.battle.add('-crit', target); + + // Final modifier. + baseDamage = this.battle.runEvent('ModifyDamage', pokemon, target, move, baseDamage); + + if (!Math.floor(baseDamage)) { + return 1; + } + + return Math.floor(baseDamage); + }, + hitStepInvulnerabilityEvent(targets, pokemon, move) { + const hitResults = this.battle.runEvent('Invulnerability', targets, pokemon, move); + for (const [i, target] of targets.entries()) { + if (hitResults[i] === false) { + this.battle.attrLastMove('[miss]'); + this.battle.add('-miss', pokemon, target); + } + } + return hitResults; + }, + hitStepAccuracy(targets, pokemon, move) { + const hitResults = []; + for (const [i, target] of targets.entries()) { + this.battle.activeTarget = target; + // calculate true accuracy + let accuracy = move.accuracy; + if (move.ohko) { // bypasses accuracy modifiers + if (!target.isSemiInvulnerable()) { + if (pokemon.level < target.level) { + this.battle.add('-immune', target, '[ohko]'); + hitResults[i] = false; + continue; + } + accuracy = 30 + pokemon.level - target.level; + } + } else { + const boostTable = [1, 4 / 3, 5 / 3, 2, 7 / 3, 8 / 3, 3]; + + let boosts; + let boost!: number; + if (accuracy !== true) { + if (!move.ignoreAccuracy) { + boosts = this.battle.runEvent('ModifyBoost', pokemon, null, null, {...pokemon.boosts}); + boost = this.battle.clampIntRange(boosts['accuracy'], -6, 6); + if (boost > 0) { + accuracy *= boostTable[boost]; + } else { + accuracy /= boostTable[-boost]; + } + } + if (!move.ignoreEvasion) { + boosts = this.battle.runEvent('ModifyBoost', target, null, null, {...target.boosts}); + boost = this.battle.clampIntRange(boosts['evasion'], -6, 6); + if (boost > 0) { + accuracy /= boostTable[boost]; + } else if (boost < 0) { + accuracy *= boostTable[-boost]; + } } } + accuracy = this.battle.runEvent('ModifyAccuracy', target, pokemon, move, accuracy); } - accuracy = this.runEvent('ModifyAccuracy', target, pokemon, move, accuracy); + if (move.alwaysHit) { + accuracy = true; // bypasses ohko accuracy modifiers + } else { + accuracy = this.battle.runEvent('Accuracy', target, pokemon, move, accuracy); + } + if (accuracy !== true && !this.battle.randomChance(accuracy, 100)) { + if (!move.spreadHit) this.battle.attrLastMove('[miss]'); + this.battle.add('-miss', pokemon, target); + hitResults[i] = false; + continue; + } + hitResults[i] = true; } - if (move.alwaysHit) { - accuracy = true; // bypasses ohko accuracy modifiers - } else { - accuracy = this.runEvent('Accuracy', target, pokemon, move, accuracy); - } - if (accuracy !== true && !this.randomChance(accuracy, 100)) { - if (!move.spreadHit) this.attrLastMove('[miss]'); - this.add('-miss', pokemon, target); - hitResults[i] = false; - continue; - } - hitResults[i] = true; - } - return hitResults; - }, - - calcRecoilDamage(damageDealt, move) { - return this.clampIntRange(Math.floor(damageDealt * move.recoil![0] / move.recoil![1]), 1); + return hitResults; + }, + calcRecoilDamage(damageDealt, move) { + return this.battle.clampIntRange(Math.floor(damageDealt * move.recoil![0] / move.recoil![1]), 1); + }, }, }; diff --git a/data/mods/gen5/moves.ts b/data/mods/gen5/moves.ts index 20ac8b8d6d..f4b9ab6b6e 100644 --- a/data/mods/gen5/moves.ts +++ b/data/mods/gen5/moves.ts @@ -45,7 +45,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { if (!randomMove) { return false; } - this.useMove(randomMove, target); + this.actions.useMove(randomMove, target); }, }, assurance: { @@ -160,7 +160,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { if (!this.lastMove || noCopycat.includes(this.lastMove.id)) { return false; } - this.useMove(this.lastMove.id, pokemon); + this.actions.useMove(this.lastMove.id, pokemon); }, }, cottonspore: { @@ -597,7 +597,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { inherit: true, onTryHit() {}, onHit(pokemon) { - this.useMove('earthquake', pokemon); + this.actions.useMove('earthquake', pokemon); }, target: "self", }, @@ -873,7 +873,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { if (target === source || move.flags['authentic']) { return; } - let damage = this.getDamage(source, target, move); + let damage = this.actions.getDamage(source, target, move); if (!damage && damage !== 0) { this.add('-fail', source); this.attrLastMove('[still]'); @@ -895,7 +895,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { this.add('-activate', target, 'Substitute', '[damage]'); } if (move.recoil && damage) { - this.damage(this.calcRecoilDamage(damage, move), source, target, 'recoil'); + this.damage(this.actions.calcRecoilDamage(damage, move), source, target, 'recoil'); } if (move.drain) { this.heal(Math.ceil(damage * move.drain[0] / move.drain[1]), source, target, 'drain'); diff --git a/data/mods/gen6/conditions.ts b/data/mods/gen6/conditions.ts index 8de8ae1e66..049e09058d 100644 --- a/data/mods/gen6/conditions.ts +++ b/data/mods/gen6/conditions.ts @@ -25,7 +25,7 @@ export const Conditions: {[k: string]: ModdedConditionData} = { if (this.randomChance(1, 2)) { return; } - const damage = this.getDamage(pokemon, pokemon, 40); + const damage = this.actions.getDamage(pokemon, pokemon, 40); if (typeof damage !== 'number') throw new Error("Confusion damage not dealt"); this.damage(damage, pokemon, pokemon, { id: 'confused', diff --git a/data/mods/gennext/abilities.ts b/data/mods/gennext/abilities.ts index d009a44535..21f44adb03 100644 --- a/data/mods/gennext/abilities.ts +++ b/data/mods/gennext/abilities.ts @@ -675,7 +675,7 @@ export const Abilities: {[k: string]: ModdedAbilityData} = { }, onFoeMaybeTrapPokemon(pokemon, source) { if (!source) source = this.effectData.target; - if (!source || !this.isAdjacent(pokemon, source)) return; + if (!source || !pokemon.isAdjacent(source)) return; if (pokemon.ability !== 'shadowtag' && !source.volatiles['shadowtag']) { pokemon.maybeTrapped = true; } diff --git a/data/mods/gennext/conditions.ts b/data/mods/gennext/conditions.ts index dd01a03018..0bb82984d3 100644 --- a/data/mods/gennext/conditions.ts +++ b/data/mods/gennext/conditions.ts @@ -67,7 +67,7 @@ export const Conditions: {[k: string]: ModdedConditionData} = { pokemon.removeVolatile('confusion'); return; } - const damage = this.getDamage(pokemon, pokemon, 40); + const damage = this.actions.getDamage(pokemon, pokemon, 40); if (typeof damage !== 'number') throw new Error("Confusion damage not dealt"); this.directDamage(damage); }, diff --git a/data/mods/gennext/moves.ts b/data/mods/gennext/moves.ts index 3b566fb1e6..5352c557f9 100644 --- a/data/mods/gennext/moves.ts +++ b/data/mods/gennext/moves.ts @@ -153,7 +153,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { if (target === source || move.flags['authentic'] || move.infiltrates) { return; } - let damage = this.getDamage(source, target, move); + let damage = this.actions.getDamage(source, target, move); if (!damage) { return null; } @@ -663,7 +663,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { const moveData = { damage: this.effectData.totalDamage * 2, } as unknown as ActiveMove; - this.moveHit(target, pokemon, this.dex.getActiveMove('bide'), moveData); + this.actions.moveHit(target, pokemon, this.dex.getActiveMove('bide'), moveData); return false; } this.add('-activate', pokemon, 'Bide'); diff --git a/data/mods/letsgo/moves.ts b/data/mods/letsgo/moves.ts index f73b8321a7..5f2ac3adc8 100644 --- a/data/mods/letsgo/moves.ts +++ b/data/mods/letsgo/moves.ts @@ -54,7 +54,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { randomMove = this.sample(moves).id; } if (!randomMove) return false; - this.useMove(randomMove, target); + this.actions.useMove(randomMove, target); }, }, sappyseed: { diff --git a/data/mods/megasforall/abilities.ts b/data/mods/megasforall/abilities.ts index 20258b81fb..9730e4f3f6 100644 --- a/data/mods/megasforall/abilities.ts +++ b/data/mods/megasforall/abilities.ts @@ -350,7 +350,7 @@ export const Abilities: {[abilityid: string]: ModdedAbilityData} = { if (!activated) { this.add('-activate', pokemon, 'ability: Trash Compactor'); activated = true; - this.useMove('stockpile', pokemon); + this.actions.useMove('stockpile', pokemon); } pokemon.side.removeSideCondition(sideCondition); this.add('-sideend', pokemon.side, this.dex.getEffect(sideCondition).name, '[from] Ability: Trash Compactor', '[of] ' + pokemon); @@ -564,13 +564,13 @@ export const Abilities: {[abilityid: string]: ModdedAbilityData} = { desc: "Prevents adjacent opposing Flying-type Pokémon from choosing to switch out unless they are immune to trapping.", shortDesc: "Prevents adjacent Flying-type foes from choosing to switch.", onFoeTrapPokemon(pokemon) { - if (pokemon.hasType('Flying') && this.isAdjacent(pokemon, this.effectData.target)) { + if (pokemon.hasType('Flying') && pokemon.isAdjacent(this.effectData.target)) { pokemon.tryTrap(true); } }, onFoeMaybeTrapPokemon(pokemon, source) { if (!source) source = this.effectData.target; - if (!source || !this.isAdjacent(pokemon, source)) return; + if (!source || !pokemon.isAdjacent(source)) return; if (!pokemon.knownType || pokemon.hasType('Flying')) { pokemon.maybeTrapped = true; } @@ -767,7 +767,7 @@ export const Abilities: {[abilityid: string]: ModdedAbilityData} = { } } else { this.add('-message', `${(target.illusion ? target.illusion.name : target.name)} suddenly exploded!`); - this.useMove('explosion', target, source, this.dex.getAbility('alchemist')); + this.actions.useMove('explosion', target, source, this.dex.getAbility('alchemist')); } } else { this.add('-ability', source, 'Alchemist'); @@ -1059,7 +1059,7 @@ export const Abilities: {[abilityid: string]: ModdedAbilityData} = { desc: "On entry, this Pokémon's type changes to match its first move that's super effective against an adjacent opponent.", shortDesc: "On entry: type changes to match its first move that's super effective against an adjacent opponent.", onStart(pokemon) { - const possibleTargets = pokemon.side.foe.active.filter(foeActive => foeActive && this.isAdjacent(pokemon, foeActive)); + const possibleTargets = pokemon.adjacentFoes(); while (possibleTargets.length) { let rand = 0; if (possibleTargets.length > 1) rand = this.random(possibleTargets.length); @@ -1286,12 +1286,9 @@ export const Abilities: {[abilityid: string]: ModdedAbilityData} = { onResidualSubOrder: 1, onResidual(pokemon) { if (pokemon.item) return; - const pickupTargets = []; - for (const target of this.getAllActive()) { - if (target.lastItem && target.usedItemThisTurn && this.isAdjacent(pokemon, target)) { - pickupTargets.push(target); - } - } + const pickupTargets = this.getAllActive().filter(target => ( + target.lastItem && target.usedItemThisTurn && pokemon.isAdjacent(target) + )); if (!pickupTargets.length) return; const randomTarget = this.sample(pickupTargets); const item = randomTarget.lastItem; @@ -1552,7 +1549,7 @@ export const Abilities: {[abilityid: string]: ModdedAbilityData} = { if (move.category === 'Special') { source.addVolatile('specialsound'); } - this.useMove('earthquake', this.effectData.target); + this.actions.useMove('earthquake', this.effectData.target); } }, name: "Seismic Scream", @@ -1563,7 +1560,7 @@ export const Abilities: {[abilityid: string]: ModdedAbilityData} = { shortDesc: "On switch-in, this Pokémon poisons every Pokémon on the field.", onStart(pokemon) { for (const target of this.getAllActive()) { - if (!target || !this.isAdjacent(target, pokemon) || target.status) continue; + if (!target.isAdjacent(pokemon) || target.status) continue; if (target.hasAbility('soundproof')) { this.add('-ability', pokemon, 'Acid Rock'); this.add('-immune', target, "[from] ability: Soundproof", "[of] " + target); @@ -1869,13 +1866,13 @@ export const Abilities: {[abilityid: string]: ModdedAbilityData} = { data.moveData.isFutureMove = true; if (move.category === 'Status') { - this.useMove(move, target, data.target); + this.actions.useMove(move, target, data.target); } else { const hitMove = new this.dex.Move(data.moveData) as ActiveMove; if (data.source.hp) { // the move should still activate, but animating can cause issues depending on the move this.add('-anim', data.source, hitMove, data.target); } - this.trySpreadMoveHit([data.target], data.source, hitMove); + this.actions.trySpreadMoveHit([data.target], data.source, hitMove); } }, }, diff --git a/data/mods/megasforall/conditions.ts b/data/mods/megasforall/conditions.ts index c7bb0948a6..a13893302d 100644 --- a/data/mods/megasforall/conditions.ts +++ b/data/mods/megasforall/conditions.ts @@ -35,7 +35,7 @@ const longwhip: ConditionData = { if (data.source.isActive) { this.add('-anim', data.source, hitMove, data.target); } - this.trySpreadMoveHit([data.target], data.source, hitMove); + this.actions.trySpreadMoveHit([data.target], data.source, hitMove); }, onEnd(target) { // unlike a future move, Long Whip activates each turn @@ -71,7 +71,7 @@ const longwhip: ConditionData = { if (data.source.isActive) { this.add('-anim', data.source, hitMove, data.target); } - this.trySpreadMoveHit([data.target], data.source, hitMove); + this.actions.trySpreadMoveHit([data.target], data.source, hitMove); }, }; export const Conditions: {[k: string]: ConditionData} = { diff --git a/data/mods/megasforall/moves.ts b/data/mods/megasforall/moves.ts index d894944aa4..d6215edfd7 100644 --- a/data/mods/megasforall/moves.ts +++ b/data/mods/megasforall/moves.ts @@ -277,7 +277,7 @@ export const Moves: {[moveid: string]: ModdedMoveData} = { onSwitchIn(pokemon) { if (pokemon.hasAbility('trashcompactor') && !this.field.getPseudoWeather('stickyresidues')) { if (!pokemon.volatiles['stockpile']) { - this.useMove('stockpile', pokemon); + this.actions.useMove('stockpile', pokemon); } this.add('-sideend', pokemon.side, 'move: G-Max Steelsurge', '[of] ' + pokemon); pokemon.side.removeSideCondition('gmaxsteelsurge'); @@ -323,7 +323,7 @@ export const Moves: {[moveid: string]: ModdedMoveData} = { if (!pokemon.isGrounded()) return; if (pokemon.hasAbility('trashcompactor') && !this.field.getPseudoWeather('stickyresidues')) { if (!pokemon.volatiles['stockpile']) { - this.useMove('stockpile', pokemon); + this.actions.useMove('stockpile', pokemon); } this.add('-sideend', pokemon.side, 'move: Spikes', '[of] ' + pokemon); pokemon.side.removeSideCondition('spikes'); @@ -356,7 +356,7 @@ export const Moves: {[moveid: string]: ModdedMoveData} = { onSwitchIn(pokemon) { if (pokemon.hasAbility('trashcompactor') && !this.field.getPseudoWeather('stickyresidues')) { if (!pokemon.volatiles['stockpile']) { - this.useMove('stockpile', pokemon); + this.actions.useMove('stockpile', pokemon); } this.add('-sideend', pokemon.side, 'move: Stealth Rock', '[of] ' + pokemon); pokemon.side.removeSideCondition('stealthrock'); @@ -389,7 +389,7 @@ export const Moves: {[moveid: string]: ModdedMoveData} = { if (!pokemon.isGrounded()) return; if (pokemon.hasAbility('trashcompactor') && !this.field.getPseudoWeather('stickyresidues')) { if (!pokemon.volatiles['stockpile']) { - this.useMove('stockpile', pokemon); + this.actions.useMove('stockpile', pokemon); } this.add('-sideend', pokemon.side, 'move: Sticky Web', '[of] ' + pokemon); pokemon.side.removeSideCondition('stickyweb'); @@ -429,7 +429,7 @@ export const Moves: {[moveid: string]: ModdedMoveData} = { if (!pokemon.isGrounded()) return; if (pokemon.hasAbility('trashcompactor') && !this.field.getPseudoWeather('stickyresidues')) { if (!pokemon.volatiles['stockpile']) { - this.useMove('stockpile', pokemon); + this.actions.useMove('stockpile', pokemon); } this.add('-sideend', pokemon.side, 'move: Toxic Spikes', '[of] ' + pokemon); pokemon.side.removeSideCondition('toxicspikes'); @@ -652,7 +652,7 @@ export const Moves: {[moveid: string]: ModdedMoveData} = { move = 'triattack'; } } - this.useMove(move, pokemon, target); + this.actions.useMove(move, pokemon, target); return null; }, }, diff --git a/data/mods/megasforall/scripts.ts b/data/mods/megasforall/scripts.ts index 8a4613b2f7..80abfe39e4 100644 --- a/data/mods/megasforall/scripts.ts +++ b/data/mods/megasforall/scripts.ts @@ -86,259 +86,262 @@ export const Scripts: ModdedBattleScriptsData = { newMoves("torterra", ["bodypress", "gravapple", "meteorbeam"]); newMoves("empoleon", ["flipturn", "haze", "originpulse", "roost"]); }, - canMegaEvo(pokemon) { - const altForme = pokemon.baseSpecies.otherFormes && this.dex.getSpecies(pokemon.baseSpecies.otherFormes[0]); - const item = pokemon.getItem(); - if ( - altForme?.isMega && altForme?.requiredMove && - pokemon.baseMoves.includes(this.toID(altForme.requiredMove)) && !item.zMove - ) { - return altForme.name; - } - if (item.name === "Lycanite" && pokemon.baseSpecies.name === "Lycanroc-Midnight") { - return "Lycanroc-Midnight-Mega"; - } - if (item.name === "Lycanite" && pokemon.baseSpecies.name === "Lycanroc-Dusk") { - return "Lycanroc-Dusk-Mega"; - } - if (item.name === "Raichunite" && pokemon.baseSpecies.name === "Raichu-Alola") { - return null; - } - if (item.name === "Slowbronite" && pokemon.baseSpecies.name === "Slowbro-Galar") { - return null; - } - if (item.name === "Slowkinite" && pokemon.baseSpecies.name === "Slowking-Galar") { - return null; - } - if (item.name === "Gourgeite" && pokemon.baseSpecies.name === "Gourgeist-Small") { - return "Gourgeist-Small-Mega"; - } - if (item.name === "Gourgeite" && pokemon.baseSpecies.name === "Gourgeist-Large") { - return "Gourgeist-Large-Mega"; - } - if (item.name === "Gourgeite" && pokemon.baseSpecies.name === "Gourgeist-Super") { - return "Gourgeist-Super-Mega"; - } - if (item.name === "Reginite" && pokemon.baseSpecies.name === "Regice") { - return "Regice-Mega"; - } - if (item.name === "Reginite" && pokemon.baseSpecies.name === "Registeel") { - return "Registeel-Mega"; - } - if (item.name === "Meowsticite" && pokemon.baseSpecies.name === "Meowstic-F") { - return "Meowstic-F-Mega"; - } - if (item.name === "Sawsbuckite" && pokemon.baseSpecies.name === "Delibird") { - return "Delibird-Mega-Festive-Rider"; - } - if (item.name === "Sawsbuckite" && pokemon.baseSpecies.name === "Sawsbuck-Summer") { - return "Sawsbuck-Summer-Mega"; - } - if (item.name === "Sawsbuckite" && pokemon.baseSpecies.name === "Sawsbuck-Autumn") { - return "Sawsbuck-Autumn-Mega"; - } - if (item.name === "Sawsbuckite" && pokemon.baseSpecies.name === "Sawsbuck-Winter") { - return "Sawsbuck-Winter-Mega"; - } - if (item.name === "Toxtricitite" && pokemon.baseSpecies.name === "Toxtricity-Low-Key") { - return "Toxtricity-Low-Key-Mega"; - } - if (item.name === "Ninetalesite" && pokemon.baseSpecies.name === "Ninetales") { - return null; - } - if (item.name === "Dugtrionite" && pokemon.baseSpecies.name === "Dugtrio-Alola") { - return null; - } - if (item.megaEvolves !== pokemon.baseSpecies.name || item.megaStone === pokemon.species.name) { - return null; - } - return item.megaStone; - }, - runMegaEvo(pokemon) { - const speciesid = pokemon.canMegaEvo || pokemon.canUltraBurst; - if (!speciesid) return false; - const side = pokemon.side; - // Pokémon affected by Sky Drop cannot mega evolve. Enforce it here for now. - for (const foeActive of side.foe.active) { - if (foeActive.volatiles['skydrop'] && foeActive.volatiles['skydrop'].source === pokemon) { - return false; + actions: { + canMegaEvo(pokemon) { + const altForme = pokemon.baseSpecies.otherFormes && this.dex.getSpecies(pokemon.baseSpecies.otherFormes[0]); + const item = pokemon.getItem(); + if ( + altForme?.isMega && altForme?.requiredMove && + pokemon.baseMoves.includes(this.battle.toID(altForme.requiredMove)) && !item.zMove + ) { + return altForme.name; } - } + if (item.name === "Lycanite" && pokemon.baseSpecies.name === "Lycanroc-Midnight") { + return "Lycanroc-Midnight-Mega"; + } + if (item.name === "Lycanite" && pokemon.baseSpecies.name === "Lycanroc-Dusk") { + return "Lycanroc-Dusk-Mega"; + } + if (item.name === "Raichunite" && pokemon.baseSpecies.name === "Raichu-Alola") { + return null; + } + if (item.name === "Slowbronite" && pokemon.baseSpecies.name === "Slowbro-Galar") { + return null; + } + if (item.name === "Slowkinite" && pokemon.baseSpecies.name === "Slowking-Galar") { + return null; + } + if (item.name === "Gourgeite" && pokemon.baseSpecies.name === "Gourgeist-Small") { + return "Gourgeist-Small-Mega"; + } + if (item.name === "Gourgeite" && pokemon.baseSpecies.name === "Gourgeist-Large") { + return "Gourgeist-Large-Mega"; + } + if (item.name === "Gourgeite" && pokemon.baseSpecies.name === "Gourgeist-Super") { + return "Gourgeist-Super-Mega"; + } + if (item.name === "Reginite" && pokemon.baseSpecies.name === "Regice") { + return "Regice-Mega"; + } + if (item.name === "Reginite" && pokemon.baseSpecies.name === "Registeel") { + return "Registeel-Mega"; + } + if (item.name === "Meowsticite" && pokemon.baseSpecies.name === "Meowstic-F") { + return "Meowstic-F-Mega"; + } + if (item.name === "Sawsbuckite" && pokemon.baseSpecies.name === "Delibird") { + return "Delibird-Mega-Festive-Rider"; + } + if (item.name === "Sawsbuckite" && pokemon.baseSpecies.name === "Sawsbuck-Summer") { + return "Sawsbuck-Summer-Mega"; + } + if (item.name === "Sawsbuckite" && pokemon.baseSpecies.name === "Sawsbuck-Autumn") { + return "Sawsbuck-Autumn-Mega"; + } + if (item.name === "Sawsbuckite" && pokemon.baseSpecies.name === "Sawsbuck-Winter") { + return "Sawsbuck-Winter-Mega"; + } + if (item.name === "Toxtricitite" && pokemon.baseSpecies.name === "Toxtricity-Low-Key") { + return "Toxtricity-Low-Key-Mega"; + } + if (item.name === "Ninetalesite" && pokemon.baseSpecies.name === "Ninetales") { + return null; + } + if (item.name === "Dugtrionite" && pokemon.baseSpecies.name === "Dugtrio-Alola") { + return null; + } + if (item.megaEvolves !== pokemon.baseSpecies.name || item.megaStone === pokemon.species.name) { + return null; + } + return item.megaStone; + }, + runMegaEvo(pokemon) { + const speciesid = pokemon.canMegaEvo || pokemon.canUltraBurst; + if (!speciesid) return false; + const side = pokemon.side; - if (pokemon.illusion) { - this.singleEvent('End', this.dex.getAbility('Illusion'), pokemon.abilityData, pokemon); - } // only part that's changed - pokemon.formeChange(speciesid, pokemon.getItem(), true); + // Pokémon affected by Sky Drop cannot mega evolve. Enforce it here for now. + for (const foeActive of side.foe.active) { + if (foeActive.volatiles['skydrop'] && foeActive.volatiles['skydrop'].source === pokemon) { + return false; + } + } - // Limit one mega evolution - const wasMega = pokemon.canMegaEvo; - for (const ally of side.pokemon) { - if (wasMega) { - ally.canMegaEvo = null; + if (pokemon.illusion) { + this.battle.singleEvent('End', this.dex.getAbility('Illusion'), pokemon.abilityData, pokemon); + } // only part that's changed + pokemon.formeChange(speciesid, pokemon.getItem(), true); + + // Limit one mega evolution + const wasMega = pokemon.canMegaEvo; + for (const ally of side.pokemon) { + if (wasMega) { + ally.canMegaEvo = null; + } else { + ally.canUltraBurst = null; + } + } + + this.battle.runEvent('AfterMega', pokemon); + return true; + }, + + getDamage( + pokemon: Pokemon, target: Pokemon, move: string | number | ActiveMove, + suppressMessages = false + ): number | undefined | null | false { + if (typeof move === 'string') move = this.dex.getActiveMove(move); + + if (typeof move === 'number') { + const basePower = move; + move = new Dex.Move({ + basePower, + type: '???', + category: 'Physical', + willCrit: false, + }) as ActiveMove; + move.hit = 0; + } + + if (!move.ignoreImmunity || (move.ignoreImmunity !== true && !move.ignoreImmunity[move.type])) { + if (!target.runImmunity(move.type, !suppressMessages)) { + return false; + } + } + + if (move.ohko) return target.maxhp; + if (move.damageCallback) return move.damageCallback.call(this.battle, pokemon, target); + if (move.damage === 'level') { + return pokemon.level; + } else if (move.damage) { + return move.damage; + } + + const category = this.battle.getCategory(move); + const defensiveCategory = move.defensiveCategory || category; + + let basePower: number | false | null = move.basePower; + if (move.basePowerCallback) { + basePower = move.basePowerCallback.call(this.battle, pokemon, target, move); + } + if (!basePower) return basePower === 0 ? undefined : basePower; + basePower = this.battle.clampIntRange(basePower, 1); + + let critMult; + let critRatio = this.battle.runEvent('ModifyCritRatio', pokemon, target, move, move.critRatio || 0); + if (this.battle.gen <= 5) { + critRatio = this.battle.clampIntRange(critRatio, 0, 5); + critMult = [0, 16, 8, 4, 3, 2]; } else { - ally.canUltraBurst = null; + critRatio = this.battle.clampIntRange(critRatio, 0, 4); + if (this.battle.gen === 6) { + critMult = [0, 16, 8, 2, 1]; + } else { + critMult = [0, 24, 8, 2, 1]; + } } - } - this.runEvent('AfterMega', pokemon); - return true; - }, - - getDamage( - pokemon: Pokemon, target: Pokemon, move: string | number | ActiveMove, - suppressMessages = false - ): number | undefined | null | false { - if (typeof move === 'string') move = this.dex.getActiveMove(move); - - if (typeof move === 'number') { - const basePower = move; - move = new Dex.Move({ - basePower, - type: '???', - category: 'Physical', - willCrit: false, - }) as ActiveMove; - move.hit = 0; - } - - if (!move.ignoreImmunity || (move.ignoreImmunity !== true && !move.ignoreImmunity[move.type])) { - if (!target.runImmunity(move.type, !suppressMessages)) { - return false; + const moveHit = target.getMoveHitData(move); + moveHit.crit = move.willCrit || false; + if (move.willCrit === undefined && critRatio) { + moveHit.crit = this.battle.randomChance(1, critMult[critRatio]); } - } - if (move.ohko) return target.maxhp; - if (move.damageCallback) return move.damageCallback.call(this, pokemon, target); - if (move.damage === 'level') { - return pokemon.level; - } else if (move.damage) { - return move.damage; - } - - const category = this.getCategory(move); - const defensiveCategory = move.defensiveCategory || category; - - let basePower: number | false | null = move.basePower; - if (move.basePowerCallback) { - basePower = move.basePowerCallback.call(this, pokemon, target, move); - } - if (!basePower) return basePower === 0 ? undefined : basePower; - basePower = this.clampIntRange(basePower, 1); - - let critMult; - let critRatio = this.runEvent('ModifyCritRatio', pokemon, target, move, move.critRatio || 0); - if (this.gen <= 5) { - critRatio = this.clampIntRange(critRatio, 0, 5); - critMult = [0, 16, 8, 4, 3, 2]; - } else { - critRatio = this.clampIntRange(critRatio, 0, 4); - if (this.gen === 6) { - critMult = [0, 16, 8, 2, 1]; - } else { - critMult = [0, 24, 8, 2, 1]; + if (moveHit.crit) { + moveHit.crit = this.battle.runEvent('CriticalHit', target, null, move); } - } - const moveHit = target.getMoveHitData(move); - moveHit.crit = move.willCrit || false; - if (move.willCrit === undefined && critRatio) { - moveHit.crit = this.randomChance(1, critMult[critRatio]); - } + // happens after crit calculation + basePower = this.battle.runEvent('BasePower', pokemon, target, move, basePower, true); - if (moveHit.crit) { - moveHit.crit = this.runEvent('CriticalHit', target, null, move); - } + if (!basePower) return 0; + basePower = this.battle.clampIntRange(basePower, 1); - // happens after crit calculation - basePower = this.runEvent('BasePower', pokemon, target, move, basePower, true); + const level = pokemon.level; - if (!basePower) return 0; - basePower = this.clampIntRange(basePower, 1); + const attacker = pokemon; + const defender = target; + let attackStat: StatNameExceptHP = category === 'Physical' ? 'atk' : 'spa'; + const defenseStat: StatNameExceptHP = defensiveCategory === 'Physical' ? 'def' : 'spd'; + if (move.useSourceDefensiveAsOffensive) { + attackStat = defenseStat; + // Body press really wants to use the def stat, + // so it switches stats to compensate for Wonder Room. + // Of course, the game thus miscalculates the boosts... + if ('wonderroom' in this.battle.field.pseudoWeather) { + if (attackStat === 'def') { + attackStat = 'spd'; + } else if (attackStat === 'spd') { + attackStat = 'def'; + } + if (attacker.boosts['def'] || attacker.boosts['spd']) { + this.battle.hint("Body Press uses Sp. Def boosts when Wonder Room is active."); + } + } + } + if ((move as any).settleBoosted) { + attackStat = 'atk'; + } - const level = pokemon.level; + const statTable = {atk: 'Atk', def: 'Def', spa: 'SpA', spd: 'SpD', spe: 'Spe'}; + let attack; + let defense; - const attacker = pokemon; - const defender = target; - let attackStat: StatNameExceptHP = category === 'Physical' ? 'atk' : 'spa'; - const defenseStat: StatNameExceptHP = defensiveCategory === 'Physical' ? 'def' : 'spd'; - if (move.useSourceDefensiveAsOffensive) { - attackStat = defenseStat; - // Body press really wants to use the def stat, - // so it switches stats to compensate for Wonder Room. - // Of course, the game thus miscalculates the boosts... - if ('wonderroom' in this.field.pseudoWeather) { + let atkBoosts = move.useTargetOffensive ? defender.boosts[attackStat] : attacker.boosts[attackStat]; + if ((move as any).bodyofwaterBoosted) { if (attackStat === 'def') { - attackStat = 'spd'; + atkBoosts = attacker.boosts['atk']; } else if (attackStat === 'spd') { - attackStat = 'def'; - } - if (attacker.boosts['def'] || attacker.boosts['spd']) { - this.hint("Body Press uses Sp. Def boosts when Wonder Room is active."); + atkBoosts = attacker.boosts['spa']; } } - } - if ((move as any).settleBoosted) { - attackStat = 'atk'; - } + let defBoosts = defender.boosts[defenseStat]; - const statTable = {atk: 'Atk', def: 'Def', spa: 'SpA', spd: 'SpD', spe: 'Spe'}; - let attack; - let defense; + let ignoreNegativeOffensive = !!move.ignoreNegativeOffensive; + let ignorePositiveDefensive = !!move.ignorePositiveDefensive; - let atkBoosts = move.useTargetOffensive ? defender.boosts[attackStat] : attacker.boosts[attackStat]; - if ((move as any).bodyofwaterBoosted) { - if (attackStat === 'def') { - atkBoosts = attacker.boosts['atk']; - } else if (attackStat === 'spd') { - atkBoosts = attacker.boosts['spa']; + if (moveHit.crit) { + ignoreNegativeOffensive = true; + ignorePositiveDefensive = true; } - } - let defBoosts = defender.boosts[defenseStat]; + const ignoreOffensive = !!(move.ignoreOffensive || (ignoreNegativeOffensive && atkBoosts < 0)); + const ignoreDefensive = !!(move.ignoreDefensive || (ignorePositiveDefensive && defBoosts > 0)); - let ignoreNegativeOffensive = !!move.ignoreNegativeOffensive; - let ignorePositiveDefensive = !!move.ignorePositiveDefensive; + if (ignoreOffensive) { + this.battle.debug('Negating (sp)atk boost/penalty.'); + atkBoosts = 0; + } + if (ignoreDefensive) { + this.battle.debug('Negating (sp)def boost/penalty.'); + defBoosts = 0; + } - if (moveHit.crit) { - ignoreNegativeOffensive = true; - ignorePositiveDefensive = true; - } - const ignoreOffensive = !!(move.ignoreOffensive || (ignoreNegativeOffensive && atkBoosts < 0)); - const ignoreDefensive = !!(move.ignoreDefensive || (ignorePositiveDefensive && defBoosts > 0)); + if (move.useTargetOffensive) { + attack = defender.calculateStat(attackStat, atkBoosts); + } else { + attack = attacker.calculateStat(attackStat, atkBoosts); + } - if (ignoreOffensive) { - this.debug('Negating (sp)atk boost/penalty.'); - atkBoosts = 0; - } - if (ignoreDefensive) { - this.debug('Negating (sp)def boost/penalty.'); - defBoosts = 0; - } + attackStat = (category === 'Physical' ? 'atk' : 'spa'); + defense = defender.calculateStat(defenseStat, defBoosts); - if (move.useTargetOffensive) { - attack = defender.calculateStat(attackStat, atkBoosts); - } else { - attack = attacker.calculateStat(attackStat, atkBoosts); - } + // Apply Stat Modifiers + attack = this.battle.runEvent('Modify' + statTable[attackStat], attacker, defender, move, attack); + defense = this.battle.runEvent('Modify' + statTable[defenseStat], defender, attacker, move, defense); - attackStat = (category === 'Physical' ? 'atk' : 'spa'); - defense = defender.calculateStat(defenseStat, defBoosts); + if (this.battle.gen <= 4 && ['explosion', 'selfdestruct'].includes(move.id) && defenseStat === 'def') { + defense = this.battle.clampIntRange(Math.floor(defense / 2), 1); + } - // Apply Stat Modifiers - attack = this.runEvent('Modify' + statTable[attackStat], attacker, defender, move, attack); - defense = this.runEvent('Modify' + statTable[defenseStat], defender, attacker, move, defense); + const tr = this.battle.trunc; - if (this.gen <= 4 && ['explosion', 'selfdestruct'].includes(move.id) && defenseStat === 'def') { - defense = this.clampIntRange(Math.floor(defense / 2), 1); - } + // int(int(int(2 * L / 5 + 2) * A * P / D) / 50); + const baseDamage = tr(tr(tr(tr(2 * level / 5 + 2) * basePower * attack) / defense) / 50); - const tr = this.trunc; - - // int(int(int(2 * L / 5 + 2) * A * P / D) / 50); - const baseDamage = tr(tr(tr(tr(2 * level / 5 + 2) * basePower * attack) / defense) / 50); - - // Calculate damage modifiers separately (order differs between generations) - return this.modifyDamage(baseDamage, pokemon, target, move, suppressMessages); + // Calculate damage modifiers separately (order differs between generations) + return this.modifyDamage(baseDamage, pokemon, target, move, suppressMessages); + }, }, pokemon: { diff --git a/data/mods/mixandmega/scripts.ts b/data/mods/mixandmega/scripts.ts index 560e4f678a..70d535a55b 100644 --- a/data/mods/mixandmega/scripts.ts +++ b/data/mods/mixandmega/scripts.ts @@ -7,108 +7,110 @@ export const Scripts: ModdedBattleScriptsData = { this.modData('FormatsData', id).isNonstandard = null; } }, - canMegaEvo(pokemon) { - if (pokemon.species.isMega) return null; + actions: { + canMegaEvo(pokemon) { + if (pokemon.species.isMega) return null; - const item = pokemon.getItem(); - if (item.megaStone) { - if (item.megaStone === pokemon.baseSpecies.name) return null; - return item.megaStone; - } else { - return null; - } - }, - runMegaEvo(pokemon) { - if (pokemon.species.isMega) return false; - - // @ts-ignore - const species: Species = this.getMixedSpecies(pokemon.m.originalSpecies, pokemon.canMegaEvo); - const side = pokemon.side; - - // Pokémon affected by Sky Drop cannot Mega Evolve. Enforce it here for now. - for (const foeActive of side.foe.active) { - if (foeActive.volatiles['skydrop'] && foeActive.volatiles['skydrop'].source === pokemon) { - return false; + const item = pokemon.getItem(); + if (item.megaStone) { + if (item.megaStone === pokemon.baseSpecies.name) return null; + return item.megaStone; + } else { + return null; } - } + }, + runMegaEvo(pokemon) { + if (pokemon.species.isMega) return false; - // Do we have a proper sprite for it? - if (this.dex.getSpecies(pokemon.canMegaEvo!).baseSpecies === pokemon.m.originalSpecies) { - pokemon.formeChange(species, pokemon.getItem(), true); - } else { - const oSpecies = this.dex.getSpecies(pokemon.m.originalSpecies); // @ts-ignore - const oMegaSpecies = this.dex.getSpecies(species.originalMega); - pokemon.formeChange(species, pokemon.getItem(), true); - this.add('-start', pokemon, oMegaSpecies.requiredItem, '[silent]'); - if (oSpecies.types.length !== pokemon.species.types.length || oSpecies.types[1] !== pokemon.species.types[1]) { - this.add('-start', pokemon, 'typechange', pokemon.species.types.join('/'), '[silent]'); - } - } + const species: Species = this.getMixedSpecies(pokemon.m.originalSpecies, pokemon.canMegaEvo); + const side = pokemon.side; - pokemon.canMegaEvo = null; - return true; - }, - getMixedSpecies(originalForme, megaForme) { - const originalSpecies = this.dex.getSpecies(originalForme); - const megaSpecies = this.dex.getSpecies(megaForme); - if (originalSpecies.baseSpecies === megaSpecies.baseSpecies) return megaSpecies; - // @ts-ignore - const deltas = this.getMegaDeltas(megaSpecies); - // @ts-ignore - const species = this.doGetMixedSpecies(originalSpecies, deltas); - return species; - }, - getMegaDeltas(megaSpecies) { - const baseSpecies = this.dex.getSpecies(megaSpecies.baseSpecies); - const deltas: { - ability: string, - baseStats: SparseStatsTable, - weighthg: number, - originalMega: string, - requiredItem: string | undefined, - type?: string, - isMega?: boolean, - } = { - ability: megaSpecies.abilities['0'], - baseStats: {}, - weighthg: megaSpecies.weighthg - baseSpecies.weighthg, - originalMega: megaSpecies.name, - requiredItem: megaSpecies.requiredItem, - }; - let statId: StatName; - for (statId in megaSpecies.baseStats) { - deltas.baseStats[statId] = megaSpecies.baseStats[statId] - baseSpecies.baseStats[statId]; - } - if (megaSpecies.types.length > baseSpecies.types.length) { - deltas.type = megaSpecies.types[1]; - } else if (megaSpecies.types.length < baseSpecies.types.length) { - deltas.type = 'mono'; - } else if (megaSpecies.types[1] !== baseSpecies.types[1]) { - deltas.type = megaSpecies.types[1]; - } - if (megaSpecies.isMega) deltas.isMega = true; - return deltas; - }, - doGetMixedSpecies(speciesOrForme, deltas) { - if (!deltas) throw new TypeError("Must specify deltas!"); - const species = this.dex.deepClone(this.dex.getSpecies(speciesOrForme)); - species.abilities = {'0': deltas.ability}; - if (species.types[0] === deltas.type) { - species.types = [deltas.type]; - } else if (deltas.type === 'mono') { - species.types = [species.types[0]]; - } else if (deltas.type) { - species.types = [species.types[0], deltas.type]; - } - const baseStats = species.baseStats; - for (const statName in baseStats) { - baseStats[statName] = this.clampIntRange(baseStats[statName] + deltas.baseStats[statName], 1, 255); - } - species.weighthg = Math.max(1, species.weighthg + deltas.weighthg); - species.originalMega = deltas.originalMega; - species.requiredItem = deltas.requiredItem; - if (deltas.isMega) species.isMega = true; - return species; + // Pokémon affected by Sky Drop cannot Mega Evolve. Enforce it here for now. + for (const foeActive of side.foe.active) { + if (foeActive.volatiles['skydrop'] && foeActive.volatiles['skydrop'].source === pokemon) { + return false; + } + } + + // Do we have a proper sprite for it? + if (this.dex.getSpecies(pokemon.canMegaEvo!).baseSpecies === pokemon.m.originalSpecies) { + pokemon.formeChange(species, pokemon.getItem(), true); + } else { + const oSpecies = this.dex.getSpecies(pokemon.m.originalSpecies); + // @ts-ignore + const oMegaSpecies = this.dex.getSpecies(species.originalMega); + pokemon.formeChange(species, pokemon.getItem(), true); + this.battle.add('-start', pokemon, oMegaSpecies.requiredItem, '[silent]'); + if (oSpecies.types.length !== pokemon.species.types.length || oSpecies.types[1] !== pokemon.species.types[1]) { + this.battle.add('-start', pokemon, 'typechange', pokemon.species.types.join('/'), '[silent]'); + } + } + + pokemon.canMegaEvo = null; + return true; + }, + getMixedSpecies(originalForme, megaForme) { + const originalSpecies = this.dex.getSpecies(originalForme); + const megaSpecies = this.dex.getSpecies(megaForme); + if (originalSpecies.baseSpecies === megaSpecies.baseSpecies) return megaSpecies; + // @ts-ignore + const deltas = this.getMegaDeltas(megaSpecies); + // @ts-ignore + const species = this.doGetMixedSpecies(originalSpecies, deltas); + return species; + }, + getMegaDeltas(megaSpecies) { + const baseSpecies = this.dex.getSpecies(megaSpecies.baseSpecies); + const deltas: { + ability: string, + baseStats: SparseStatsTable, + weighthg: number, + originalMega: string, + requiredItem: string | undefined, + type?: string, + isMega?: boolean, + } = { + ability: megaSpecies.abilities['0'], + baseStats: {}, + weighthg: megaSpecies.weighthg - baseSpecies.weighthg, + originalMega: megaSpecies.name, + requiredItem: megaSpecies.requiredItem, + }; + let statId: StatName; + for (statId in megaSpecies.baseStats) { + deltas.baseStats[statId] = megaSpecies.baseStats[statId] - baseSpecies.baseStats[statId]; + } + if (megaSpecies.types.length > baseSpecies.types.length) { + deltas.type = megaSpecies.types[1]; + } else if (megaSpecies.types.length < baseSpecies.types.length) { + deltas.type = 'mono'; + } else if (megaSpecies.types[1] !== baseSpecies.types[1]) { + deltas.type = megaSpecies.types[1]; + } + if (megaSpecies.isMega) deltas.isMega = true; + return deltas; + }, + doGetMixedSpecies(speciesOrForme, deltas) { + if (!deltas) throw new TypeError("Must specify deltas!"); + const species = this.dex.deepClone(this.dex.getSpecies(speciesOrForme)); + species.abilities = {'0': deltas.ability}; + if (species.types[0] === deltas.type) { + species.types = [deltas.type]; + } else if (deltas.type === 'mono') { + species.types = [species.types[0]]; + } else if (deltas.type) { + species.types = [species.types[0], deltas.type]; + } + const baseStats = species.baseStats; + for (const statName in baseStats) { + baseStats[statName] = this.battle.clampIntRange(baseStats[statName] + deltas.baseStats[statName], 1, 255); + } + species.weighthg = Math.max(1, species.weighthg + deltas.weighthg); + species.originalMega = deltas.originalMega; + species.requiredItem = deltas.requiredItem; + if (deltas.isMega) species.isMega = true; + return species; + }, }, }; diff --git a/data/mods/mixandmega7/scripts.ts b/data/mods/mixandmega7/scripts.ts index ad62a902df..2ac85ea329 100644 --- a/data/mods/mixandmega7/scripts.ts +++ b/data/mods/mixandmega7/scripts.ts @@ -6,114 +6,116 @@ export const Scripts: ModdedBattleScriptsData = { this.modData('Items', id).onTakeItem = false; } }, - canMegaEvo(pokemon) { - if (pokemon.species.isMega || pokemon.species.isPrimal) return null; + actions: { + canMegaEvo(pokemon) { + if (pokemon.species.isMega || pokemon.species.isPrimal) return null; - const item = pokemon.getItem(); - if (item.megaStone) { - if (item.megaStone === pokemon.name) return null; - return item.megaStone; - } else if (pokemon.baseMoves.includes('dragonascent' as ID)) { - return 'Rayquaza-Mega'; - } else { - return null; - } - }, - runMegaEvo(pokemon) { - if (pokemon.species.isMega || pokemon.species.isPrimal) return false; - - const isUltraBurst = !pokemon.canMegaEvo; - // @ts-ignore - const species: Species = this.getMixedSpecies(pokemon.m.originalSpecies, pokemon.canMegaEvo || pokemon.canUltraBurst); - const side = pokemon.side; - - // Pokémon affected by Sky Drop cannot Mega Evolve. Enforce it here for now. - for (const foeActive of side.foe.active) { - if (foeActive.volatiles['skydrop'] && foeActive.volatiles['skydrop'].source === pokemon) { - return false; + const item = pokemon.getItem(); + if (item.megaStone) { + if (item.megaStone === pokemon.name) return null; + return item.megaStone; + } else if (pokemon.baseMoves.includes('dragonascent' as ID)) { + return 'Rayquaza-Mega'; + } else { + return null; } - } + }, + runMegaEvo(pokemon) { + if (pokemon.species.isMega || pokemon.species.isPrimal) return false; - // Do we have a proper sprite for it? - // @ts-ignore assert non-null pokemon.canMegaEvo - if (isUltraBurst || this.dex.getSpecies(pokemon.canMegaEvo).baseSpecies === pokemon.m.originalSpecies) { - pokemon.formeChange(species, pokemon.getItem(), true); - } else { - const oSpecies = this.dex.getSpecies(pokemon.m.originalSpecies); + const isUltraBurst = !pokemon.canMegaEvo; // @ts-ignore - const oMegaSpecies = this.dex.getSpecies(species.originalMega); - pokemon.formeChange(species, pokemon.getItem(), true); - this.add('-start', pokemon, oMegaSpecies.requiredItem || oMegaSpecies.requiredMove, '[silent]'); - if (oSpecies.types.length !== pokemon.species.types.length || oSpecies.types[1] !== pokemon.species.types[1]) { - this.add('-start', pokemon, 'typechange', pokemon.species.types.join('/'), '[silent]'); - } - } + const species: Species = this.getMixedSpecies(pokemon.m.originalSpecies, pokemon.canMegaEvo || pokemon.canUltraBurst); + const side = pokemon.side; - pokemon.canMegaEvo = null; - if (isUltraBurst) pokemon.canUltraBurst = null; - return true; - }, - getMixedSpecies(originalSpecies, megaSpecies) { - const oSpecies = this.dex.getSpecies(originalSpecies); - const mSpecies = this.dex.getSpecies(megaSpecies); - if (oSpecies.baseSpecies === mSpecies.baseSpecies) return mSpecies; - // @ts-ignore - const deltas = this.getMegaDeltas(mSpecies); - // @ts-ignore - const species = this.doGetMixedSpecies(oSpecies, deltas); - return species; - }, - getMegaDeltas(megaSpecies) { - const baseSpecies = this.dex.getSpecies(megaSpecies.baseSpecies); - const deltas: { - ability: string, - baseStats: SparseStatsTable, - weighthg: number, - originalMega: string, - requiredItem: string | undefined, - type?: string, - isMega?: boolean, - isPrimal?: boolean, - } = { - ability: megaSpecies.abilities['0'], - baseStats: {}, - weighthg: megaSpecies.weighthg - baseSpecies.weighthg, - originalMega: megaSpecies.name, - requiredItem: megaSpecies.requiredItem, - }; - let stat: StatName; - for (stat in megaSpecies.baseStats) { - deltas.baseStats[stat] = megaSpecies.baseStats[stat] - baseSpecies.baseStats[stat]; - } - if (megaSpecies.types.length > baseSpecies.types.length) { - deltas.type = megaSpecies.types[1]; - } else if (megaSpecies.types.length < baseSpecies.types.length) { - deltas.type = baseSpecies.types[0]; - } else if (megaSpecies.types[1] !== baseSpecies.types[1]) { - deltas.type = megaSpecies.types[1]; - } - if (megaSpecies.isMega) deltas.isMega = true; - if (megaSpecies.isPrimal) deltas.isPrimal = true; - return deltas; - }, - doGetMixedSpecies(speciesOrSpeciesName, deltas) { - if (!deltas) throw new TypeError("Must specify deltas!"); - const species = this.dex.deepClone(this.dex.getSpecies(speciesOrSpeciesName)); - species.abilities = {'0': deltas.ability}; - if (species.types[0] === deltas.type) { - species.types = [deltas.type]; - } else if (deltas.type) { - species.types = [species.types[0], deltas.type]; - } - const baseStats = species.baseStats; - for (const statName in baseStats) { - baseStats[statName] = this.clampIntRange(baseStats[statName] + deltas.baseStats[statName], 1, 255); - } - species.weighthg = Math.max(1, species.weighthg + deltas.weighthg); - species.originalMega = deltas.originalMega; - species.requiredItem = deltas.requiredItem; - if (deltas.isMega) species.isMega = true; - if (deltas.isPrimal) species.isPrimal = true; - return species; + // Pokémon affected by Sky Drop cannot Mega Evolve. Enforce it here for now. + for (const foeActive of side.foe.active) { + if (foeActive.volatiles['skydrop'] && foeActive.volatiles['skydrop'].source === pokemon) { + return false; + } + } + + // Do we have a proper sprite for it? + // @ts-ignore assert non-null pokemon.canMegaEvo + if (isUltraBurst || this.dex.getSpecies(pokemon.canMegaEvo).baseSpecies === pokemon.m.originalSpecies) { + pokemon.formeChange(species, pokemon.getItem(), true); + } else { + const oSpecies = this.dex.getSpecies(pokemon.m.originalSpecies); + // @ts-ignore + const oMegaSpecies = this.dex.getSpecies(species.originalMega); + pokemon.formeChange(species, pokemon.getItem(), true); + this.battle.add('-start', pokemon, oMegaSpecies.requiredItem || oMegaSpecies.requiredMove, '[silent]'); + if (oSpecies.types.length !== pokemon.species.types.length || oSpecies.types[1] !== pokemon.species.types[1]) { + this.battle.add('-start', pokemon, 'typechange', pokemon.species.types.join('/'), '[silent]'); + } + } + + pokemon.canMegaEvo = null; + if (isUltraBurst) pokemon.canUltraBurst = null; + return true; + }, + getMixedSpecies(originalSpecies, megaSpecies) { + const oSpecies = this.dex.getSpecies(originalSpecies); + const mSpecies = this.dex.getSpecies(megaSpecies); + if (oSpecies.baseSpecies === mSpecies.baseSpecies) return mSpecies; + // @ts-ignore + const deltas = this.getMegaDeltas(mSpecies); + // @ts-ignore + const species = this.doGetMixedSpecies(oSpecies, deltas); + return species; + }, + getMegaDeltas(megaSpecies) { + const baseSpecies = this.dex.getSpecies(megaSpecies.baseSpecies); + const deltas: { + ability: string, + baseStats: SparseStatsTable, + weighthg: number, + originalMega: string, + requiredItem: string | undefined, + type?: string, + isMega?: boolean, + isPrimal?: boolean, + } = { + ability: megaSpecies.abilities['0'], + baseStats: {}, + weighthg: megaSpecies.weighthg - baseSpecies.weighthg, + originalMega: megaSpecies.name, + requiredItem: megaSpecies.requiredItem, + }; + let stat: StatName; + for (stat in megaSpecies.baseStats) { + deltas.baseStats[stat] = megaSpecies.baseStats[stat] - baseSpecies.baseStats[stat]; + } + if (megaSpecies.types.length > baseSpecies.types.length) { + deltas.type = megaSpecies.types[1]; + } else if (megaSpecies.types.length < baseSpecies.types.length) { + deltas.type = baseSpecies.types[0]; + } else if (megaSpecies.types[1] !== baseSpecies.types[1]) { + deltas.type = megaSpecies.types[1]; + } + if (megaSpecies.isMega) deltas.isMega = true; + if (megaSpecies.isPrimal) deltas.isPrimal = true; + return deltas; + }, + doGetMixedSpecies(speciesOrSpeciesName, deltas) { + if (!deltas) throw new TypeError("Must specify deltas!"); + const species = this.dex.deepClone(this.dex.getSpecies(speciesOrSpeciesName)); + species.abilities = {'0': deltas.ability}; + if (species.types[0] === deltas.type) { + species.types = [deltas.type]; + } else if (deltas.type) { + species.types = [species.types[0], deltas.type]; + } + const baseStats = species.baseStats; + for (const statName in baseStats) { + baseStats[statName] = this.battle.clampIntRange(baseStats[statName] + deltas.baseStats[statName], 1, 255); + } + species.weighthg = Math.max(1, species.weighthg + deltas.weighthg); + species.originalMega = deltas.originalMega; + species.requiredItem = deltas.requiredItem; + if (deltas.isMega) species.isMega = true; + if (deltas.isPrimal) species.isPrimal = true; + return species; + }, }, }; diff --git a/data/mods/ssb/abilities.ts b/data/mods/ssb/abilities.ts index c2164dc219..c01114aca1 100644 --- a/data/mods/ssb/abilities.ts +++ b/data/mods/ssb/abilities.ts @@ -340,7 +340,7 @@ export const Abilities: {[k: string]: ModdedAbilityData} = { newMove.hasBounced = true; newMove.pranksterBoosted = false; this.add('-ability', target, 'Carefree'); - this.useMove(newMove, target, source); + this.actions.useMove(newMove, target, source); return null; }, onAllyTryHitSide(target, source, move) { @@ -351,7 +351,7 @@ export const Abilities: {[k: string]: ModdedAbilityData} = { newMove.hasBounced = true; newMove.pranksterBoosted = false; this.add('-ability', target, 'Carefree'); - this.useMove(newMove, this.effectData.target, source); + this.actions.useMove(newMove, this.effectData.target, source); return null; }, condition: { @@ -407,7 +407,7 @@ export const Abilities: {[k: string]: ModdedAbilityData} = { newMove.hasBounced = true; newMove.pranksterBoosted = false; this.add('-ability', target, 'Magic Hat'); - this.useMove(newMove, target, source); + this.actions.useMove(newMove, target, source); return null; }, onAllyTryHitSide(target, source, move) { @@ -418,7 +418,7 @@ export const Abilities: {[k: string]: ModdedAbilityData} = { newMove.hasBounced = true; newMove.pranksterBoosted = false; this.add('-ability', target, 'Magic Hat'); - this.useMove(newMove, this.effectData.target, source); + this.actions.useMove(newMove, this.effectData.target, source); return null; }, condition: { @@ -1869,7 +1869,7 @@ export const Abilities: {[k: string]: ModdedAbilityData} = { if (target.species.id.includes('aggron') && !target.illusion && !target.transformed) { this.boost({atk: 1}, target); if (target.species.name !== 'Aggron') return; - this.runMegaEvo(target); + this.actions.runMegaEvo(target); } } }, diff --git a/data/mods/ssb/moves.ts b/data/mods/ssb/moves.ts index e1f76415e1..3979215229 100644 --- a/data/mods/ssb/moves.ts +++ b/data/mods/ssb/moves.ts @@ -2215,7 +2215,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { this.add('-activate', pokemon, 'move: The Hunt is On!'); alreadyAdded = true; } - this.runMove('thehuntison', source, this.getTargetLoc(pokemon, source)); + this.actions.runMove('thehuntison', source, this.getTargetLoc(pokemon, source)); } }, }, @@ -2967,12 +2967,12 @@ export const Moves: {[k: string]: ModdedMoveData} = { // fruit this move. onHit(target, source) { for (const move of ['Haze', 'Worry Seed', 'Poison Powder', 'Stun Spore', 'Leech Seed']) { - this.useMove(move, source); + this.actions.useMove(move, source); this.add(`c|${getName('Meicoo')}|That is not the answer - try again!`); } const strgl = this.dex.getActiveMove('Struggle'); strgl.basePower = 150; - this.useMove(strgl, source); + this.actions.useMove(strgl, source); this.add(`c|${getName('Meicoo')}|That is not the answer - try again!`); }, secondary: null, @@ -3170,13 +3170,13 @@ export const Moves: {[k: string]: ModdedMoveData} = { const hax = this.sample(['slp', 'brn', 'par', 'tox']); target.trySetStatus(hax, source); if (hax === 'slp') { - this.useMove('Dream Eater', source); + this.actions.useMove('Dream Eater', source); } else if (hax === 'par') { - this.useMove('Iron Head', source); + this.actions.useMove('Iron Head', source); } else if (hax === 'brn') { - this.useMove('Fire Blast', source); + this.actions.useMove('Fire Blast', source); } else if (hax === 'tox') { - this.useMove('Venoshock', source); + this.actions.useMove('Venoshock', source); } }, secondary: null, @@ -3393,7 +3393,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { this.add('-anim', source, "Celebrate", target); }, onTryHit(target, source) { - this.useMove('Substitute', source); + this.actions.useMove('Substitute', source); }, onHit(target, source) { target.trySetStatus('brn', source); @@ -3958,7 +3958,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { }, onHit(target, source) { if (source.species.id === 'charizard') { - this.runMegaEvo(source); + this.actions.runMegaEvo(source); } }, secondary: null, @@ -4687,7 +4687,7 @@ export const Moves: {[k: string]: ModdedMoveData} = { for (let x = 1; x <= randomTurns; x++) { const randomMove = this.sample(supportMoves); supportMoves.splice(supportMoves.indexOf(randomMove), 1); - this.useMove(randomMove, target); + this.actions.useMove(randomMove, target); successes++; } if (successes === 1) { @@ -4940,19 +4940,19 @@ export const Moves: {[k: string]: ModdedMoveData} = { source.m.yukiCosplayForme = this.sample(formes); switch (source.m.yukiCosplayForme) { case 'Cleric': - this.useMove("Strength Sap", source); + this.actions.useMove("Strength Sap", source); break; case 'Ninja': - this.useMove("Confuse Ray", source); + this.actions.useMove("Confuse Ray", source); break; case 'Dancer': - this.useMove("Feather Dance", source); + this.actions.useMove("Feather Dance", source); break; case 'Songstress': - this.useMove("Sing", source); + this.actions.useMove("Sing", source); break; case 'Jester': - this.useMove("Charm", source); + this.actions.useMove("Charm", source); break; } }, @@ -5073,10 +5073,10 @@ export const Moves: {[k: string]: ModdedMoveData} = { }, onTry(pokemon, target) { pokemon.addVolatile('bigstormcomingmod'); - this.useMove("Hurricane", pokemon); - this.useMove("Thunder", pokemon); - this.useMove("Blizzard", pokemon); - this.useMove("Weather Ball", pokemon); + this.actions.useMove("Hurricane", pokemon); + this.actions.useMove("Thunder", pokemon); + this.actions.useMove("Blizzard", pokemon); + this.actions.useMove("Weather Ball", pokemon); }, secondary: null, target: "normal", diff --git a/data/mods/ssb/scripts.ts b/data/mods/ssb/scripts.ts index 12f4a1b334..16b4445d6c 100644 --- a/data/mods/ssb/scripts.ts +++ b/data/mods/ssb/scripts.ts @@ -1,868 +1,869 @@ export const Scripts: ModdedBattleScriptsData = { inherit: 'gen8', - // 1 mega per pokemon - runMegaEvo(pokemon) { - if (pokemon.name === 'Struchni' && pokemon.species.name === 'Aggron') pokemon.canMegaEvo = 'Aggron-Mega'; - if (pokemon.name === 'Raj.Shoot' && pokemon.species.name === 'Charizard') pokemon.canMegaEvo = 'Charizard-Mega-X'; - const speciesid = pokemon.canMegaEvo || pokemon.canUltraBurst; - if (!speciesid) return false; - const side = pokemon.side; + actions: { + // 1 mega per pokemon + runMegaEvo(pokemon) { + if (pokemon.name === 'Struchni' && pokemon.species.name === 'Aggron') pokemon.canMegaEvo = 'Aggron-Mega'; + if (pokemon.name === 'Raj.Shoot' && pokemon.species.name === 'Charizard') pokemon.canMegaEvo = 'Charizard-Mega-X'; + const speciesid = pokemon.canMegaEvo || pokemon.canUltraBurst; + if (!speciesid) return false; + const side = pokemon.side; - // Pokémon affected by Sky Drop cannot mega evolve. Enforce it here for now. - for (const foeActive of side.foe.active) { - if (foeActive.volatiles['skydrop'] && foeActive.volatiles['skydrop'].source === pokemon) { - return false; + // Pokémon affected by Sky Drop cannot mega evolve. Enforce it here for now. + for (const foeActive of side.foe.active) { + if (foeActive.volatiles['skydrop'] && foeActive.volatiles['skydrop'].source === pokemon) { + return false; + } } - } - pokemon.formeChange(speciesid, pokemon.getItem(), true); - if (pokemon.canMegaEvo) { - pokemon.canMegaEvo = null; - } else { - pokemon.canUltraBurst = null; - } - - this.runEvent('AfterMega', pokemon); - - if (['Kaiju Bunny', 'Overneat', 'EpicNikolai'].includes(pokemon.name) && !pokemon.illusion) { - this.add('-start', pokemon, 'typechange', pokemon.types.join('/')); - } - - this.add('-ability', pokemon, `${pokemon.getAbility().name}`); - - return true; - }, - - // Modded for Mega Rayquaza - canMegaEvo(pokemon) { - const species = pokemon.baseSpecies; - const altForme = species.otherFormes && this.dex.getSpecies(species.otherFormes[0]); - const item = pokemon.getItem(); - // Mega Rayquaza - if (altForme?.isMega && altForme?.requiredMove && - pokemon.baseMoves.includes(this.toID(altForme.requiredMove)) && !item.zMove) { - return altForme.name; - } - // a hacked-in Megazard X can mega evolve into Megazard Y, but not into Megazard X - if (item.megaEvolves === species.baseSpecies && item.megaStone !== species.name) { - return item.megaStone; - } - return null; - }, - - // 1 Z per pokemon - canZMove(pokemon) { - if (pokemon.m.zMoveUsed || - (pokemon.transformed && - (pokemon.species.isMega || pokemon.species.isPrimal || pokemon.species.forme === "Ultra")) - ) return; - const item = pokemon.getItem(); - if (!item.zMove) return; - if (item.itemUser && !item.itemUser.includes(pokemon.species.name)) return; - let atLeastOne = false; - let mustStruggle = true; - const zMoves: ZMoveOptions = []; - for (const moveSlot of pokemon.moveSlots) { - if (moveSlot.pp <= 0) { - zMoves.push(null); - continue; - } - if (!moveSlot.disabled) { - mustStruggle = false; - } - const move = this.dex.getMove(moveSlot.move); - let zMoveName = this.getZMove(move, pokemon, true) || ''; - if (zMoveName) { - const zMove = this.dex.getMove(zMoveName); - if (!zMove.isZ && zMove.category === 'Status') zMoveName = "Z-" + zMoveName; - zMoves.push({move: zMoveName, target: zMove.target}); + pokemon.formeChange(speciesid, pokemon.getItem(), true); + if (pokemon.canMegaEvo) { + pokemon.canMegaEvo = null; } else { - zMoves.push(null); + pokemon.canUltraBurst = null; } - if (zMoveName) atLeastOne = true; - } - if (atLeastOne && !mustStruggle) return zMoves; - }, - getZMove(move, pokemon, skipChecks) { - const item = pokemon.getItem(); - if (!skipChecks) { - if (pokemon.m.zMoveUsed) return; + this.battle.runEvent('AfterMega', pokemon); + + if (['Kaiju Bunny', 'Overneat', 'EpicNikolai'].includes(pokemon.name) && !pokemon.illusion) { + this.battle.add('-start', pokemon, 'typechange', pokemon.types.join('/')); + } + + this.battle.add('-ability', pokemon, `${pokemon.getAbility().name}`); + + return true; + }, + + // Modded for Mega Rayquaza + canMegaEvo(pokemon) { + const species = pokemon.baseSpecies; + const altForme = species.otherFormes && this.dex.getSpecies(species.otherFormes[0]); + const item = pokemon.getItem(); + // Mega Rayquaza + if (altForme?.isMega && altForme?.requiredMove && + pokemon.baseMoves.includes(this.battle.toID(altForme.requiredMove)) && !item.zMove) { + return altForme.name; + } + // a hacked-in Megazard X can mega evolve into Megazard Y, but not into Megazard X + if (item.megaEvolves === species.baseSpecies && item.megaStone !== species.name) { + return item.megaStone; + } + return null; + }, + + // 1 Z per pokemon + canZMove(pokemon) { + if (pokemon.m.zMoveUsed || + (pokemon.transformed && + (pokemon.species.isMega || pokemon.species.isPrimal || pokemon.species.forme === "Ultra")) + ) return; + const item = pokemon.getItem(); if (!item.zMove) return; if (item.itemUser && !item.itemUser.includes(pokemon.species.name)) return; - const moveData = pokemon.getMoveData(move); - // Draining the PP of the base move prevents the corresponding Z-move from being used. - if (!moveData?.pp) return; - } - - if (move.name === item.zMoveFrom) { - return item.zMove as string; - } else if (item.zMove === true && move.type === item.zMoveType) { - if (move.category === "Status") { - return move.name; - } else if (move.zMove?.basePower) { - return this.zMoveTable[move.type]; + let atLeastOne = false; + let mustStruggle = true; + const zMoves: ZMoveOptions = []; + for (const moveSlot of pokemon.moveSlots) { + if (moveSlot.pp <= 0) { + zMoves.push(null); + continue; + } + if (!moveSlot.disabled) { + mustStruggle = false; + } + const move = this.dex.getMove(moveSlot.move); + let zMoveName = this.getZMove(move, pokemon, true) || ''; + if (zMoveName) { + const zMove = this.dex.getMove(zMoveName); + if (!zMove.isZ && zMove.category === 'Status') zMoveName = "Z-" + zMoveName; + zMoves.push({move: zMoveName, target: zMove.target}); + } else { + zMoves.push(null); + } + if (zMoveName) atLeastOne = true; } - } - }, + if (atLeastOne && !mustStruggle) return zMoves; + }, - runMove(moveOrMoveName, pokemon, targetLoc, sourceEffect, zMove, externalMove, maxMove, originalTarget) { - pokemon.activeMoveActions++; - let target = this.getTarget(pokemon, maxMove || zMove || moveOrMoveName, targetLoc, originalTarget); - let baseMove = this.dex.getActiveMove(moveOrMoveName); - const pranksterBoosted = baseMove.pranksterBoosted; - if (baseMove.id !== 'struggle' && !zMove && !maxMove && !externalMove) { - const changedMove = this.runEvent('OverrideAction', pokemon, target, baseMove); - if (changedMove && changedMove !== true) { - baseMove = this.dex.getActiveMove(changedMove); - if (pranksterBoosted) baseMove.pranksterBoosted = pranksterBoosted; - target = this.getRandomTarget(pokemon, baseMove); + getZMove(move, pokemon, skipChecks) { + const item = pokemon.getItem(); + if (!skipChecks) { + if (pokemon.m.zMoveUsed) return; + if (!item.zMove) return; + if (item.itemUser && !item.itemUser.includes(pokemon.species.name)) return; + const moveData = pokemon.getMoveData(move); + // Draining the PP of the base move prevents the corresponding Z-move from being used. + if (!moveData?.pp) return; } - } - let move = baseMove; - if (zMove) { - move = this.getActiveZMove(baseMove, pokemon); - } else if (maxMove) { - move = this.getActiveMaxMove(baseMove, pokemon); - } - move.isExternal = externalMove; + if (move.name === item.zMoveFrom) { + return item.zMove as string; + } else if (item.zMove === true && move.type === item.zMoveType) { + if (move.category === "Status") { + return move.name; + } else if (move.zMove?.basePower) { + return this.Z_MOVES[move.type]; + } + } + }, - this.setActiveMove(move, pokemon, target); + runMove(moveOrMoveName, pokemon, targetLoc, sourceEffect, zMove, externalMove, maxMove, originalTarget) { + pokemon.activeMoveActions++; + let target = this.battle.getTarget(pokemon, maxMove || zMove || moveOrMoveName, targetLoc, originalTarget); + let baseMove = this.dex.getActiveMove(moveOrMoveName); + const pranksterBoosted = baseMove.pranksterBoosted; + if (baseMove.id !== 'struggle' && !zMove && !maxMove && !externalMove) { + const changedMove = this.battle.runEvent('OverrideAction', pokemon, target, baseMove); + if (changedMove && changedMove !== true) { + baseMove = this.dex.getActiveMove(changedMove); + if (pranksterBoosted) baseMove.pranksterBoosted = pranksterBoosted; + target = this.battle.getRandomTarget(pokemon, baseMove); + } + } + let move = baseMove; + if (zMove) { + move = this.getActiveZMove(baseMove, pokemon); + } else if (maxMove) { + move = this.getActiveMaxMove(baseMove, pokemon); + } - /* if (pokemon.moveThisTurn) { - // THIS IS PURELY A SANITY CHECK - // DO NOT TAKE ADVANTAGE OF THIS TO PREVENT A POKEMON FROM MOVING; - // USE this.queue.cancelMove INSTEAD - this.debug('' + pokemon.id + ' INCONSISTENT STATE, ALREADY MOVED: ' + pokemon.moveThisTurn); - this.clearActiveMove(true); - return; - } */ - const willTryMove = this.runEvent('BeforeMove', pokemon, target, move); - if (!willTryMove) { - this.runEvent('MoveAborted', pokemon, target, move); - this.clearActiveMove(true); - // The event 'BeforeMove' could have returned false or null - // false indicates that this counts as a move failing for the purpose of calculating Stomping Tantrum's base power - // null indicates the opposite, as the Pokemon didn't have an option to choose anything - pokemon.moveThisTurnResult = willTryMove; - return; - } - if (move.beforeMoveCallback) { - if (move.beforeMoveCallback.call(this, pokemon, target, move)) { - this.clearActiveMove(true); - pokemon.moveThisTurnResult = false; + move.isExternal = externalMove; + + this.battle.setActiveMove(move, pokemon, target); + + /* if (pokemon.moveThisTurn) { + // THIS IS PURELY A SANITY CHECK + // DO NOT TAKE ADVANTAGE OF THIS TO PREVENT A POKEMON FROM MOVING; + // USE this.battle.queue.cancelMove INSTEAD + this.battle.debug('' + pokemon.id + ' INCONSISTENT STATE, ALREADY MOVED: ' + pokemon.moveThisTurn); + this.battle.clearActiveMove(true); + return; + } */ + const willTryMove = this.battle.runEvent('BeforeMove', pokemon, target, move); + if (!willTryMove) { + this.battle.runEvent('MoveAborted', pokemon, target, move); + this.battle.clearActiveMove(true); + // The event 'BeforeMove' could have returned false or null + // false indicates that this counts as a move failing for the purpose of calculating Stomping Tantrum's base power + // null indicates the opposite, as the Pokemon didn't have an option to choose anything + pokemon.moveThisTurnResult = willTryMove; return; } - } - pokemon.lastDamage = 0; - let lockedMove; - if (!externalMove) { - lockedMove = this.runEvent('LockMove', pokemon); - if (lockedMove === true) lockedMove = false; - if (!lockedMove) { - if (!pokemon.deductPP(baseMove, null, target) && (move.id !== 'struggle')) { - this.add('cant', pokemon, 'nopp', move); - const gameConsole = [ - null, 'Game Boy', 'Game Boy Color', 'Game Boy Advance', 'DS', 'DS', '3DS', '3DS', - ][this.gen] || 'Switch'; - this.hint(`This is not a bug, this is really how it works on the ${gameConsole}; try it yourself if you don't believe us.`); - this.clearActiveMove(true); + if (move.beforeMoveCallback) { + if (move.beforeMoveCallback.call(this.battle, pokemon, target, move)) { + this.battle.clearActiveMove(true); pokemon.moveThisTurnResult = false; return; } - } else { - sourceEffect = this.dex.getEffect('lockedmove'); } - pokemon.moveUsed(move, targetLoc); - } - - // Dancer Petal Dance hack - // TODO: implement properly - const noLock = externalMove && !pokemon.volatiles['lockedmove']; - - if (zMove) { - if (pokemon.illusion) { - this.singleEvent('End', this.dex.getAbility('Illusion'), pokemon.abilityData, pokemon); + pokemon.lastDamage = 0; + let lockedMove; + if (!externalMove) { + lockedMove = this.battle.runEvent('LockMove', pokemon); + if (lockedMove === true) lockedMove = false; + if (!lockedMove) { + if (!pokemon.deductPP(baseMove, null, target) && (move.id !== 'struggle')) { + this.battle.add('cant', pokemon, 'nopp', move); + const gameConsole = [ + null, 'Game Boy', 'Game Boy Color', 'Game Boy Advance', 'DS', 'DS', '3DS', '3DS', + ][this.battle.gen] || 'Switch'; + this.battle.hint(`This is not a bug, this is really how it works on the ${gameConsole}; try it yourself if you don't believe us.`); + this.battle.clearActiveMove(true); + pokemon.moveThisTurnResult = false; + return; + } + } else { + sourceEffect = this.dex.getEffect('lockedmove'); + } + pokemon.moveUsed(move, targetLoc); } - this.add('-zpower', pokemon); - // In SSB Z-Moves are limited to 1 per pokemon. - pokemon.m.zMoveUsed = true; - } - const moveDidSomething = this.useMove(baseMove, pokemon, target, sourceEffect, zMove, maxMove); - if (this.activeMove) move = this.activeMove; - this.singleEvent('AfterMove', move, null, pokemon, target, move); - this.runEvent('AfterMove', pokemon, target, move); - // Dancer's activation order is completely different from any other event, so it's handled separately - if (move.flags['dance'] && moveDidSomething && !move.isExternal) { - const dancers = []; - for (const currentPoke of this.getAllActive()) { - if (pokemon === currentPoke) continue; - if (currentPoke.hasAbility('dancer') && !currentPoke.isSemiInvulnerable()) { - dancers.push(currentPoke); + // Dancer Petal Dance hack + // TODO: implement properly + const noLock = externalMove && !pokemon.volatiles['lockedmove']; + + if (zMove) { + if (pokemon.illusion) { + this.battle.singleEvent('End', this.dex.getAbility('Illusion'), pokemon.abilityData, pokemon); + } + this.battle.add('-zpower', pokemon); + // In SSB Z-Moves are limited to 1 per pokemon. + pokemon.m.zMoveUsed = true; + } + const moveDidSomething = this.battle.actions.useMove(baseMove, pokemon, target, sourceEffect, zMove, maxMove); + if (this.battle.activeMove) move = this.battle.activeMove; + this.battle.singleEvent('AfterMove', move, null, pokemon, target, move); + this.battle.runEvent('AfterMove', pokemon, target, move); + + // Dancer's activation order is completely different from any other event, so it's handled separately + if (move.flags['dance'] && moveDidSomething && !move.isExternal) { + const dancers = []; + for (const currentPoke of this.battle.getAllActive()) { + if (pokemon === currentPoke) continue; + if (currentPoke.hasAbility('dancer') && !currentPoke.isSemiInvulnerable()) { + dancers.push(currentPoke); + } + } + // Dancer activates in order of lowest speed stat to highest + // Note that the speed stat used is after any volatile replacements like Speed Swap, + // but before any multipliers like Agility or Choice Scarf + // Ties go to whichever Pokemon has had the ability for the least amount of time + dancers.sort( + (a, b) => -(b.storedStats['spe'] - a.storedStats['spe']) || b.abilityOrder - a.abilityOrder + ); + for (const dancer of dancers) { + if (this.battle.faintMessages()) break; + if (dancer.fainted) continue; + this.battle.add('-activate', dancer, 'ability: Dancer'); + const dancersTarget = target!.side !== dancer.side && pokemon.side === dancer.side ? target! : pokemon; + const dancersTargetLoc = this.battle.getTargetLoc(dancersTarget, dancer); + this.runMove(move.id, dancer, dancersTargetLoc, this.dex.getAbility('dancer'), undefined, true); } } - // Dancer activates in order of lowest speed stat to highest - // Note that the speed stat used is after any volatile replacements like Speed Swap, - // but before any multipliers like Agility or Choice Scarf - // Ties go to whichever Pokemon has had the ability for the least amount of time - dancers.sort( - (a, b) => -(b.storedStats['spe'] - a.storedStats['spe']) || b.abilityOrder - a.abilityOrder - ); - for (const dancer of dancers) { - if (this.faintMessages()) break; - if (dancer.fainted) continue; - this.add('-activate', dancer, 'ability: Dancer'); - // @ts-ignore - the Dancer ability can't trigger on a move where target is null because it does not copy failed moves. - const dancersTarget = target.side !== dancer.side && pokemon.side === dancer.side ? target : pokemon; - // @ts-ignore - this.runMove(move.id, dancer, this.getTargetLoc(dancersTarget, dancer), this.dex.getAbility('dancer'), undefined, true); - } - } - if (noLock && pokemon.volatiles['lockedmove']) delete pokemon.volatiles['lockedmove']; - }, + if (noLock && pokemon.volatiles['lockedmove']) delete pokemon.volatiles['lockedmove']; + }, - // Dollar Store Brand prankster immunity implementation - hitStepTryImmunity(targets, pokemon, move) { - const hitResults = []; - for (const [i, target] of targets.entries()) { - if (this.gen >= 6 && move.flags['powder'] && target !== pokemon && !this.dex.getImmunity('powder', target)) { - this.debug('natural powder immunity'); - this.add('-immune', target); - hitResults[i] = false; - } else if (!this.singleEvent('TryImmunity', move, {}, target, pokemon, move)) { - this.add('-immune', target); - hitResults[i] = false; - } else if (this.gen >= 7 && move.pranksterBoosted && - // eslint-disable-next-line max-len - (pokemon.hasAbility('prankster') || pokemon.hasAbility('plausibledeniability') || pokemon.volatiles['nol']) && - targets[i].side !== pokemon.side && !this.dex.getImmunity('prankster', target)) { - this.debug('natural prankster immunity'); - if (!target.illusion) this.hint("Since gen 7, Dark is immune to Prankster moves."); - this.add('-immune', target); - hitResults[i] = false; - } else { - hitResults[i] = true; - } - } - return hitResults; - }, - - // For Jett's The Hunt is On! - useMoveInner(moveOrMoveName, pokemon, target, sourceEffect, zMove, maxMove) { - if (!sourceEffect && this.effect.id) sourceEffect = this.effect; - if (sourceEffect && ['instruct', 'custapberry'].includes(sourceEffect.id)) sourceEffect = null; - - let move = this.dex.getActiveMove(moveOrMoveName); - if (move.id === 'weatherball' && zMove) { - // Z-Weather Ball only changes types if it's used directly, - // not if it's called by Z-Sleep Talk or something. - this.singleEvent('ModifyType', move, null, pokemon, target, move, move); - if (move.type !== 'Normal') sourceEffect = move; - } - if (zMove || (move.category !== 'Status' && sourceEffect && (sourceEffect as ActiveMove).isZ)) { - move = this.getActiveZMove(move, pokemon); - } - if (maxMove && move.category !== 'Status') { - // Max move outcome is dependent on the move type after type modifications from ability and the move itself - this.singleEvent('ModifyType', move, null, pokemon, target, move, move); - this.runEvent('ModifyType', pokemon, target, move, move); - } - if (maxMove || (move.category !== 'Status' && sourceEffect && (sourceEffect as ActiveMove).isMax)) { - move = this.getActiveMaxMove(move, pokemon); - } - - if (this.activeMove) { - move.priority = this.activeMove.priority; - if (!move.hasBounced) move.pranksterBoosted = this.activeMove.pranksterBoosted; - } - const baseTarget = move.target; - if (target === undefined) target = this.getRandomTarget(pokemon, move); - if (move.target === 'self' || move.target === 'allies') { - target = pokemon; - } - if (sourceEffect) { - move.sourceEffect = sourceEffect.id; - move.ignoreAbility = false; - } - let moveResult = false; - - this.setActiveMove(move, pokemon, target); - - this.singleEvent('ModifyType', move, null, pokemon, target, move, move); - this.singleEvent('ModifyMove', move, null, pokemon, target, move, move); - if (baseTarget !== move.target) { - // Target changed in ModifyMove, so we must adjust it here - // Adjust before the next event so the correct target is passed to the - // event - target = this.getRandomTarget(pokemon, move); - } - move = this.runEvent('ModifyType', pokemon, target, move, move); - move = this.runEvent('ModifyMove', pokemon, target, move, move); - if (baseTarget !== move.target) { - // Adjust again - target = this.getRandomTarget(pokemon, move); - } - if (!move || pokemon.fainted) { - return false; - } - - let attrs = ''; - - let movename = move.name; - if (move.id === 'hiddenpower') movename = 'Hidden Power'; - if (sourceEffect) attrs += '|[from]' + this.dex.getEffect(sourceEffect); - if (zMove && move.isZ === true) { - attrs = '|[anim]' + movename + attrs; - movename = 'Z-' + movename; - } - this.addMove('move', pokemon, movename, target + attrs); - - if (zMove) this.runZPower(move, pokemon); - - if (!target) { - this.attrLastMove('[notarget]'); - this.add(this.gen >= 5 ? '-fail' : '-notarget', pokemon); - return false; - } - - const {targets, pressureTargets} = pokemon.getMoveTargets(move, target); - if (targets.length) { - target = targets[targets.length - 1]; // in case of redirection - } - - if (!sourceEffect || sourceEffect.id === 'pursuit' || sourceEffect.id === 'thehuntison') { - let extraPP = 0; - for (const source of pressureTargets) { - const ppDrop = this.runEvent('DeductPP', source, pokemon, move); - if (ppDrop !== true) { - extraPP += ppDrop || 0; + // Dollar Store Brand prankster immunity implementation + hitStepTryImmunity(targets, pokemon, move) { + const hitResults = []; + for (const [i, target] of targets.entries()) { + if (this.battle.gen >= 6 && move.flags['powder'] && target !== pokemon && !this.dex.getImmunity('powder', target)) { + this.battle.debug('natural powder immunity'); + this.battle.add('-immune', target); + hitResults[i] = false; + } else if (!this.battle.singleEvent('TryImmunity', move, {}, target, pokemon, move)) { + this.battle.add('-immune', target); + hitResults[i] = false; + } else if (this.battle.gen >= 7 && move.pranksterBoosted && + // eslint-disable-next-line max-len + (pokemon.hasAbility('prankster') || pokemon.hasAbility('plausibledeniability') || pokemon.volatiles['nol']) && + targets[i].side !== pokemon.side && !this.dex.getImmunity('prankster', target)) { + this.battle.debug('natural prankster immunity'); + if (!target.illusion) this.battle.hint("Since gen 7, Dark is immune to Prankster moves."); + this.battle.add('-immune', target); + hitResults[i] = false; + } else { + hitResults[i] = true; } } - if (extraPP > 0) { - pokemon.deductPP(move, extraPP); + return hitResults; + }, + + // For Jett's The Hunt is On! + useMoveInner(moveOrMoveName, pokemon, target, sourceEffect, zMove, maxMove) { + if (!sourceEffect && this.battle.effect.id) sourceEffect = this.battle.effect; + if (sourceEffect && ['instruct', 'custapberry'].includes(sourceEffect.id)) sourceEffect = null; + + let move = this.dex.getActiveMove(moveOrMoveName); + if (move.id === 'weatherball' && zMove) { + // Z-Weather Ball only changes types if it's used directly, + // not if it's called by Z-Sleep Talk or something. + this.battle.singleEvent('ModifyType', move, null, pokemon, target, move, move); + if (move.type !== 'Normal') sourceEffect = move; + } + if (zMove || (move.category !== 'Status' && sourceEffect && (sourceEffect as ActiveMove).isZ)) { + move = this.getActiveZMove(move, pokemon); + } + if (maxMove && move.category !== 'Status') { + // Max move outcome is dependent on the move type after type modifications from ability and the move itself + this.battle.singleEvent('ModifyType', move, null, pokemon, target, move, move); + this.battle.runEvent('ModifyType', pokemon, target, move, move); + } + if (maxMove || (move.category !== 'Status' && sourceEffect && (sourceEffect as ActiveMove).isMax)) { + move = this.getActiveMaxMove(move, pokemon); } - } - if (!this.singleEvent('TryMove', move, null, pokemon, target, move) || - !this.runEvent('TryMove', pokemon, target, move)) { - move.mindBlownRecoil = false; - return false; - } + if (this.battle.activeMove) { + move.priority = this.battle.activeMove.priority; + if (!move.hasBounced) move.pranksterBoosted = this.battle.activeMove.pranksterBoosted; + } + const baseTarget = move.target; + if (target === undefined) target = this.battle.getRandomTarget(pokemon, move); + if (move.target === 'self' || move.target === 'allies') { + target = pokemon; + } + if (sourceEffect) { + move.sourceEffect = sourceEffect.id; + move.ignoreAbility = false; + } + let moveResult = false; - this.singleEvent('UseMoveMessage', move, null, pokemon, target, move); + this.battle.setActiveMove(move, pokemon, target); - if (move.ignoreImmunity === undefined) { - move.ignoreImmunity = (move.category === 'Status'); - } - - if (this.gen !== 4 && move.selfdestruct === 'always') { - this.faint(pokemon, pokemon, move); - } - - let damage: number | false | undefined | '' = false; - if (move.target === 'all' || move.target === 'foeSide' || move.target === 'allySide' || move.target === 'allyTeam') { - damage = this.tryMoveHit(target, pokemon, move); - if (damage === this.NOT_FAIL) pokemon.moveThisTurnResult = null; - if (damage || damage === 0 || damage === undefined) moveResult = true; - } else { - if (!targets.length) { - this.attrLastMove('[notarget]'); - this.add(this.gen >= 5 ? '-fail' : '-notarget', pokemon); + this.battle.singleEvent('ModifyType', move, null, pokemon, target, move, move); + this.battle.singleEvent('ModifyMove', move, null, pokemon, target, move, move); + if (baseTarget !== move.target) { + // Target changed in ModifyMove, so we must adjust it here + // Adjust before the next event so the correct target is passed to the + // event + target = this.battle.getRandomTarget(pokemon, move); + } + move = this.battle.runEvent('ModifyType', pokemon, target, move, move); + move = this.battle.runEvent('ModifyMove', pokemon, target, move, move); + if (baseTarget !== move.target) { + // Adjust again + target = this.battle.getRandomTarget(pokemon, move); + } + if (!move || pokemon.fainted) { return false; } - if (this.gen === 4 && move.selfdestruct === 'always') { - this.faint(pokemon, pokemon, move); + + let attrs = ''; + + let movename = move.name; + if (move.id === 'hiddenpower') movename = 'Hidden Power'; + if (sourceEffect) attrs += '|[from]' + this.dex.getEffect(sourceEffect); + if (zMove && move.isZ === true) { + attrs = '|[anim]' + movename + attrs; + movename = 'Z-' + movename; } - moveResult = this.trySpreadMoveHit(targets, pokemon, move); - } - if (move.selfBoost && moveResult) this.moveHit(pokemon, pokemon, move, move.selfBoost, false, true); - if (!pokemon.hp) { - this.faint(pokemon, pokemon, move); - } + this.battle.addMove('move', pokemon, movename, target + attrs); - if (!moveResult) { - this.singleEvent('MoveFail', move, null, target, pokemon, move); - return false; - } + if (zMove) this.runZPower(move, pokemon); - if ( - !move.negateSecondary && - !(move.hasSheerForce && pokemon.hasAbility(['sheerforce', 'aquilasblessing'])) && - !this.getAllActive().some(x => x.hasAbility('skilldrain')) - ) { - const originalHp = pokemon.hp; - this.singleEvent('AfterMoveSecondarySelf', move, null, pokemon, target, move); - this.runEvent('AfterMoveSecondarySelf', pokemon, target, move); - if (pokemon && pokemon !== target && move && move.category !== 'Status') { - if (pokemon.hp <= pokemon.maxhp / 2 && originalHp > pokemon.maxhp / 2) { - this.runEvent('EmergencyExit', pokemon, pokemon); - } - } - } - - if (move.selfSwitch && this.getAllActive().some(x => x.hasAbility('skilldrain'))) { - this.hint(`Self-switching doesn't trigger when a Pokemon with Skill Drain is active.`); - } - - return true; - }, - afterMoveSecondaryEvent(targets, pokemon, move) { - // console.log(`${targets}, ${pokemon}, ${move}`) - if ( - !move.negateSecondary && - !(move.hasSheerForce && pokemon.hasAbility(['sheerforce', 'aquilasblessing'])) && - !this.getAllActive().some(x => x.hasAbility('skilldrain')) - ) { - this.singleEvent('AfterMoveSecondary', move, null, targets[0], pokemon, move); - this.runEvent('AfterMoveSecondary', targets, pokemon, move); - } - return undefined; - }, - hitStepMoveHitLoop(targets, pokemon, move) { // Temporary name - const damage: (number | boolean | undefined)[] = []; - for (const i of targets.keys()) { - damage[i] = 0; - } - move.totalDamage = 0; - pokemon.lastDamage = 0; - let targetHits = move.multihit || 1; - if (Array.isArray(targetHits)) { - // yes, it's hardcoded... meh - if (targetHits[0] === 2 && targetHits[1] === 5) { - if (this.gen >= 5) { - targetHits = this.sample([2, 2, 3, 3, 4, 5]); - } else { - targetHits = this.sample([2, 2, 2, 3, 3, 3, 4, 5]); - } - } else { - targetHits = this.random(targetHits[0], targetHits[1] + 1); - } - } - targetHits = Math.floor(targetHits); - let nullDamage = true; - let moveDamage: (number | boolean | undefined)[]; - // There is no need to recursively check the ´sleepUsable´ flag as Sleep Talk can only be used while asleep. - const isSleepUsable = move.sleepUsable || this.dex.getMove(move.sourceEffect).sleepUsable; - - let targetsCopy: (Pokemon | false | null)[] = targets.slice(0); - let hit: number; - for (hit = 1; hit <= targetHits; hit++) { - if (damage.includes(false)) break; - if (hit > 1 && pokemon.status === 'slp' && !isSleepUsable) break; - if (targets.every(target => !target?.hp)) break; - move.hit = hit; - if (move.smartTarget && targets.length > 1) { - targetsCopy = [targets[hit - 1]]; - } else { - targetsCopy = targets.slice(0); - } - const target = targetsCopy[0]; // some relevant-to-single-target-moves-only things are hardcoded - if (target && typeof move.smartTarget === 'boolean') { - if (hit > 1) { - this.addMove('-anim', pokemon, move.name, target); - } else { - this.retargetLastMove(target); - } + if (!target) { + this.battle.attrLastMove('[notarget]'); + this.battle.add(this.battle.gen >= 5 ? '-fail' : '-notarget', pokemon); + return false; } - // like this (Triple Kick) - if (target && move.multiaccuracy && hit > 1) { - let accuracy = move.accuracy; - const boostTable = [1, 4 / 3, 5 / 3, 2, 7 / 3, 8 / 3, 3]; - if (accuracy !== true) { - if (!move.ignoreAccuracy) { - const boosts = this.runEvent('ModifyBoost', pokemon, null, null, {...pokemon.boosts}); - const boost = this.clampIntRange(boosts['accuracy'], -6, 6); - if (boost > 0) { - accuracy *= boostTable[boost]; - } else { - accuracy /= boostTable[-boost]; - } - } - if (!move.ignoreEvasion) { - const boosts = this.runEvent('ModifyBoost', target, null, null, {...target.boosts}); - const boost = this.clampIntRange(boosts['evasion'], -6, 6); - if (boost > 0) { - accuracy /= boostTable[boost]; - } else if (boost < 0) { - accuracy *= boostTable[-boost]; - } + const {targets, pressureTargets} = pokemon.getMoveTargets(move, target); + if (targets.length) { + target = targets[targets.length - 1]; // in case of redirection + } + + if (!sourceEffect || sourceEffect.id === 'pursuit' || sourceEffect.id === 'thehuntison') { + let extraPP = 0; + for (const source of pressureTargets) { + const ppDrop = this.battle.runEvent('DeductPP', source, pokemon, move); + if (ppDrop !== true) { + extraPP += ppDrop || 0; } } - accuracy = this.runEvent('ModifyAccuracy', target, pokemon, move, accuracy); - if (!move.alwaysHit) { - accuracy = this.runEvent('Accuracy', target, pokemon, move, accuracy); - if (accuracy !== true && !this.randomChance(accuracy, 100)) break; + if (extraPP > 0) { + pokemon.deductPP(move, extraPP); } } - const moveData = move; - if (!moveData.flags) moveData.flags = {}; - - // Modifies targetsCopy (which is why it's a copy) - [moveDamage, targetsCopy] = this.spreadMoveHit(targetsCopy, pokemon, move, moveData); - - if (!moveDamage.some(val => val !== false)) break; - nullDamage = false; - - for (const [i, md] of moveDamage.entries()) { - // Damage from each hit is individually counted for the - // purposes of Counter, Metal Burst, and Mirror Coat. - damage[i] = md === true || !md ? 0 : md; - // Total damage dealt is accumulated for the purposes of recoil (Parental Bond). - // @ts-ignore - move.totalDamage += damage[i]; - } - if (move.mindBlownRecoil) { - this.damage(Math.round(pokemon.maxhp / 2), pokemon, pokemon, this.dex.getEffect('Mind Blown'), true); + if (!this.battle.singleEvent('TryMove', move, null, pokemon, target, move) || + !this.battle.runEvent('TryMove', pokemon, target, move)) { move.mindBlownRecoil = false; - } - this.eachEvent('Update'); - if (!pokemon.hp && targets.length === 1) { - hit++; // report the correct number of hits for multihit moves - break; - } - } - // hit is 1 higher than the actual hit count - if (hit === 1) return damage.fill(false); - if (nullDamage) damage.fill(false); - if (move.multihit && typeof move.smartTarget !== 'boolean') { - this.add('-hitcount', targets[0], hit - 1); - } - - if (move.recoil && move.totalDamage) { - this.damage(this.calcRecoilDamage(move.totalDamage, move), pokemon, pokemon, 'recoil'); - } - - if (move.struggleRecoil) { - let recoilDamage; - if (this.dex.gen >= 5) { - recoilDamage = this.clampIntRange(Math.round(pokemon.baseMaxhp / 4), 1); - } else { - recoilDamage = this.trunc(pokemon.maxhp / 4); - } - this.directDamage(recoilDamage, pokemon, pokemon, {id: 'strugglerecoil'} as Condition); - } - - // smartTarget messes up targetsCopy, but smartTarget should in theory ensure that targets will never fail, anyway - if (move.smartTarget) targetsCopy = targets.slice(0); - - for (const [i, target] of targetsCopy.entries()) { - if (target && pokemon !== target) { - target.gotAttacked(move, damage[i] as number | false | undefined, pokemon); - } - } - - if (move.ohko && !targets[0].hp) this.add('-ohko'); - - if (!damage.some(val => !!val || val === 0)) return damage; - - this.eachEvent('Update'); - - this.afterMoveSecondaryEvent(targetsCopy.filter(val => !!val) as Pokemon[], pokemon, move); - - if ( - !move.negateSecondary && - !(move.hasSheerForce && pokemon.hasAbility(['sheerforce', 'aquilasblessing'])) && - !this.getAllActive().some(x => x.hasAbility('skilldrain')) - ) { - for (const [i, d] of damage.entries()) { - // There are no multihit spread moves, so it's safe to use move.totalDamage for multihit moves - // The previous check was for `move.multihit`, but that fails for Dragon Darts - const curDamage = targets.length === 1 ? move.totalDamage : d; - if (typeof curDamage === 'number' && targets[i].hp) { - if (targets[i].hp <= targets[i].maxhp / 2 && targets[i].hp + curDamage > targets[i].maxhp / 2) { - this.runEvent('EmergencyExit', targets[i], pokemon); - } - } - } - } - - return damage; - }, - - // For Spandan's custom move and Brandon's ability - getDamage(pokemon, target, move, suppressMessages = false) { - if (typeof move === 'string') move = this.dex.getActiveMove(move); - - if (typeof move === 'number') { - const basePower = move; - move = new Dex.Move({ - basePower, - type: '???', - category: 'Physical', - willCrit: false, - }) as unknown as ActiveMove; - move.hit = 0; - } - - if (!move.ignoreImmunity || (move.ignoreImmunity !== true && !move.ignoreImmunity[move.type])) { - if (!target.runImmunity(move.type, !suppressMessages)) { return false; } - } - if (move.ohko) return target.maxhp; - if (move.damageCallback) return move.damageCallback.call(this, pokemon, target); - if (move.damage === 'level') { - return pokemon.level; - } else if (move.damage) { - return move.damage; - } + this.battle.singleEvent('UseMoveMessage', move, null, pokemon, target, move); - const category = this.getCategory(move); - const defensiveCategory = move.defensiveCategory || category; + if (move.ignoreImmunity === undefined) { + move.ignoreImmunity = (move.category === 'Status'); + } - let basePower: number | false | null = move.basePower; - if (move.basePowerCallback) { - basePower = move.basePowerCallback.call(this, pokemon, target, move); - } - if (!basePower) return basePower === 0 ? undefined : basePower; - basePower = this.clampIntRange(basePower, 1); + if (this.battle.gen !== 4 && move.selfdestruct === 'always') { + this.battle.faint(pokemon, pokemon, move); + } - let critMult; - let critRatio = this.runEvent('ModifyCritRatio', pokemon, target, move, move.critRatio || 0); - if (this.gen <= 5) { - critRatio = this.clampIntRange(critRatio, 0, 5); - critMult = [0, 16, 8, 4, 3, 2]; - } else { - critRatio = this.clampIntRange(critRatio, 0, 4); - if (this.gen === 6) { - critMult = [0, 16, 8, 2, 1]; + let damage: number | false | undefined | '' = false; + if (move.target === 'all' || move.target === 'foeSide' || move.target === 'allySide' || move.target === 'allyTeam') { + damage = this.tryMoveHit(target, pokemon, move); + if (damage === this.battle.NOT_FAIL) pokemon.moveThisTurnResult = null; + if (damage || damage === 0 || damage === undefined) moveResult = true; } else { - critMult = [0, 24, 8, 2, 1]; - } - } - - const moveHit = target.getMoveHitData(move); - moveHit.crit = move.willCrit || false; - if (move.willCrit === undefined) { - if (critRatio) { - moveHit.crit = this.randomChance(1, critMult[critRatio]); - } - } - - if (moveHit.crit) { - moveHit.crit = this.runEvent('CriticalHit', target, null, move); - } - - // happens after crit calculation - basePower = this.runEvent('BasePower', pokemon, target, move, basePower, true); - - if (!basePower) return 0; - basePower = this.clampIntRange(basePower, 1); - - const level = pokemon.level; - - const attacker = pokemon; - const defender = target; - let attackStat: StatNameExceptHP = category === 'Physical' ? 'atk' : 'spa'; - const defenseStat: StatNameExceptHP = defensiveCategory === 'Physical' ? 'def' : 'spd'; - if (this.field.isTerrain('baneterrain')) { - if (attacker.getStat('atk') > attacker.getStat('spa')) { - attackStat = 'spa'; - } else { - attackStat = 'atk'; - } - } - if (move.useSourceDefensiveAsOffensive) { - attackStat = defenseStat; - // Body press really wants to use the def stat, - // so it switches stats to compensate for Wonder Room. - // Of course, the game thus miscalculates the boosts... - if ('wonderroom' in this.field.pseudoWeather) { - if (attackStat === 'def') { - attackStat = 'spd'; - } else if (attackStat === 'spd') { - attackStat = 'def'; + if (!targets.length) { + this.battle.attrLastMove('[notarget]'); + this.battle.add(this.battle.gen >= 5 ? '-fail' : '-notarget', pokemon); + return false; } - if (attacker.boosts['def'] || attacker.boosts['spd']) { - this.hint("Body Press uses Sp. Def boosts when Wonder Room is active."); + if (this.battle.gen === 4 && move.selfdestruct === 'always') { + this.battle.faint(pokemon, pokemon, move); + } + moveResult = this.trySpreadMoveHit(targets, pokemon, move); + } + if (move.selfBoost && moveResult) this.moveHit(pokemon, pokemon, move, move.selfBoost, false, true); + if (!pokemon.hp) { + this.battle.faint(pokemon, pokemon, move); + } + + if (!moveResult) { + this.battle.singleEvent('MoveFail', move, null, target, pokemon, move); + return false; + } + + if ( + !move.negateSecondary && + !(move.hasSheerForce && pokemon.hasAbility(['sheerforce', 'aquilasblessing'])) && + !this.battle.getAllActive().some(x => x.hasAbility('skilldrain')) + ) { + const originalHp = pokemon.hp; + this.battle.singleEvent('AfterMoveSecondarySelf', move, null, pokemon, target, move); + this.battle.runEvent('AfterMoveSecondarySelf', pokemon, target, move); + if (pokemon && pokemon !== target && move && move.category !== 'Status') { + if (pokemon.hp <= pokemon.maxhp / 2 && originalHp > pokemon.maxhp / 2) { + this.battle.runEvent('EmergencyExit', pokemon, pokemon); + } } } - } - const statTable = {atk: 'Atk', def: 'Def', spa: 'SpA', spd: 'SpD', spe: 'Spe'}; - let attack; - let defense; + if (move.selfSwitch && this.battle.getAllActive().some(x => x.hasAbility('skilldrain'))) { + this.battle.hint(`Self-switching doesn't trigger when a Pokemon with Skill Drain is active.`); + } - let atkBoosts = move.useTargetOffensive ? defender.boosts[attackStat] : attacker.boosts[attackStat]; - if (move.id === 'imtoxicyoureslippinunder') atkBoosts = defender.boosts['spd']; - let defBoosts = defender.boosts[defenseStat]; - - let ignoreNegativeOffensive = !!move.ignoreNegativeOffensive; - let ignorePositiveDefensive = !!move.ignorePositiveDefensive; - - if (moveHit.crit) { - ignoreNegativeOffensive = true; - ignorePositiveDefensive = true; - } - const ignoreOffensive = !!(move.ignoreOffensive || (ignoreNegativeOffensive && atkBoosts < 0)); - const ignoreDefensive = !!(move.ignoreDefensive || (ignorePositiveDefensive && defBoosts > 0)); - - if (ignoreOffensive) { - this.debug('Negating (sp)atk boost/penalty.'); - atkBoosts = 0; - } - if (ignoreDefensive) { - this.debug('Negating (sp)def boost/penalty.'); - defBoosts = 0; - } - - if (move.useTargetOffensive) { - attack = defender.calculateStat(attackStat, atkBoosts); - } else if (move.id === 'imtoxicyoureslippinunder') { - attack = defender.calculateStat("spd", atkBoosts); - } else { - attack = attacker.calculateStat(attackStat, atkBoosts); - } - - attackStat = (category === 'Physical' ? 'atk' : 'spa'); - defense = defender.calculateStat(defenseStat, defBoosts); - - // Apply Stat Modifiers - attack = this.runEvent('Modify' + statTable[attackStat], attacker, defender, move, attack); - defense = this.runEvent('Modify' + statTable[defenseStat], defender, attacker, move, defense); - - if (this.gen <= 4 && ['explosion', 'selfdestruct'].includes(move.id) && defenseStat === 'def') { - defense = this.clampIntRange(Math.floor(defense / 2), 1); - } - - const tr = this.trunc; - - // int(int(int(2 * L / 5 + 2) * A * P / D) / 50); - const baseDamage = tr(tr(tr(tr(2 * level / 5 + 2) * basePower * attack) / defense) / 50); - - // Calculate damage modifiers separately (order differs between generations) - return this.modifyDamage(baseDamage, pokemon, target, move, suppressMessages); - }, - - runMoveEffects(damage, targets, pokemon, move, moveData, isSecondary, isSelf) { - let didAnything: number | boolean | null | undefined = damage.reduce(this.combineResults); - for (const [i, target] of targets.entries()) { - if (target === false) continue; - let hitResult; - let didSomething: number | boolean | null | undefined = undefined; - - if (target) { - if (moveData.boosts && !target.fainted) { - hitResult = this.boost(moveData.boosts, target, pokemon, move, isSecondary, isSelf); - didSomething = this.combineResults(didSomething, hitResult); - } - if (moveData.heal && !target.fainted) { - if (target.hp >= target.maxhp) { - this.add('-fail', target, 'heal'); - this.attrLastMove('[still]'); - damage[i] = this.combineResults(damage[i], false); - didAnything = this.combineResults(didAnything, null); - continue; - } - const amount = target.baseMaxhp * moveData.heal[0] / moveData.heal[1]; - const d = target.heal((this.gen < 5 ? Math.floor : Math.round)(amount)); - if (!d && d !== 0) { - this.add('-fail', pokemon); - this.attrLastMove('[still]'); - this.debug('heal interrupted'); - damage[i] = this.combineResults(damage[i], false); - didAnything = this.combineResults(didAnything, null); - continue; - } - this.add('-heal', target, target.getHealth); - didSomething = true; - } - if (moveData.status) { - hitResult = target.trySetStatus(moveData.status, pokemon, moveData.ability ? moveData.ability : move); - if (!hitResult && move.status) { - damage[i] = this.combineResults(damage[i], false); - didAnything = this.combineResults(didAnything, null); - continue; - } - didSomething = this.combineResults(didSomething, hitResult); - } - if (moveData.forceStatus) { - hitResult = target.setStatus(moveData.forceStatus, pokemon, move); - didSomething = this.combineResults(didSomething, hitResult); - } - if (moveData.volatileStatus) { - hitResult = target.addVolatile(moveData.volatileStatus, pokemon, move); - didSomething = this.combineResults(didSomething, hitResult); - } - if (moveData.sideCondition) { - hitResult = target.side.addSideCondition(moveData.sideCondition, pokemon, move); - didSomething = this.combineResults(didSomething, hitResult); - } - if (moveData.slotCondition) { - hitResult = target.side.addSlotCondition(target, moveData.slotCondition, pokemon, move); - didSomething = this.combineResults(didSomething, hitResult); - } - if (moveData.weather) { - hitResult = this.field.setWeather(moveData.weather, pokemon, move); - didSomething = this.combineResults(didSomething, hitResult); - } - if (moveData.terrain) { - hitResult = this.field.setTerrain(moveData.terrain, pokemon, move); - didSomething = this.combineResults(didSomething, hitResult); - } - if (moveData.pseudoWeather) { - hitResult = this.field.addPseudoWeather(moveData.pseudoWeather, pokemon, move); - didSomething = this.combineResults(didSomething, hitResult); - } - if (moveData.forceSwitch && !this.getAllActive().some(x => x.hasAbility('skilldrain'))) { - hitResult = !!this.canSwitch(target.side); - didSomething = this.combineResults(didSomething, hitResult); - } - // Hit events - // These are like the TryHit events, except we don't need a FieldHit event. - // Scroll up for the TryHit event documentation, and just ignore the "Try" part. ;) - if (move.target === 'all' && !isSelf) { - if (moveData.onHitField) { - hitResult = this.singleEvent('HitField', moveData, {}, target, pokemon, move); - didSomething = this.combineResults(didSomething, hitResult); - } - } else if ((move.target === 'foeSide' || move.target === 'allySide') && !isSelf) { - if (moveData.onHitSide) { - hitResult = this.singleEvent('HitSide', moveData, {}, target.side, pokemon, move); - didSomething = this.combineResults(didSomething, hitResult); + return true; + }, + afterMoveSecondaryEvent(targets, pokemon, move) { + // console.log(`${targets}, ${pokemon}, ${move}`) + if ( + !move.negateSecondary && + !(move.hasSheerForce && pokemon.hasAbility(['sheerforce', 'aquilasblessing'])) && + !this.battle.getAllActive().some(x => x.hasAbility('skilldrain')) + ) { + this.battle.singleEvent('AfterMoveSecondary', move, null, targets[0], pokemon, move); + this.battle.runEvent('AfterMoveSecondary', targets, pokemon, move); + } + return undefined; + }, + hitStepMoveHitLoop(targets, pokemon, move) { // Temporary name + const damage: (number | boolean | undefined)[] = []; + for (const i of targets.keys()) { + damage[i] = 0; + } + move.totalDamage = 0; + pokemon.lastDamage = 0; + let targetHits = move.multihit || 1; + if (Array.isArray(targetHits)) { + // yes, it's hardcoded... meh + if (targetHits[0] === 2 && targetHits[1] === 5) { + if (this.battle.gen >= 5) { + targetHits = this.battle.sample([2, 2, 3, 3, 4, 5]); + } else { + targetHits = this.battle.sample([2, 2, 2, 3, 3, 3, 4, 5]); } } else { - if (moveData.onHit) { - hitResult = this.singleEvent('Hit', moveData, {}, target, pokemon, move); + targetHits = this.battle.random(targetHits[0], targetHits[1] + 1); + } + } + targetHits = Math.floor(targetHits); + let nullDamage = true; + let moveDamage: (number | boolean | undefined)[]; + // There is no need to recursively check the ´sleepUsable´ flag as Sleep Talk can only be used while asleep. + const isSleepUsable = move.sleepUsable || this.dex.getMove(move.sourceEffect).sleepUsable; + + let targetsCopy: (Pokemon | false | null)[] = targets.slice(0); + let hit: number; + for (hit = 1; hit <= targetHits; hit++) { + if (damage.includes(false)) break; + if (hit > 1 && pokemon.status === 'slp' && !isSleepUsable) break; + if (targets.every(target => !target?.hp)) break; + move.hit = hit; + if (move.smartTarget && targets.length > 1) { + targetsCopy = [targets[hit - 1]]; + } else { + targetsCopy = targets.slice(0); + } + const target = targetsCopy[0]; // some relevant-to-single-target-moves-only things are hardcoded + if (target && typeof move.smartTarget === 'boolean') { + if (hit > 1) { + this.battle.addMove('-anim', pokemon, move.name, target); + } else { + this.battle.retargetLastMove(target); + } + } + + // like this (Triple Kick) + if (target && move.multiaccuracy && hit > 1) { + let accuracy = move.accuracy; + const boostTable = [1, 4 / 3, 5 / 3, 2, 7 / 3, 8 / 3, 3]; + if (accuracy !== true) { + if (!move.ignoreAccuracy) { + const boosts = this.battle.runEvent('ModifyBoost', pokemon, null, null, {...pokemon.boosts}); + const boost = this.battle.clampIntRange(boosts['accuracy'], -6, 6); + if (boost > 0) { + accuracy *= boostTable[boost]; + } else { + accuracy /= boostTable[-boost]; + } + } + if (!move.ignoreEvasion) { + const boosts = this.battle.runEvent('ModifyBoost', target, null, null, {...target.boosts}); + const boost = this.battle.clampIntRange(boosts['evasion'], -6, 6); + if (boost > 0) { + accuracy /= boostTable[boost]; + } else if (boost < 0) { + accuracy *= boostTable[-boost]; + } + } + } + accuracy = this.battle.runEvent('ModifyAccuracy', target, pokemon, move, accuracy); + if (!move.alwaysHit) { + accuracy = this.battle.runEvent('Accuracy', target, pokemon, move, accuracy); + if (accuracy !== true && !this.battle.randomChance(accuracy, 100)) break; + } + } + + const moveData = move; + if (!moveData.flags) moveData.flags = {}; + + // Modifies targetsCopy (which is why it's a copy) + [moveDamage, targetsCopy] = this.spreadMoveHit(targetsCopy, pokemon, move, moveData); + + if (!moveDamage.some(val => val !== false)) break; + nullDamage = false; + + for (const [i, md] of moveDamage.entries()) { + // Damage from each hit is individually counted for the + // purposes of Counter, Metal Burst, and Mirror Coat. + damage[i] = md === true || !md ? 0 : md; + // Total damage dealt is accumulated for the purposes of recoil (Parental Bond). + // @ts-ignore + move.totalDamage += damage[i]; + } + if (move.mindBlownRecoil) { + this.battle.damage(Math.round(pokemon.maxhp / 2), pokemon, pokemon, this.dex.getEffect('Mind Blown'), true); + move.mindBlownRecoil = false; + } + this.battle.eachEvent('Update'); + if (!pokemon.hp && targets.length === 1) { + hit++; // report the correct number of hits for multihit moves + break; + } + } + // hit is 1 higher than the actual hit count + if (hit === 1) return damage.fill(false); + if (nullDamage) damage.fill(false); + if (move.multihit && typeof move.smartTarget !== 'boolean') { + this.battle.add('-hitcount', targets[0], hit - 1); + } + + if (move.recoil && move.totalDamage) { + this.battle.damage(this.calcRecoilDamage(move.totalDamage, move), pokemon, pokemon, 'recoil'); + } + + if (move.struggleRecoil) { + let recoilDamage; + if (this.dex.gen >= 5) { + recoilDamage = this.battle.clampIntRange(Math.round(pokemon.baseMaxhp / 4), 1); + } else { + recoilDamage = this.battle.trunc(pokemon.maxhp / 4); + } + this.battle.directDamage(recoilDamage, pokemon, pokemon, {id: 'strugglerecoil'} as Condition); + } + + // smartTarget messes up targetsCopy, but smartTarget should in theory ensure that targets will never fail, anyway + if (move.smartTarget) targetsCopy = targets.slice(0); + + for (const [i, target] of targetsCopy.entries()) { + if (target && pokemon !== target) { + target.gotAttacked(move, damage[i] as number | false | undefined, pokemon); + } + } + + if (move.ohko && !targets[0].hp) this.battle.add('-ohko'); + + if (!damage.some(val => !!val || val === 0)) return damage; + + this.battle.eachEvent('Update'); + + this.afterMoveSecondaryEvent(targetsCopy.filter(val => !!val) as Pokemon[], pokemon, move); + + if ( + !move.negateSecondary && + !(move.hasSheerForce && pokemon.hasAbility(['sheerforce', 'aquilasblessing'])) && + !this.battle.getAllActive().some(x => x.hasAbility('skilldrain')) + ) { + for (const [i, d] of damage.entries()) { + // There are no multihit spread moves, so it's safe to use move.totalDamage for multihit moves + // The previous check was for `move.multihit`, but that fails for Dragon Darts + const curDamage = targets.length === 1 ? move.totalDamage : d; + if (typeof curDamage === 'number' && targets[i].hp) { + if (targets[i].hp <= targets[i].maxhp / 2 && targets[i].hp + curDamage > targets[i].maxhp / 2) { + this.battle.runEvent('EmergencyExit', targets[i], pokemon); + } + } + } + } + + return damage; + }, + + // For Spandan's custom move and Brandon's ability + getDamage(pokemon, target, move, suppressMessages = false) { + if (typeof move === 'string') move = this.dex.getActiveMove(move); + + if (typeof move === 'number') { + const basePower = move; + move = new Dex.Move({ + basePower, + type: '???', + category: 'Physical', + willCrit: false, + }) as unknown as ActiveMove; + move.hit = 0; + } + + if (!move.ignoreImmunity || (move.ignoreImmunity !== true && !move.ignoreImmunity[move.type])) { + if (!target.runImmunity(move.type, !suppressMessages)) { + return false; + } + } + + if (move.ohko) return target.maxhp; + if (move.damageCallback) return move.damageCallback.call(this.battle, pokemon, target); + if (move.damage === 'level') { + return pokemon.level; + } else if (move.damage) { + return move.damage; + } + + const category = this.battle.getCategory(move); + const defensiveCategory = move.defensiveCategory || category; + + let basePower: number | false | null = move.basePower; + if (move.basePowerCallback) { + basePower = move.basePowerCallback.call(this.battle, pokemon, target, move); + } + if (!basePower) return basePower === 0 ? undefined : basePower; + basePower = this.battle.clampIntRange(basePower, 1); + + let critMult; + let critRatio = this.battle.runEvent('ModifyCritRatio', pokemon, target, move, move.critRatio || 0); + if (this.battle.gen <= 5) { + critRatio = this.battle.clampIntRange(critRatio, 0, 5); + critMult = [0, 16, 8, 4, 3, 2]; + } else { + critRatio = this.battle.clampIntRange(critRatio, 0, 4); + if (this.battle.gen === 6) { + critMult = [0, 16, 8, 2, 1]; + } else { + critMult = [0, 24, 8, 2, 1]; + } + } + + const moveHit = target.getMoveHitData(move); + moveHit.crit = move.willCrit || false; + if (move.willCrit === undefined) { + if (critRatio) { + moveHit.crit = this.battle.randomChance(1, critMult[critRatio]); + } + } + + if (moveHit.crit) { + moveHit.crit = this.battle.runEvent('CriticalHit', target, null, move); + } + + // happens after crit calculation + basePower = this.battle.runEvent('BasePower', pokemon, target, move, basePower, true); + + if (!basePower) return 0; + basePower = this.battle.clampIntRange(basePower, 1); + + const level = pokemon.level; + + const attacker = pokemon; + const defender = target; + let attackStat: StatNameExceptHP = category === 'Physical' ? 'atk' : 'spa'; + const defenseStat: StatNameExceptHP = defensiveCategory === 'Physical' ? 'def' : 'spd'; + if (this.battle.field.isTerrain('baneterrain')) { + if (attacker.getStat('atk') > attacker.getStat('spa')) { + attackStat = 'spa'; + } else { + attackStat = 'atk'; + } + } + if (move.useSourceDefensiveAsOffensive) { + attackStat = defenseStat; + // Body press really wants to use the def stat, + // so it switches stats to compensate for Wonder Room. + // Of course, the game thus miscalculates the boosts... + if ('wonderroom' in this.battle.field.pseudoWeather) { + if (attackStat === 'def') { + attackStat = 'spd'; + } else if (attackStat === 'spd') { + attackStat = 'def'; + } + if (attacker.boosts['def'] || attacker.boosts['spd']) { + this.battle.hint("Body Press uses Sp. Def boosts when Wonder Room is active."); + } + } + } + + const statTable = {atk: 'Atk', def: 'Def', spa: 'SpA', spd: 'SpD', spe: 'Spe'}; + let attack; + let defense; + + let atkBoosts = move.useTargetOffensive ? defender.boosts[attackStat] : attacker.boosts[attackStat]; + if (move.id === 'imtoxicyoureslippinunder') atkBoosts = defender.boosts['spd']; + let defBoosts = defender.boosts[defenseStat]; + + let ignoreNegativeOffensive = !!move.ignoreNegativeOffensive; + let ignorePositiveDefensive = !!move.ignorePositiveDefensive; + + if (moveHit.crit) { + ignoreNegativeOffensive = true; + ignorePositiveDefensive = true; + } + const ignoreOffensive = !!(move.ignoreOffensive || (ignoreNegativeOffensive && atkBoosts < 0)); + const ignoreDefensive = !!(move.ignoreDefensive || (ignorePositiveDefensive && defBoosts > 0)); + + if (ignoreOffensive) { + this.battle.debug('Negating (sp)atk boost/penalty.'); + atkBoosts = 0; + } + if (ignoreDefensive) { + this.battle.debug('Negating (sp)def boost/penalty.'); + defBoosts = 0; + } + + if (move.useTargetOffensive) { + attack = defender.calculateStat(attackStat, atkBoosts); + } else if (move.id === 'imtoxicyoureslippinunder') { + attack = defender.calculateStat("spd", atkBoosts); + } else { + attack = attacker.calculateStat(attackStat, atkBoosts); + } + + attackStat = (category === 'Physical' ? 'atk' : 'spa'); + defense = defender.calculateStat(defenseStat, defBoosts); + + // Apply Stat Modifiers + attack = this.battle.runEvent('Modify' + statTable[attackStat], attacker, defender, move, attack); + defense = this.battle.runEvent('Modify' + statTable[defenseStat], defender, attacker, move, defense); + + if (this.battle.gen <= 4 && ['explosion', 'selfdestruct'].includes(move.id) && defenseStat === 'def') { + defense = this.battle.clampIntRange(Math.floor(defense / 2), 1); + } + + const tr = this.battle.trunc; + + // int(int(int(2 * L / 5 + 2) * A * P / D) / 50); + const baseDamage = tr(tr(tr(tr(2 * level / 5 + 2) * basePower * attack) / defense) / 50); + + // Calculate damage modifiers separately (order differs between generations) + return this.modifyDamage(baseDamage, pokemon, target, move, suppressMessages); + }, + + runMoveEffects(damage, targets, pokemon, move, moveData, isSecondary, isSelf) { + let didAnything: number | boolean | null | undefined = damage.reduce(this.combineResults); + for (const [i, target] of targets.entries()) { + if (target === false) continue; + let hitResult; + let didSomething: number | boolean | null | undefined = undefined; + + if (target) { + if (moveData.boosts && !target.fainted) { + hitResult = this.battle.boost(moveData.boosts, target, pokemon, move, isSecondary, isSelf); didSomething = this.combineResults(didSomething, hitResult); } - if (!isSelf && !isSecondary) { - this.runEvent('Hit', target, pokemon, move); + if (moveData.heal && !target.fainted) { + if (target.hp >= target.maxhp) { + this.battle.add('-fail', target, 'heal'); + this.battle.attrLastMove('[still]'); + damage[i] = this.combineResults(damage[i], false); + didAnything = this.combineResults(didAnything, null); + continue; + } + const amount = target.baseMaxhp * moveData.heal[0] / moveData.heal[1]; + const d = target.heal((this.battle.gen < 5 ? Math.floor : Math.round)(amount)); + if (!d && d !== 0) { + this.battle.add('-fail', pokemon); + this.battle.attrLastMove('[still]'); + this.battle.debug('heal interrupted'); + damage[i] = this.combineResults(damage[i], false); + didAnything = this.combineResults(didAnything, null); + continue; + } + this.battle.add('-heal', target, target.getHealth); + didSomething = true; + } + if (moveData.status) { + hitResult = target.trySetStatus(moveData.status, pokemon, moveData.ability ? moveData.ability : move); + if (!hitResult && move.status) { + damage[i] = this.combineResults(damage[i], false); + didAnything = this.combineResults(didAnything, null); + continue; + } + didSomething = this.combineResults(didSomething, hitResult); + } + if (moveData.forceStatus) { + hitResult = target.setStatus(moveData.forceStatus, pokemon, move); + didSomething = this.combineResults(didSomething, hitResult); + } + if (moveData.volatileStatus) { + hitResult = target.addVolatile(moveData.volatileStatus, pokemon, move); + didSomething = this.combineResults(didSomething, hitResult); + } + if (moveData.sideCondition) { + hitResult = target.side.addSideCondition(moveData.sideCondition, pokemon, move); + didSomething = this.combineResults(didSomething, hitResult); + } + if (moveData.slotCondition) { + hitResult = target.side.addSlotCondition(target, moveData.slotCondition, pokemon, move); + didSomething = this.combineResults(didSomething, hitResult); + } + if (moveData.weather) { + hitResult = this.battle.field.setWeather(moveData.weather, pokemon, move); + didSomething = this.combineResults(didSomething, hitResult); + } + if (moveData.terrain) { + hitResult = this.battle.field.setTerrain(moveData.terrain, pokemon, move); + didSomething = this.combineResults(didSomething, hitResult); + } + if (moveData.pseudoWeather) { + hitResult = this.battle.field.addPseudoWeather(moveData.pseudoWeather, pokemon, move); + didSomething = this.combineResults(didSomething, hitResult); + } + if (moveData.forceSwitch && !this.battle.getAllActive().some(x => x.hasAbility('skilldrain'))) { + hitResult = !!this.battle.canSwitch(target.side); + didSomething = this.combineResults(didSomething, hitResult); + } + // Hit events + // These are like the TryHit events, except we don't need a FieldHit event. + // Scroll up for the TryHit event documentation, and just ignore the "Try" part. ;) + if (move.target === 'all' && !isSelf) { + if (moveData.onHitField) { + hitResult = this.battle.singleEvent('HitField', moveData, {}, target, pokemon, move); + didSomething = this.combineResults(didSomething, hitResult); + } + } else if ((move.target === 'foeSide' || move.target === 'allySide') && !isSelf) { + if (moveData.onHitSide) { + hitResult = this.battle.singleEvent('HitSide', moveData, {}, target.side, pokemon, move); + didSomething = this.combineResults(didSomething, hitResult); + } + } else { + if (moveData.onHit) { + hitResult = this.battle.singleEvent('Hit', moveData, {}, target, pokemon, move); + didSomething = this.combineResults(didSomething, hitResult); + } + if (!isSelf && !isSecondary) { + this.battle.runEvent('Hit', target, pokemon, move); + } } } - } - if (moveData.selfSwitch && !this.getAllActive().some(x => x.hasAbility('skilldrain'))) { - if (this.canSwitch(pokemon.side)) { - didSomething = true; - } else { - didSomething = this.combineResults(didSomething, false); + if (moveData.selfSwitch && !this.battle.getAllActive().some(x => x.hasAbility('skilldrain'))) { + if (this.battle.canSwitch(pokemon.side)) { + didSomething = true; + } else { + didSomething = this.combineResults(didSomething, false); + } } + // Move didn't fail because it didn't try to do anything + if (didSomething === undefined) didSomething = true; + damage[i] = this.combineResults(damage[i], didSomething === null ? false : didSomething); + didAnything = this.combineResults(didAnything, didSomething); } - // Move didn't fail because it didn't try to do anything - if (didSomething === undefined) didSomething = true; - damage[i] = this.combineResults(damage[i], didSomething === null ? false : didSomething); - didAnything = this.combineResults(didAnything, didSomething); - } - if (!didAnything && didAnything !== 0 && !moveData.self && !moveData.selfdestruct) { - if (!isSelf && !isSecondary) { - if (didAnything === false) { - this.add('-fail', pokemon); - this.attrLastMove('[still]'); + if (!didAnything && didAnything !== 0 && !moveData.self && !moveData.selfdestruct) { + if (!isSelf && !isSecondary) { + if (didAnything === false) { + this.battle.add('-fail', pokemon); + this.battle.attrLastMove('[still]'); + } } + this.battle.debug('move failed because it did nothing'); + } else if (move.selfSwitch && pokemon.hp && !this.battle.getAllActive().some(x => x.hasAbility('skilldrain'))) { + pokemon.switchFlag = move.id; } - this.debug('move failed because it did nothing'); - } else if (move.selfSwitch && pokemon.hp && !this.getAllActive().some(x => x.hasAbility('skilldrain'))) { - pokemon.switchFlag = move.id; - } - return damage; + return damage; + }, }, pokemon: { @@ -1057,7 +1058,7 @@ export const Scripts: ModdedBattleScriptsData = { if (this.gameType === 'triples' && !this.sides.filter(side => side.pokemonLeft > 1).length) { // If both sides have one Pokemon left in triples and they are not adjacent, they are both moved to the center. const actives = this.getAllActive(); - if (actives.length > 1 && !this.isAdjacent(actives[0], actives[1])) { + if (actives.length > 1 && !actives[0].isAdjacent(actives[1])) { this.swapPosition(actives[0], 1, '[silent]'); this.swapPosition(actives[1], 1, '[silent]'); this.add('-center'); diff --git a/data/moves.ts b/data/moves.ts index 9ea9cd5719..2b6bef193e 100644 --- a/data/moves.ts +++ b/data/moves.ts @@ -568,7 +568,7 @@ export const Moves: {[moveid: string]: MoveData} = { if (!randomMove) { return false; } - this.useMove(randomMove, target); + this.actions.useMove(randomMove, target); }, secondary: null, target: "self", @@ -1218,7 +1218,7 @@ export const Moves: {[moveid: string]: MoveData} = { effectType: 'Move', type: 'Normal', }; - this.tryMoveHit(target, pokemon, moveData as ActiveMove); + this.actions.tryMoveHit(target, pokemon, moveData as ActiveMove); return false; } this.add('-activate', pokemon, 'move: Bide'); @@ -2490,7 +2490,7 @@ export const Moves: {[moveid: string]: MoveData} = { if (noCopycat.includes(move.id) || move.isZ || move.isMax) { return false; } - this.useMove(move.id, pokemon); + this.actions.useMove(move.id, pokemon); }, secondary: null, target: "self", @@ -5038,20 +5038,16 @@ export const Moves: {[moveid: string]: MoveData} = { if (target.side.active.length === 1) { return; } - for (const ally of target.side.active) { - if (ally && this.isAdjacent(target, ally)) { - this.damage(ally.baseMaxhp / 16, ally, source, this.dex.getEffect('Flame Burst')); - } + for (const ally of target.adjacentAllies()) { + this.damage(ally.baseMaxhp / 16, ally, source, this.dex.getEffect('Flame Burst')); } }, onAfterSubDamage(damage, target, source, move) { if (target.side.active.length === 1) { return; } - for (const ally of target.side.active) { - if (ally && this.isAdjacent(target, ally)) { - this.damage(ally.baseMaxhp / 16, ally, source, this.dex.getEffect('Flame Burst')); - } + for (const ally of target.adjacentAllies()) { + this.damage(ally.baseMaxhp / 16, ally, source, this.dex.getEffect('Flame Burst')); } }, secondary: null, @@ -9051,7 +9047,7 @@ export const Moves: {[moveid: string]: MoveData} = { return false; } this.add('-singleturn', target, 'move: Instruct', '[of] ' + source); - this.runMove(target.lastMove.id, target, target.lastMoveTargetLoc!); + this.actions.runMove(target.lastMove.id, target, target.lastMoveTargetLoc!); }, secondary: null, target: "normal", @@ -10003,7 +9999,7 @@ export const Moves: {[moveid: string]: MoveData} = { const newMove = this.dex.getActiveMove(move.id); newMove.hasBounced = true; newMove.pranksterBoosted = this.effectData.pranksterBoosted; - this.useMove(newMove, target, source); + this.actions.useMove(newMove, target, source); return null; }, onAllyTryHitSide(target, source, move) { @@ -10013,7 +10009,7 @@ export const Moves: {[moveid: string]: MoveData} = { const newMove = this.dex.getActiveMove(move.id); newMove.hasBounced = true; newMove.pranksterBoosted = false; - this.useMove(newMove, this.effectData.target, source); + this.actions.useMove(newMove, this.effectData.target, source); return null; }, }, @@ -10781,7 +10777,7 @@ export const Moves: {[moveid: string]: MoveData} = { if (move.category === 'Status' || noMeFirst.includes(move.id)) return false; pokemon.addVolatile('mefirst'); - this.useMove(move, pokemon, target); + this.actions.useMove(move, pokemon, target); return null; }, condition: { @@ -11054,7 +11050,7 @@ export const Moves: {[moveid: string]: MoveData} = { if (!randomMove) { return false; } - this.useMove(randomMove, target); + this.actions.useMove(randomMove, target); }, secondary: null, target: "self", @@ -11288,7 +11284,7 @@ export const Moves: {[moveid: string]: MoveData} = { if (!move?.flags['mirror'] || move.isZ || move.isMax) { return false; } - this.useMove(move.id, pokemon, target); + this.actions.useMove(move.id, pokemon, target); return null; }, secondary: null, @@ -11774,7 +11770,7 @@ export const Moves: {[moveid: string]: MoveData} = { } else if (this.field.isTerrain('psychicterrain')) { move = 'psychic'; } - this.useMove(move, pokemon, target); + this.actions.useMove(move, pokemon, target); return null; }, secondary: null, @@ -13455,13 +13451,13 @@ export const Moves: {[moveid: string]: MoveData} = { if (source.canMegaEvo || source.canUltraBurst) { for (const [actionIndex, action] of this.queue.entries()) { if (action.pokemon === source && action.choice === 'megaEvo') { - this.runMegaEvo(source); + this.actions.runMegaEvo(source); this.queue.list.splice(actionIndex, 1); break; } } } - this.runMove('pursuit', source, this.getTargetLoc(pokemon, source)); + this.actions.runMove('pursuit', source, this.getTargetLoc(pokemon, source)); } }, }, @@ -15747,7 +15743,7 @@ export const Moves: {[moveid: string]: MoveData} = { if (!randomMove) { return false; } - this.useMove(randomMove, pokemon); + this.actions.useMove(randomMove, pokemon); }, secondary: null, target: "self", @@ -15981,7 +15977,7 @@ export const Moves: {[moveid: string]: MoveData} = { } snatchUser.removeVolatile('snatch'); this.add('-activate', snatchUser, 'move: Snatch', '[of] ' + source); - this.useMove(move.id, snatchUser); + this.actions.useMove(move.id, snatchUser); return null; }, }, @@ -17120,7 +17116,7 @@ export const Moves: {[moveid: string]: MoveData} = { if (target === source || move.flags['authentic'] || move.infiltrates) { return; } - let damage = this.getDamage(source, target, move); + let damage = this.actions.getDamage(source, target, move); if (!damage && damage !== 0) { this.add('-fail', source); this.attrLastMove('[still]'); @@ -17142,7 +17138,7 @@ export const Moves: {[moveid: string]: MoveData} = { this.add('-activate', target, 'move: Substitute', '[damage]'); } if (move.recoil) { - this.damage(this.calcRecoilDamage(damage, move), source, target, 'recoil'); + this.damage(this.actions.calcRecoilDamage(damage, move), source, target, 'recoil'); } if (move.drain) { this.heal(Math.ceil(damage * move.drain[0] / move.drain[1]), source, target, 'drain'); diff --git a/data/scripts.ts b/data/scripts.ts index cf4ffa592b..810338b6b0 100644 --- a/data/scripts.ts +++ b/data/scripts.ts @@ -1,1330 +1,3 @@ -import type {Dex} from '../sim/dex'; - -const CHOOSABLE_TARGETS = new Set(['normal', 'any', 'adjacentAlly', 'adjacentAllyOrSelf', 'adjacentFoe']); - export const Scripts: BattleScriptsData = { gen: 8, - /** - * runMove is the "outside" move caller. It handles deducting PP, - * flinching, full paralysis, etc. All the stuff up to and including - * the "POKEMON used MOVE" message. - * - * For details of the difference between runMove and useMove, see - * useMove's info. - * - * externalMove skips LockMove and PP deduction, mostly for use by - * Dancer. - */ - runMove(moveOrMoveName, pokemon, targetLoc, sourceEffect, zMove, externalMove, maxMove, originalTarget) { - pokemon.activeMoveActions++; - let target = this.getTarget(pokemon, maxMove || zMove || moveOrMoveName, targetLoc, originalTarget); - let baseMove = this.dex.getActiveMove(moveOrMoveName); - const pranksterBoosted = baseMove.pranksterBoosted; - if (baseMove.id !== 'struggle' && !zMove && !maxMove && !externalMove) { - const changedMove = this.runEvent('OverrideAction', pokemon, target, baseMove); - if (changedMove && changedMove !== true) { - baseMove = this.dex.getActiveMove(changedMove); - if (pranksterBoosted) baseMove.pranksterBoosted = pranksterBoosted; - target = this.getRandomTarget(pokemon, baseMove); - } - } - let move = baseMove; - if (zMove) { - move = this.getActiveZMove(baseMove, pokemon); - } else if (maxMove) { - move = this.getActiveMaxMove(baseMove, pokemon); - } - - move.isExternal = externalMove; - - this.setActiveMove(move, pokemon, target); - - /* if (pokemon.moveThisTurn) { - // THIS IS PURELY A SANITY CHECK - // DO NOT TAKE ADVANTAGE OF THIS TO PREVENT A POKEMON FROM MOVING; - // USE this.queue.cancelMove INSTEAD - this.debug('' + pokemon.id + ' INCONSISTENT STATE, ALREADY MOVED: ' + pokemon.moveThisTurn); - this.clearActiveMove(true); - return; - } */ - const willTryMove = this.runEvent('BeforeMove', pokemon, target, move); - if (!willTryMove) { - this.runEvent('MoveAborted', pokemon, target, move); - this.clearActiveMove(true); - // The event 'BeforeMove' could have returned false or null - // false indicates that this counts as a move failing for the purpose of calculating Stomping Tantrum's base power - // null indicates the opposite, as the Pokemon didn't have an option to choose anything - pokemon.moveThisTurnResult = willTryMove; - return; - } - if (move.beforeMoveCallback) { - if (move.beforeMoveCallback.call(this, pokemon, target, move)) { - this.clearActiveMove(true); - pokemon.moveThisTurnResult = false; - return; - } - } - pokemon.lastDamage = 0; - let lockedMove; - if (!externalMove) { - lockedMove = this.runEvent('LockMove', pokemon); - if (lockedMove === true) lockedMove = false; - if (!lockedMove) { - if (!pokemon.deductPP(baseMove, null, target) && (move.id !== 'struggle')) { - this.add('cant', pokemon, 'nopp', move); - this.clearActiveMove(true); - pokemon.moveThisTurnResult = false; - return; - } - } else { - sourceEffect = this.dex.getEffect('lockedmove'); - } - pokemon.moveUsed(move, targetLoc); - } - - // Dancer Petal Dance hack - // TODO: implement properly - const noLock = externalMove && !pokemon.volatiles['lockedmove']; - - if (zMove) { - if (pokemon.illusion) { - this.singleEvent('End', this.dex.getAbility('Illusion'), pokemon.abilityData, pokemon); - } - this.add('-zpower', pokemon); - pokemon.side.zMoveUsed = true; - } - const moveDidSomething = this.useMove(baseMove, pokemon, target, sourceEffect, zMove, maxMove); - this.lastSuccessfulMoveThisTurn = moveDidSomething ? this.activeMove && this.activeMove.id : null; - if (this.activeMove) move = this.activeMove; - this.singleEvent('AfterMove', move, null, pokemon, target, move); - this.runEvent('AfterMove', pokemon, target, move); - - // Dancer's activation order is completely different from any other event, so it's handled separately - if (move.flags['dance'] && moveDidSomething && !move.isExternal) { - const dancers = []; - for (const currentPoke of this.getAllActive()) { - if (pokemon === currentPoke) continue; - if (currentPoke.hasAbility('dancer') && !currentPoke.isSemiInvulnerable()) { - dancers.push(currentPoke); - } - } - // Dancer activates in order of lowest speed stat to highest - // Note that the speed stat used is after any volatile replacements like Speed Swap, - // but before any multipliers like Agility or Choice Scarf - // Ties go to whichever Pokemon has had the ability for the least amount of time - dancers.sort( - (a, b) => -(b.storedStats['spe'] - a.storedStats['spe']) || b.abilityOrder - a.abilityOrder - ); - for (const dancer of dancers) { - if (this.faintMessages()) break; - if (dancer.fainted) continue; - this.add('-activate', dancer, 'ability: Dancer'); - const dancersTarget = target!.side !== dancer.side && pokemon.side === dancer.side ? target! : pokemon; - this.runMove(move.id, dancer, this.getTargetLoc(dancersTarget, dancer), this.dex.getAbility('dancer'), undefined, true); - } - } - if (noLock && pokemon.volatiles['lockedmove']) delete pokemon.volatiles['lockedmove']; - }, - /** - * useMove is the "inside" move caller. It handles effects of the - * move itself, but not the idea of using the move. - * - * Most caller effects, like Sleep Talk, Nature Power, Magic Bounce, - * etc use useMove. - * - * The only ones that use runMove are Instruct, Pursuit, and - * Dancer. - */ - useMove(move, pokemon, target, sourceEffect, zMove, maxMove) { - pokemon.moveThisTurnResult = undefined; - const oldMoveResult: boolean | null | undefined = pokemon.moveThisTurnResult; - const moveResult = this.useMoveInner(move, pokemon, target, sourceEffect, zMove, maxMove); - if (oldMoveResult === pokemon.moveThisTurnResult) pokemon.moveThisTurnResult = moveResult; - return moveResult; - }, - useMoveInner(moveOrMoveName, pokemon, target, sourceEffect, zMove, maxMove) { - if (!sourceEffect && this.effect.id) sourceEffect = this.effect; - if (sourceEffect && ['instruct', 'custapberry'].includes(sourceEffect.id)) sourceEffect = null; - - let move = this.dex.getActiveMove(moveOrMoveName); - pokemon.lastMoveUsed = move; - if (move.id === 'weatherball' && zMove) { - // Z-Weather Ball only changes types if it's used directly, - // not if it's called by Z-Sleep Talk or something. - this.singleEvent('ModifyType', move, null, pokemon, target, move, move); - if (move.type !== 'Normal') sourceEffect = move; - } - if (zMove || (move.category !== 'Status' && sourceEffect && (sourceEffect as ActiveMove).isZ)) { - move = this.getActiveZMove(move, pokemon); - } - if (maxMove && move.category !== 'Status') { - // Max move outcome is dependent on the move type after type modifications from ability and the move itself - this.singleEvent('ModifyType', move, null, pokemon, target, move, move); - this.runEvent('ModifyType', pokemon, target, move, move); - } - if (maxMove || (move.category !== 'Status' && sourceEffect && (sourceEffect as ActiveMove).isMax)) { - move = this.getActiveMaxMove(move, pokemon); - } - - if (this.activeMove) { - move.priority = this.activeMove.priority; - if (!move.hasBounced) move.pranksterBoosted = this.activeMove.pranksterBoosted; - } - const baseTarget = move.target; - let targetRelayVar = {target}; - targetRelayVar = this.runEvent('ModifyTarget', pokemon, target, move, targetRelayVar, true); - if (targetRelayVar.target !== undefined) target = targetRelayVar.target; - if (target === undefined) target = this.getRandomTarget(pokemon, move); - if (move.target === 'self' || move.target === 'allies') { - target = pokemon; - } - if (sourceEffect) { - move.sourceEffect = sourceEffect.id; - move.ignoreAbility = false; - } - let moveResult = false; - - this.setActiveMove(move, pokemon, target); - - this.singleEvent('ModifyType', move, null, pokemon, target, move, move); - this.singleEvent('ModifyMove', move, null, pokemon, target, move, move); - if (baseTarget !== move.target) { - // Target changed in ModifyMove, so we must adjust it here - // Adjust before the next event so the correct target is passed to the - // event - target = this.getRandomTarget(pokemon, move); - } - move = this.runEvent('ModifyType', pokemon, target, move, move); - move = this.runEvent('ModifyMove', pokemon, target, move, move); - if (baseTarget !== move.target) { - // Adjust again - target = this.getRandomTarget(pokemon, move); - } - if (!move || pokemon.fainted) { - return false; - } - - let attrs = ''; - - let movename = move.name; - if (move.id === 'hiddenpower') movename = 'Hidden Power'; - if (sourceEffect) attrs += `|[from]${sourceEffect.fullname}`; - if (zMove && move.isZ === true) { - attrs = '|[anim]' + movename + attrs; - movename = 'Z-' + movename; - } - this.addMove('move', pokemon, movename, target + attrs); - - if (zMove) this.runZPower(move, pokemon); - - if (!target) { - this.attrLastMove('[notarget]'); - this.add(this.gen >= 5 ? '-fail' : '-notarget', pokemon); - return false; - } - - const {targets, pressureTargets} = pokemon.getMoveTargets(move, target); - if (targets.length) { - target = targets[targets.length - 1]; // in case of redirection - } - - if (!sourceEffect || sourceEffect.id === 'pursuit') { - let extraPP = 0; - for (const source of pressureTargets) { - const ppDrop = this.runEvent('DeductPP', source, pokemon, move); - if (ppDrop !== true) { - extraPP += ppDrop || 0; - } - } - if (extraPP > 0) { - pokemon.deductPP(moveOrMoveName, extraPP); - } - } - - if (!this.singleEvent('TryMove', move, null, pokemon, target, move) || - !this.runEvent('TryMove', pokemon, target, move)) { - move.mindBlownRecoil = false; - return false; - } - - this.singleEvent('UseMoveMessage', move, null, pokemon, target, move); - - if (move.ignoreImmunity === undefined) { - move.ignoreImmunity = (move.category === 'Status'); - } - - if (this.gen !== 4 && move.selfdestruct === 'always') { - this.faint(pokemon, pokemon, move); - } - - let damage: number | false | undefined | '' = false; - if (move.target === 'all' || move.target === 'foeSide' || move.target === 'allySide' || move.target === 'allyTeam') { - damage = this.tryMoveHit(target, pokemon, move); - if (damage === this.NOT_FAIL) pokemon.moveThisTurnResult = null; - if (damage || damage === 0 || damage === undefined) moveResult = true; - } else { - if (!targets.length) { - this.attrLastMove('[notarget]'); - this.add(this.gen >= 5 ? '-fail' : '-notarget', pokemon); - return false; - } - if (this.gen === 4 && move.selfdestruct === 'always') { - this.faint(pokemon, pokemon, move); - } - moveResult = this.trySpreadMoveHit(targets, pokemon, move); - } - if (move.selfBoost && moveResult) this.moveHit(pokemon, pokemon, move, move.selfBoost, false, true); - if (!pokemon.hp) { - this.faint(pokemon, pokemon, move); - } - - if (!moveResult) { - this.singleEvent('MoveFail', move, null, target, pokemon, move); - return false; - } - - if (!move.negateSecondary && !(move.hasSheerForce && pokemon.hasAbility('sheerforce'))) { - const originalHp = pokemon.hp; - this.singleEvent('AfterMoveSecondarySelf', move, null, pokemon, target, move); - this.runEvent('AfterMoveSecondarySelf', pokemon, target, move); - if (pokemon && pokemon !== target && move.category !== 'Status') { - if (pokemon.hp <= pokemon.maxhp / 2 && originalHp > pokemon.maxhp / 2) { - this.runEvent('EmergencyExit', pokemon, pokemon); - } - } - } - - return true; - }, - /** NOTE: includes single-target moves */ - trySpreadMoveHit(targets, pokemon, move, notActive) { - if (targets.length > 1 && !move.smartTarget) move.spreadHit = true; - - const moveSteps: ((targets: Pokemon[], pokemon: Pokemon, move: ActiveMove) => - (number | boolean | "" | undefined)[] | undefined)[] = [ - // 0. check for semi invulnerability - this.hitStepInvulnerabilityEvent, - - // 1. run the 'TryHit' event (Protect, Magic Bounce, Volt Absorb, etc.) (this is step 2 in gens 5 & 6, and step 4 in gen 4) - this.hitStepTryHitEvent, - - // 2. check for type immunity (this is step 1 in gens 4-6) - this.hitStepTypeImmunity, - - // 3. check for various move-specific immunities - this.hitStepTryImmunity, - - // 4. check accuracy - this.hitStepAccuracy, - - // 5. break protection effects - this.hitStepBreakProtect, - - // 6. steal positive boosts (Spectral Thief) - this.hitStepStealBoosts, - - // 7. loop that processes each hit of the move (has its own steps per iteration) - this.hitStepMoveHitLoop, - ]; - if (this.gen <= 6) { - // Swap step 1 with step 2 - [moveSteps[1], moveSteps[2]] = [moveSteps[2], moveSteps[1]]; - } - if (this.gen === 4) { - // Swap step 4 with new step 2 (old step 1) - [moveSteps[2], moveSteps[4]] = [moveSteps[4], moveSteps[2]]; - } - - if (!notActive) this.setActiveMove(move, pokemon, targets[0]); - - const hitResult = this.singleEvent('Try', move, null, pokemon, targets[0], move) && - this.singleEvent('PrepareHit', move, {}, targets[0], pokemon, move) && - this.runEvent('PrepareHit', pokemon, targets[0], move); - if (!hitResult) { - if (hitResult === false) { - this.add('-fail', pokemon); - this.attrLastMove('[still]'); - } - return hitResult === this.NOT_FAIL; - } - - let atLeastOneFailure!: boolean; - for (const step of moveSteps) { - const hitResults: (number | boolean | "" | undefined)[] | undefined = step.call(this, targets, pokemon, move); - if (!hitResults) continue; - targets = targets.filter((val, i) => hitResults[i] || hitResults[i] === 0); - atLeastOneFailure = atLeastOneFailure || hitResults.some(val => val === false); - if (!targets.length) { - // console.log(step.name); - break; - } - } - - const moveResult = !!targets.length; - if (!moveResult && !atLeastOneFailure) pokemon.moveThisTurnResult = null; - const hitSlot = targets.map(p => p.getSlot()); - if (move.spreadHit) this.attrLastMove('[spread] ' + hitSlot.join(',')); - return moveResult; - }, - hitStepInvulnerabilityEvent(targets, pokemon, move) { - if (move.id === 'helpinghand' || (this.gen >= 8 && move.id === 'toxic' && pokemon.hasType('Poison'))) { - return new Array(targets.length).fill(true); - } - const hitResults = this.runEvent('Invulnerability', targets, pokemon, move); - for (const [i, target] of targets.entries()) { - if (hitResults[i] === false) { - if (move.smartTarget) { - move.smartTarget = false; - } else { - if (!move.spreadHit) this.attrLastMove('[miss]'); - this.add('-miss', pokemon, target); - } - } - } - return hitResults; - }, - hitStepTryHitEvent(targets, pokemon, move) { - const hitResults = this.runEvent('TryHit', targets, pokemon, move); - if (!hitResults.includes(true) && hitResults.includes(false)) { - this.add('-fail', pokemon); - this.attrLastMove('[still]'); - } - for (const i of targets.keys()) { - if (hitResults[i] !== this.NOT_FAIL) hitResults[i] = hitResults[i] || false; - } - return hitResults; - }, - hitStepTypeImmunity(targets, pokemon, move) { - if (move.ignoreImmunity === undefined) { - move.ignoreImmunity = (move.category === 'Status'); - } - - const hitResults = []; - for (const i of targets.keys()) { - hitResults[i] = (move.ignoreImmunity && (move.ignoreImmunity === true || move.ignoreImmunity[move.type])) || - targets[i].runImmunity(move.type, !move.smartTarget); - if (move.smartTarget && !hitResults[i]) move.smartTarget = false; - } - - return hitResults; - }, - hitStepTryImmunity(targets, pokemon, move) { - const hitResults = []; - for (const [i, target] of targets.entries()) { - if (this.gen >= 6 && move.flags['powder'] && target !== pokemon && !this.dex.getImmunity('powder', target)) { - this.debug('natural powder immunity'); - this.add('-immune', target); - hitResults[i] = false; - } else if (!this.singleEvent('TryImmunity', move, {}, target, pokemon, move)) { - this.add('-immune', target); - hitResults[i] = false; - } else if (this.gen >= 7 && move.pranksterBoosted && pokemon.hasAbility('prankster') && - targets[i].side !== pokemon.side && !this.dex.getImmunity('prankster', target)) { - this.debug('natural prankster immunity'); - if (!target.illusion) this.hint("Since gen 7, Dark is immune to Prankster moves."); - this.add('-immune', target); - hitResults[i] = false; - } else { - hitResults[i] = true; - } - } - return hitResults; - }, - hitStepAccuracy(targets, pokemon, move) { - const hitResults = []; - for (const [i, target] of targets.entries()) { - this.activeTarget = target; - // calculate true accuracy - let accuracy = move.accuracy; - if (move.ohko) { // bypasses accuracy modifiers - if (!target.isSemiInvulnerable()) { - accuracy = 30; - if (move.ohko === 'Ice' && this.gen >= 7 && !pokemon.hasType('Ice')) { - accuracy = 20; - } - if (!target.volatiles['dynamax'] && pokemon.level >= target.level && - (move.ohko === true || !target.hasType(move.ohko))) { - accuracy += (pokemon.level - target.level); - } else { - this.add('-immune', target, '[ohko]'); - hitResults[i] = false; - continue; - } - } - } else { - accuracy = this.runEvent('ModifyAccuracy', target, pokemon, move, accuracy); - if (accuracy !== true) { - let boost = 0; - if (!move.ignoreAccuracy) { - const boosts = this.runEvent('ModifyBoost', pokemon, null, null, {...pokemon.boosts}); - boost = this.clampIntRange(boosts['accuracy'], -6, 6); - } - if (!move.ignoreEvasion) { - const boosts = this.runEvent('ModifyBoost', target, null, null, {...target.boosts}); - boost = this.clampIntRange(boost - boosts['evasion'], -6, 6); - } - if (boost > 0) { - accuracy = this.trunc(accuracy * (3 + boost) / 3); - } else if (boost < 0) { - accuracy = this.trunc(accuracy * 3 / (3 - boost)); - } - } - } - if (move.alwaysHit || (move.id === 'toxic' && this.gen >= 8 && pokemon.hasType('Poison')) || - (move.target === 'self' && move.category === 'Status' && !target.isSemiInvulnerable())) { - accuracy = true; // bypasses ohko accuracy modifiers - } else { - accuracy = this.runEvent('Accuracy', target, pokemon, move, accuracy); - } - if (accuracy !== true && !this.randomChance(accuracy, 100)) { - if (move.smartTarget) { - move.smartTarget = false; - } else { - if (!move.spreadHit) this.attrLastMove('[miss]'); - this.add('-miss', pokemon, target); - } - if (!move.ohko && pokemon.hasItem('blunderpolicy') && pokemon.useItem()) { - this.boost({spe: 2}, pokemon); - } - hitResults[i] = false; - continue; - } - hitResults[i] = true; - } - return hitResults; - }, - hitStepBreakProtect(targets, pokemon, move) { - if (move.breaksProtect) { - for (const target of targets) { - let broke = false; - for (const effectid of ['banefulbunker', 'kingsshield', 'obstruct', 'protect', 'spikyshield']) { - if (target.removeVolatile(effectid)) broke = true; - } - if (this.gen >= 6 || target.side !== pokemon.side) { - for (const effectid of ['craftyshield', 'matblock', 'quickguard', 'wideguard']) { - if (target.side.removeSideCondition(effectid)) broke = true; - } - } - if (broke) { - if (move.id === 'feint') { - this.add('-activate', target, 'move: Feint'); - } else { - this.add('-activate', target, 'move: ' + move.name, '[broken]'); - } - if (this.gen >= 6) delete target.volatiles['stall']; - } - } - } - return undefined; - }, - hitStepStealBoosts(targets, pokemon, move) { - const target = targets[0]; // hardcoded - if (move.stealsBoosts) { - const boosts: SparseBoostsTable = {}; - let stolen = false; - let statName: BoostName; - for (statName in target.boosts) { - const stage = target.boosts[statName]; - if (stage > 0) { - boosts[statName] = stage; - stolen = true; - } - } - if (stolen) { - this.attrLastMove('[still]'); - this.add('-clearpositiveboost', target, pokemon, 'move: ' + move.name); - this.boost(boosts, pokemon, pokemon); - - let statName2: BoostName; - for (statName2 in boosts) { - boosts[statName2] = 0; - } - target.setBoost(boosts); - this.addMove('-anim', pokemon, "Spectral Thief", target); - } - } - return undefined; - }, - afterMoveSecondaryEvent(targets, pokemon, move) { - // console.log(`${targets}, ${pokemon}, ${move}`) - if (!move.negateSecondary && !(move.hasSheerForce && pokemon.hasAbility('sheerforce'))) { - this.singleEvent('AfterMoveSecondary', move, null, targets[0], pokemon, move); - this.runEvent('AfterMoveSecondary', targets, pokemon, move); - } - return undefined; - }, - /** NOTE: used only for moves that target sides/fields rather than pokemon */ - tryMoveHit(target, pokemon, move) { - this.setActiveMove(move, pokemon, target); - - let hitResult = this.singleEvent('Try', move, null, pokemon, target, move) && - this.singleEvent('PrepareHit', move, {}, target, pokemon, move) && - this.runEvent('PrepareHit', pokemon, target, move); - if (!hitResult) { - if (hitResult === false) { - this.add('-fail', pokemon); - this.attrLastMove('[still]'); - } - return false; - } - - if (move.target === 'all') { - hitResult = this.runEvent('TryHitField', target, pokemon, move); - } else { - hitResult = this.runEvent('TryHitSide', target, pokemon, move); - } - if (!hitResult) { - if (hitResult === false) { - this.add('-fail', pokemon); - this.attrLastMove('[still]'); - } - return false; - } - return this.moveHit(target, pokemon, move); - }, - hitStepMoveHitLoop(targets, pokemon, move) { // Temporary name - const damage: (number | boolean | undefined)[] = []; - for (const i of targets.keys()) { - damage[i] = 0; - } - move.totalDamage = 0; - pokemon.lastDamage = 0; - let targetHits = move.multihit || 1; - if (Array.isArray(targetHits)) { - // yes, it's hardcoded... meh - if (targetHits[0] === 2 && targetHits[1] === 5) { - if (this.gen >= 5) { - // 35-35-15-15 out of 100 for 2-3-4-5 hits - targetHits = this.sample([2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 5, 5, 5]); - } else { - targetHits = this.sample([2, 2, 2, 3, 3, 3, 4, 5]); - } - } else { - targetHits = this.random(targetHits[0], targetHits[1] + 1); - } - } - targetHits = Math.floor(targetHits); - let nullDamage = true; - let moveDamage: (number | boolean | undefined)[] = []; - // There is no need to recursively check the ´sleepUsable´ flag as Sleep Talk can only be used while asleep. - const isSleepUsable = move.sleepUsable || this.dex.getMove(move.sourceEffect).sleepUsable; - - let targetsCopy: (Pokemon | false | null)[] = targets.slice(0); - let hit: number; - for (hit = 1; hit <= targetHits; hit++) { - if (damage.includes(false)) break; - if (hit > 1 && pokemon.status === 'slp' && !isSleepUsable) break; - if (targets.every(target => !target?.hp)) break; - move.hit = hit; - if (move.smartTarget && targets.length > 1) { - targetsCopy = [targets[hit - 1]]; - } else { - targetsCopy = targets.slice(0); - } - const target = targetsCopy[0]; // some relevant-to-single-target-moves-only things are hardcoded - if (target && typeof move.smartTarget === 'boolean') { - if (hit > 1) { - this.addMove('-anim', pokemon, move.name, target); - } else { - this.retargetLastMove(target); - } - } - - // like this (Triple Kick) - if (target && move.multiaccuracy && hit > 1) { - let accuracy = move.accuracy; - const boostTable = [1, 4 / 3, 5 / 3, 2, 7 / 3, 8 / 3, 3]; - if (accuracy !== true) { - if (!move.ignoreAccuracy) { - const boosts = this.runEvent('ModifyBoost', pokemon, null, null, {...pokemon.boosts}); - const boost = this.clampIntRange(boosts['accuracy'], -6, 6); - if (boost > 0) { - accuracy *= boostTable[boost]; - } else { - accuracy /= boostTable[-boost]; - } - } - if (!move.ignoreEvasion) { - const boosts = this.runEvent('ModifyBoost', target, null, null, {...target.boosts}); - const boost = this.clampIntRange(boosts['evasion'], -6, 6); - if (boost > 0) { - accuracy /= boostTable[boost]; - } else if (boost < 0) { - accuracy *= boostTable[-boost]; - } - } - } - accuracy = this.runEvent('ModifyAccuracy', target, pokemon, move, accuracy); - if (!move.alwaysHit) { - accuracy = this.runEvent('Accuracy', target, pokemon, move, accuracy); - if (accuracy !== true && !this.randomChance(accuracy, 100)) break; - } - } - - const moveData = move; - if (!moveData.flags) moveData.flags = {}; - - // Modifies targetsCopy (which is why it's a copy) - [moveDamage, targetsCopy] = this.spreadMoveHit(targetsCopy, pokemon, move, moveData); - - if (!moveDamage.some(val => val !== false)) break; - nullDamage = false; - - for (const [i, md] of moveDamage.entries()) { - // Damage from each hit is individually counted for the - // purposes of Counter, Metal Burst, and Mirror Coat. - damage[i] = md === true || !md ? 0 : md; - // Total damage dealt is accumulated for the purposes of recoil (Parental Bond). - move.totalDamage += damage[i] as number; - } - if (move.mindBlownRecoil) { - this.damage(Math.round(pokemon.maxhp / 2), pokemon, pokemon, this.dex.getEffect('Mind Blown'), true); - move.mindBlownRecoil = false; - } - this.eachEvent('Update'); - if (!pokemon.hp && targets.length === 1) { - hit++; // report the correct number of hits for multihit moves - break; - } - } - // hit is 1 higher than the actual hit count - if (hit === 1) return damage.fill(false); - if (nullDamage) damage.fill(false); - if (move.multihit && typeof move.smartTarget !== 'boolean') { - this.add('-hitcount', targets[0], hit - 1); - } - - if (move.recoil && move.totalDamage) { - this.damage(this.calcRecoilDamage(move.totalDamage, move), pokemon, pokemon, 'recoil'); - } - - if (move.struggleRecoil) { - let recoilDamage; - if (this.dex.gen >= 5) { - recoilDamage = this.clampIntRange(Math.round(pokemon.baseMaxhp / 4), 1); - } else { - recoilDamage = this.clampIntRange(this.trunc(pokemon.maxhp / 4), 1); - } - this.directDamage(recoilDamage, pokemon, pokemon, {id: 'strugglerecoil'} as Condition); - } - - // smartTarget messes up targetsCopy, but smartTarget should in theory ensure that targets will never fail, anyway - if (move.smartTarget) targetsCopy = targets.slice(0); - - for (const [i, target] of targetsCopy.entries()) { - if (target && pokemon !== target) { - target.gotAttacked(move, moveDamage[i] as number | false | undefined, pokemon); - } - } - - if (move.ohko && !targets[0].hp) this.add('-ohko'); - - if (!damage.some(val => !!val || val === 0)) return damage; - - this.eachEvent('Update'); - - this.afterMoveSecondaryEvent(targetsCopy.filter(val => !!val) as Pokemon[], pokemon, move); - - if (!move.negateSecondary && !(move.hasSheerForce && pokemon.hasAbility('sheerforce'))) { - for (const [i, d] of damage.entries()) { - // There are no multihit spread moves, so it's safe to use move.totalDamage for multihit moves - // The previous check was for `move.multihit`, but that fails for Dragon Darts - const curDamage = targets.length === 1 ? move.totalDamage : d; - if (typeof curDamage === 'number' && targets[i].hp) { - const targetHPBeforeDamage = (targets[i].hurtThisTurn || 0) + curDamage; - if (targets[i].hp <= targets[i].maxhp / 2 && targetHPBeforeDamage > targets[i].maxhp / 2) { - this.runEvent('EmergencyExit', targets[i], pokemon); - } - } - } - } - - return damage; - }, - spreadMoveHit(targets, pokemon, moveOrMoveName, moveData, isSecondary, isSelf) { - // Hardcoded for single-target purposes - // (no spread moves have any kind of onTryHit handler) - const target = targets[0]; - let damage: (number | boolean | undefined)[] = []; - for (const i of targets.keys()) { - damage[i] = true; - } - const move = this.dex.getActiveMove(moveOrMoveName); - let hitResult: boolean | number | null = true; - if (!moveData) moveData = move; - if (!moveData.flags) moveData.flags = {}; - if (move.target === 'all' && !isSelf) { - hitResult = this.singleEvent('TryHitField', moveData, {}, target || null, pokemon, move); - } else if ((move.target === 'foeSide' || move.target === 'allySide') && !isSelf) { - hitResult = this.singleEvent('TryHitSide', moveData, {}, (target ? target.side : null), pokemon, move); - } else if (target) { - hitResult = this.singleEvent('TryHit', moveData, {}, target, pokemon, move); - } - if (!hitResult) { - if (hitResult === false) { - this.add('-fail', pokemon); - this.attrLastMove('[still]'); - } - return [[false], targets]; // single-target only - } - - // 0. check for substitute - if (!isSecondary && !isSelf) { - if (move.target !== 'all' && move.target !== 'allySide' && move.target !== 'foeSide') { - damage = this.tryPrimaryHitEvent(damage, targets, pokemon, move, moveData, isSecondary); - } - } - - for (const i of targets.keys()) { - if (damage[i] === this.HIT_SUBSTITUTE) { - damage[i] = true; - targets[i] = null; - } - if (targets[i] && isSecondary && !moveData.self) { - damage[i] = true; - } - if (!damage[i]) targets[i] = false; - } - // 1. call to this.getDamage - damage = this.getSpreadDamage(damage, targets, pokemon, move, moveData, isSecondary, isSelf); - - for (const i of targets.keys()) { - if (damage[i] === false) targets[i] = false; - } - - // 2. call to this.spreadDamage - damage = this.spreadDamage(damage, targets, pokemon, move); - - for (const i of targets.keys()) { - if (damage[i] === false) targets[i] = false; - } - - // 3. onHit event happens here - damage = this.runMoveEffects(damage, targets, pokemon, move, moveData, isSecondary, isSelf); - - for (const i of targets.keys()) { - if (!damage[i] && damage[i] !== 0) targets[i] = false; - } - - // 4. self drops (start checking for targets[i] === false here) - if (moveData.self && !move.selfDropped) this.selfDrops(targets, pokemon, move, moveData, isSecondary); - - // 5. secondary effects - if (moveData.secondaries) this.secondaries(targets, pokemon, move, moveData, isSelf); - - // 6. force switch - if (moveData.forceSwitch) damage = this.forceSwitch(damage, targets, pokemon, move, moveData, isSecondary, isSelf); - - for (const i of targets.keys()) { - if (!damage[i] && damage[i] !== 0) targets[i] = false; - } - - const damagedTargets: Pokemon[] = []; - const damagedDamage = []; - for (const [i, t] of targets.entries()) { - if (typeof damage[i] === 'number' && t) { - damagedTargets.push(t); - damagedDamage.push(damage[i]); - } - } - const pokemonOriginalHP = pokemon.hp; - if (damagedDamage.length && !isSecondary && !isSelf) { - this.runEvent('DamagingHit', damagedTargets, pokemon, move, damagedDamage); - if (moveData.onAfterHit) { - for (const t of damagedTargets) { - this.singleEvent('AfterHit', moveData, {}, t, pokemon, move); - } - } - if (pokemon.hp && pokemon.hp <= pokemon.maxhp / 2 && pokemonOriginalHP > pokemon.maxhp / 2) { - this.runEvent('EmergencyExit', pokemon); - } - } - - return [damage, targets]; - }, - tryPrimaryHitEvent(damage, targets, pokemon, move, moveData, isSecondary) { - for (const [i, target] of targets.entries()) { - if (!target) continue; - damage[i] = this.runEvent('TryPrimaryHit', target, pokemon, moveData); - } - return damage; - }, - getSpreadDamage(damage, targets, pokemon, move, moveData, isSecondary, isSelf) { - for (const [i, target] of targets.entries()) { - if (!target) continue; - this.activeTarget = target; - damage[i] = undefined; - const curDamage = this.getDamage(pokemon, target, moveData); - // getDamage has several possible return values: - // - // a number: - // means that much damage is dealt (0 damage still counts as dealing - // damage for the purposes of things like Static) - // false: - // gives error message: "But it failed!" and move ends - // null: - // the move ends, with no message (usually, a custom fail message - // was already output by an event handler) - // undefined: - // means no damage is dealt and the move continues - // - // basically, these values have the same meanings as they do for event - // handlers. - - if (curDamage === false || curDamage === null) { - if (damage[i] === false && !isSecondary && !isSelf) { - this.add('-fail', pokemon); - this.attrLastMove('[still]'); - } - this.debug('damage calculation interrupted'); - damage[i] = false; - continue; - } - damage[i] = curDamage; - if (move.selfdestruct === 'ifHit') { - this.faint(pokemon, pokemon, move); - } - } - return damage; - }, - runMoveEffects(damage, targets, pokemon, move, moveData, isSecondary, isSelf) { - let didAnything: number | boolean | null | undefined = damage.reduce(this.combineResults); - for (const [i, target] of targets.entries()) { - if (target === false) continue; - let hitResult; - let didSomething: number | boolean | null | undefined = undefined; - - if (target) { - if (moveData.boosts && !target.fainted) { - hitResult = this.boost(moveData.boosts, target, pokemon, move, isSecondary, isSelf); - didSomething = this.combineResults(didSomething, hitResult); - } - if (moveData.heal && !target.fainted) { - if (target.hp >= target.maxhp) { - this.add('-fail', target, 'heal'); - this.attrLastMove('[still]'); - damage[i] = this.combineResults(damage[i], false); - didAnything = this.combineResults(didAnything, null); - continue; - } - const amount = target.baseMaxhp * moveData.heal[0] / moveData.heal[1]; - const d = target.heal((this.gen < 5 ? Math.floor : Math.round)(amount)); - if (!d && d !== 0) { - this.add('-fail', pokemon); - this.attrLastMove('[still]'); - this.debug('heal interrupted'); - damage[i] = this.combineResults(damage[i], false); - didAnything = this.combineResults(didAnything, null); - continue; - } - this.add('-heal', target, target.getHealth); - didSomething = true; - } - if (moveData.status) { - hitResult = target.trySetStatus(moveData.status, pokemon, moveData.ability ? moveData.ability : move); - if (!hitResult && move.status) { - damage[i] = this.combineResults(damage[i], false); - didAnything = this.combineResults(didAnything, null); - continue; - } - didSomething = this.combineResults(didSomething, hitResult); - } - if (moveData.forceStatus) { - hitResult = target.setStatus(moveData.forceStatus, pokemon, move); - didSomething = this.combineResults(didSomething, hitResult); - } - if (moveData.volatileStatus) { - hitResult = target.addVolatile(moveData.volatileStatus, pokemon, move); - didSomething = this.combineResults(didSomething, hitResult); - } - if (moveData.sideCondition) { - hitResult = target.side.addSideCondition(moveData.sideCondition, pokemon, move); - didSomething = this.combineResults(didSomething, hitResult); - } - if (moveData.slotCondition) { - hitResult = target.side.addSlotCondition(target, moveData.slotCondition, pokemon, move); - didSomething = this.combineResults(didSomething, hitResult); - } - if (moveData.weather) { - hitResult = this.field.setWeather(moveData.weather, pokemon, move); - didSomething = this.combineResults(didSomething, hitResult); - } - if (moveData.terrain) { - hitResult = this.field.setTerrain(moveData.terrain, pokemon, move); - didSomething = this.combineResults(didSomething, hitResult); - } - if (moveData.pseudoWeather) { - hitResult = this.field.addPseudoWeather(moveData.pseudoWeather, pokemon, move); - didSomething = this.combineResults(didSomething, hitResult); - } - if (moveData.forceSwitch) { - hitResult = !!this.canSwitch(target.side); - didSomething = this.combineResults(didSomething, hitResult); - } - // Hit events - // These are like the TryHit events, except we don't need a FieldHit event. - // Scroll up for the TryHit event documentation, and just ignore the "Try" part. ;) - if (move.target === 'all' && !isSelf) { - if (moveData.onHitField) { - hitResult = this.singleEvent('HitField', moveData, {}, target, pokemon, move); - didSomething = this.combineResults(didSomething, hitResult); - } - } else if ((move.target === 'foeSide' || move.target === 'allySide') && !isSelf) { - if (moveData.onHitSide) { - hitResult = this.singleEvent('HitSide', moveData, {}, target.side, pokemon, move); - didSomething = this.combineResults(didSomething, hitResult); - } - } else { - if (moveData.onHit) { - hitResult = this.singleEvent('Hit', moveData, {}, target, pokemon, move); - didSomething = this.combineResults(didSomething, hitResult); - } - if (!isSelf && !isSecondary) { - this.runEvent('Hit', target, pokemon, move); - } - } - } - if (moveData.selfSwitch) { - if (this.canSwitch(pokemon.side)) { - didSomething = true; - } else { - didSomething = this.combineResults(didSomething, false); - } - } - // Move didn't fail because it didn't try to do anything - if (didSomething === undefined) didSomething = true; - damage[i] = this.combineResults(damage[i], didSomething === null ? false : didSomething); - didAnything = this.combineResults(didAnything, didSomething); - } - - - if (!didAnything && didAnything !== 0 && !moveData.self && !moveData.selfdestruct) { - if (!isSelf && !isSecondary) { - if (didAnything === false) { - this.add('-fail', pokemon); - this.attrLastMove('[still]'); - } - } - this.debug('move failed because it did nothing'); - } else if (move.selfSwitch && pokemon.hp) { - pokemon.switchFlag = move.id; - } - - return damage; - }, - selfDrops(targets, pokemon, move, moveData, isSecondary) { - for (const target of targets) { - if (target === false) continue; - if (moveData.self && !move.selfDropped) { - if (!isSecondary && moveData.self.boosts) { - const secondaryRoll = this.random(100); - if (typeof moveData.self.chance === 'undefined' || secondaryRoll < moveData.self.chance) { - this.moveHit(pokemon, pokemon, move, moveData.self, isSecondary, true); - } - if (!move.multihit) move.selfDropped = true; - } else { - this.moveHit(pokemon, pokemon, move, moveData.self, isSecondary, true); - } - } - } - }, - secondaries(targets, pokemon, move, moveData, isSelf) { - if (!moveData.secondaries) return; - for (const target of targets) { - if (target === false) continue; - const secondaries: Dex.SecondaryEffect[] = - this.runEvent('ModifySecondaries', target, pokemon, moveData, moveData.secondaries.slice()); - for (const secondary of secondaries) { - const secondaryRoll = this.random(100); - if (typeof secondary.chance === 'undefined' || secondaryRoll < secondary.chance) { - this.moveHit(target, pokemon, move, secondary, true, isSelf); - } - } - } - }, - forceSwitch(damage, targets, pokemon, move) { - for (const [i, target] of targets.entries()) { - if (target && target.hp > 0 && pokemon.hp > 0 && this.canSwitch(target.side)) { - const hitResult = this.runEvent('DragOut', target, pokemon, move); - if (hitResult) { - target.forceSwitchFlag = true; - } else if (hitResult === false && move.category === 'Status') { - this.add('-fail', pokemon); - this.attrLastMove('[still]'); - damage[i] = false; - } - } - } - return damage; - }, - moveHit(target, pokemon, moveOrMoveName, moveData, isSecondary, isSelf) { - const retVal = this.spreadMoveHit([target], pokemon, moveOrMoveName, moveData, isSecondary, isSelf)[0][0]; - return retVal === true ? undefined : retVal; - }, - - calcRecoilDamage(damageDealt, move) { - return this.clampIntRange(Math.round(damageDealt * move.recoil![0] / move.recoil![1]), 1); - }, - - zMoveTable: { - Poison: "Acid Downpour", - Fighting: "All-Out Pummeling", - Dark: "Black Hole Eclipse", - Grass: "Bloom Doom", - Normal: "Breakneck Blitz", - Rock: "Continental Crush", - Steel: "Corkscrew Crash", - Dragon: "Devastating Drake", - Electric: "Gigavolt Havoc", - Water: "Hydro Vortex", - Fire: "Inferno Overdrive", - Ghost: "Never-Ending Nightmare", - Bug: "Savage Spin-Out", - Psychic: "Shattered Psyche", - Ice: "Subzero Slammer", - Flying: "Supersonic Skystrike", - Ground: "Tectonic Rage", - Fairy: "Twinkle Tackle", - }, - - getZMove(move, pokemon, skipChecks) { - const item = pokemon.getItem(); - if (!skipChecks) { - if (pokemon.side.zMoveUsed) return; - if (!item.zMove) return; - if (item.itemUser && !item.itemUser.includes(pokemon.species.name)) return; - const moveData = pokemon.getMoveData(move); - // Draining the PP of the base move prevents the corresponding Z-move from being used. - if (!moveData?.pp) return; - } - - if (item.zMoveFrom) { - if (move.name === item.zMoveFrom) return item.zMove as string; - } else if (item.zMove === true) { - if (move.type === item.zMoveType) { - if (move.category === "Status") { - return move.name; - } else if (move.zMove?.basePower) { - return this.zMoveTable[move.type]; - } - } - } - }, - - getActiveZMove(move, pokemon) { - if (pokemon) { - const item = pokemon.getItem(); - if (move.name === item.zMoveFrom) { - const zMove = this.dex.getActiveMove(item.zMove as string); - zMove.isZOrMaxPowered = true; - return zMove; - } - } - - if (move.category === 'Status') { - const zMove = this.dex.getActiveMove(move); - zMove.isZ = true; - zMove.isZOrMaxPowered = true; - return zMove; - } - const zMove = this.dex.getActiveMove(this.zMoveTable[move.type]); - zMove.basePower = move.zMove!.basePower!; - zMove.category = move.category; - // copy the priority for Quick Guard - zMove.priority = move.priority; - zMove.isZOrMaxPowered = true; - return zMove; - }, - - canZMove(pokemon) { - if (pokemon.side.zMoveUsed || - (pokemon.transformed && - (pokemon.species.isMega || pokemon.species.isPrimal || pokemon.species.forme === "Ultra")) - ) return; - const item = pokemon.getItem(); - if (!item.zMove) return; - if (item.itemUser && !item.itemUser.includes(pokemon.species.name)) return; - let atLeastOne = false; - let mustStruggle = true; - const zMoves: ZMoveOptions = []; - for (const moveSlot of pokemon.moveSlots) { - if (moveSlot.pp <= 0) { - zMoves.push(null); - continue; - } - if (!moveSlot.disabled) { - mustStruggle = false; - } - const move = this.dex.getMove(moveSlot.move); - let zMoveName = this.getZMove(move, pokemon, true) || ''; - if (zMoveName) { - const zMove = this.dex.getMove(zMoveName); - if (!zMove.isZ && zMove.category === 'Status') zMoveName = "Z-" + zMoveName; - zMoves.push({move: zMoveName, target: zMove.target}); - } else { - zMoves.push(null); - } - if (zMoveName) atLeastOne = true; - } - if (atLeastOne && !mustStruggle) return zMoves; - }, - - canMegaEvo(pokemon) { - const species = pokemon.baseSpecies; - const altForme = species.otherFormes && this.dex.getSpecies(species.otherFormes[0]); - const item = pokemon.getItem(); - // Mega Rayquaza - if ((this.gen <= 7 || this.ruleTable.has('standardnatdex')) && - altForme?.isMega && altForme?.requiredMove && - pokemon.baseMoves.includes(this.toID(altForme.requiredMove)) && !item.zMove) { - return altForme.name; - } - // a hacked-in Megazard X can mega evolve into Megazard Y, but not into Megazard X - if (item.megaEvolves === species.baseSpecies && item.megaStone !== species.name) { - return item.megaStone; - } - return null; - }, - - canUltraBurst(pokemon) { - if (['Necrozma-Dawn-Wings', 'Necrozma-Dusk-Mane'].includes(pokemon.baseSpecies.name) && - pokemon.getItem().id === 'ultranecroziumz') { - return "Necrozma-Ultra"; - } - return null; - }, - - maxMoveTable: { - Flying: 'Max Airstream', - Dark: 'Max Darkness', - Fire: 'Max Flare', - Bug: 'Max Flutterby', - Water: 'Max Geyser', - Status: 'Max Guard', - Ice: 'Max Hailstorm', - Fighting: 'Max Knuckle', - Electric: 'Max Lightning', - Psychic: 'Max Mindstorm', - Poison: 'Max Ooze', - Grass: 'Max Overgrowth', - Ghost: 'Max Phantasm', - Ground: 'Max Quake', - Rock: 'Max Rockfall', - Fairy: 'Max Starfall', - Steel: 'Max Steelspike', - Normal: 'Max Strike', - Dragon: 'Max Wyrmwind', - }, - - getMaxMove(move, pokemon) { - if (typeof move === 'string') move = this.dex.getMove(move); - if (move.name === 'Struggle') return move; - if (pokemon.gigantamax && pokemon.canGigantamax && move.category !== 'Status') { - const gMaxMove = this.dex.getMove(pokemon.canGigantamax); - if (gMaxMove.exists && gMaxMove.type === move.type) return gMaxMove; - } - const maxMove = this.dex.getMove(this.maxMoveTable[move.category === 'Status' ? move.category : move.type]); - if (maxMove.exists) return maxMove; - }, - - getActiveMaxMove(move, pokemon) { - if (typeof move === 'string') move = this.dex.getActiveMove(move); - if (move.name === 'Struggle') return this.dex.getActiveMove(move); - let maxMove = this.dex.getActiveMove(this.maxMoveTable[move.category === 'Status' ? move.category : move.type]); - if (move.category !== 'Status') { - if (pokemon.gigantamax && pokemon.canGigantamax) { - const gMaxMove = this.dex.getActiveMove(pokemon.canGigantamax); - if (gMaxMove.exists && gMaxMove.type === move.type) maxMove = gMaxMove; - } - if (!move.maxMove?.basePower) throw new Error(`${move.name} doesn't have a maxMove basePower`); - if (!['gmaxdrumsolo', 'gmaxfireball', 'gmaxhydrosnipe'].includes(maxMove.id)) { - maxMove.basePower = move.maxMove.basePower; - } - maxMove.category = move.category; - } - maxMove.baseMove = move.id; - // copy the priority for Psychic Terrain, Quick Guard - maxMove.priority = move.priority; - maxMove.isZOrMaxPowered = true; - return maxMove; - }, - - runMegaEvo(pokemon) { - const speciesid = pokemon.canMegaEvo || pokemon.canUltraBurst; - if (!speciesid) return false; - const side = pokemon.side; - - // Pokémon affected by Sky Drop cannot mega evolve. Enforce it here for now. - for (const foeActive of side.foe.active) { - if (foeActive.volatiles['skydrop'] && foeActive.volatiles['skydrop'].source === pokemon) { - return false; - } - } - - pokemon.formeChange(speciesid, pokemon.getItem(), true); - - // Limit one mega evolution - const wasMega = pokemon.canMegaEvo; - for (const ally of side.pokemon) { - if (wasMega) { - ally.canMegaEvo = null; - } else { - ally.canUltraBurst = null; - } - } - - this.runEvent('AfterMega', pokemon); - return true; - }, - - runZPower(move, pokemon) { - const zPower = this.dex.getEffect('zpower'); - if (move.category !== 'Status') { - this.attrLastMove('[zeffect]'); - } else if (move.zMove?.boost) { - this.boost(move.zMove.boost, pokemon, pokemon, zPower); - } else if (move.zMove?.effect) { - switch (move.zMove.effect) { - case 'heal': - this.heal(pokemon.maxhp, pokemon, pokemon, zPower); - break; - case 'healreplacement': - move.self = {slotCondition: 'healreplacement'}; - break; - case 'clearnegativeboost': - const boosts: SparseBoostsTable = {}; - let i: BoostName; - for (i in pokemon.boosts) { - if (pokemon.boosts[i] < 0) { - boosts[i] = 0; - } - } - pokemon.setBoost(boosts); - this.add('-clearnegativeboost', pokemon, '[zeffect]'); - break; - case 'redirect': - pokemon.addVolatile('followme', pokemon, zPower); - break; - case 'crit2': - pokemon.addVolatile('focusenergy', pokemon, zPower); - break; - case 'curse': - if (pokemon.hasType('Ghost')) { - this.heal(pokemon.maxhp, pokemon, pokemon, zPower); - } else { - this.boost({atk: 1}, pokemon, pokemon, zPower); - } - } - } - }, - - isAdjacent(pokemon1, pokemon2) { - if (pokemon1.fainted || pokemon2.fainted) return false; - if (pokemon1.side === pokemon2.side) return Math.abs(pokemon1.position - pokemon2.position) === 1; - return Math.abs(pokemon1.position + pokemon2.position + 1 - pokemon1.side.active.length) <= 1; - }, - - targetTypeChoices(targetType) { - return CHOOSABLE_TARGETS.has(targetType); - }, }; diff --git a/package.json b/package.json index cf52386931..6670b9c7c3 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,8 @@ "sucrase": "^3.15.0" }, "optionalDependencies": { - "brain.js": "^2.0.0-beta.2", "better-sqlite3": "^7.1.0", + "brain.js": "^2.0.0-beta.2", "cloud-env": "^0.2.3", "node-static": "^0.7.11", "nodemailer": "^6.4.6", diff --git a/sim/battle-actions.ts b/sim/battle-actions.ts new file mode 100644 index 0000000000..234691c940 --- /dev/null +++ b/sim/battle-actions.ts @@ -0,0 +1,1757 @@ +import {Dex, toID} from './dex'; + +const CHOOSABLE_TARGETS = new Set(['normal', 'any', 'adjacentAlly', 'adjacentAllyOrSelf', 'adjacentFoe']); + +export class BattleActions { + battle: Battle; + dex: ModdedDex; + + readonly MAX_MOVES: {readonly [k: string]: string} = { + Flying: 'Max Airstream', + Dark: 'Max Darkness', + Fire: 'Max Flare', + Bug: 'Max Flutterby', + Water: 'Max Geyser', + Status: 'Max Guard', + Ice: 'Max Hailstorm', + Fighting: 'Max Knuckle', + Electric: 'Max Lightning', + Psychic: 'Max Mindstorm', + Poison: 'Max Ooze', + Grass: 'Max Overgrowth', + Ghost: 'Max Phantasm', + Ground: 'Max Quake', + Rock: 'Max Rockfall', + Fairy: 'Max Starfall', + Steel: 'Max Steelspike', + Normal: 'Max Strike', + Dragon: 'Max Wyrmwind', + }; + + readonly Z_MOVES: {readonly [k: string]: string} = { + Poison: "Acid Downpour", + Fighting: "All-Out Pummeling", + Dark: "Black Hole Eclipse", + Grass: "Bloom Doom", + Normal: "Breakneck Blitz", + Rock: "Continental Crush", + Steel: "Corkscrew Crash", + Dragon: "Devastating Drake", + Electric: "Gigavolt Havoc", + Water: "Hydro Vortex", + Fire: "Inferno Overdrive", + Ghost: "Never-Ending Nightmare", + Bug: "Savage Spin-Out", + Psychic: "Shattered Psyche", + Ice: "Subzero Slammer", + Flying: "Supersonic Skystrike", + Ground: "Tectonic Rage", + Fairy: "Twinkle Tackle", + }; + + constructor(battle: Battle) { + this.battle = battle; + this.dex = battle.dex; + if (this.dex.data.Scripts.actions) Object.assign(this, this.dex.data.Scripts.actions); + if (battle.format.actions) Object.assign(this, battle.format.actions); + } + + // #region SWITCH + // ================================================================== + + switchIn(pokemon: Pokemon, pos: number, sourceEffect: Effect | null = null, isDrag?: boolean) { + if (!pokemon || pokemon.isActive) { + this.battle.hint("A switch failed because the Pokémon trying to switch in is already in."); + return false; + } + + const side = pokemon.side; + if (pos >= side.active.length) { + throw new Error(`Invalid switch position ${pos} / ${side.active.length}`); + } + const oldActive = side.active[pos]; + const unfaintedActive = oldActive?.hp ? oldActive : null; + if (unfaintedActive) { + oldActive.beingCalledBack = true; + let switchCopyFlag = false; + if (sourceEffect && (sourceEffect as Move).selfSwitch === 'copyvolatile') { + switchCopyFlag = true; + } + if (!oldActive.skipBeforeSwitchOutEventFlag && !isDrag) { + this.battle.runEvent('BeforeSwitchOut', oldActive); + if (this.battle.gen >= 5) { + this.battle.eachEvent('Update'); + } + } + oldActive.skipBeforeSwitchOutEventFlag = false; + if (!this.battle.runEvent('SwitchOut', oldActive)) { + // Warning: DO NOT interrupt a switch-out if you just want to trap a pokemon. + // To trap a pokemon and prevent it from switching out, (e.g. Mean Look, Magnet Pull) + // use the 'trapped' flag instead. + + // Note: Nothing in the real games can interrupt a switch-out (except Pursuit KOing, + // which is handled elsewhere); this is just for custom formats. + return false; + } + if (!oldActive.hp) { + // a pokemon fainted from Pursuit before it could switch + return 'pursuitfaint'; + } + + // will definitely switch out at this point + + oldActive.illusion = null; + this.battle.singleEvent('End', oldActive.getAbility(), oldActive.abilityData, oldActive); + + // if a pokemon is forced out by Whirlwind/etc or Eject Button/Pack, it can't use its chosen move + this.battle.queue.cancelAction(oldActive); + + let newMove = null; + if (this.battle.gen === 4 && sourceEffect) { + newMove = oldActive.lastMove; + } + if (switchCopyFlag) { + pokemon.copyVolatileFrom(oldActive); + } + if (newMove) pokemon.lastMove = newMove; + oldActive.clearVolatile(); + } + if (oldActive) { + oldActive.isActive = false; + oldActive.isStarted = false; + oldActive.usedItemThisTurn = false; + oldActive.position = pokemon.position; + pokemon.position = pos; + side.pokemon[pokemon.position] = pokemon; + side.pokemon[oldActive.position] = oldActive; + } + pokemon.isActive = true; + side.active[pos] = pokemon; + pokemon.activeTurns = 0; + pokemon.activeMoveActions = 0; + for (const moveSlot of pokemon.moveSlots) { + moveSlot.used = false; + } + this.battle.runEvent('BeforeSwitchIn', pokemon); + this.battle.add(isDrag ? 'drag' : 'switch', pokemon, pokemon.getDetails); + pokemon.abilityOrder = this.battle.abilityOrder++; + if (isDrag && this.battle.gen === 2) pokemon.draggedIn = this.battle.turn; + if (sourceEffect) this.battle.log[this.battle.log.length - 1] += `|[from]${sourceEffect.fullname}`; + pokemon.previouslySwitchedIn++; + + if (isDrag && this.battle.gen >= 5) { + // runSwitch happens immediately so that Mold Breaker can make hazards bypass Clear Body and Levitate + this.battle.singleEvent('PreStart', pokemon.getAbility(), pokemon.abilityData, pokemon); + this.runSwitch(pokemon); + } else { + this.battle.queue.insertChoice({choice: 'runUnnerve', pokemon}); + this.battle.queue.insertChoice({choice: 'runSwitch', pokemon}); + } + + return true; + } + dragIn(side: Side, pos: number) { + const pokemon = this.battle.getRandomSwitchable(side); + if (!pokemon || pokemon.isActive) return false; + const oldActive = side.active[pos]; + if (!oldActive) throw new Error(`nothing to drag out`); + if (!oldActive.hp) return false; + + if (!this.battle.runEvent('DragOut', oldActive)) { + return false; + } + if (!this.switchIn(pokemon, pos, null, true)) return false; + return true; + } + runSwitch(pokemon: Pokemon) { + this.battle.runEvent('Swap', pokemon); + this.battle.runEvent('SwitchIn', pokemon); + if (this.battle.gen <= 2 && !pokemon.side.faintedThisTurn && pokemon.draggedIn !== this.battle.turn) { + this.battle.runEvent('AfterSwitchInSelf', pokemon); + } + if (!pokemon.hp) return false; + pokemon.isStarted = true; + if (!pokemon.fainted) { + this.battle.singleEvent('Start', pokemon.getAbility(), pokemon.abilityData, pokemon); + this.battle.singleEvent('Start', pokemon.getItem(), pokemon.itemData, pokemon); + } + if (this.battle.gen === 4) { + for (const foeActive of pokemon.side.foe.active) { + foeActive.removeVolatile('substitutebroken'); + } + } + pokemon.draggedIn = null; + return true; + } + + // #endregion + + // #region MOVES + // ================================================================== + + /** + * runMove is the "outside" move caller. It handles deducting PP, + * flinching, full paralysis, etc. All the stuff up to and including + * the "POKEMON used MOVE" message. + * + * For details of the difference between runMove and useMove, see + * useMove's info. + * + * externalMove skips LockMove and PP deduction, mostly for use by + * Dancer. + */ + runMove( + moveOrMoveName: Move | string, pokemon: Pokemon, targetLoc: number, sourceEffect?: Effect | null, + zMove?: string, externalMove?: boolean, maxMove?: string, originalTarget?: Pokemon + ) { + pokemon.activeMoveActions++; + let target = this.battle.getTarget(pokemon, maxMove || zMove || moveOrMoveName, targetLoc, originalTarget); + let baseMove = this.dex.getActiveMove(moveOrMoveName); + const pranksterBoosted = baseMove.pranksterBoosted; + if (baseMove.id !== 'struggle' && !zMove && !maxMove && !externalMove) { + const changedMove = this.battle.runEvent('OverrideAction', pokemon, target, baseMove); + if (changedMove && changedMove !== true) { + baseMove = this.dex.getActiveMove(changedMove); + if (pranksterBoosted) baseMove.pranksterBoosted = pranksterBoosted; + target = this.battle.getRandomTarget(pokemon, baseMove); + } + } + let move = baseMove; + if (zMove) { + move = this.getActiveZMove(baseMove, pokemon); + } else if (maxMove) { + move = this.getActiveMaxMove(baseMove, pokemon); + } + + move.isExternal = externalMove; + + this.battle.setActiveMove(move, pokemon, target); + + /* if (pokemon.moveThisTurn) { + // THIS IS PURELY A SANITY CHECK + // DO NOT TAKE ADVANTAGE OF THIS TO PREVENT A POKEMON FROM MOVING; + // USE this.queue.cancelMove INSTEAD + this.battle.debug('' + pokemon.id + ' INCONSISTENT STATE, ALREADY MOVED: ' + pokemon.moveThisTurn); + this.battle.clearActiveMove(true); + return; + } */ + const willTryMove = this.battle.runEvent('BeforeMove', pokemon, target, move); + if (!willTryMove) { + this.battle.runEvent('MoveAborted', pokemon, target, move); + this.battle.clearActiveMove(true); + // The event 'BeforeMove' could have returned false or null + // false indicates that this counts as a move failing for the purpose of calculating Stomping Tantrum's base power + // null indicates the opposite, as the Pokemon didn't have an option to choose anything + pokemon.moveThisTurnResult = willTryMove; + return; + } + if (move.beforeMoveCallback) { + if (move.beforeMoveCallback.call(this.battle, pokemon, target, move)) { + this.battle.clearActiveMove(true); + pokemon.moveThisTurnResult = false; + return; + } + } + pokemon.lastDamage = 0; + let lockedMove; + if (!externalMove) { + lockedMove = this.battle.runEvent('LockMove', pokemon); + if (lockedMove === true) lockedMove = false; + if (!lockedMove) { + if (!pokemon.deductPP(baseMove, null, target) && (move.id !== 'struggle')) { + this.battle.add('cant', pokemon, 'nopp', move); + this.battle.clearActiveMove(true); + pokemon.moveThisTurnResult = false; + return; + } + } else { + sourceEffect = this.dex.getEffect('lockedmove'); + } + pokemon.moveUsed(move, targetLoc); + } + + // Dancer Petal Dance hack + // TODO: implement properly + const noLock = externalMove && !pokemon.volatiles['lockedmove']; + + if (zMove) { + if (pokemon.illusion) { + this.battle.singleEvent('End', this.dex.getAbility('Illusion'), pokemon.abilityData, pokemon); + } + this.battle.add('-zpower', pokemon); + pokemon.side.zMoveUsed = true; + } + const moveDidSomething = this.useMove(baseMove, pokemon, target, sourceEffect, zMove, maxMove); + this.battle.lastSuccessfulMoveThisTurn = moveDidSomething ? this.battle.activeMove && this.battle.activeMove.id : null; + if (this.battle.activeMove) move = this.battle.activeMove; + this.battle.singleEvent('AfterMove', move, null, pokemon, target, move); + this.battle.runEvent('AfterMove', pokemon, target, move); + + // Dancer's activation order is completely different from any other event, so it's handled separately + if (move.flags['dance'] && moveDidSomething && !move.isExternal) { + const dancers = []; + for (const currentPoke of this.battle.getAllActive()) { + if (pokemon === currentPoke) continue; + if (currentPoke.hasAbility('dancer') && !currentPoke.isSemiInvulnerable()) { + dancers.push(currentPoke); + } + } + // Dancer activates in order of lowest speed stat to highest + // Note that the speed stat used is after any volatile replacements like Speed Swap, + // but before any multipliers like Agility or Choice Scarf + // Ties go to whichever Pokemon has had the ability for the least amount of time + dancers.sort( + (a, b) => -(b.storedStats['spe'] - a.storedStats['spe']) || b.abilityOrder - a.abilityOrder + ); + for (const dancer of dancers) { + if (this.battle.faintMessages()) break; + if (dancer.fainted) continue; + this.battle.add('-activate', dancer, 'ability: Dancer'); + const dancersTarget = target!.side !== dancer.side && pokemon.side === dancer.side ? target! : pokemon; + const dancersTargetLoc = this.battle.getTargetLoc(dancersTarget, dancer); + this.runMove(move.id, dancer, dancersTargetLoc, this.dex.getAbility('dancer'), undefined, true); + } + } + if (noLock && pokemon.volatiles['lockedmove']) delete pokemon.volatiles['lockedmove']; + } + /** + * useMove is the "inside" move caller. It handles effects of the + * move itself, but not the idea of using the move. + * + * Most caller effects, like Sleep Talk, Nature Power, Magic Bounce, + * etc use useMove. + * + * The only ones that use runMove are Instruct, Pursuit, and + * Dancer. + */ + useMove( + move: Move | string, pokemon: Pokemon, target?: Pokemon | null, + sourceEffect?: Effect | null, zMove?: string, maxMove?: string + ) { + pokemon.moveThisTurnResult = undefined; + const oldMoveResult: boolean | null | undefined = pokemon.moveThisTurnResult; + const moveResult = this.useMoveInner(move, pokemon, target, sourceEffect, zMove, maxMove); + if (oldMoveResult === pokemon.moveThisTurnResult) pokemon.moveThisTurnResult = moveResult; + return moveResult; + } + useMoveInner( + moveOrMoveName: Move | string, pokemon: Pokemon, target?: Pokemon | null, + sourceEffect?: Effect | null, zMove?: string, maxMove?: string + ) { + if (!sourceEffect && this.battle.effect.id) sourceEffect = this.battle.effect; + if (sourceEffect && ['instruct', 'custapberry'].includes(sourceEffect.id)) sourceEffect = null; + + let move = this.dex.getActiveMove(moveOrMoveName); + pokemon.lastMoveUsed = move; + if (move.id === 'weatherball' && zMove) { + // Z-Weather Ball only changes types if it's used directly, + // not if it's called by Z-Sleep Talk or something. + this.battle.singleEvent('ModifyType', move, null, pokemon, target, move, move); + if (move.type !== 'Normal') sourceEffect = move; + } + if (zMove || (move.category !== 'Status' && sourceEffect && (sourceEffect as ActiveMove).isZ)) { + move = this.getActiveZMove(move, pokemon); + } + if (maxMove && move.category !== 'Status') { + // Max move outcome is dependent on the move type after type modifications from ability and the move itself + this.battle.singleEvent('ModifyType', move, null, pokemon, target, move, move); + this.battle.runEvent('ModifyType', pokemon, target, move, move); + } + if (maxMove || (move.category !== 'Status' && sourceEffect && (sourceEffect as ActiveMove).isMax)) { + move = this.getActiveMaxMove(move, pokemon); + } + + if (this.battle.activeMove) { + move.priority = this.battle.activeMove.priority; + if (!move.hasBounced) move.pranksterBoosted = this.battle.activeMove.pranksterBoosted; + } + const baseTarget = move.target; + let targetRelayVar = {target}; + targetRelayVar = this.battle.runEvent('ModifyTarget', pokemon, target, move, targetRelayVar, true); + if (targetRelayVar.target !== undefined) target = targetRelayVar.target; + if (target === undefined) target = this.battle.getRandomTarget(pokemon, move); + if (move.target === 'self' || move.target === 'allies') { + target = pokemon; + } + if (sourceEffect) { + move.sourceEffect = sourceEffect.id; + move.ignoreAbility = false; + } + let moveResult = false; + + this.battle.setActiveMove(move, pokemon, target); + + this.battle.singleEvent('ModifyType', move, null, pokemon, target, move, move); + this.battle.singleEvent('ModifyMove', move, null, pokemon, target, move, move); + if (baseTarget !== move.target) { + // Target changed in ModifyMove, so we must adjust it here + // Adjust before the next event so the correct target is passed to the + // event + target = this.battle.getRandomTarget(pokemon, move); + } + move = this.battle.runEvent('ModifyType', pokemon, target, move, move); + move = this.battle.runEvent('ModifyMove', pokemon, target, move, move); + if (baseTarget !== move.target) { + // Adjust again + target = this.battle.getRandomTarget(pokemon, move); + } + if (!move || pokemon.fainted) { + return false; + } + + let attrs = ''; + + let movename = move.name; + if (move.id === 'hiddenpower') movename = 'Hidden Power'; + if (sourceEffect) attrs += `|[from]${sourceEffect.fullname}`; + if (zMove && move.isZ === true) { + attrs = '|[anim]' + movename + attrs; + movename = 'Z-' + movename; + } + this.battle.addMove('move', pokemon, movename, target + attrs); + + if (zMove) this.runZPower(move, pokemon); + + if (!target) { + this.battle.attrLastMove('[notarget]'); + this.battle.add(this.battle.gen >= 5 ? '-fail' : '-notarget', pokemon); + return false; + } + + const {targets, pressureTargets} = pokemon.getMoveTargets(move, target); + if (targets.length) { + target = targets[targets.length - 1]; // in case of redirection + } + + if (!sourceEffect || sourceEffect.id === 'pursuit') { + let extraPP = 0; + for (const source of pressureTargets) { + const ppDrop = this.battle.runEvent('DeductPP', source, pokemon, move); + if (ppDrop !== true) { + extraPP += ppDrop || 0; + } + } + if (extraPP > 0) { + pokemon.deductPP(moveOrMoveName, extraPP); + } + } + + if (!this.battle.singleEvent('TryMove', move, null, pokemon, target, move) || + !this.battle.runEvent('TryMove', pokemon, target, move)) { + move.mindBlownRecoil = false; + return false; + } + + this.battle.singleEvent('UseMoveMessage', move, null, pokemon, target, move); + + if (move.ignoreImmunity === undefined) { + move.ignoreImmunity = (move.category === 'Status'); + } + + if (this.battle.gen !== 4 && move.selfdestruct === 'always') { + this.battle.faint(pokemon, pokemon, move); + } + + let damage: number | false | undefined | '' = false; + if (move.target === 'all' || move.target === 'foeSide' || move.target === 'allySide' || move.target === 'allyTeam') { + damage = this.tryMoveHit(target, pokemon, move); + if (damage === this.battle.NOT_FAIL) pokemon.moveThisTurnResult = null; + if (damage || damage === 0 || damage === undefined) moveResult = true; + } else { + if (!targets.length) { + this.battle.attrLastMove('[notarget]'); + this.battle.add(this.battle.gen >= 5 ? '-fail' : '-notarget', pokemon); + return false; + } + if (this.battle.gen === 4 && move.selfdestruct === 'always') { + this.battle.faint(pokemon, pokemon, move); + } + moveResult = this.trySpreadMoveHit(targets, pokemon, move); + } + if (move.selfBoost && moveResult) this.moveHit(pokemon, pokemon, move, move.selfBoost, false, true); + if (!pokemon.hp) { + this.battle.faint(pokemon, pokemon, move); + } + + if (!moveResult) { + this.battle.singleEvent('MoveFail', move, null, target, pokemon, move); + return false; + } + + if (!move.negateSecondary && !(move.hasSheerForce && pokemon.hasAbility('sheerforce'))) { + const originalHp = pokemon.hp; + this.battle.singleEvent('AfterMoveSecondarySelf', move, null, pokemon, target, move); + this.battle.runEvent('AfterMoveSecondarySelf', pokemon, target, move); + if (pokemon && pokemon !== target && move.category !== 'Status') { + if (pokemon.hp <= pokemon.maxhp / 2 && originalHp > pokemon.maxhp / 2) { + this.battle.runEvent('EmergencyExit', pokemon, pokemon); + } + } + } + + return true; + } + /** NOTE: includes single-target moves */ + trySpreadMoveHit(targets: Pokemon[], pokemon: Pokemon, move: ActiveMove, notActive?: boolean) { + if (targets.length > 1 && !move.smartTarget) move.spreadHit = true; + + const moveSteps: ((targets: Pokemon[], pokemon: Pokemon, move: ActiveMove) => + (number | boolean | "" | undefined)[] | undefined)[] = [ + // 0. check for semi invulnerability + this.hitStepInvulnerabilityEvent, + + // 1. run the 'TryHit' event (Protect, Magic Bounce, Volt Absorb, etc.) (this is step 2 in gens 5 & 6, and step 4 in gen 4) + this.hitStepTryHitEvent, + + // 2. check for type immunity (this is step 1 in gens 4-6) + this.hitStepTypeImmunity, + + // 3. check for various move-specific immunities + this.hitStepTryImmunity, + + // 4. check accuracy + this.hitStepAccuracy, + + // 5. break protection effects + this.hitStepBreakProtect, + + // 6. steal positive boosts (Spectral Thief) + this.hitStepStealBoosts, + + // 7. loop that processes each hit of the move (has its own steps per iteration) + this.hitStepMoveHitLoop, + ]; + if (this.battle.gen <= 6) { + // Swap step 1 with step 2 + [moveSteps[1], moveSteps[2]] = [moveSteps[2], moveSteps[1]]; + } + if (this.battle.gen === 4) { + // Swap step 4 with new step 2 (old step 1) + [moveSteps[2], moveSteps[4]] = [moveSteps[4], moveSteps[2]]; + } + + if (!notActive) this.battle.setActiveMove(move, pokemon, targets[0]); + + const hitResult = this.battle.singleEvent('Try', move, null, pokemon, targets[0], move) && + this.battle.singleEvent('PrepareHit', move, {}, targets[0], pokemon, move) && + this.battle.runEvent('PrepareHit', pokemon, targets[0], move); + if (!hitResult) { + if (hitResult === false) { + this.battle.add('-fail', pokemon); + this.battle.attrLastMove('[still]'); + } + return hitResult === this.battle.NOT_FAIL; + } + + let atLeastOneFailure!: boolean; + for (const step of moveSteps) { + const hitResults: (number | boolean | "" | undefined)[] | undefined = step.call(this, targets, pokemon, move); + if (!hitResults) continue; + targets = targets.filter((val, i) => hitResults[i] || hitResults[i] === 0); + atLeastOneFailure = atLeastOneFailure || hitResults.some(val => val === false); + if (!targets.length) { + // console.log(step.name); + break; + } + } + + const moveResult = !!targets.length; + if (!moveResult && !atLeastOneFailure) pokemon.moveThisTurnResult = null; + const hitSlot = targets.map(p => p.getSlot()); + if (move.spreadHit) this.battle.attrLastMove('[spread] ' + hitSlot.join(',')); + return moveResult; + } + hitStepInvulnerabilityEvent(targets: Pokemon[], pokemon: Pokemon, move: ActiveMove) { + if (move.id === 'helpinghand' || (this.battle.gen >= 8 && move.id === 'toxic' && pokemon.hasType('Poison'))) { + return new Array(targets.length).fill(true); + } + const hitResults = this.battle.runEvent('Invulnerability', targets, pokemon, move); + for (const [i, target] of targets.entries()) { + if (hitResults[i] === false) { + if (move.smartTarget) { + move.smartTarget = false; + } else { + if (!move.spreadHit) this.battle.attrLastMove('[miss]'); + this.battle.add('-miss', pokemon, target); + } + } + } + return hitResults; + } + hitStepTryHitEvent(targets: Pokemon[], pokemon: Pokemon, move: ActiveMove) { + const hitResults = this.battle.runEvent('TryHit', targets, pokemon, move); + if (!hitResults.includes(true) && hitResults.includes(false)) { + this.battle.add('-fail', pokemon); + this.battle.attrLastMove('[still]'); + } + for (const i of targets.keys()) { + if (hitResults[i] !== this.battle.NOT_FAIL) hitResults[i] = hitResults[i] || false; + } + return hitResults; + } + hitStepTypeImmunity(targets: Pokemon[], pokemon: Pokemon, move: ActiveMove) { + if (move.ignoreImmunity === undefined) { + move.ignoreImmunity = (move.category === 'Status'); + } + + const hitResults = []; + for (const i of targets.keys()) { + hitResults[i] = (move.ignoreImmunity && (move.ignoreImmunity === true || move.ignoreImmunity[move.type])) || + targets[i].runImmunity(move.type, !move.smartTarget); + if (move.smartTarget && !hitResults[i]) move.smartTarget = false; + } + + return hitResults; + } + hitStepTryImmunity(targets: Pokemon[], pokemon: Pokemon, move: ActiveMove) { + const hitResults = []; + for (const [i, target] of targets.entries()) { + if (this.battle.gen >= 6 && move.flags['powder'] && target !== pokemon && !this.dex.getImmunity('powder', target)) { + this.battle.debug('natural powder immunity'); + this.battle.add('-immune', target); + hitResults[i] = false; + } else if (!this.battle.singleEvent('TryImmunity', move, {}, target, pokemon, move)) { + this.battle.add('-immune', target); + hitResults[i] = false; + } else if (this.battle.gen >= 7 && move.pranksterBoosted && pokemon.hasAbility('prankster') && + targets[i].side !== pokemon.side && !this.dex.getImmunity('prankster', target)) { + this.battle.debug('natural prankster immunity'); + if (!target.illusion) this.battle.hint("Since gen 7, Dark is immune to Prankster moves."); + this.battle.add('-immune', target); + hitResults[i] = false; + } else { + hitResults[i] = true; + } + } + return hitResults; + } + hitStepAccuracy(targets: Pokemon[], pokemon: Pokemon, move: ActiveMove) { + const hitResults = []; + for (const [i, target] of targets.entries()) { + this.battle.activeTarget = target; + // calculate true accuracy + let accuracy = move.accuracy; + if (move.ohko) { // bypasses accuracy modifiers + if (!target.isSemiInvulnerable()) { + accuracy = 30; + if (move.ohko === 'Ice' && this.battle.gen >= 7 && !pokemon.hasType('Ice')) { + accuracy = 20; + } + if (!target.volatiles['dynamax'] && pokemon.level >= target.level && + (move.ohko === true || !target.hasType(move.ohko))) { + accuracy += (pokemon.level - target.level); + } else { + this.battle.add('-immune', target, '[ohko]'); + hitResults[i] = false; + continue; + } + } + } else { + accuracy = this.battle.runEvent('ModifyAccuracy', target, pokemon, move, accuracy); + if (accuracy !== true) { + let boost = 0; + if (!move.ignoreAccuracy) { + const boosts = this.battle.runEvent('ModifyBoost', pokemon, null, null, {...pokemon.boosts}); + boost = this.battle.clampIntRange(boosts['accuracy'], -6, 6); + } + if (!move.ignoreEvasion) { + const boosts = this.battle.runEvent('ModifyBoost', target, null, null, {...target.boosts}); + boost = this.battle.clampIntRange(boost - boosts['evasion'], -6, 6); + } + if (boost > 0) { + accuracy = this.battle.trunc(accuracy * (3 + boost) / 3); + } else if (boost < 0) { + accuracy = this.battle.trunc(accuracy * 3 / (3 - boost)); + } + } + } + if (move.alwaysHit || (move.id === 'toxic' && this.battle.gen >= 8 && pokemon.hasType('Poison')) || + (move.target === 'self' && move.category === 'Status' && !target.isSemiInvulnerable())) { + accuracy = true; // bypasses ohko accuracy modifiers + } else { + accuracy = this.battle.runEvent('Accuracy', target, pokemon, move, accuracy); + } + if (accuracy !== true && !this.battle.randomChance(accuracy, 100)) { + if (move.smartTarget) { + move.smartTarget = false; + } else { + if (!move.spreadHit) this.battle.attrLastMove('[miss]'); + this.battle.add('-miss', pokemon, target); + } + if (!move.ohko && pokemon.hasItem('blunderpolicy') && pokemon.useItem()) { + this.battle.boost({spe: 2}, pokemon); + } + hitResults[i] = false; + continue; + } + hitResults[i] = true; + } + return hitResults; + } + hitStepBreakProtect(targets: Pokemon[], pokemon: Pokemon, move: ActiveMove) { + if (move.breaksProtect) { + for (const target of targets) { + let broke = false; + for (const effectid of ['banefulbunker', 'kingsshield', 'obstruct', 'protect', 'spikyshield']) { + if (target.removeVolatile(effectid)) broke = true; + } + if (this.battle.gen >= 6 || target.side !== pokemon.side) { + for (const effectid of ['craftyshield', 'matblock', 'quickguard', 'wideguard']) { + if (target.side.removeSideCondition(effectid)) broke = true; + } + } + if (broke) { + if (move.id === 'feint') { + this.battle.add('-activate', target, 'move: Feint'); + } else { + this.battle.add('-activate', target, 'move: ' + move.name, '[broken]'); + } + if (this.battle.gen >= 6) delete target.volatiles['stall']; + } + } + } + return undefined; + } + hitStepStealBoosts(targets: Pokemon[], pokemon: Pokemon, move: ActiveMove) { + const target = targets[0]; // hardcoded + if (move.stealsBoosts) { + const boosts: SparseBoostsTable = {}; + let stolen = false; + let statName: BoostName; + for (statName in target.boosts) { + const stage = target.boosts[statName]; + if (stage > 0) { + boosts[statName] = stage; + stolen = true; + } + } + if (stolen) { + this.battle.attrLastMove('[still]'); + this.battle.add('-clearpositiveboost', target, pokemon, 'move: ' + move.name); + this.battle.boost(boosts, pokemon, pokemon); + + let statName2: BoostName; + for (statName2 in boosts) { + boosts[statName2] = 0; + } + target.setBoost(boosts); + this.battle.addMove('-anim', pokemon, "Spectral Thief", target); + } + } + return undefined; + } + afterMoveSecondaryEvent(targets: Pokemon[], pokemon: Pokemon, move: ActiveMove) { + // console.log(`${targets}, ${pokemon}, ${move}`) + if (!move.negateSecondary && !(move.hasSheerForce && pokemon.hasAbility('sheerforce'))) { + this.battle.singleEvent('AfterMoveSecondary', move, null, targets[0], pokemon, move); + this.battle.runEvent('AfterMoveSecondary', targets, pokemon, move); + } + return undefined; + } + /** NOTE: used only for moves that target sides/fields rather than pokemon */ + tryMoveHit(target: Pokemon, pokemon: Pokemon, move: ActiveMove): number | undefined | false | '' { + this.battle.setActiveMove(move, pokemon, target); + + let hitResult = this.battle.singleEvent('Try', move, null, pokemon, target, move) && + this.battle.singleEvent('PrepareHit', move, {}, target, pokemon, move) && + this.battle.runEvent('PrepareHit', pokemon, target, move); + if (!hitResult) { + if (hitResult === false) { + this.battle.add('-fail', pokemon); + this.battle.attrLastMove('[still]'); + } + return false; + } + + if (move.target === 'all') { + hitResult = this.battle.runEvent('TryHitField', target, pokemon, move); + } else { + hitResult = this.battle.runEvent('TryHitSide', target, pokemon, move); + } + if (!hitResult) { + if (hitResult === false) { + this.battle.add('-fail', pokemon); + this.battle.attrLastMove('[still]'); + } + return false; + } + return this.moveHit(target, pokemon, move); + } + hitStepMoveHitLoop(targets: Pokemon[], pokemon: Pokemon, move: ActiveMove) { // Temporary name + const damage: (number | boolean | undefined)[] = []; + for (const i of targets.keys()) { + damage[i] = 0; + } + move.totalDamage = 0; + pokemon.lastDamage = 0; + let targetHits = move.multihit || 1; + if (Array.isArray(targetHits)) { + // yes, it's hardcoded... meh + if (targetHits[0] === 2 && targetHits[1] === 5) { + if (this.battle.gen >= 5) { + // 35-35-15-15 out of 100 for 2-3-4-5 hits + targetHits = this.battle.sample([2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 5, 5, 5]); + } else { + targetHits = this.battle.sample([2, 2, 2, 3, 3, 3, 4, 5]); + } + } else { + targetHits = this.battle.random(targetHits[0], targetHits[1] + 1); + } + } + targetHits = Math.floor(targetHits); + let nullDamage = true; + let moveDamage: (number | boolean | undefined)[] = []; + // There is no need to recursively check the ´sleepUsable´ flag as Sleep Talk can only be used while asleep. + const isSleepUsable = move.sleepUsable || this.dex.getMove(move.sourceEffect).sleepUsable; + + let targetsCopy: (Pokemon | false | null)[] = targets.slice(0); + let hit: number; + for (hit = 1; hit <= targetHits; hit++) { + if (damage.includes(false)) break; + if (hit > 1 && pokemon.status === 'slp' && !isSleepUsable) break; + if (targets.every(target => !target?.hp)) break; + move.hit = hit; + if (move.smartTarget && targets.length > 1) { + targetsCopy = [targets[hit - 1]]; + } else { + targetsCopy = targets.slice(0); + } + const target = targetsCopy[0]; // some relevant-to-single-target-moves-only things are hardcoded + if (target && typeof move.smartTarget === 'boolean') { + if (hit > 1) { + this.battle.addMove('-anim', pokemon, move.name, target); + } else { + this.battle.retargetLastMove(target); + } + } + + // like this (Triple Kick) + if (target && move.multiaccuracy && hit > 1) { + let accuracy = move.accuracy; + const boostTable = [1, 4 / 3, 5 / 3, 2, 7 / 3, 8 / 3, 3]; + if (accuracy !== true) { + if (!move.ignoreAccuracy) { + const boosts = this.battle.runEvent('ModifyBoost', pokemon, null, null, {...pokemon.boosts}); + const boost = this.battle.clampIntRange(boosts['accuracy'], -6, 6); + if (boost > 0) { + accuracy *= boostTable[boost]; + } else { + accuracy /= boostTable[-boost]; + } + } + if (!move.ignoreEvasion) { + const boosts = this.battle.runEvent('ModifyBoost', target, null, null, {...target.boosts}); + const boost = this.battle.clampIntRange(boosts['evasion'], -6, 6); + if (boost > 0) { + accuracy /= boostTable[boost]; + } else if (boost < 0) { + accuracy *= boostTable[-boost]; + } + } + } + accuracy = this.battle.runEvent('ModifyAccuracy', target, pokemon, move, accuracy); + if (!move.alwaysHit) { + accuracy = this.battle.runEvent('Accuracy', target, pokemon, move, accuracy); + if (accuracy !== true && !this.battle.randomChance(accuracy, 100)) break; + } + } + + const moveData = move; + if (!moveData.flags) moveData.flags = {}; + + // Modifies targetsCopy (which is why it's a copy) + [moveDamage, targetsCopy] = this.spreadMoveHit(targetsCopy, pokemon, move, moveData); + + if (!moveDamage.some(val => val !== false)) break; + nullDamage = false; + + for (const [i, md] of moveDamage.entries()) { + // Damage from each hit is individually counted for the + // purposes of Counter, Metal Burst, and Mirror Coat. + damage[i] = md === true || !md ? 0 : md; + // Total damage dealt is accumulated for the purposes of recoil (Parental Bond). + move.totalDamage += damage[i] as number; + } + if (move.mindBlownRecoil) { + this.battle.damage(Math.round(pokemon.maxhp / 2), pokemon, pokemon, this.dex.getEffect('Mind Blown'), true); + move.mindBlownRecoil = false; + } + this.battle.eachEvent('Update'); + if (!pokemon.hp && targets.length === 1) { + hit++; // report the correct number of hits for multihit moves + break; + } + } + // hit is 1 higher than the actual hit count + if (hit === 1) return damage.fill(false); + if (nullDamage) damage.fill(false); + if (move.multihit && typeof move.smartTarget !== 'boolean') { + this.battle.add('-hitcount', targets[0], hit - 1); + } + + if (move.recoil && move.totalDamage) { + this.battle.damage(this.calcRecoilDamage(move.totalDamage, move), pokemon, pokemon, 'recoil'); + } + + if (move.struggleRecoil) { + let recoilDamage; + if (this.dex.gen >= 5) { + recoilDamage = this.battle.clampIntRange(Math.round(pokemon.baseMaxhp / 4), 1); + } else { + recoilDamage = this.battle.clampIntRange(this.battle.trunc(pokemon.maxhp / 4), 1); + } + this.battle.directDamage(recoilDamage, pokemon, pokemon, {id: 'strugglerecoil'} as Condition); + } + + // smartTarget messes up targetsCopy, but smartTarget should in theory ensure that targets will never fail, anyway + if (move.smartTarget) targetsCopy = targets.slice(0); + + for (const [i, target] of targetsCopy.entries()) { + if (target && pokemon !== target) { + target.gotAttacked(move, moveDamage[i] as number | false | undefined, pokemon); + } + } + + if (move.ohko && !targets[0].hp) this.battle.add('-ohko'); + + if (!damage.some(val => !!val || val === 0)) return damage; + + this.battle.eachEvent('Update'); + + this.afterMoveSecondaryEvent(targetsCopy.filter(val => !!val) as Pokemon[], pokemon, move); + + if (!move.negateSecondary && !(move.hasSheerForce && pokemon.hasAbility('sheerforce'))) { + for (const [i, d] of damage.entries()) { + // There are no multihit spread moves, so it's safe to use move.totalDamage for multihit moves + // The previous check was for `move.multihit`, but that fails for Dragon Darts + const curDamage = targets.length === 1 ? move.totalDamage : d; + if (typeof curDamage === 'number' && targets[i].hp) { + const targetHPBeforeDamage = (targets[i].hurtThisTurn || 0) + curDamage; + if (targets[i].hp <= targets[i].maxhp / 2 && targetHPBeforeDamage > targets[i].maxhp / 2) { + this.battle.runEvent('EmergencyExit', targets[i], pokemon); + } + } + } + } + + return damage; + } + spreadMoveHit( + targets: SpreadMoveTargets, pokemon: Pokemon, moveOrMoveName: ActiveMove, + hitEffect?: Dex.HitEffect, isSecondary?: boolean, isSelf?: boolean + ): [SpreadMoveDamage, SpreadMoveTargets] { + // Hardcoded for single-target purposes + // (no spread moves have any kind of onTryHit handler) + const target = targets[0]; + let damage: (number | boolean | undefined)[] = []; + for (const i of targets.keys()) { + damage[i] = true; + } + const move = this.dex.getActiveMove(moveOrMoveName); + let hitResult: boolean | number | null = true; + let moveData = hitEffect as ActiveMove; + if (!moveData) moveData = move; + if (!moveData.flags) moveData.flags = {}; + if (move.target === 'all' && !isSelf) { + hitResult = this.battle.singleEvent('TryHitField', moveData, {}, target || null, pokemon, move); + } else if ((move.target === 'foeSide' || move.target === 'allySide') && !isSelf) { + hitResult = this.battle.singleEvent('TryHitSide', moveData, {}, (target ? target.side : null), pokemon, move); + } else if (target) { + hitResult = this.battle.singleEvent('TryHit', moveData, {}, target, pokemon, move); + } + if (!hitResult) { + if (hitResult === false) { + this.battle.add('-fail', pokemon); + this.battle.attrLastMove('[still]'); + } + return [[false], targets]; // single-target only + } + + // 0. check for substitute + if (!isSecondary && !isSelf) { + if (move.target !== 'all' && move.target !== 'allySide' && move.target !== 'foeSide') { + damage = this.tryPrimaryHitEvent(damage, targets, pokemon, move, moveData, isSecondary); + } + } + + for (const i of targets.keys()) { + if (damage[i] === this.battle.HIT_SUBSTITUTE) { + damage[i] = true; + targets[i] = null; + } + if (targets[i] && isSecondary && !moveData.self) { + damage[i] = true; + } + if (!damage[i]) targets[i] = false; + } + // 1. call to this.battle.getDamage + damage = this.getSpreadDamage(damage, targets, pokemon, move, moveData, isSecondary, isSelf); + + for (const i of targets.keys()) { + if (damage[i] === false) targets[i] = false; + } + + // 2. call to this.battle.spreadDamage + damage = this.battle.spreadDamage(damage, targets, pokemon, move); + + for (const i of targets.keys()) { + if (damage[i] === false) targets[i] = false; + } + + // 3. onHit event happens here + damage = this.runMoveEffects(damage, targets, pokemon, move, moveData, isSecondary, isSelf); + + for (const i of targets.keys()) { + if (!damage[i] && damage[i] !== 0) targets[i] = false; + } + + // 4. self drops (start checking for targets[i] === false here) + if (moveData.self && !move.selfDropped) this.selfDrops(targets, pokemon, move, moveData, isSecondary); + + // 5. secondary effects + if (moveData.secondaries) this.secondaries(targets, pokemon, move, moveData, isSelf); + + // 6. force switch + if (moveData.forceSwitch) damage = this.forceSwitch(damage, targets, pokemon, move); + + for (const i of targets.keys()) { + if (!damage[i] && damage[i] !== 0) targets[i] = false; + } + + const damagedTargets: Pokemon[] = []; + const damagedDamage = []; + for (const [i, t] of targets.entries()) { + if (typeof damage[i] === 'number' && t) { + damagedTargets.push(t); + damagedDamage.push(damage[i]); + } + } + const pokemonOriginalHP = pokemon.hp; + if (damagedDamage.length && !isSecondary && !isSelf) { + this.battle.runEvent('DamagingHit', damagedTargets, pokemon, move, damagedDamage); + if (moveData.onAfterHit) { + for (const t of damagedTargets) { + this.battle.singleEvent('AfterHit', moveData, {}, t, pokemon, move); + } + } + if (pokemon.hp && pokemon.hp <= pokemon.maxhp / 2 && pokemonOriginalHP > pokemon.maxhp / 2) { + this.battle.runEvent('EmergencyExit', pokemon); + } + } + + return [damage, targets]; + } + tryPrimaryHitEvent( + damage: SpreadMoveDamage, targets: SpreadMoveTargets, pokemon: Pokemon, + move: ActiveMove, moveData: ActiveMove, isSecondary?: boolean + ): SpreadMoveDamage { + for (const [i, target] of targets.entries()) { + if (!target) continue; + damage[i] = this.battle.runEvent('TryPrimaryHit', target, pokemon, moveData); + } + return damage; + } + getSpreadDamage( + damage: SpreadMoveDamage, targets: SpreadMoveTargets, source: Pokemon, + move: ActiveMove, moveData: ActiveMove, isSecondary?: boolean, isSelf?: boolean + ): SpreadMoveDamage { + for (const [i, target] of targets.entries()) { + if (!target) continue; + this.battle.activeTarget = target; + damage[i] = undefined; + const curDamage = this.getDamage(source, target, moveData); + // getDamage has several possible return values: + // + // a number: + // means that much damage is dealt (0 damage still counts as dealing + // damage for the purposes of things like Static) + // false: + // gives error message: "But it failed!" and move ends + // null: + // the move ends, with no message (usually, a custom fail message + // was already output by an event handler) + // undefined: + // means no damage is dealt and the move continues + // + // basically, these values have the same meanings as they do for event + // handlers. + + if (curDamage === false || curDamage === null) { + if (damage[i] === false && !isSecondary && !isSelf) { + this.battle.add('-fail', source); + this.battle.attrLastMove('[still]'); + } + this.battle.debug('damage calculation interrupted'); + damage[i] = false; + continue; + } + damage[i] = curDamage; + if (move.selfdestruct === 'ifHit') { + this.battle.faint(source, source, move); + } + } + return damage; + } + runMoveEffects( + damage: SpreadMoveDamage, targets: SpreadMoveTargets, source: Pokemon, + move: ActiveMove, moveData: ActiveMove, isSecondary?: boolean, isSelf?: boolean + ) { + let didAnything: number | boolean | null | undefined = damage.reduce(this.combineResults); + for (const [i, target] of targets.entries()) { + if (target === false) continue; + let hitResult; + let didSomething: number | boolean | null | undefined = undefined; + + if (target) { + if (moveData.boosts && !target.fainted) { + hitResult = this.battle.boost(moveData.boosts, target, source, move, isSecondary, isSelf); + didSomething = this.combineResults(didSomething, hitResult); + } + if (moveData.heal && !target.fainted) { + if (target.hp >= target.maxhp) { + this.battle.add('-fail', target, 'heal'); + this.battle.attrLastMove('[still]'); + damage[i] = this.combineResults(damage[i], false); + didAnything = this.combineResults(didAnything, null); + continue; + } + const amount = target.baseMaxhp * moveData.heal[0] / moveData.heal[1]; + const d = target.heal((this.battle.gen < 5 ? Math.floor : Math.round)(amount)); + if (!d && d !== 0) { + this.battle.add('-fail', source); + this.battle.attrLastMove('[still]'); + this.battle.debug('heal interrupted'); + damage[i] = this.combineResults(damage[i], false); + didAnything = this.combineResults(didAnything, null); + continue; + } + this.battle.add('-heal', target, target.getHealth); + didSomething = true; + } + if (moveData.status) { + hitResult = target.trySetStatus(moveData.status, source, moveData.ability ? moveData.ability : move); + if (!hitResult && move.status) { + damage[i] = this.combineResults(damage[i], false); + didAnything = this.combineResults(didAnything, null); + continue; + } + didSomething = this.combineResults(didSomething, hitResult); + } + if (moveData.forceStatus) { + hitResult = target.setStatus(moveData.forceStatus, source, move); + didSomething = this.combineResults(didSomething, hitResult); + } + if (moveData.volatileStatus) { + hitResult = target.addVolatile(moveData.volatileStatus, source, move); + didSomething = this.combineResults(didSomething, hitResult); + } + if (moveData.sideCondition) { + hitResult = target.side.addSideCondition(moveData.sideCondition, source, move); + didSomething = this.combineResults(didSomething, hitResult); + } + if (moveData.slotCondition) { + hitResult = target.side.addSlotCondition(target, moveData.slotCondition, source, move); + didSomething = this.combineResults(didSomething, hitResult); + } + if (moveData.weather) { + hitResult = this.battle.field.setWeather(moveData.weather, source, move); + didSomething = this.combineResults(didSomething, hitResult); + } + if (moveData.terrain) { + hitResult = this.battle.field.setTerrain(moveData.terrain, source, move); + didSomething = this.combineResults(didSomething, hitResult); + } + if (moveData.pseudoWeather) { + hitResult = this.battle.field.addPseudoWeather(moveData.pseudoWeather, source, move); + didSomething = this.combineResults(didSomething, hitResult); + } + if (moveData.forceSwitch) { + hitResult = !!this.battle.canSwitch(target.side); + didSomething = this.combineResults(didSomething, hitResult); + } + // Hit events + // These are like the TryHit events, except we don't need a FieldHit event. + // Scroll up for the TryHit event documentation, and just ignore the "Try" part. ;) + if (move.target === 'all' && !isSelf) { + if (moveData.onHitField) { + hitResult = this.battle.singleEvent('HitField', moveData, {}, target, source, move); + didSomething = this.combineResults(didSomething, hitResult); + } + } else if ((move.target === 'foeSide' || move.target === 'allySide') && !isSelf) { + if (moveData.onHitSide) { + hitResult = this.battle.singleEvent('HitSide', moveData, {}, target.side, source, move); + didSomething = this.combineResults(didSomething, hitResult); + } + } else { + if (moveData.onHit) { + hitResult = this.battle.singleEvent('Hit', moveData, {}, target, source, move); + didSomething = this.combineResults(didSomething, hitResult); + } + if (!isSelf && !isSecondary) { + this.battle.runEvent('Hit', target, source, move); + } + } + } + if (moveData.selfSwitch) { + if (this.battle.canSwitch(source.side)) { + didSomething = true; + } else { + didSomething = this.combineResults(didSomething, false); + } + } + // Move didn't fail because it didn't try to do anything + if (didSomething === undefined) didSomething = true; + damage[i] = this.combineResults(damage[i], didSomething === null ? false : didSomething); + didAnything = this.combineResults(didAnything, didSomething); + } + + + if (!didAnything && didAnything !== 0 && !moveData.self && !moveData.selfdestruct) { + if (!isSelf && !isSecondary) { + if (didAnything === false) { + this.battle.add('-fail', source); + this.battle.attrLastMove('[still]'); + } + } + this.battle.debug('move failed because it did nothing'); + } else if (move.selfSwitch && source.hp) { + source.switchFlag = move.id; + } + + return damage; + } + selfDrops( + targets: SpreadMoveTargets, source: Pokemon, + move: ActiveMove, moveData: ActiveMove, isSecondary?: boolean + ) { + for (const target of targets) { + if (target === false) continue; + if (moveData.self && !move.selfDropped) { + if (!isSecondary && moveData.self.boosts) { + const secondaryRoll = this.battle.random(100); + if (typeof moveData.self.chance === 'undefined' || secondaryRoll < moveData.self.chance) { + this.moveHit(source, source, move, moveData.self, isSecondary, true); + } + if (!move.multihit) move.selfDropped = true; + } else { + this.moveHit(source, source, move, moveData.self, isSecondary, true); + } + } + } + } + secondaries(targets: SpreadMoveTargets, source: Pokemon, move: ActiveMove, moveData: ActiveMove, isSelf?: boolean) { + if (!moveData.secondaries) return; + for (const target of targets) { + if (target === false) continue; + const secondaries: Dex.SecondaryEffect[] = + this.battle.runEvent('ModifySecondaries', target, source, moveData, moveData.secondaries.slice()); + for (const secondary of secondaries) { + const secondaryRoll = this.battle.random(100); + if (typeof secondary.chance === 'undefined' || secondaryRoll < secondary.chance) { + this.moveHit(target, source, move, secondary, true, isSelf); + } + } + } + } + forceSwitch( + damage: SpreadMoveDamage, targets: SpreadMoveTargets, source: Pokemon, move: ActiveMove + ) { + for (const [i, target] of targets.entries()) { + if (target && target.hp > 0 && source.hp > 0 && this.battle.canSwitch(target.side)) { + const hitResult = this.battle.runEvent('DragOut', target, source, move); + if (hitResult) { + target.forceSwitchFlag = true; + } else if (hitResult === false && move.category === 'Status') { + this.battle.add('-fail', source); + this.battle.attrLastMove('[still]'); + damage[i] = false; + } + } + } + return damage; + } + moveHit( + target: Pokemon | null, pokemon: Pokemon, moveOrMoveName: ActiveMove, + moveData?: Dex.HitEffect, isSecondary?: boolean, isSelf?: boolean + ): number | undefined | false { + const retVal = this.spreadMoveHit([target], pokemon, moveOrMoveName, moveData, isSecondary, isSelf)[0][0]; + return retVal === true ? undefined : retVal; + } + + calcRecoilDamage(damageDealt: number, move: Move): number { + return this.battle.clampIntRange(Math.round(damageDealt * move.recoil![0] / move.recoil![1]), 1); + } + + getZMove(move: Move, pokemon: Pokemon, skipChecks?: boolean): string | undefined { + const item = pokemon.getItem(); + if (!skipChecks) { + if (pokemon.side.zMoveUsed) return; + if (!item.zMove) return; + if (item.itemUser && !item.itemUser.includes(pokemon.species.name)) return; + const moveData = pokemon.getMoveData(move); + // Draining the PP of the base move prevents the corresponding Z-move from being used. + if (!moveData?.pp) return; + } + + if (item.zMoveFrom) { + if (move.name === item.zMoveFrom) return item.zMove as string; + } else if (item.zMove === true) { + if (move.type === item.zMoveType) { + if (move.category === "Status") { + return move.name; + } else if (move.zMove?.basePower) { + return this.Z_MOVES[move.type]; + } + } + } + } + + getActiveZMove(move: Move, pokemon: Pokemon): ActiveMove { + if (pokemon) { + const item = pokemon.getItem(); + if (move.name === item.zMoveFrom) { + const zMove = this.dex.getActiveMove(item.zMove as string); + zMove.isZOrMaxPowered = true; + return zMove; + } + } + + if (move.category === 'Status') { + const zMove = this.dex.getActiveMove(move); + zMove.isZ = true; + zMove.isZOrMaxPowered = true; + return zMove; + } + const zMove = this.dex.getActiveMove(this.Z_MOVES[move.type]); + zMove.basePower = move.zMove!.basePower!; + zMove.category = move.category; + // copy the priority for Quick Guard + zMove.priority = move.priority; + zMove.isZOrMaxPowered = true; + return zMove; + } + + canZMove(pokemon: Pokemon) { + if (pokemon.side.zMoveUsed || + (pokemon.transformed && + (pokemon.species.isMega || pokemon.species.isPrimal || pokemon.species.forme === "Ultra")) + ) return; + const item = pokemon.getItem(); + if (!item.zMove) return; + if (item.itemUser && !item.itemUser.includes(pokemon.species.name)) return; + let atLeastOne = false; + let mustStruggle = true; + const zMoves: ZMoveOptions = []; + for (const moveSlot of pokemon.moveSlots) { + if (moveSlot.pp <= 0) { + zMoves.push(null); + continue; + } + if (!moveSlot.disabled) { + mustStruggle = false; + } + const move = this.dex.getMove(moveSlot.move); + let zMoveName = this.getZMove(move, pokemon, true) || ''; + if (zMoveName) { + const zMove = this.dex.getMove(zMoveName); + if (!zMove.isZ && zMove.category === 'Status') zMoveName = "Z-" + zMoveName; + zMoves.push({move: zMoveName, target: zMove.target}); + } else { + zMoves.push(null); + } + if (zMoveName) atLeastOne = true; + } + if (atLeastOne && !mustStruggle) return zMoves; + } + + getMaxMove(move: Move, pokemon: Pokemon) { + if (typeof move === 'string') move = this.dex.getMove(move); + if (move.name === 'Struggle') return move; + if (pokemon.gigantamax && pokemon.canGigantamax && move.category !== 'Status') { + const gMaxMove = this.dex.getMove(pokemon.canGigantamax); + if (gMaxMove.exists && gMaxMove.type === move.type) return gMaxMove; + } + const maxMove = this.dex.getMove(this.MAX_MOVES[move.category === 'Status' ? move.category : move.type]); + if (maxMove.exists) return maxMove; + } + + getActiveMaxMove(move: Move, pokemon: Pokemon) { + if (typeof move === 'string') move = this.dex.getActiveMove(move); + if (move.name === 'Struggle') return this.dex.getActiveMove(move); + let maxMove = this.dex.getActiveMove(this.MAX_MOVES[move.category === 'Status' ? move.category : move.type]); + if (move.category !== 'Status') { + if (pokemon.gigantamax && pokemon.canGigantamax) { + const gMaxMove = this.dex.getActiveMove(pokemon.canGigantamax); + if (gMaxMove.exists && gMaxMove.type === move.type) maxMove = gMaxMove; + } + if (!move.maxMove?.basePower) throw new Error(`${move.name} doesn't have a maxMove basePower`); + if (!['gmaxdrumsolo', 'gmaxfireball', 'gmaxhydrosnipe'].includes(maxMove.id)) { + maxMove.basePower = move.maxMove.basePower; + } + maxMove.category = move.category; + } + maxMove.baseMove = move.id; + // copy the priority for Psychic Terrain, Quick Guard + maxMove.priority = move.priority; + maxMove.isZOrMaxPowered = true; + return maxMove; + } + + runZPower(move: ActiveMove, pokemon: Pokemon) { + const zPower = this.dex.getEffect('zpower'); + if (move.category !== 'Status') { + this.battle.attrLastMove('[zeffect]'); + } else if (move.zMove?.boost) { + this.battle.boost(move.zMove.boost, pokemon, pokemon, zPower); + } else if (move.zMove?.effect) { + switch (move.zMove.effect) { + case 'heal': + this.battle.heal(pokemon.maxhp, pokemon, pokemon, zPower); + break; + case 'healreplacement': + move.self = {slotCondition: 'healreplacement'}; + break; + case 'clearnegativeboost': + const boosts: SparseBoostsTable = {}; + let i: BoostName; + for (i in pokemon.boosts) { + if (pokemon.boosts[i] < 0) { + boosts[i] = 0; + } + } + pokemon.setBoost(boosts); + this.battle.add('-clearnegativeboost', pokemon, '[zeffect]'); + break; + case 'redirect': + pokemon.addVolatile('followme', pokemon, zPower); + break; + case 'crit2': + pokemon.addVolatile('focusenergy', pokemon, zPower); + break; + case 'curse': + if (pokemon.hasType('Ghost')) { + this.battle.heal(pokemon.maxhp, pokemon, pokemon, zPower); + } else { + this.battle.boost({atk: 1}, pokemon, pokemon, zPower); + } + } + } + } + + targetTypeChoices(targetType: string) { + return CHOOSABLE_TARGETS.has(targetType); + } + + combineResults( + left: T, right: U + ): T | U { + const NOT_FAILURE = 'string'; + const NULL = 'object'; + const resultsPriorities = ['undefined', NOT_FAILURE, NULL, 'boolean', 'number']; + if (resultsPriorities.indexOf(typeof left) > resultsPriorities.indexOf(typeof right)) { + return left; + } else if (left && !right && right !== 0) { + return left; + } else if (typeof left === 'number' && typeof right === 'number') { + return (left + right) as T; + } else { + return right; + } + } + + /** + * 0 is a success dealing 0 damage, such as from False Swipe at 1 HP. + * + * Normal PS return value rules apply: + * undefined = success, null = silent failure, false = loud failure + */ + getDamage( + pokemon: Pokemon, target: Pokemon, move: string | number | ActiveMove, + suppressMessages = false + ): number | undefined | null | false { + if (typeof move === 'string') move = this.dex.getActiveMove(move); + + if (typeof move === 'number') { + const basePower = move; + move = new Dex.Move({ + basePower, + type: '???', + category: 'Physical', + willCrit: false, + }) as ActiveMove; + move.hit = 0; + } + + if (!move.ignoreImmunity || (move.ignoreImmunity !== true && !move.ignoreImmunity[move.type])) { + if (!target.runImmunity(move.type, !suppressMessages)) { + return false; + } + } + + if (move.ohko) return target.maxhp; + if (move.damageCallback) return move.damageCallback.call(this.battle, pokemon, target); + if (move.damage === 'level') { + return pokemon.level; + } else if (move.damage) { + return move.damage; + } + + const category = this.battle.getCategory(move); + const defensiveCategory = move.defensiveCategory || category; + + let basePower: number | false | null = move.basePower; + if (move.basePowerCallback) { + basePower = move.basePowerCallback.call(this.battle, pokemon, target, move); + } + if (!basePower) return basePower === 0 ? undefined : basePower; + basePower = this.battle.clampIntRange(basePower, 1); + + let critMult; + let critRatio = this.battle.runEvent('ModifyCritRatio', pokemon, target, move, move.critRatio || 0); + if (this.battle.gen <= 5) { + critRatio = this.battle.clampIntRange(critRatio, 0, 5); + critMult = [0, 16, 8, 4, 3, 2]; + } else { + critRatio = this.battle.clampIntRange(critRatio, 0, 4); + if (this.battle.gen === 6) { + critMult = [0, 16, 8, 2, 1]; + } else { + critMult = [0, 24, 8, 2, 1]; + } + } + + const moveHit = target.getMoveHitData(move); + moveHit.crit = move.willCrit || false; + if (move.willCrit === undefined) { + if (critRatio) { + moveHit.crit = this.battle.randomChance(1, critMult[critRatio]); + } + } + + if (moveHit.crit) { + moveHit.crit = this.battle.runEvent('CriticalHit', target, null, move); + } + + // happens after crit calculation + basePower = this.battle.runEvent('BasePower', pokemon, target, move, basePower, true); + + if (!basePower) return 0; + basePower = this.battle.clampIntRange(basePower, 1); + + const level = pokemon.level; + + const attacker = pokemon; + const defender = target; + let attackStat: StatNameExceptHP = category === 'Physical' ? 'atk' : 'spa'; + const defenseStat: StatNameExceptHP = defensiveCategory === 'Physical' ? 'def' : 'spd'; + if (move.useSourceDefensiveAsOffensive) { + attackStat = defenseStat; + // Body press really wants to use the def stat, + // so it switches stats to compensate for Wonder Room. + // Of course, the game thus miscalculates the boosts... + if ('wonderroom' in this.battle.field.pseudoWeather) { + if (attackStat === 'def') { + attackStat = 'spd'; + } else if (attackStat === 'spd') { + attackStat = 'def'; + } + if (attacker.boosts['def'] || attacker.boosts['spd']) { + this.battle.hint("Body Press uses Sp. Def boosts when Wonder Room is active."); + } + } + } + + const statTable = {atk: 'Atk', def: 'Def', spa: 'SpA', spd: 'SpD', spe: 'Spe'}; + let attack; + let defense; + + let atkBoosts = move.useTargetOffensive ? defender.boosts[attackStat] : attacker.boosts[attackStat]; + let defBoosts = defender.boosts[defenseStat]; + + let ignoreNegativeOffensive = !!move.ignoreNegativeOffensive; + let ignorePositiveDefensive = !!move.ignorePositiveDefensive; + + if (moveHit.crit) { + ignoreNegativeOffensive = true; + ignorePositiveDefensive = true; + } + const ignoreOffensive = !!(move.ignoreOffensive || (ignoreNegativeOffensive && atkBoosts < 0)); + const ignoreDefensive = !!(move.ignoreDefensive || (ignorePositiveDefensive && defBoosts > 0)); + + if (ignoreOffensive) { + this.battle.debug('Negating (sp)atk boost/penalty.'); + atkBoosts = 0; + } + if (ignoreDefensive) { + this.battle.debug('Negating (sp)def boost/penalty.'); + defBoosts = 0; + } + + if (move.useTargetOffensive) { + attack = defender.calculateStat(attackStat, atkBoosts); + } else { + attack = attacker.calculateStat(attackStat, atkBoosts); + } + + attackStat = (category === 'Physical' ? 'atk' : 'spa'); + defense = defender.calculateStat(defenseStat, defBoosts); + + // Apply Stat Modifiers + attack = this.battle.runEvent('Modify' + statTable[attackStat], attacker, defender, move, attack); + defense = this.battle.runEvent('Modify' + statTable[defenseStat], defender, attacker, move, defense); + + if (this.battle.gen <= 4 && ['explosion', 'selfdestruct'].includes(move.id) && defenseStat === 'def') { + defense = this.battle.clampIntRange(Math.floor(defense / 2), 1); + } + + const tr = this.battle.trunc; + + // int(int(int(2 * L / 5 + 2) * A * P / D) / 50); + const baseDamage = tr(tr(tr(tr(2 * level / 5 + 2) * basePower * attack) / defense) / 50); + + // Calculate damage modifiers separately (order differs between generations) + return this.modifyDamage(baseDamage, pokemon, target, move, suppressMessages); + } + + modifyDamage( + baseDamage: number, pokemon: Pokemon, target: Pokemon, move: ActiveMove, suppressMessages = false + ) { + const tr = this.battle.trunc; + if (!move.type) move.type = '???'; + const type = move.type; + + baseDamage += 2; + + // multi-target modifier (doubles only) + if (move.spreadHit) { + const spreadModifier = move.spreadModifier || (this.battle.gameType === 'free-for-all' ? 0.5 : 0.75); + this.battle.debug('Spread modifier: ' + spreadModifier); + baseDamage = this.battle.modify(baseDamage, spreadModifier); + } + + // weather modifier + baseDamage = this.battle.runEvent('WeatherModifyDamage', pokemon, target, move, baseDamage); + + // crit - not a modifier + const isCrit = target.getMoveHitData(move).crit; + if (isCrit) { + baseDamage = tr(baseDamage * (move.critModifier || (this.battle.gen >= 6 ? 1.5 : 2))); + } + + // random factor - also not a modifier + baseDamage = this.battle.randomizer(baseDamage); + + // STAB + if (move.forceSTAB || (type !== '???' && pokemon.hasType(type))) { + // The "???" type never gets STAB + // Not even if you Roost in Gen 4 and somehow manage to use + // Struggle in the same turn. + // (On second thought, it might be easier to get a MissingNo.) + baseDamage = this.battle.modify(baseDamage, move.stab || 1.5); + } + // types + let typeMod = target.runEffectiveness(move); + typeMod = this.battle.clampIntRange(typeMod, -6, 6); + target.getMoveHitData(move).typeMod = typeMod; + if (typeMod > 0) { + if (!suppressMessages) this.battle.add('-supereffective', target); + + for (let i = 0; i < typeMod; i++) { + baseDamage *= 2; + } + } + if (typeMod < 0) { + if (!suppressMessages) this.battle.add('-resisted', target); + + for (let i = 0; i > typeMod; i--) { + baseDamage = tr(baseDamage / 2); + } + } + + if (isCrit && !suppressMessages) this.battle.add('-crit', target); + + if (pokemon.status === 'brn' && move.category === 'Physical' && !pokemon.hasAbility('guts')) { + if (this.battle.gen < 6 || move.id !== 'facade') { + baseDamage = this.battle.modify(baseDamage, 0.5); + } + } + + // Generation 5, but nothing later, sets damage to 1 before the final damage modifiers + if (this.battle.gen === 5 && !baseDamage) baseDamage = 1; + + // Final modifier. Modifiers that modify damage after min damage check, such as Life Orb. + baseDamage = this.battle.runEvent('ModifyDamage', pokemon, target, move, baseDamage); + + if (move.isZOrMaxPowered && target.getMoveHitData(move).zBrokeProtect) { + baseDamage = this.battle.modify(baseDamage, 0.25); + this.battle.add('-zbroken', target); + } + + // Generation 6-7 moves the check for minimum 1 damage after the final modifier... + if (this.battle.gen !== 5 && !baseDamage) return 1; + + // ...but 16-bit truncation happens even later, and can truncate to 0 + return tr(baseDamage, 16); + } + + // #endregion + + // #region MEGA EVOLUTION + // ================================================================== + + canMegaEvo(pokemon: Pokemon) { + const species = pokemon.baseSpecies; + const altForme = species.otherFormes && this.dex.getSpecies(species.otherFormes[0]); + const item = pokemon.getItem(); + // Mega Rayquaza + if ((this.battle.gen <= 7 || this.battle.ruleTable.has('standardnatdex')) && + altForme?.isMega && altForme?.requiredMove && + pokemon.baseMoves.includes(toID(altForme.requiredMove)) && !item.zMove) { + return altForme.name; + } + // a hacked-in Megazard X can mega evolve into Megazard Y, but not into Megazard X + if (item.megaEvolves === species.baseSpecies && item.megaStone !== species.name) { + return item.megaStone; + } + return null; + } + + canUltraBurst(pokemon: Pokemon) { + if (['Necrozma-Dawn-Wings', 'Necrozma-Dusk-Mane'].includes(pokemon.baseSpecies.name) && + pokemon.getItem().id === 'ultranecroziumz') { + return "Necrozma-Ultra"; + } + return null; + } + + runMegaEvo(pokemon: Pokemon) { + const speciesid = pokemon.canMegaEvo || pokemon.canUltraBurst; + if (!speciesid) return false; + const side = pokemon.side; + + // Pokémon affected by Sky Drop cannot mega evolve. Enforce it here for now. + for (const foeActive of side.foe.active) { + if (foeActive.volatiles['skydrop'] && foeActive.volatiles['skydrop'].source === pokemon) { + return false; + } + } + + pokemon.formeChange(speciesid, pokemon.getItem(), true); + + // Limit one mega evolution + const wasMega = pokemon.canMegaEvo; + for (const ally of side.pokemon) { + if (wasMega) { + ally.canMegaEvo = null; + } else { + ally.canUltraBurst = null; + } + } + + this.battle.runEvent('AfterMega', pokemon); + return true; + } + + // #endregion +} diff --git a/sim/battle-queue.ts b/sim/battle-queue.ts index 61386297e2..0a60b19efd 100644 --- a/sim/battle-queue.ts +++ b/sim/battle-queue.ts @@ -234,7 +234,7 @@ export class BattleQueue { // TODO: what actually happens here? if (target) action.targetLoc = this.battle.getTargetLoc(target, action.pokemon); } - action.originalTarget = this.battle.getAtLoc(action.pokemon, action.targetLoc); + action.originalTarget = action.pokemon.getAtLoc(action.targetLoc); } if (!deferPriority) this.battle.getActionSpeed(action); return actions as any; diff --git a/sim/battle.ts b/sim/battle.ts index bf9e09968b..cf4ac202d4 100644 --- a/sim/battle.ts +++ b/sim/battle.ts @@ -11,15 +11,9 @@ import {PRNG, PRNGSeed} from './prng'; import {Side} from './side'; import {State} from './state'; import {BattleQueue, Action} from './battle-queue'; +import {BattleActions} from './battle-actions'; import {Utils} from '../lib'; -/** A Pokemon that has fainted. */ -interface FaintedPokemon { - target: Pokemon; - source: Pokemon | null; - effect: Effect | null; -} - interface BattleOptions { format?: Format; formatid: ID; @@ -88,8 +82,13 @@ export class Battle { reportPercentages: boolean; supportCancel: boolean; + actions: BattleActions; queue: BattleQueue; - readonly faintQueue: FaintedPokemon[]; + readonly faintQueue: { + target: Pokemon, + source: Pokemon | null, + effect: Effect | null, + }[]; readonly log: string[]; readonly inputLog: string[]; @@ -118,6 +117,7 @@ export class Battle { lastMove: ActiveMove | null; lastSuccessfulMoveThisTurn: ID | null; lastMoveLine: number; + /** The last damage dealt by a move in the battle - only used by Gen 1 Counter. */ lastDamage: number; abilityOrder: number; @@ -125,9 +125,6 @@ export class Battle { readonly hints: Set; - readonly zMoveTable: {[k: string]: string}; - readonly maxMoveTable: {[k: string]: string}; - readonly NOT_FAIL: ''; readonly HIT_SUBSTITUTE: 0; readonly FAIL: false; @@ -148,11 +145,13 @@ export class Battle { this.gen = this.dex.gen; this.ruleTable = this.dex.getRuleTable(format); - this.zMoveTable = {}; - this.maxMoveTable = {}; this.trunc = this.dex.trunc; this.clampIntRange = Utils.clampIntRange; - Object.assign(this, this.dex.data.Scripts); + // Object.assign(this, this.dex.data.Scripts); + for (const i in this.dex.data.Scripts) { + const entry = this.dex.data.Scripts[i]; + if (typeof entry === 'function') (this as any)[i] = entry; + } if (format.battle) Object.assign(this, format.battle); this.id = ''; @@ -163,8 +162,7 @@ export class Battle { this.gameType = (format.gameType || 'singles'); this.field = new Field(this); const isFourPlayer = this.gameType === 'multi' || this.gameType === 'free-for-all'; - // @ts-ignore - this.sides = Array(isFourPlayer ? 4 : 2).fill(null!); + this.sides = Array(isFourPlayer ? 4 : 2).fill(null) as any; this.prng = options.prng || new PRNG(options.seed || undefined); this.prngSeed = this.prng.startingSeed.slice() as PRNGSeed; this.rated = options.rated || !!options.rated; @@ -173,6 +171,7 @@ export class Battle { this.supportCancel = false; this.queue = new BattleQueue(this); + this.actions = new BattleActions(this); this.faintQueue = []; this.inputLog = []; @@ -1241,119 +1240,6 @@ export class Battle { return true; } - switchIn(pokemon: Pokemon, pos: number, sourceEffect: Effect | null = null, isDrag?: boolean) { - if (!pokemon || pokemon.isActive) { - this.hint("A switch failed because the Pokémon trying to switch in is already in."); - return false; - } - - const side = pokemon.side; - if (pos >= side.active.length) { - console.log(this.getDebugLog()); - throw new Error(`Invalid switch position ${pos} / ${side.active.length}`); - } - const oldActive = side.active[pos]; - const unfaintedActive = oldActive?.hp ? oldActive : null; - if (unfaintedActive) { - oldActive.beingCalledBack = true; - let switchCopyFlag = false; - if (sourceEffect && (sourceEffect as Move).selfSwitch === 'copyvolatile') { - switchCopyFlag = true; - } - if (!oldActive.skipBeforeSwitchOutEventFlag && !isDrag) { - this.runEvent('BeforeSwitchOut', oldActive); - if (this.gen >= 5) { - this.eachEvent('Update'); - } - } - oldActive.skipBeforeSwitchOutEventFlag = false; - if (!this.runEvent('SwitchOut', oldActive)) { - // Warning: DO NOT interrupt a switch-out if you just want to trap a pokemon. - // To trap a pokemon and prevent it from switching out, (e.g. Mean Look, Magnet Pull) - // use the 'trapped' flag instead. - - // Note: Nothing in the real games can interrupt a switch-out (except Pursuit KOing, - // which is handled elsewhere); this is just for custom formats. - return false; - } - if (!oldActive.hp) { - // a pokemon fainted from Pursuit before it could switch - return 'pursuitfaint'; - } - - // will definitely switch out at this point - - oldActive.illusion = null; - this.singleEvent('End', oldActive.getAbility(), oldActive.abilityData, oldActive); - - // if a pokemon is forced out by Whirlwind/etc or Eject Button/Pack, it can't use its chosen move - this.queue.cancelAction(oldActive); - - let newMove = null; - if (this.gen === 4 && sourceEffect) { - newMove = oldActive.lastMove; - } - if (switchCopyFlag) { - pokemon.copyVolatileFrom(oldActive); - } - if (newMove) pokemon.lastMove = newMove; - oldActive.clearVolatile(); - } - if (oldActive) { - oldActive.isActive = false; - oldActive.isStarted = false; - oldActive.usedItemThisTurn = false; - oldActive.position = pokemon.position; - pokemon.position = pos; - side.pokemon[pokemon.position] = pokemon; - side.pokemon[oldActive.position] = oldActive; - } - pokemon.isActive = true; - side.active[pos] = pokemon; - pokemon.activeTurns = 0; - pokemon.activeMoveActions = 0; - for (const moveSlot of pokemon.moveSlots) { - moveSlot.used = false; - } - this.runEvent('BeforeSwitchIn', pokemon); - this.add(isDrag ? 'drag' : 'switch', pokemon, pokemon.getDetails); - pokemon.abilityOrder = this.abilityOrder++; - if (isDrag && this.gen === 2) pokemon.draggedIn = this.turn; - if (sourceEffect) this.log[this.log.length - 1] += `|[from]${sourceEffect.fullname}`; - pokemon.previouslySwitchedIn++; - - if (isDrag && this.gen >= 5) { - // runSwitch happens immediately so that Mold Breaker can make hazards bypass Clear Body and Levitate - this.singleEvent('PreStart', pokemon.getAbility(), pokemon.abilityData, pokemon); - this.runSwitch(pokemon); - } else { - this.queue.insertChoice({choice: 'runUnnerve', pokemon}); - this.queue.insertChoice({choice: 'runSwitch', pokemon}); - } - - return true; - } - runSwitch(pokemon: Pokemon) { - this.runEvent('Swap', pokemon); - this.runEvent('SwitchIn', pokemon); - if (this.gen <= 2 && !pokemon.side.faintedThisTurn && pokemon.draggedIn !== this.turn) { - this.runEvent('AfterSwitchInSelf', pokemon); - } - if (!pokemon.hp) return false; - pokemon.isStarted = true; - if (!pokemon.fainted) { - this.singleEvent('Start', pokemon.getAbility(), pokemon.abilityData, pokemon); - this.singleEvent('Start', pokemon.getItem(), pokemon.itemData, pokemon); - } - if (this.gen === 4) { - for (const foeActive of pokemon.side.foe.active) { - foeActive.removeVolatile('substitutebroken'); - } - } - pokemon.draggedIn = null; - return true; - } - canSwitch(side: Side) { return this.possibleSwitches(side).length; } @@ -1374,20 +1260,6 @@ export class Battle { return canSwitchIn; } - dragIn(side: Side, pos: number) { - const pokemon = this.getRandomSwitchable(side); - if (!pokemon || pokemon.isActive) return false; - const oldActive = side.active[pos]; - if (!oldActive) throw new Error(`nothing to drag out`); - if (!oldActive.hp) return false; - - if (!this.runEvent('DragOut', oldActive)) { - return false; - } - if (!this.switchIn(pokemon, pos, null, true)) return false; - return true; - } - swapPosition(pokemon: Pokemon, slot: number, attributes?: string) { if (slot >= pokemon.side.active.length) { throw new Error("Invalid swap position"); @@ -1525,7 +1397,7 @@ export class Battle { if (this.gameType === 'triples' && !this.sides.filter(side => side.pokemonLeft > 1).length) { // If both sides have one Pokemon left in triples and they are not adjacent, they are both moved to the center. const actives = this.getAllActive(); - if (actives.length > 1 && !this.isAdjacent(actives[0], actives[1])) { + if (actives.length > 1 && !actives[0].isAdjacent(actives[1])) { this.swapPosition(actives[0], 1, '[silent]'); this.swapPosition(actives[1], 1, '[silent]'); this.add('-center'); @@ -2037,241 +1909,6 @@ export class Battle { return this.dex.getMove(move).category || 'Physical'; } - /** - * 0 is a success dealing 0 damage, such as from False Swipe at 1 HP. - * - * Normal PS return value rules apply: - * undefined = success, null = silent failure, false = loud failure - */ - getDamage( - pokemon: Pokemon, target: Pokemon, move: string | number | ActiveMove, - suppressMessages = false - ): number | undefined | null | false { - if (typeof move === 'string') move = this.dex.getActiveMove(move); - - if (typeof move === 'number') { - const basePower = move; - move = new Dex.Move({ - basePower, - type: '???', - category: 'Physical', - willCrit: false, - }) as ActiveMove; - move.hit = 0; - } - - if (!move.ignoreImmunity || (move.ignoreImmunity !== true && !move.ignoreImmunity[move.type])) { - if (!target.runImmunity(move.type, !suppressMessages)) { - return false; - } - } - - if (move.ohko) return target.maxhp; - if (move.damageCallback) return move.damageCallback.call(this, pokemon, target); - if (move.damage === 'level') { - return pokemon.level; - } else if (move.damage) { - return move.damage; - } - - const category = this.getCategory(move); - const defensiveCategory = move.defensiveCategory || category; - - let basePower: number | false | null = move.basePower; - if (move.basePowerCallback) { - basePower = move.basePowerCallback.call(this, pokemon, target, move); - } - if (!basePower) return basePower === 0 ? undefined : basePower; - basePower = this.clampIntRange(basePower, 1); - - let critMult; - let critRatio = this.runEvent('ModifyCritRatio', pokemon, target, move, move.critRatio || 0); - if (this.gen <= 5) { - critRatio = this.clampIntRange(critRatio, 0, 5); - critMult = [0, 16, 8, 4, 3, 2]; - } else { - critRatio = this.clampIntRange(critRatio, 0, 4); - if (this.gen === 6) { - critMult = [0, 16, 8, 2, 1]; - } else { - critMult = [0, 24, 8, 2, 1]; - } - } - - const moveHit = target.getMoveHitData(move); - moveHit.crit = move.willCrit || false; - if (move.willCrit === undefined) { - if (critRatio) { - moveHit.crit = this.randomChance(1, critMult[critRatio]); - } - } - - if (moveHit.crit) { - moveHit.crit = this.runEvent('CriticalHit', target, null, move); - } - - // happens after crit calculation - basePower = this.runEvent('BasePower', pokemon, target, move, basePower, true); - - if (!basePower) return 0; - basePower = this.clampIntRange(basePower, 1); - - const level = pokemon.level; - - const attacker = pokemon; - const defender = target; - let attackStat: StatNameExceptHP = category === 'Physical' ? 'atk' : 'spa'; - const defenseStat: StatNameExceptHP = defensiveCategory === 'Physical' ? 'def' : 'spd'; - if (move.useSourceDefensiveAsOffensive) { - attackStat = defenseStat; - // Body press really wants to use the def stat, - // so it switches stats to compensate for Wonder Room. - // Of course, the game thus miscalculates the boosts... - if ('wonderroom' in this.field.pseudoWeather) { - if (attackStat === 'def') { - attackStat = 'spd'; - } else if (attackStat === 'spd') { - attackStat = 'def'; - } - if (attacker.boosts['def'] || attacker.boosts['spd']) { - this.hint("Body Press uses Sp. Def boosts when Wonder Room is active."); - } - } - } - - const statTable = {atk: 'Atk', def: 'Def', spa: 'SpA', spd: 'SpD', spe: 'Spe'}; - let attack; - let defense; - - let atkBoosts = move.useTargetOffensive ? defender.boosts[attackStat] : attacker.boosts[attackStat]; - let defBoosts = defender.boosts[defenseStat]; - - let ignoreNegativeOffensive = !!move.ignoreNegativeOffensive; - let ignorePositiveDefensive = !!move.ignorePositiveDefensive; - - if (moveHit.crit) { - ignoreNegativeOffensive = true; - ignorePositiveDefensive = true; - } - const ignoreOffensive = !!(move.ignoreOffensive || (ignoreNegativeOffensive && atkBoosts < 0)); - const ignoreDefensive = !!(move.ignoreDefensive || (ignorePositiveDefensive && defBoosts > 0)); - - if (ignoreOffensive) { - this.debug('Negating (sp)atk boost/penalty.'); - atkBoosts = 0; - } - if (ignoreDefensive) { - this.debug('Negating (sp)def boost/penalty.'); - defBoosts = 0; - } - - if (move.useTargetOffensive) { - attack = defender.calculateStat(attackStat, atkBoosts); - } else { - attack = attacker.calculateStat(attackStat, atkBoosts); - } - - attackStat = (category === 'Physical' ? 'atk' : 'spa'); - defense = defender.calculateStat(defenseStat, defBoosts); - - // Apply Stat Modifiers - attack = this.runEvent('Modify' + statTable[attackStat], attacker, defender, move, attack); - defense = this.runEvent('Modify' + statTable[defenseStat], defender, attacker, move, defense); - - if (this.gen <= 4 && ['explosion', 'selfdestruct'].includes(move.id) && defenseStat === 'def') { - defense = this.clampIntRange(Math.floor(defense / 2), 1); - } - - const tr = this.trunc; - - // int(int(int(2 * L / 5 + 2) * A * P / D) / 50); - const baseDamage = tr(tr(tr(tr(2 * level / 5 + 2) * basePower * attack) / defense) / 50); - - // Calculate damage modifiers separately (order differs between generations) - return this.modifyDamage(baseDamage, pokemon, target, move, suppressMessages); - } - - modifyDamage( - baseDamage: number, pokemon: Pokemon, target: Pokemon, move: ActiveMove, suppressMessages = false - ) { - const tr = this.trunc; - if (!move.type) move.type = '???'; - const type = move.type; - - baseDamage += 2; - - // multi-target modifier (doubles only) - if (move.spreadHit) { - const spreadModifier = move.spreadModifier || (this.gameType === 'free-for-all' ? 0.5 : 0.75); - this.debug('Spread modifier: ' + spreadModifier); - baseDamage = this.modify(baseDamage, spreadModifier); - } - - // weather modifier - baseDamage = this.runEvent('WeatherModifyDamage', pokemon, target, move, baseDamage); - - // crit - not a modifier - const isCrit = target.getMoveHitData(move).crit; - if (isCrit) { - baseDamage = tr(baseDamage * (move.critModifier || (this.gen >= 6 ? 1.5 : 2))); - } - - // random factor - also not a modifier - baseDamage = this.randomizer(baseDamage); - - // STAB - if (move.forceSTAB || (type !== '???' && pokemon.hasType(type))) { - // The "???" type never gets STAB - // Not even if you Roost in Gen 4 and somehow manage to use - // Struggle in the same turn. - // (On second thought, it might be easier to get a MissingNo.) - baseDamage = this.modify(baseDamage, move.stab || 1.5); - } - // types - let typeMod = target.runEffectiveness(move); - typeMod = this.clampIntRange(typeMod, -6, 6); - target.getMoveHitData(move).typeMod = typeMod; - if (typeMod > 0) { - if (!suppressMessages) this.add('-supereffective', target); - - for (let i = 0; i < typeMod; i++) { - baseDamage *= 2; - } - } - if (typeMod < 0) { - if (!suppressMessages) this.add('-resisted', target); - - for (let i = 0; i > typeMod; i--) { - baseDamage = tr(baseDamage / 2); - } - } - - if (isCrit && !suppressMessages) this.add('-crit', target); - - if (pokemon.status === 'brn' && move.category === 'Physical' && !pokemon.hasAbility('guts')) { - if (this.gen < 6 || move.id !== 'facade') { - baseDamage = this.modify(baseDamage, 0.5); - } - } - - // Generation 5, but nothing later, sets damage to 1 before the final damage modifiers - if (this.gen === 5 && !baseDamage) baseDamage = 1; - - // Final modifier. Modifiers that modify damage after min damage check, such as Life Orb. - baseDamage = this.runEvent('ModifyDamage', pokemon, target, move, baseDamage); - - if (move.isZOrMaxPowered && target.getMoveHitData(move).zBrokeProtect) { - baseDamage = this.modify(baseDamage, 0.25); - this.add('-zbroken', target); - } - - // Generation 6-7 moves the check for minimum 1 damage after the final modifier... - if (this.gen !== 5 && !baseDamage) return 1; - - // ...but 16-bit truncation happens even later, and can truncate to 0 - return tr(baseDamage, 16); - } - randomizer(baseDamage: number) { const tr = this.trunc; return tr(tr(baseDamage * (100 - this.random(16))) / 100); @@ -2319,14 +1956,6 @@ export class Battle { return this.validTargetLoc(this.getTargetLoc(target, source), source, targetType); } - getAtLoc(pokemon: Pokemon, targetLoc: number) { - if (targetLoc > 0) { - return pokemon.side.foe.active[targetLoc - 1]; - } else { - return pokemon.side.active[-targetLoc - 1]; - } - } - getTarget(pokemon: Pokemon, move: string | Move, targetLoc: number, originalTarget?: Pokemon) { move = this.dex.getMove(move); @@ -2341,7 +1970,7 @@ export class Battle { // banning Dragon Darts from directly targeting itself is done in side.ts, but // Dragon Darts can target itself if Ally Switch is used afterwards - if (move.smartTarget) return this.getAtLoc(pokemon, targetLoc); + if (move.smartTarget) return pokemon.getAtLoc(targetLoc); // Fails if the target is the user and the move can't target its own position if (['adjacentAlly', 'any', 'normal'].includes(move.target) && targetLoc === -(pokemon.position + 1) && @@ -2349,7 +1978,7 @@ export class Battle { return move.isFutureMove ? pokemon : null; } if (move.target !== 'randomNormal' && this.validTargetLoc(targetLoc, pokemon, move.target)) { - const target = this.getAtLoc(pokemon, targetLoc); + const target = pokemon.getAtLoc(targetLoc); if (target?.fainted && target.side === pokemon.side) { // Target is a fainted ally: attack shouldn't retarget return target; @@ -2497,7 +2126,7 @@ export class Battle { if (action.choice === 'move') { let move = action.move; if (action.zmove) { - const zMoveName = this.getZMove(action.move, action.pokemon, true); + const zMoveName = this.actions.getZMove(action.move, action.pokemon, true); if (zMoveName) { const zMove = this.dex.getActiveMove(zMoveName); if (zMove.exists && zMove.isZ) { @@ -2506,9 +2135,9 @@ export class Battle { } } if (action.maxMove) { - const maxMoveName = this.getMaxMove(action.maxMove, action.pokemon); + const maxMoveName = this.actions.getMaxMove(action.maxMove, action.pokemon); if (maxMoveName) { - const maxMove = this.getActiveMaxMove(action.move, action.pokemon); + const maxMove = this.actions.getActiveMaxMove(action.move, action.pokemon); if (maxMove.exists && maxMove.isMax) { move = maxMove; } @@ -2557,7 +2186,7 @@ export class Battle { this.add('start'); for (const side of this.sides) { for (let pos = 0; pos < side.active.length; pos++) { - this.switchIn(side.pokemon[pos], pos); + this.actions.switchIn(side.pokemon[pos], pos); } } for (const pokemon of this.getAllPokemon()) { @@ -2570,11 +2199,11 @@ export class Battle { case 'move': if (!action.pokemon.isActive) return false; if (action.pokemon.fainted) return false; - this.runMove(action.move, action.pokemon, action.targetLoc, action.sourceEffect, + this.actions.runMove(action.move, action.pokemon, action.targetLoc, action.sourceEffect, action.zmove, undefined, action.maxMove, action.originalTarget); break; case 'megaEvo': - this.runMegaEvo(action.pokemon); + this.actions.runMegaEvo(action.pokemon); break; case 'runDynamax': action.pokemon.addVolatile('dynamax'); @@ -2610,7 +2239,7 @@ export class Battle { if (action.choice === 'switch' && action.pokemon.status && this.dex.data.Abilities.naturalcure) { this.singleEvent('CheckShow', this.dex.getAbility('naturalcure'), null, action.pokemon); } - if (this.switchIn(action.target, action.pokemon.position, action.sourceEffect) === 'pursuitfaint') { + if (this.actions.switchIn(action.target, action.pokemon.position, action.sourceEffect) === 'pursuitfaint') { // a pokemon fainted from Pursuit before it could switch if (this.gen <= 4) { // in gen 2-4, the switch still happens @@ -2629,7 +2258,7 @@ export class Battle { this.singleEvent('PreStart', action.pokemon.getAbility(), action.pokemon.abilityData, action.pokemon); break; case 'runSwitch': - this.runSwitch(action.pokemon); + this.actions.runSwitch(action.pokemon); break; case 'runPrimal': if (!action.pokemon.transformed) { @@ -2660,7 +2289,7 @@ export class Battle { for (const side of this.sides) { for (const pokemon of side.active) { if (pokemon.forceSwitchFlag) { - if (pokemon.hp) this.dragIn(pokemon.side, pokemon.position); + if (pokemon.hp) this.actions.dragIn(pokemon.side, pokemon.position); pokemon.forceSwitchFlag = false; } } @@ -3077,208 +2706,10 @@ export class Battle { } } - combineResults( - left: T, right: U - ): T | U { - const NOT_FAILURE = 'string'; - const NULL = 'object'; - const resultsPriorities = ['undefined', NOT_FAILURE, NULL, 'boolean', 'number']; - if (resultsPriorities.indexOf(typeof left) > resultsPriorities.indexOf(typeof right)) { - return left; - } else if (left && !right && right !== 0) { - return left; - } else if (typeof left === 'number' && typeof right === 'number') { - return (left + right) as T; - } else { - return right; - } - } - getSide(sideid: SideID): Side { return this.sides[parseInt(sideid[1]) - 1]; } - afterMoveSecondaryEvent(targets: Pokemon[], pokemon: Pokemon, move: ActiveMove): undefined { - throw new UnimplementedError('afterMoveSecondary'); - } - - calcRecoilDamage(damageDealt: number, move: Move): number { - throw new UnimplementedError('calcRecoilDamage'); - } - - canMegaEvo(pokemon: Pokemon): string | null | undefined { - throw new UnimplementedError('canMegaEvo'); - } - - canUltraBurst(pokemon: Pokemon): string | null { - throw new UnimplementedError('canUltraBurst'); - } - - canZMove(pokemon: Pokemon): (AnyObject | null)[] | void { - throw new UnimplementedError('canZMove'); - } - - forceSwitch( - damage: SpreadMoveDamage, targets: SpreadMoveTargets, source: Pokemon, move: ActiveMove, - moveData: ActiveMove, isSecondary?: boolean, isSelf?: boolean - ): SpreadMoveDamage { - throw new UnimplementedError('forceSwitch'); - } - - getActiveMaxMove(move: Move, pokemon: Pokemon): ActiveMove { - throw new UnimplementedError('getActiveMaxMove'); - } - - getActiveZMove(move: Move, pokemon: Pokemon): ActiveMove { - throw new UnimplementedError('getActiveZMove'); - } - - getMaxMove(move: Move, pokemon: Pokemon): Move | undefined { - throw new UnimplementedError('getMaxMove'); - } - - getSpreadDamage( - damage: SpreadMoveDamage, targets: SpreadMoveTargets, source: Pokemon, move: ActiveMove, - moveData: ActiveMove, isSecondary?: boolean, isSelf?: boolean - ): SpreadMoveDamage { - throw new UnimplementedError('getSpreadDamage'); - } - - getZMove(move: Move, pokemon: Pokemon, skipChecks?: boolean): string | undefined { - throw new UnimplementedError('getZMove'); - } - - hitStepAccuracy(targets: Pokemon[], pokemon: Pokemon, move: ActiveMove): boolean[] { - throw new UnimplementedError('hitStepAccuracy'); - } - - hitStepBreakProtect(targets: Pokemon[], pokemon: Pokemon, move: ActiveMove): undefined { - throw new UnimplementedError('hitStepBreakProtect'); - } - - hitStepMoveHitLoop(targets: Pokemon[], pokemon: Pokemon, move: ActiveMove): SpreadMoveDamage { - throw new UnimplementedError('hitStepMoveHitLoop'); - } - - hitStepTryImmunity(targets: Pokemon[], pokemon: Pokemon, move: ActiveMove): boolean[] { - throw new UnimplementedError('hitStepTryImmunityEvent'); - } - - hitStepStealBoosts(targets: Pokemon[], pokemon: Pokemon, move: ActiveMove): undefined { - throw new UnimplementedError('hitStepStealBoosts'); - } - - hitStepTryHitEvent(targets: Pokemon[], pokemon: Pokemon, move: ActiveMove): (boolean | '')[] { - throw new UnimplementedError('hitStepTryHitEvent'); - } - - hitStepInvulnerabilityEvent(targets: Pokemon[], pokemon: Pokemon, move: ActiveMove): boolean[] { - throw new UnimplementedError('hitStepInvulnerabilityEvent '); - } - - hitStepTypeImmunity(targets: Pokemon[], pokemon: Pokemon, move: ActiveMove): boolean[] { - throw new UnimplementedError('hitStepTypeImmunity'); - } - - isAdjacent(pokemon1: Pokemon, pokemon2: Pokemon): boolean { - throw new UnimplementedError('isAdjacent'); - } - - moveHit( - target: Pokemon | null, pokemon: Pokemon, move: ActiveMove, - moveData?: Dex.HitEffect, - isSecondary?: boolean, isSelf?: boolean - ): number | undefined | false { - throw new UnimplementedError('moveHit'); - } - - /** - * This function is also used for Ultra Bursting. - * Takes the Pokemon that will Mega Evolve or Ultra Burst as a parameter. - * Returns false if the Pokemon cannot Mega Evolve or Ultra Burst, otherwise returns true. - */ - runMegaEvo(pokemon: Pokemon): boolean { - throw new UnimplementedError('runMegaEvo'); - } - - runMove( - moveOrMoveName: Move | string, pokemon: Pokemon, targetLoc: number, - sourceEffect?: Effect | null, zMove?: string, externalMove?: boolean, - maxMove?: string, originalTarget?: Pokemon - ) { - throw new UnimplementedError('runMove'); - } - - runMoveEffects( - damage: SpreadMoveDamage, targets: SpreadMoveTargets, source: Pokemon, move: ActiveMove, - moveData: ActiveMove, isSecondary?: boolean, isSelf?: boolean - ): SpreadMoveDamage { - throw new UnimplementedError('runMoveEffects'); - } - - runZPower(move: ActiveMove, pokemon: Pokemon) { - throw new UnimplementedError('runZPower'); - } - - secondaries( - targets: SpreadMoveTargets, source: Pokemon, move: ActiveMove, moveData: ActiveMove, - isSelf?: boolean - ): SpreadMoveDamage { - throw new UnimplementedError('secondaries'); - } - - selfDrops( - targets: SpreadMoveTargets, source: Pokemon, move: ActiveMove, moveData: ActiveMove, - isSecondary?: boolean - ): SpreadMoveDamage { - throw new UnimplementedError('selfDrops'); - } - - spreadMoveHit( - targets: SpreadMoveTargets, pokemon: Pokemon, move: ActiveMove, moveData?: ActiveMove, - isSecondary?: boolean, isSelf?: boolean - ): [SpreadMoveDamage, SpreadMoveTargets] { - throw new UnimplementedError('spreadMoveHit'); - } - - targetTypeChoices(targetType: string): boolean { - throw new UnimplementedError('targetTypeChoices'); - } - - tryMoveHit(target: Pokemon, pokemon: Pokemon, move: ActiveMove): number | undefined | false | '' { - throw new UnimplementedError('tryMoveHit'); - } - - tryPrimaryHitEvent( - damage: SpreadMoveDamage, targets: SpreadMoveTargets, pokemon: Pokemon, move: ActiveMove, - moveData: ActiveMove, isSecondary?: boolean - ): SpreadMoveDamage { - throw new UnimplementedError('tryPrimaryHitEvent'); - } - - trySpreadMoveHit(targets: Pokemon[], pokemon: Pokemon, move: ActiveMove, notActive?: boolean): boolean { - throw new UnimplementedError('trySpreadMoveHit'); - } - - useMove( - move: string | Move, pokemon: Pokemon, target?: Pokemon | null, - sourceEffect?: Effect | null, zMove?: string, maxMove?: string - ): boolean { - throw new UnimplementedError('useMove'); - } - - /** - * target = undefined: automatically resolve target - * target = null: no target (move will fail) - */ - useMoveInner( - move: string | Move, pokemon: Pokemon, target?: Pokemon | null, - sourceEffect?: Effect | null, zMove?: string, maxMove?: string - ): boolean { - throw new UnimplementedError('useMoveInner'); - } - destroy() { // deallocate ourself @@ -3302,10 +2733,3 @@ export class Battle { (this as any).log = []; } } - -class UnimplementedError extends Error { - constructor(name: string) { - super(`The ${name} function needs to be implemented in scripts.js or the battle format.`); - this.name = 'UnimplementedError'; - } -} diff --git a/sim/dex-formats.ts b/sim/dex-formats.ts index bebab03d0b..53ef0fc1c3 100644 --- a/sim/dex-formats.ts +++ b/sim/dex-formats.ts @@ -205,6 +205,7 @@ export class Format extends BasicEffect implements Readonly { readonly pokemon?: ModdedBattlePokemon; readonly queue?: ModdedBattleQueue; readonly field?: ModdedField; + readonly actions?: ModdedBattleActions; readonly cannotMega?: string[]; readonly challengeShow?: boolean; readonly searchShow?: boolean; diff --git a/sim/global-types.ts b/sim/global-types.ts index ac9a46680a..4979e157b4 100644 --- a/sim/global-types.ts +++ b/sim/global-types.ts @@ -2,6 +2,7 @@ type Battle = import('./battle').Battle; type BattleQueue = import('./battle-queue').BattleQueue; +type BattleActions = import('./battle-actions').BattleActions; type Field = import('./field').Field; type Action = import('./battle-queue').Action; type MoveAction = import('./battle-queue').MoveAction; @@ -229,86 +230,100 @@ interface DynamaxOptions { interface BattleScriptsData { gen: number; - zMoveTable?: {[k: string]: string}; - maxMoveTable?: {[k: string]: string}; - afterMoveSecondaryEvent?: (this: Battle, targets: Pokemon[], pokemon: Pokemon, move: ActiveMove) => undefined; - calcRecoilDamage?: (this: Battle, damageDealt: number, move: Move) => number; - canMegaEvo?: (this: Battle, pokemon: Pokemon) => string | undefined | null; - canUltraBurst?: (this: Battle, pokemon: Pokemon) => string | null; - canZMove?: (this: Battle, pokemon: Pokemon) => ZMoveOptions | void; - canDynamax?: (this: Battle, pokemon: Pokemon, skipChecks?: boolean) => DynamaxOptions | void; +} + +interface ModdedBattleActions { + inherit?: true; + afterMoveSecondaryEvent?: (this: BattleActions, targets: Pokemon[], pokemon: Pokemon, move: ActiveMove) => undefined; + calcRecoilDamage?: (this: BattleActions, damageDealt: number, move: Move) => number; + canMegaEvo?: (this: BattleActions, pokemon: Pokemon) => string | undefined | null; + canUltraBurst?: (this: BattleActions, pokemon: Pokemon) => string | null; + canZMove?: (this: BattleActions, pokemon: Pokemon) => ZMoveOptions | void; + canDynamax?: (this: BattleActions, pokemon: Pokemon, skipChecks?: boolean) => DynamaxOptions | void; forceSwitch?: ( - this: Battle, damage: SpreadMoveDamage, targets: SpreadMoveTargets, source: Pokemon, + this: BattleActions, damage: SpreadMoveDamage, targets: SpreadMoveTargets, source: Pokemon, move: ActiveMove, moveData: ActiveMove, isSecondary?: boolean, isSelf?: boolean ) => SpreadMoveDamage; - getActiveMaxMove?: (this: Battle, move: Move, pokemon: Pokemon) => ActiveMove; - getActiveZMove?: (this: Battle, move: Move, pokemon: Pokemon) => ActiveMove; - getMaxMove?: (this: Battle, move: Move, pokemon: Pokemon) => Move | undefined; + getActiveMaxMove?: (this: BattleActions, move: Move, pokemon: Pokemon) => ActiveMove; + getActiveZMove?: (this: BattleActions, move: Move, pokemon: Pokemon) => ActiveMove; + getMaxMove?: (this: BattleActions, move: Move, pokemon: Pokemon) => Move | undefined; getSpreadDamage?: ( - this: Battle, damage: SpreadMoveDamage, targets: SpreadMoveTargets, source: Pokemon, + this: BattleActions, damage: SpreadMoveDamage, targets: SpreadMoveTargets, source: Pokemon, move: ActiveMove, moveData: ActiveMove, isSecondary?: boolean, isSelf?: boolean ) => SpreadMoveDamage; - getZMove?: (this: Battle, move: Move, pokemon: Pokemon, skipChecks?: boolean) => string | true | undefined; - hitStepAccuracy?: (this: Battle, targets: Pokemon[], pokemon: Pokemon, move: ActiveMove) => boolean[]; - hitStepBreakProtect?: (this: Battle, targets: Pokemon[], pokemon: Pokemon, move: ActiveMove) => undefined; - hitStepMoveHitLoop?: (this: Battle, targets: Pokemon[], pokemon: Pokemon, move: ActiveMove) => SpreadMoveDamage; - hitStepTryImmunity?: (this: Battle, targets: Pokemon[], pokemon: Pokemon, move: ActiveMove) => boolean[]; - hitStepStealBoosts?: (this: Battle, targets: Pokemon[], pokemon: Pokemon, move: ActiveMove) => undefined; - hitStepTryHitEvent?: (this: Battle, targets: Pokemon[], pokemon: Pokemon, move: ActiveMove) => (boolean | '')[]; - hitStepInvulnerabilityEvent?: (this: Battle, targets: Pokemon[], pokemon: Pokemon, move: ActiveMove) => boolean[]; - hitStepTypeImmunity?: (this: Battle, targets: Pokemon[], pokemon: Pokemon, move: ActiveMove) => boolean[]; - isAdjacent?: (this: Battle, pokemon1: Pokemon, pokemon2: Pokemon) => boolean; + getZMove?: (this: BattleActions, move: Move, pokemon: Pokemon, skipChecks?: boolean) => string | true | undefined; + hitStepAccuracy?: (this: BattleActions, targets: Pokemon[], pokemon: Pokemon, move: ActiveMove) => boolean[]; + hitStepBreakProtect?: (this: BattleActions, targets: Pokemon[], pokemon: Pokemon, move: ActiveMove) => undefined; + hitStepMoveHitLoop?: (this: BattleActions, targets: Pokemon[], pokemon: Pokemon, move: ActiveMove) => SpreadMoveDamage; + hitStepTryImmunity?: (this: BattleActions, targets: Pokemon[], pokemon: Pokemon, move: ActiveMove) => boolean[]; + hitStepStealBoosts?: (this: BattleActions, targets: Pokemon[], pokemon: Pokemon, move: ActiveMove) => undefined; + hitStepTryHitEvent?: (this: BattleActions, targets: Pokemon[], pokemon: Pokemon, move: ActiveMove) => (boolean | '')[]; + hitStepInvulnerabilityEvent?: ( + this: BattleActions, targets: Pokemon[], pokemon: Pokemon, move: ActiveMove + ) => boolean[]; + hitStepTypeImmunity?: (this: BattleActions, targets: Pokemon[], pokemon: Pokemon, move: ActiveMove) => boolean[]; moveHit?: ( - this: Battle, target: Pokemon | null, pokemon: Pokemon, move: ActiveMove, + this: BattleActions, target: Pokemon | null, pokemon: Pokemon, move: ActiveMove, moveData?: ActiveMove, isSecondary?: boolean, isSelf?: boolean ) => number | undefined | false; - runAction?: (this: Battle, action: Action) => void; - runMegaEvo?: (this: Battle, pokemon: Pokemon) => boolean; + runAction?: (this: BattleActions, action: Action) => void; + runMegaEvo?: (this: BattleActions, pokemon: Pokemon) => boolean; runMove?: ( - this: Battle, moveOrMoveName: Move | string, pokemon: Pokemon, targetLoc: number, sourceEffect?: Effect | null, + this: BattleActions, moveOrMoveName: Move | string, pokemon: Pokemon, targetLoc: number, sourceEffect?: Effect | null, zMove?: string, externalMove?: boolean, maxMove?: string, originalTarget?: Pokemon ) => void; runMoveEffects?: ( - this: Battle, damage: SpreadMoveDamage, targets: SpreadMoveTargets, source: Pokemon, + this: BattleActions, damage: SpreadMoveDamage, targets: SpreadMoveTargets, source: Pokemon, move: ActiveMove, moveData: ActiveMove, isSecondary?: boolean, isSelf?: boolean ) => SpreadMoveDamage; - runZPower?: (this: Battle, move: ActiveMove, pokemon: Pokemon) => void; + runZPower?: (this: BattleActions, move: ActiveMove, pokemon: Pokemon) => void; secondaries?: ( - this: Battle, targets: SpreadMoveTargets, source: Pokemon, move: ActiveMove, moveData: ActiveMove, isSelf?: boolean + this: BattleActions, targets: SpreadMoveTargets, source: Pokemon, move: ActiveMove, + moveData: ActiveMove, isSelf?: boolean ) => void; selfDrops?: ( - this: Battle, targets: SpreadMoveTargets, source: Pokemon, + this: BattleActions, targets: SpreadMoveTargets, source: Pokemon, move: ActiveMove, moveData: ActiveMove, isSecondary?: boolean ) => void; spreadMoveHit?: ( - this: Battle, targets: SpreadMoveTargets, pokemon: Pokemon, move: ActiveMove, + this: BattleActions, targets: SpreadMoveTargets, pokemon: Pokemon, move: ActiveMove, moveData?: ActiveMove, isSecondary?: boolean, isSelf?: boolean ) => [SpreadMoveDamage, SpreadMoveTargets]; - targetTypeChoices?: (this: Battle, targetType: string) => boolean; - tryMoveHit?: (this: Battle, target: Pokemon, pokemon: Pokemon, move: ActiveMove) => number | undefined | false | ''; + targetTypeChoices?: (this: BattleActions, targetType: string) => boolean; + tryMoveHit?: ( + this: BattleActions, target: Pokemon, pokemon: Pokemon, move: ActiveMove + ) => number | undefined | false | ''; tryPrimaryHitEvent?: ( - this: Battle, damage: SpreadMoveDamage, targets: SpreadMoveTargets, pokemon: Pokemon, + this: BattleActions, damage: SpreadMoveDamage, targets: SpreadMoveTargets, pokemon: Pokemon, move: ActiveMove, moveData: ActiveMove, isSecondary?: boolean ) => SpreadMoveDamage; trySpreadMoveHit?: ( - this: Battle, targets: Pokemon[], pokemon: Pokemon, move: ActiveMove, notActive?: boolean + this: BattleActions, targets: Pokemon[], pokemon: Pokemon, move: ActiveMove, notActive?: boolean ) => boolean; useMove?: ( - this: Battle, move: Move, pokemon: Pokemon, target?: Pokemon | null, + this: BattleActions, move: Move, pokemon: Pokemon, target?: Pokemon | null, sourceEffect?: Effect | null, zMove?: string, maxMove?: string ) => boolean; useMoveInner?: ( - this: Battle, move: Move, pokemon: Pokemon, target?: Pokemon | null, + this: BattleActions, move: Move, pokemon: Pokemon, target?: Pokemon | null, sourceEffect?: Effect | null, zMove?: string, maxMove?: string ) => boolean; + getDamage?: ( + this: BattleActions, pokemon: Pokemon, target: Pokemon, move: string | number | ActiveMove, suppressMessages: boolean + ) => number | undefined | null | false; + modifyDamage?: ( + this: BattleActions, baseDamage: number, pokemon: Pokemon, target: Pokemon, move: ActiveMove, suppressMessages?: boolean + ) => void; + + // oms + doGetMixedSpecies?: (this: BattleActions, species: Species, deltas: AnyObject) => Species; + getMegaDeltas?: (this: BattleActions, megaSpecies: Species) => AnyObject; + getMixedSpecies?: (this: BattleActions, originalName: string, megaName: string) => Species; } -interface ModdedBattleSide { - lastMove?: Move | null; -} +type ModdedBattleSide = never; interface ModdedBattlePokemon { - /** TODO: remove, completely meaningless */ inherit?: true; lostItemForDelibird?: Item | null; boostBy?: (this: Pokemon, boost: SparseBoostsTable) => boolean | number; @@ -356,7 +371,7 @@ interface ModdedField extends Partial { interface ModdedBattleScriptsData extends Partial { inherit?: string; - lastDamage?: number; + actions?: ModdedBattleActions; pokemon?: ModdedBattlePokemon; queue?: ModdedBattleQueue; field?: ModdedField; @@ -366,33 +381,13 @@ interface ModdedBattleScriptsData extends Partial { effect?: Effect | string | null, isSecondary?: boolean, isSelf?: boolean ) => boolean | null | 0; debug?: (this: Battle, activity: string) => void; - getDamage?: ( - this: Battle, pokemon: Pokemon, target: Pokemon, move: string | number | ActiveMove, suppressMessages: boolean - ) => number | undefined | null | false; getActionSpeed?: (this: Battle, action: AnyObject) => void; - getEffect?: (this: Battle, name: string | Effect | null) => Effect; init?: (this: ModdedDex) => void; - modifyDamage?: ( - this: Battle, baseDamage: number, pokemon: Pokemon, target: Pokemon, move: ActiveMove, suppressMessages?: boolean - ) => void; natureModify?: (this: Battle, stats: StatsTable, set: PokemonSet) => StatsTable; nextTurn?: (this: Battle) => void; - runMove?: ( - this: Battle, moveOrMoveName: Move | string, pokemon: Pokemon, targetLoc: number, sourceEffect?: Effect | null, - zMove?: string, externalMove?: boolean, maxMove?: string, originalTarget?: Pokemon - ) => void; spreadModify?: (this: Battle, baseStats: StatsTable, set: PokemonSet) => StatsTable; suppressingWeather?: (this: Battle) => boolean; trunc?: (n: number) => number; - - // oms - doGetMixedSpecies?: (this: Battle, species: Species, deltas: AnyObject) => Species; - getMegaDeltas?: (this: Battle, megaSpecies: Species) => AnyObject; - getMixedSpecies?: (this: Battle, originalName: string, megaName: string) => Species; - getAbility?: (this: Battle, name: string | Ability) => Ability; - getZMove?: (this: Battle, move: Move, pokemon: Pokemon, skipChecks?: boolean) => string | undefined; - getActiveZMove?: (this: Battle, move: Move, pokemon: Pokemon) => ActiveMove; - canZMove?: (this: Battle, pokemon: Pokemon) => ZMoveOptions | void; win?: (this: Battle, side?: SideID | '' | Side | null) => boolean; faintMessages?: (this: Battle, lastFirst?: boolean) => boolean | undefined; tiebreak?: (this: Battle) => boolean; diff --git a/sim/pokemon.ts b/sim/pokemon.ts index 115c881947..c11621de32 100644 --- a/sim/pokemon.ts +++ b/sim/pokemon.ts @@ -424,8 +424,8 @@ export class Pokemon { this.speed = 0; this.abilityOrder = 0; - this.canMegaEvo = this.battle.canMegaEvo(this); - this.canUltraBurst = this.battle.canUltraBurst(this); + this.canMegaEvo = this.battle.actions.canMegaEvo(this); + this.canUltraBurst = this.battle.actions.canUltraBurst(this); // Normally would want to use battle.canDynamax to set this, but it references this property. this.canDynamax = (this.battle.gen >= 8); this.canGigantamax = this.baseSpecies.canGigantamax || null; @@ -549,7 +549,7 @@ export class Pokemon { // stat modifier effects if (!unmodified) { - const statTable: {[s in StatNameExceptHP]?: string} = {atk: 'Atk', def: 'Def', spa: 'SpA', spd: 'SpD', spe: 'Spe'}; + const statTable: {[s in StatNameExceptHP]: string} = {atk: 'Atk', def: 'Def', spa: 'SpA', spd: 'SpD', spe: 'Spe'}; stat = this.battle.runEvent('Modify' + statTable[statName], this, null, null, stat); } @@ -609,7 +609,6 @@ export class Pokemon { let allies = this.side.active; if (this.battle.gameType === 'multi') { const team = this.side.n % 2; - // @ts-ignore allies = this.battle.sides.flatMap( (side: Side) => side.n % 2 === team ? side.active : [] ); @@ -617,15 +616,14 @@ export class Pokemon { return allies.filter(ally => ally && !ally.fainted); } - nearbyAllies(): Pokemon[] { - return this.allies().filter(ally => this.battle.isAdjacent(this, ally)); + adjacentAllies(): Pokemon[] { + return this.allies().filter(ally => this.isAdjacent(ally)); } foes(): Pokemon[] { let foes = this.side.foe.active; if (this.battle.gameType === 'multi') { const team = this.side.foe.n % 2; - // @ts-ignore foes = this.battle.sides.flatMap( (side: Side) => side.n % 2 === team ? side.active : [] ); @@ -633,8 +631,14 @@ export class Pokemon { return foes.filter(foe => foe && !foe.fainted); } - nearbyFoes(): Pokemon[] { - return this.foes().filter(foe => this.battle.isAdjacent(this, foe)); + adjacentFoes(): Pokemon[] { + return this.foes().filter(foe => this.isAdjacent(foe)); + } + + isAdjacent(pokemon2: Pokemon) { + if (this.fainted || pokemon2.fainted) return false; + if (this.side === pokemon2.side) return Math.abs(this.position - pokemon2.position) === 1; + return Math.abs(this.position + pokemon2.position + 1 - this.side.active.length) <= 1; } getUndynamaxedHP(amount?: number) { @@ -647,7 +651,7 @@ export class Pokemon { /** Get targets for Dragon Darts */ getSmartTargets(target: Pokemon, move: ActiveMove) { - const target2 = target.nearbyAllies()[0]; + const target2 = target.adjacentAllies()[0]; if (!target2 || target2 === this || !target2.hp) { move.smartTarget = false; return [target]; @@ -659,6 +663,14 @@ export class Pokemon { return [target, target2]; } + getAtLoc(targetLoc: number) { + if (targetLoc > 0) { + return this.side.foe.active[targetLoc - 1]; + } else { + return this.side.active[-targetLoc - 1]; + } + } + getMoveTargets(move: ActiveMove, target: Pokemon): {targets: Pokemon[], pressureTargets: Pokemon[]} { let targets: Pokemon[] = []; @@ -678,10 +690,10 @@ export class Pokemon { } break; case 'allAdjacent': - targets.push(...this.nearbyAllies()); + targets.push(...this.adjacentAllies()); // falls through case 'allAdjacentFoes': - targets.push(...this.nearbyFoes()); + targets.push(...this.adjacentFoes()); if (targets.length && !targets.includes(target)) { this.battle.retargetLastMove(targets[targets.length - 1]); } @@ -871,7 +883,7 @@ export class Pokemon { disabled = this.maxMoveDisabled(moveSlot.id) || disabled && canCauseStruggle.includes(moveSlot.disabledSource!); } else if ( (moveSlot.pp <= 0 && !this.volatiles['partialtrappinglock']) || disabled && - this.side.active.length >= 2 && this.battle.targetTypeChoices(target!) + this.side.active.length >= 2 && this.battle.actions.targetTypeChoices(target!) ) { disabled = true; } @@ -918,7 +930,7 @@ export class Pokemon { let atLeastOne = false; for (const moveSlot of this.moveSlots) { const move = this.battle.dex.getMove(moveSlot.id); - const maxMove = this.battle.getMaxMove(move, this); + const maxMove = this.battle.actions.getMaxMove(move, this); if (maxMove) { if (this.maxMoveDisabled(move)) { result.maxMoves.push({move: maxMove.id, target: maxMove.target, disabled: true}); @@ -979,7 +991,7 @@ export class Pokemon { if (!lockedMove) { if (this.canMegaEvo) data.canMegaEvo = true; if (this.canUltraBurst) data.canUltraBurst = true; - const canZMove = this.battle.canZMove(this); + const canZMove = this.battle.actions.canZMove(this); if (canZMove) data.canZMove = canZMove; if (this.getDynamaxRequest()) data.canDynamax = true; diff --git a/sim/side.ts b/sim/side.ts index 99f9baf663..abeb6ee8c1 100644 --- a/sim/side.ts +++ b/sim/side.ts @@ -67,6 +67,10 @@ export class Side { activeRequest: AnyObject | null; choice: Choice; + /** + * In gen 1, all lastMove stuff is tracked on Side rather than Pokemon + * (this is for Counter and Mirror Move) + */ lastMove: Move | null; constructor(name: string, battle: Battle, sideNum: number, team: PokemonSet[]) { @@ -410,7 +414,7 @@ export class Side { // Z-move - const zMove = megaDynaOrZ === 'zmove' ? this.battle.getZMove(move, pokemon) : undefined; + const zMove = megaDynaOrZ === 'zmove' ? this.battle.actions.getZMove(move, pokemon) : undefined; if (megaDynaOrZ === 'zmove' && !zMove) { return this.emitChoiceError(`Can't move: ${pokemon.name} can't use ${move.name} as a Z-move`); } @@ -423,7 +427,7 @@ export class Side { // Dynamax // Is dynamaxed or will dynamax this turn. const maxMove = (megaDynaOrZ === 'dynamax' || pokemon.volatiles['dynamax']) ? - this.battle.getMaxMove(move, pokemon) : undefined; + this.battle.actions.getMaxMove(move, pokemon) : undefined; if (megaDynaOrZ === 'dynamax' && !maxMove) { return this.emitChoiceError(`Can't move: ${pokemon.name} can't use ${move.name} as a Max Move`); } @@ -434,7 +438,7 @@ export class Side { if (autoChoose) { targetLoc = 0; - } else if (this.battle.targetTypeChoices(targetType)) { + } else if (this.battle.actions.targetTypeChoices(targetType)) { if (!targetLoc && this.active.length >= 2) { return this.emitChoiceError(`Can't move: ${move.name} needs a target`); } diff --git a/sim/state.ts b/sim/state.ts index 039e00eb9e..c781b2193e 100644 --- a/sim/state.ts +++ b/sim/state.ts @@ -33,9 +33,9 @@ type Referable = Battle | Field | Side | Pokemon | Condition | Ability | Item | // need special treatment from these sets are then handled manually. const BATTLE = new Set([ - 'dex', 'gen', 'ruleTable', 'id', 'log', 'inherit', 'format', 'zMoveTable', 'teamGenerator', + 'dex', 'gen', 'ruleTable', 'id', 'log', 'inherit', 'format', 'teamGenerator', 'HIT_SUBSTITUTE', 'NOT_FAIL', 'FAIL', 'SILENT_FAIL', 'field', 'sides', 'prng', 'hints', - 'deserialized', 'maxMoveTable', 'queue', + 'deserialized', 'queue', 'actions', ]); const FIELD = new Set(['id', 'battle']); const SIDE = new Set(['battle', 'team', 'pokemon', 'choice', 'activeRequest']); diff --git a/test/sim/misc/zmoves.js b/test/sim/misc/zmoves.js index b2a7ccdc48..33cb52d860 100644 --- a/test/sim/misc/zmoves.js +++ b/test/sim/misc/zmoves.js @@ -19,7 +19,7 @@ describe('Z Moves', function () { assert.statStage(chansey, 'atk', -1); battle.makeChoices('auto', 'move taunt'); - assert(battle.canZMove(chansey), `Chansey should be able to use its Z Move`); + assert(battle.actions.canZMove(chansey), `Chansey should be able to use its Z Move`); battle.makeChoices('move doubleteam zmove', 'auto'); // Z-Effect: Restores negative stat stages to 0 assert.statStage(chansey, 'atk', 0); }); @@ -33,7 +33,7 @@ describe('Z Moves', function () { assert.statStage(chansey, 'atk', -1); battle.makeChoices('auto', 'move taunt'); - assert.false(battle.canZMove(chansey), `Chansey should not be able to use its Z Move`); + assert.false(battle.actions.canZMove(chansey), `Chansey should not be able to use its Z Move`); battle.makeChoices('auto', 'auto'); assert.statStage(chansey, 'atk', -1); assert.cantMove(() => battle.makeChoices('move doubleteam zmove', ''));