diff --git a/src/data/ability.ts b/src/data/ability.ts index 383d8fb1af8..2cbc13a8e9e 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1,10 +1,10 @@ -import Pokemon, { EnemyPokemon, HitResult, PlayerPokemon, PokemonMove } from "../field/pokemon"; +import Pokemon, { EnemyPokemon, HitResult, MoveResult, PlayerPokemon, PokemonMove } from "../field/pokemon"; import { Type } from "./type"; import { Constructor } from "#app/utils"; import * as Utils from "../utils"; import { getPokemonNameWithAffix } from "../messages"; import { Weather, WeatherType } from "./weather"; -import { BattlerTag, DamagingTrapTag, GroundedTag } from "./battler-tags"; +import { BattlerTag, GroundedTag } from "./battler-tags"; import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect"; import { Gender } from "./gender"; import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, ChargeAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move"; @@ -34,9 +34,7 @@ import { SwitchPhase } from "#app/phases/switch-phase"; import { SwitchSummonPhase } from "#app/phases/switch-summon-phase"; import { BattleEndPhase } from "#app/phases/battle-end-phase"; import { NewBattlePhase } from "#app/phases/new-battle-phase"; -import { PostSummonPhase } from "#app/phases/post-summon-phase"; -import { WeatherEffectPhase } from "#app/phases/weather-effect-phase"; -import { TurnEndPhase } from "#app/phases/turn-end-phase"; +import { MoveEndPhase } from "#app/phases/move-end-phase"; export class Ability implements Localizable { public id: Abilities; @@ -3091,223 +3089,6 @@ function getSheerForceHitDisableAbCondition(): AbAttrCondition { }; } -/** - * Function to determine if the Pokémon's health has been knocked below half. - * - * This function checks different phases like {@linkcode PostSummonPhase}, {@linkcode WeatherEffectPhase}, - * {@linkcode TurnEndPhase}, and evaluates various conditions like Arena traps, status effects, passive damage sources, - * and healing items, to determine if the Pokémon's health has been knocked below half. - * - * Uses: - * - {@linkcode calculateTurnEndDamage} to calculate damage during the {@linkcode TurnEndPhase}. - * - {@linkcode calculateSleepDamage} to calculate additional damage while the Pokémon is sleeping. - * - {@linkcode calculateShellBellRecovery} to account for Shell Bell recovery. - * - {@linkcode checkSheerForceCondition} to verify if Sheer Force ability prevents effects of attacks. - * - * @returns {AbAttrCondition} A condition function that checks if the Pokémon's health has been knocked below half. - */ -function getHealthKnockedBelowHalf(): AbAttrCondition { - return (pokemon: Pokemon) => { - const maxPokemonHealth = pokemon.getMaxHp(); - const pokemonHealth = pokemon.hp; - const currentPhase = pokemon.scene.getCurrentPhase(); - - // Helper function to calculate initial health based on damage taken - const getInitialHealth = (damageTaken: number = 0) => pokemonHealth + damageTaken; - - // Helper function to check if health has dropped below half - const isHealthBelowHalf = (initialHealth: number, currentHealth: number) => - initialHealth >= maxPokemonHealth / 2 && currentHealth < maxPokemonHealth / 2; - - // PostSummonPhase - /** - * In the {@linkcode PostSummonPhase}, the function checks for Arena traps on the - * Pokemon's side and calculates damage taken during the summon. - * If the initial health (current HP + damage taken) was above half but now is below half, - * it returns true. - */ - if (currentPhase instanceof PostSummonPhase) { - const side = pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; - if (pokemon.scene?.arena.findTagsOnSide(t => t instanceof ArenaTrapTag, side).length > 0) { - - const damageTaken = pokemon.turnData?.damageTaken; - const initialHealth = pokemonHealth + damageTaken; - - // Check if the initial health was above half - if (initialHealth <= maxPokemonHealth / 2) { - return false; - } else { - // Check if current health dropped below half - if (pokemonHealth < maxPokemonHealth / 2) { - return true; - } else { - return false; - } - } - } - } - - - // WeatherEffectPhase - /** - * In the {@linkcode WeatherEffectPhase}, this function automatically returns true. - * This is because certain weather effects (like Hail or Sandstorm) might reduce HP below half, - * and the specific weather effect calculations are handled {@linkcode PostWeatherForceSwitchOutAttr}. - */ - if (currentPhase instanceof WeatherEffectPhase) { - return true; - } - - // TurnEndPhase - /** - * During the {@linkcode TurnEndPhase}, the function calculates damage from various sources: - * - Poison, Toxic, Burn status effects - * - Leech Seed, Curse, Salt Cure, and traps like Whirlpool - * It uses {@linkcode calculateTurnEndDamage} for this purpose and checks if the Pokémon's health - * drops below half after the turn-end effects. - */ - if (currentPhase instanceof TurnEndPhase) { - const damageTaken = calculateTurnEndDamage(pokemon); - const initialHealth = getInitialHealth(damageTaken); - return isHealthBelowHalf(initialHealth, pokemonHealth); - } - - // TurnData checks - /** - * The function checks if the last move received was affected by Sheer Force. If so, - * Sheer Force prevents the activation of the ability. The function calls - * {@linkcode checkSheerForceCondition} to verify this condition. - */ - if (pokemon.turnData) { - const sheerForceCondition = checkSheerForceCondition(pokemon); - if (sheerForceCondition !== null) { - return sheerForceCondition; - } - - /** - * Additionally, the function calculates the health recovery from Shell Bell using - * {@linkcode calculateShellBellRecovery} and checks if the Pokemon's health dropped - * below half, irregardless of Shell Bell recovery - */ - const shellBellRecovery = calculateShellBellRecovery(pokemon); - const initialHealth = getInitialHealth(pokemon.turnData?.damageTaken ?? 0); - return isHealthBelowHalf(initialHealth, pokemonHealth - shellBellRecovery); - } - - return false; - }; -} - -/** - * Calculates the damage taken by the Pokémon during the {@linkcode TurnEndPhase}. - * - * This function considers different status effects like Poison, Toxic, Burn, and also - * additional damage from Leech Seed, Curse, Salt Cure, and traps like Whirlpool. - * - * @param {Pokemon} pokemon - The Pokémon whose damage is being calculated. - * @returns {number} The total damage taken during the turn end phase. - */ -function calculateTurnEndDamage(pokemon: Pokemon): number { - let damageTaken = 0; - const maxHp = pokemon.getMaxHp(); - - if (pokemon.status?.isPostTurn()) { - switch (pokemon.status.effect) { - case StatusEffect.POISON: - damageTaken += Math.max(maxHp >> 3, 1); // Poison damage - break; - case StatusEffect.TOXIC: - damageTaken += Math.max(Math.floor((maxHp / 16) * pokemon.status.turnCount), 1); // Toxic damage - break; - case StatusEffect.BURN: - damageTaken += Math.max(maxHp >> 4, 1); // Burn damage - break; - } - } - - // Additional damage sources - if (pokemon.status?.effect === StatusEffect.SLEEP) { - damageTaken += calculateSleepDamage(pokemon); - } - if (pokemon.getTag(BattlerTagType.SEEDED)) { - damageTaken += Utils.toDmgValue(maxHp / 8); // Seeded damage - } - if (pokemon.getTag(BattlerTagType.CURSED)) { - damageTaken += Utils.toDmgValue(maxHp / 4); // Cursed damage - } - if (pokemon.getTag(BattlerTagType.SALT_CURED)) { - const isSteelOrWater = pokemon.isOfType(Type.STEEL) || pokemon.isOfType(Type.WATER); - damageTaken += Utils.toDmgValue(isSteelOrWater ? maxHp / 4 : maxHp / 8); - } - if (pokemon.findTags(tag => tag instanceof DamagingTrapTag).length > 0) { - damageTaken += Utils.toDmgValue(maxHp / 8); // Trap damage - } - - return damageTaken; -} - -/** - * Calculates damage that occurs due to sleeping, if any opponents have abilities - * that hurt sleeping Pokémon during {@linkcode TurnEndPhase}. - * - * @param {Pokemon} pokemon - The Pokémon whose damage is being calculated. - * @returns {number} The additional damage caused by sleep-related abilities (Bad Dreams). - */ -function calculateSleepDamage(pokemon: Pokemon): number { - let damageTaken = 0; - for (const opponent of pokemon.getOpponents()) { - if (opponent.hasAbilityWithAttr(PostTurnHurtIfSleepingAbAttr)) { - damageTaken += Utils.toDmgValue(pokemon.getMaxHp() / 8); - } - } - if (pokemon.getTag(BattlerTagType.NIGHTMARE)) { - damageTaken += Utils.toDmgValue(pokemon.getMaxHp() / 4); - } - return damageTaken; -} - -/** - * Calculates the amount of recovery from the Shell Bell item. - * - * If the Pokémon is holding a Shell Bell, this function computes the amount of health - * recovered based on the damage dealt in the current turn. The recovery is multiplied by the - * Shell Bell's modifier (if any). This covers the case where if the pokemon's health falls - * below half and recovers back above half from a Shell Bell, - * Wimp Out will activate even after the Shell Bell recovery - * - * @param {Pokemon} pokemon - The Pokémon whose Shell Bell recovery is being calculated. - * @returns {number} The amount of health recovered by Shell Bell. - */ -function calculateShellBellRecovery(pokemon: Pokemon): number { - const shellBellModifier = pokemon.getHeldItems().find(m => m instanceof HitHealModifier); - if (shellBellModifier) { - return Utils.toDmgValue(pokemon.turnData.damageDealt / 8) * shellBellModifier.stackCount; - } - return 0; -} - -/** - * Checks whether Sheer Force prevents the effects of the last attack received by the Pokémon. - * - * If the last attack received by the Pokémon is affected by Sheer Force (meaning that - * additional effects like status changes are suppressed), the function returns `false` - * to indicate that the additional effects won't apply. Otherwise, the function returns `null`. - * - * @param {Pokemon} pokemon - The Pokémon being evaluated. - * @returns {boolean | null} `false` if Sheer Force prevents effects, otherwise `null` if not applicable. - */ -function checkSheerForceCondition(pokemon: Pokemon): boolean | null { - const lastReceivedAttack = pokemon.turnData?.attacksReceived[0]; - if (lastReceivedAttack) { - const lastAttacker = pokemon.getOpponents().find(p => p.id === lastReceivedAttack.sourceId); - if (lastAttacker) { - const isSheerForceAffected = allMoves[lastReceivedAttack.move].chance >= 0 && lastAttacker.hasAbility(Abilities.SHEER_FORCE); - return isSheerForceAffected ? false : null; - } - } - return null; -} - function getWeatherCondition(...weatherTypes: WeatherType[]): AbAttrCondition { return (pokemon: Pokemon) => { if (!pokemon.scene?.arena) { @@ -4933,7 +4714,7 @@ async function applyAbAttrsInternal( } } -// Shared helper class for common functionality +//Helper class for Switch out logic class ForceSwitchOutHelper { constructor(private switchType: SwitchType) {} /** @@ -4958,7 +4739,7 @@ class ForceSwitchOutHelper { if (switchOutTarget.hp > 0) { switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH); - pokemon.scene.unshiftPhase(new SwitchPhase(pokemon.scene, SwitchType.SWITCH, switchOutTarget.getFieldIndex(), true, true)); + pokemon.scene.prependToPhase(new SwitchPhase(pokemon.scene, this.switchType, switchOutTarget.getFieldIndex(), true, true), MoveEndPhase); return true; } // If the battle is not a wild Pokémon encounter @@ -4967,16 +4748,14 @@ class ForceSwitchOutHelper { * If yes, the Pokémon leaves the field and a new SwitchSummonPhase is initiated. */ } else if (pokemon.scene.currentBattle.battleType !== BattleType.WILD) { - if (switchOutTarget.scene.getEnemyParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) { return false; } - if (switchOutTarget.hp > 0) { switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH); - pokemon.scene.unshiftPhase(new SwitchSummonPhase(pokemon.scene, this.switchType, switchOutTarget.getFieldIndex(), + pokemon.scene.prependToPhase(new SwitchSummonPhase(pokemon.scene, this.switchType, switchOutTarget.getFieldIndex(), (pokemon.scene.currentBattle.trainer ? pokemon.scene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0), - false, false)); + false, false), MoveEndPhase); return true; } // If it's a wild Pokémon encounter @@ -5057,313 +4836,105 @@ class ForceSwitchOutHelper { } /** - * Attribute that forces a switch-out after defending against an attack. - * This triggers after the Pokémon is attacked and takes damage. + * Calculates the amount of recovery from the Shell Bell item. + * + * If the Pokémon is holding a Shell Bell, this function computes the amount of health + * recovered based on the damage dealt in the current turn. The recovery is multiplied by the + * Shell Bell's modifier (if any). + * + * @param {Pokemon} pokemon - The Pokémon whose Shell Bell recovery is being calculated. + * @returns {number} The amount of health recovered by Shell Bell. */ -export class PostDefendForceSwitchOutAttr extends PostDefendAbAttr { - private helper: ForceSwitchOutHelper; - - constructor(private switchType: SwitchType = SwitchType.SWITCH) { - super(); - this.helper = new ForceSwitchOutHelper(switchType); +function calculateShellBellRecovery(pokemon: Pokemon): number { + const shellBellModifier = pokemon.getHeldItems().find(m => m instanceof HitHealModifier); + if (shellBellModifier) { + return Utils.toDmgValue(pokemon.turnData.damageDealt / 8) * shellBellModifier.stackCount; } + return 0; +} - /** - * Applies the switch-out logic after an attack hits the Pokemon. - * Checks if the Pokemon's health drops below half for a forced switch-out - * through {@linkcode getHealthKnockedBelowHalf}, which tracks the damage taken from the attack. - * - * If the attacker is attacking with a multi-hit move, the ability will trigger after the last hit - * If hit with a phasing move, the phasing move's effect is applied and not the ability - * - * @param {Pokemon} pokemon The defending Pokémon with this ability. - * @param {boolean} _passive N/A - * @param {boolean} simulated Whether the ability is being simulated. - * @param {Pokemon} attacker The attacking Pokémon. - * @param {Move} move The move that triggered the post-defend effect. - * @param {HitResult | null} hitResult N/A - * @param {any[]} args N/A - * @returns {boolean} True if the switch-out logic was successfully applied. - */ - - applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): boolean { - for (const opponent of pokemon.getOpponents()) { - if (!this.helper.getSwitchOutCondition(pokemon, opponent)) { - return false; - } - } - if (attacker.turnData.hitsLeft > 1) { - return false; - } - if (pokemon.getTag(BattlerTagType.TRAPPED) !== undefined) { - pokemon.removeTag(BattlerTagType.TRAPPED); - } - if (move.id === Moves.DRAGON_TAIL || move.id === Moves.CIRCLE_THROW) { - return false; - } - return this.helper.switchOutLogic(pokemon); - } - - getFailedText(user: Pokemon, target: Pokemon, move: Move, cancelled: Utils.BooleanHolder): string | null { - return this.helper.getFailedText(target); +/** + * Triggers after the Pokemon takes any damage + * @extends AbAttr + */ +export class PostDamageAbAttr extends AbAttr { + applyPostDamageAbAttrs(pokemon: Pokemon, damage: number, passive: boolean, simulated: boolean, args: any[]): boolean | Promise { + return false; } } /** - * Ability attribute for forcing a Pokémon to switch out after an attack. - * This triggers after the Pokémon attacks and takes damage themselves. + * Ability attribute for forcing a Pokémon to switch out after its health drops below half. + * This attribute checks various conditions related to the damage received, the moves used by the Pokémon + * and its opponents, and determines whether a forced switch-out should occur. * - * @extends PostAttackAbAttr + * @extends PostDamageAbAttr */ -export class PostAttackForceSwitchOutAttr extends PostAttackAbAttr { +export class PostDamageForceSwitchAttr extends PostDamageAbAttr { private helper: ForceSwitchOutHelper; + private hpRatio: number; - constructor(private switchType: SwitchType = SwitchType.SWITCH) { + constructor(hpRatio:number = 0.5) { super(); - this.helper = new ForceSwitchOutHelper(switchType); - } - - /** - * Applies the switch-out logic after an attack is made. Checks if the Pokemon's health drops - * below half for a forced switch-out through {@linkcode getHealthKnockedBelowHalf}, which - * tracks the damage taken following its attack. - * If the attacker cuts their own health with a move like Curse or Substitute, the ability is - * not triggered - * - * @param {Pokemon} pokemon The attacking Pokémon with this ability. - * @param {boolean} passive N/A - * @param {boolean} simulated Whether the ability is being simulated. - * @param {Pokemon} defender The defending Pokémon. - * @param {Move} move The move that triggered the post-attack effect. - * @param {HitResult | null} hitResult N/A - * @param {any[]} args N/A - * @returns {boolean} True if the switch-out logic was successfully applied. - */ - applyPostAttack(pokemon: Pokemon, passive: boolean, simulated: boolean, defender: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean { - - for (const opponent of pokemon.getOpponents()) { - if (!this.helper.getSwitchOutCondition(pokemon, opponent)) { - return false; - } - } - if (pokemon.getTag(BattlerTagType.TRAPPED) !== undefined) { - pokemon.removeTag(BattlerTagType.TRAPPED); - } - if ([ Moves.PAIN_SPLIT, Moves.SUBSTITUTE, Moves.BELLY_DRUM ].includes(move.id)) { - - return false; - } - if (pokemon.getTypes(true).includes(Type.GHOST) && move.id === Moves.CURSE) { - return false; - } - - return this.helper.switchOutLogic(pokemon); - } - - getFailedText(user: Pokemon, target: Pokemon, move: Move, cancelled: Utils.BooleanHolder): string | null { - return this.helper.getFailedText(target); - } -} - -/** - * Ability attribute for forcing a Pokémon to switch out after being summoned. - * This triggers after the Pokémon is summoned and takes damage from Arena Traps like - * Stealth Rocks. - * - * @extends PostSummonAbAttr - */ -export class PostSummonForceSwitchOutAttr extends PostSummonAbAttr { - private helper: ForceSwitchOutHelper; - - constructor(private switchType: SwitchType = SwitchType.SWITCH) { - super(); - this.helper = new ForceSwitchOutHelper(switchType); - } - - /** - * Applies the switch-out logic after the Pokémon is summoned. Checks if the Pokemon's health drops - * below half for a forced switch-out through {@linkcode getHealthKnockedBelowHalf}, which - * tracks the damage taken upon entry. If it drops below half, the pokemon is switched out - * - * @param {Pokemon} pokemon The Pokémon summoned with this ability. - * @param {boolean} passive N/A - * @param {boolean} simulated Whether the ability is being simulated. - * @param {any[]} args N/A - * @returns {boolean} True if the switch-out logic was successfully applied. - */ - applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean { - if (pokemon.hasAbilityWithAttr(BlockNonDirectDamageAbAttr)) { - return false; - } - for (const opponent of pokemon.getOpponents()) { - if (!this.helper.getSwitchOutCondition(pokemon, opponent)) { - return false; - } - } - if (pokemon.getTag(BattlerTagType.TRAPPED) !== undefined) { - pokemon.removeTag(BattlerTagType.TRAPPED); - } - - return this.helper.switchOutLogic(pokemon); - } - - getFailedText(user: Pokemon, target: Pokemon, move: Move, cancelled: Utils.BooleanHolder): string | null { - return this.helper.getFailedText(target); - } - -} - -/** - * Ability attribute for forcing a Pokémon to switch out at the end of the turn, - * if its health drops below half. This attribute triggers from effects like - * Poison, Leech Seed, Curse etc. - * - * @extends PostTurnAbAttr - */ -export class PostTurnForceSwitchOutAttr extends PostTurnAbAttr { - private helper: ForceSwitchOutHelper; - - constructor(private switchType: SwitchType = SwitchType.SWITCH) { - super(); - this.helper = new ForceSwitchOutHelper(switchType); - } - - /** - * Applies the switch-out logic at the end of the turn. Checks if the Pokemon's health drops - * below half for a forced switch-out through {@linkcode getHealthKnockedBelowHalf}, which - * simulates the damage that it would take. If it drops below half, the pokemon is switched out - * - * @param {Pokemon} pokemon The Pokémon attempting to switch out. - * @param {boolean} passive N/A - * @param {boolean} simulated Whether the ability is being simulated. - * @param {any[]} args N/A - * @returns {boolean | Promise} True if the switch-out logic was successfully applied. - */ - applyPostTurn(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean | Promise { - if (pokemon.hasAbilityWithAttr(BlockNonDirectDamageAbAttr)) { - return false; - } - if (pokemon.status?.isPostTurn() && pokemon.hasAbilityWithAttr(BlockStatusDamageAbAttr)) { - return false; - } - for (const opponent of pokemon.getOpponents()) { - if (!this.helper.getSwitchOutCondition(pokemon, opponent)) { - return false; - } - } - if (pokemon.getTag(BattlerTagType.TRAPPED) !== undefined) { - pokemon.removeTag(BattlerTagType.TRAPPED); - } - return this.helper.switchOutLogic(pokemon); - - } - - getFailedText(user: Pokemon, target: Pokemon, move: Move, cancelled: Utils.BooleanHolder): string | null { - return this.helper.getFailedText(target); - } -} - -/** - * Ability attribute for forcing a Pokémon to switch out during weather. - * This attribute is triggered during a weather lapse and applies switch-out logic based on - * the damage taken during weather conditions. - * - * @extends PostWeatherLapseAbAttr - */ -export class PostWeatherForceSwitchOutAttr extends PostWeatherLapseAbAttr { - private helper: ForceSwitchOutHelper; - private damageFactor: integer; - - constructor(damageFactor: integer, ...weatherTypes: WeatherType[]) { - super(...weatherTypes); - this.damageFactor = damageFactor; + this.hpRatio = hpRatio; this.helper = new ForceSwitchOutHelper(SwitchType.SWITCH); } /** - * Applies the switch-out logic during a weather that damages Pokemon such as Hail or Sandstorm. - * If the Pokemon would fall below half health from the simulated weather damage amount, the weather damage is - * applied and the Pokmeon is switched out. + * Applies the switch-out logic after the Pokémon takes damage. + * Checks various conditions based on the moves used by the Pokémon, the opponents' moves, and + * the Pokémon's health after damage to determine whether the switch-out should occur. * - * @param {Pokemon} pokemon The Pokémon affected by the weather. + * @param {Pokemon} pokemon The Pokémon that took damage. + * @param {number} damage The amount of damage taken by the Pokémon. * @param {boolean} passive N/A * @param {boolean} simulated Whether the ability is being simulated. - * @param {Weather} weather The current weather condition. * @param {any[]} args N/A - * @returns {boolean} True if the switch-out logic was successfully applied. + * @returns {boolean | Promise} True if the switch-out logic was successfully applied, false otherwise. */ - - applyPostWeatherLapse(pokemon: Pokemon, passive: boolean, simulated: boolean, weather: Weather, args: any[]): boolean { - if (pokemon.hasAbilityWithAttr(BlockNonDirectDamageAbAttr)) { - return false; + applyPostDamageAbAttrs(pokemon: Pokemon, damage: number, passive: boolean, simulated: boolean, args: any[]): boolean | Promise { + const moveHistory = pokemon.getMoveHistory(); + // Will not activate when the Pokémon's HP is lowered by cutting its own HP + const fordbiddenAttackingMoves = [ Moves.BELLY_DRUM, Moves.SUBSTITUTE, Moves.CURSE, Moves.PAIN_SPLIT ]; + if (moveHistory.length > 0) { + const lastMoveUsed = moveHistory[moveHistory.length - 1]; + // Will not activate if the Pokémon's HP falls below half while it is in the air during Sky Drop. + if (fordbiddenAttackingMoves.includes(lastMoveUsed.move) || (lastMoveUsed.move === Moves.SKY_DROP && lastMoveUsed.result === MoveResult.OTHER)) { + return false; + // Will not activate if the Pokémon's HP falls below half due to hurting itself in confusion + } else if (lastMoveUsed.move === 0 && pokemon.getTag(BattlerTagType.CONFUSED)) { + return false; + } } + // Dragon Tail and Circle Throw switch out Pokémon before the Ability activates. + const fordbiddenDefendingMoves = [ Moves.DRAGON_TAIL, Moves.CIRCLE_THROW ]; for (const opponent of pokemon.getOpponents()) { - if (!this.helper.getSwitchOutCondition(pokemon, opponent)) { - return false; + const enemyMoveHistory = opponent.getMoveHistory(); + if (enemyMoveHistory.length > 0) { + const enemyLastMoveUsed = enemyMoveHistory[enemyMoveHistory.length - 1]; + if (fordbiddenDefendingMoves.includes(enemyLastMoveUsed.move)) { + return false; + // Will not activate if the Pokémon's HP falls below half by a move affected by Sheer Force. + } else if (allMoves[enemyLastMoveUsed.move].chance >= 0 && opponent.hasAbility(Abilities.SHEER_FORCE)) { + return false; + // Activate only after the last hit of multistrike moves + } else if (opponent.turnData.hitsLeft > 1) { + return false; + } } } - if (pokemon.getTag(BattlerTagType.TRAPPED) !== undefined) { - pokemon.removeTag(BattlerTagType.TRAPPED); - } - const damageAmount = Utils.toDmgValue(pokemon.getMaxHp() / (16 / this.damageFactor)); - if (pokemon.hp + damageAmount >= pokemon.getMaxHp() / 2) { - if (pokemon.hp < pokemon.getMaxHp() / 2) { - return this.helper.switchOutLogic(pokemon); - } else { - return false; - } - } else { - return false; - } - } - - getFailedText(user: Pokemon, target: Pokemon, move: Move, cancelled: Utils.BooleanHolder): string | null { - return this.helper.getFailedText(target); - } -} - -/** - * Ability attribute for forcing a Pokémon to switch out after taking damage - * from KOing an enemy (Aftermath, Innards Out). - * - * @extends PostVictoryAbAttr - */ -export class PostVictoryForceSwitchOutAttr extends PostVictoryAbAttr { - private helper: ForceSwitchOutHelper; - - constructor(private switchType: SwitchType = SwitchType.SWITCH) { - super(); - this.helper = new ForceSwitchOutHelper(switchType); - } - - /** - * Applies the switch-out logic after a KO. Checks if the conditions for a forced switch-out - * are met based on whether the Pokémon's health dropped below half following damage taken during the KO. - * - * @param {Pokemon} pokemon the {@linkcode Pokemon} with this ability - * @param {boolean} passive N/A - * @param {boolean} simulated Whether the ability is being simulated. - * @param {any[]} args N/A - * @returns {boolean} True if the switch-out logic was successfully applied. - */ - applyPostVictory(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean | Promise { - const pokemonHealth = pokemon.hp; - const maxPokemonHealth = pokemon.getMaxHp(); - const damageTaken = pokemon.turnData?.damageTaken; - const initialHealth = pokemonHealth + damageTaken; - // Check if the initial health was above half - if (initialHealth <= maxPokemonHealth / 2) { - return false; - } else { - // Check if current health dropped below half - if (pokemonHealth < maxPokemonHealth / 2) { + if (pokemon.hp + damage >= pokemon.getMaxHp() * this.hpRatio) { + // Activates if it falls below half and recovers back above half from a Shell Bell + const shellBellHeal = calculateShellBellRecovery(pokemon); + if (pokemon.hp - shellBellHeal < pokemon.getMaxHp() / 2) { for (const opponent of pokemon.getOpponents()) { if (!this.helper.getSwitchOutCondition(pokemon, opponent)) { return false; } } + // Trapping moves do not prevent activation if (pokemon.getTag(BattlerTagType.TRAPPED) !== undefined) { pokemon.removeTag(BattlerTagType.TRAPPED); } @@ -5371,9 +4942,10 @@ export class PostVictoryForceSwitchOutAttr extends PostVictoryAbAttr { } else { return false; } + } else { + return false; } } - getFailedText(user: Pokemon, target: Pokemon, move: Move, cancelled: Utils.BooleanHolder): string | null { return this.helper.getFailedText(target); } @@ -5413,6 +4985,11 @@ export function applyPostSetStatusAbAttrs(attrType: Constructor(attrType, pokemon, (attr, passive) => attr.applyPostSetStatus(pokemon, sourcePokemon, passive, effect, simulated, args), args, false, simulated); } +export function applyPostDamageAbAttrs(attrType: Constructor, + pokemon: Pokemon, damage: number, passive: boolean, simulated: boolean = false, args: any[]): Promise { + return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPostDamageAbAttrs(pokemon, damage, passive, simulated, args), args); +} + /** * Applies a field Stat multiplier attribute * @param attrType {@linkcode FieldMultiplyStatAbAttr} should always be FieldMultiplyBattleStatAbAttr for the time being @@ -6154,21 +5731,11 @@ export function initAbilities() { new Ability(Abilities.STAMINA, 7) .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, Stat.DEF, 1), new Ability(Abilities.WIMP_OUT, 7) - .attr(PostDefendForceSwitchOutAttr) - .attr(PostAttackForceSwitchOutAttr) - .attr(PostSummonForceSwitchOutAttr) - .attr(PostTurnForceSwitchOutAttr) - .attr(PostWeatherForceSwitchOutAttr, 1, WeatherType.HAIL, WeatherType.SANDSTORM) - .attr(PostVictoryForceSwitchOutAttr) - .condition(getHealthKnockedBelowHalf()), + .attr(PostDamageForceSwitchAttr), + // .condition(getSheerForceHitDisableAbCondition()), new Ability(Abilities.EMERGENCY_EXIT, 7) - .attr(PostDefendForceSwitchOutAttr) - .attr(PostAttackForceSwitchOutAttr) - .attr(PostSummonForceSwitchOutAttr) - .attr(PostTurnForceSwitchOutAttr) - .attr(PostWeatherForceSwitchOutAttr, 1, WeatherType.HAIL, WeatherType.SANDSTORM) - .attr(PostVictoryForceSwitchOutAttr) - .condition(getHealthKnockedBelowHalf()), + .attr(PostDamageForceSwitchAttr), + // .condition(getSheerForceHitDisableAbCondition()), new Ability(Abilities.WATER_COMPACTION, 7) .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => user.getMoveType(move) === Type.WATER && move.category !== MoveCategory.STATUS, Stat.DEF, 2), new Ability(Abilities.MERCILESS, 7) diff --git a/src/data/move.ts b/src/data/move.ts index d5e77c2f56b..16b7a22f075 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -8,7 +8,7 @@ import { Constructor, NumberHolder } from "#app/utils"; import * as Utils from "../utils"; import { WeatherType } from "./weather"; import { ArenaTagSide, ArenaTrapTag, WeakenMoveTypeTag } from "./arena-tag"; -import { allAbilities, AllyMoveCategoryPowerBoostAbAttr, applyAbAttrs, applyPostAttackAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, BlockItemTheftAbAttr, BlockNonDirectDamageAbAttr, BlockOneHitKOAbAttr, BlockRecoilDamageAttr, ConfusionOnStatusEffectAbAttr, FieldMoveTypePowerBoostAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, HealFromBerryUseAbAttr, IgnoreContactAbAttr, IgnoreMoveEffectsAbAttr, IgnoreProtectOnContactAbAttr, InfiltratorAbAttr, MaxMultiHitAbAttr, MoveAbilityBypassAbAttr, MoveEffectChanceMultiplierAbAttr, MoveTypeChangeAbAttr, PostDefendForceSwitchOutAttr, ReverseDrainAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, UnswappableAbilityAbAttr, UserFieldMoveTypePowerBoostAbAttr, VariableMovePowerAbAttr, WonderSkinAbAttr } from "./ability"; +import { allAbilities, AllyMoveCategoryPowerBoostAbAttr, applyAbAttrs, applyPostAttackAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, BlockItemTheftAbAttr, BlockNonDirectDamageAbAttr, BlockOneHitKOAbAttr, BlockRecoilDamageAttr, ConfusionOnStatusEffectAbAttr, FieldMoveTypePowerBoostAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, HealFromBerryUseAbAttr, IgnoreContactAbAttr, IgnoreMoveEffectsAbAttr, IgnoreProtectOnContactAbAttr, InfiltratorAbAttr, MaxMultiHitAbAttr, MoveAbilityBypassAbAttr, MoveEffectChanceMultiplierAbAttr, MoveTypeChangeAbAttr, PostDamageForceSwitchAttr, ReverseDrainAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, UnswappableAbilityAbAttr, UserFieldMoveTypePowerBoostAbAttr, VariableMovePowerAbAttr, WonderSkinAbAttr } from "./ability"; import { AttackTypeBoosterModifier, BerryModifier, PokemonHeldItemModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PreserveBerryModifier } from "../modifier/modifier"; import { BattlerIndex, BattleType } from "../battle"; import { TerrainType } from "./terrain"; @@ -5659,7 +5659,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { */ const switchOutTarget = this.selfSwitch ? user : target; if (switchOutTarget instanceof PlayerPokemon) { - if (target.getAbility().hasAttr(PostDefendForceSwitchOutAttr) && + if (target.getAbility().hasAttr(PostDamageForceSwitchAttr) && (move.id === Moves.U_TURN || move.id === Moves.VOLT_SWITCH)) { if (shouldPreventSwitchOut(target)) { return false; @@ -5694,7 +5694,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { * Check if Wimp Out/Emergency Exit activates due to being hit by U-turn or Volt Switch * If it did, the user of U-turn or Volt Switch will not be switched out. */ - if (target.getAbility().hasAttr(PostDefendForceSwitchOutAttr) && + if (target.getAbility().hasAttr(PostDamageForceSwitchAttr) && (move.id === Moves.U_TURN || move.id === Moves.VOLT_SWITCH)) { if (shouldPreventSwitchOut(target)) { return false; diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 94b9fd12540..fb3dd57bf08 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -22,7 +22,7 @@ import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "#app/data/balance/ import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag, AutotomizedTag, PowerTrickTag } from "../data/battler-tags"; import { WeatherType } from "#app/data/weather"; import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag"; -import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr } from "#app/data/ability"; +import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, applyPostDamageAbAttrs, PostDamageAbAttr } from "#app/data/ability"; import PokemonData from "#app/system/pokemon-data"; import { BattlerIndex } from "#app/battle"; import { Mode } from "#app/ui/ui"; @@ -2892,6 +2892,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { damage = this.damage(damage, ignoreSegments, preventEndure, ignoreFaintPhase); // Damage amount may have changed, but needed to be queued before calling damage function damagePhase.updateAmount(damage); + applyPostDamageAbAttrs(PostDamageAbAttr, this, damage, this.hasPassive(), false, []); return damage; } diff --git a/src/phases/post-turn-status-effect-phase.ts b/src/phases/post-turn-status-effect-phase.ts index 06681b733f0..220db196760 100644 --- a/src/phases/post-turn-status-effect-phase.ts +++ b/src/phases/post-turn-status-effect-phase.ts @@ -1,6 +1,6 @@ import BattleScene from "#app/battle-scene"; import { BattlerIndex } from "#app/battle"; -import { applyAbAttrs, BlockNonDirectDamageAbAttr, BlockStatusDamageAbAttr, ReduceBurnDamageAbAttr } from "#app/data/ability"; +import { applyAbAttrs, applyPostDamageAbAttrs, BlockNonDirectDamageAbAttr, BlockStatusDamageAbAttr, PostDamageAbAttr, ReduceBurnDamageAbAttr } from "#app/data/ability"; import { CommonBattleAnim, CommonAnim } from "#app/data/battle-anims"; import { getStatusEffectActivationText } from "#app/data/status-effect"; import { BattleSpec } from "#app/enums/battle-spec"; @@ -41,6 +41,7 @@ export class PostTurnStatusEffectPhase extends PokemonPhase { // Set preventEndure flag to avoid pokemon surviving thanks to focus band, sturdy, endure ... this.scene.damageNumberHandler.add(this.getPokemon(), pokemon.damage(damage.value, false, true)); pokemon.updateInfo(); + applyPostDamageAbAttrs(PostDamageAbAttr, pokemon, damage.value, pokemon.hasPassive(), false, []); } new CommonBattleAnim(CommonAnim.POISON + (pokemon.status.effect - 1), pokemon).play(this.scene, false, () => this.end()); } else { diff --git a/src/test/abilities/wimp_out.test.ts b/src/test/abilities/wimp_out.test.ts index 6c3eae32924..e170ae943ee 100644 --- a/src/test/abilities/wimp_out.test.ts +++ b/src/test/abilities/wimp_out.test.ts @@ -62,29 +62,6 @@ describe("Abilities - Wimp Out", () => { expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase"); expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.TYRUNT); }); - - it("triggers status on the wimp out user before a new pokemon is switched in", async () => { - // arrange - game.override - .enemyMoveset(Moves.SLUDGE_BOMB) - .startingLevel(85); - await game.startBattle([ - Species.WIMPOD, - Species.TYRUNT - ]); - vi.spyOn(game.scene.getEnemyPokemon()!, "randSeedInt").mockReturnValue(0); - - // act - game.move.select(Moves.SLUDGE_BOMB); - game.doSelectPartyPokemon(1); - await game.phaseInterceptor.to(TurnEndPhase); - - // assert - const playerPkm = game.scene.getPlayerPokemon()!; - expect(game.scene.getParty()[1].status?.effect).toEqual(StatusEffect.POISON); - expect(playerPkm.species.speciesId).toEqual(Species.TYRUNT); - expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase"); - }); it("It makes wild pokemon flee if triggered", async () => { // arrange game.override @@ -128,30 +105,6 @@ describe("Abilities - Wimp Out", () => { expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase"); expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.WIMPOD); }); - - it("triggers after last hit of multi hit move", async () => { - // arrange - game.override - .enemyMoveset(Moves.BULLET_SEED) - .enemyAbility(Abilities.SKILL_LINK) - .startingLevel(110) - .enemyLevel(80); - await game.startBattle([ - Species.WIMPOD, - Species.TYRUNT - ]); - - // act - game.move.select(Moves.BULLET_SEED); - game.doSelectPartyPokemon(1); - await game.phaseInterceptor.to(TurnEndPhase); - - - // assert - const enemyPokemon = game.scene.getEnemyPokemon()!; - expect(enemyPokemon.turnData.hitsLeft).toBe(0); - expect(enemyPokemon.turnData.hitCount).toBe(5); - }); it("Trapping moves do not prevent Wimp Out from activating.", async () => { // arrange game.override @@ -675,4 +628,48 @@ describe("Abilities - Wimp Out", () => { expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase"); expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.TYRUNT); }); + it("triggers status on the wimp out user before a new pokemon is switched in", async () => { + // arrange + game.override + .enemyMoveset(Moves.SLUDGE_BOMB) + .startingLevel(80); + await game.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + vi.spyOn(game.scene.getEnemyPokemon()!, "randSeedInt").mockReturnValue(0); + + // act + game.move.select(Moves.SLUDGE_BOMB); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to(TurnEndPhase); + + // assert + const playerPkm = game.scene.getPlayerPokemon()!; + expect(game.scene.getParty()[1].status?.effect).toEqual(StatusEffect.POISON); + expect(playerPkm.species.speciesId).toEqual(Species.TYRUNT); + expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase"); + }); + it("triggers after last hit of multi hit move", async () => { + // arrange + game.override + .enemyMoveset(Moves.BULLET_SEED) + .enemyAbility(Abilities.SKILL_LINK) + .startingLevel(110) + .enemyLevel(80); + await game.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + // act + game.move.select(Moves.BULLET_SEED); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to(TurnEndPhase); + + // assert + const enemyPokemon = game.scene.getEnemyPokemon()!; + expect(enemyPokemon.turnData.hitsLeft).toBe(0); + expect(enemyPokemon.turnData.hitCount).toBe(5); + }); });