diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index bec6fde98a5..bf1ca871193 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -2466,6 +2466,19 @@ export class StatusEffectAttr extends MoveEffectAttr { return false; } + /** + * Wrapper function to attempt to set status of a pokemon. + * Exists to allow super classes to override parameters. + * @param pokemon - The {@linkcode Pokemon} being statused. + * @param source - The {@linkcode Pokemon} doing the statusing. + * @param quiet - Whether to suppress messages for status immunities. + * @returns Whether the status was sucessfully applied. + * @see {@linkcode Pokemon.trySetStatus} + */ + protected doSetStatus(pokemon: Pokemon, source: Pokemon, quiet: boolean): boolean { + return pokemon.trySetStatus(this.effect, true, source, undefined, null, false, quiet) + } + getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { const moveChance = this.getMoveChance(user, target, move, this.selfTarget, false); const score = moveChance < 0 ? -10 : Math.floor(moveChance * -0.1); @@ -2473,19 +2486,6 @@ export class StatusEffectAttr extends MoveEffectAttr { return pokemon.canSetStatus(this.effect, true, false, user) ? score : 0; } - - /** - * Wrapper function to attempt to set status of a pokemon. - * Exists to allow super classes to override parameters. - * @param pokemon - The {@linkcode Pokemon} being statused. - * @param user - The {@linkcode Pokemon} doing the statusing. - * @param quiet - Whether to suppress messages for status immunities. - * @returns Whether the status was sucessfully applied. - * @see {@linkcode Pokemon.trySetStatus} - */ - protected doSetStatus(pokemon: Pokemon, user: Pokemon, quiet: boolean): boolean { - return pokemon.trySetStatus(this.effect, true, user, undefined, null, false, quiet) - } } /** @@ -2501,13 +2501,16 @@ export class RestAttr extends StatusEffectAttr { this.duration = duration; } - // TODO: Add custom text for rest and make `HealAttr` no longer cause status + // TODO: Add custom text for rest and make `HealAttr` no longer show the message protected override doSetStatus(pokemon: Pokemon, user: Pokemon, quiet: boolean): boolean { return pokemon.trySetStatus(this.effect, true, user, this.duration, null, true, quiet) } } - +/** + * Attribute to randomly apply one of several statuses to the target. + * Used for {@linkcode Moves.TRI_ATTACK} and {@linkcode Moves.DIRE_CLAW}. + */ export class MultiStatusEffectAttr extends StatusEffectAttr { public effects: StatusEffect[]; diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index ec5a5712c46..28ccd76179a 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -5406,12 +5406,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } // TODO: Add messages for misty/electric terrain - private queueImmuneMessage(quiet: boolean, effect?: StatusEffect): void { + private queueImmuneMessage(quiet: boolean, effect: StatusEffect): void { if (!effect || quiet) { return; } - const message = effect && this.status?.effect === effect - ? getStatusEffectOverlapText(effect ?? StatusEffect.NONE, getPokemonNameWithAffix(this)) + const message = this.status?.effect === effect + ? getStatusEffectOverlapText(effect, getPokemonNameWithAffix(this)) : i18next.t("abilityTriggers:moveImmunity", { pokemonNameWithAffix: getPokemonNameWithAffix(this), }); @@ -5419,13 +5419,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Check if a status effect can be applied to this {@linckode Pokemon}. + * Check if a status effect can be applied to this {@linkcode Pokemon}. * - * @param effect - The {@linkcode StatusEffect} whose applicability is being checked - * @param quiet - Whether to suppress in-battle messages for status checks; default `false` - * @param overrideStatus - Whether to allow overriding the Pokemon's current status with a different one; default `false` - * @param sourcePokemon - The {@linkcode Pokemon} applying the status effect to the target - * @param ignoreField Whether any field effects (weather, terrain, etc.) should be considered + * @param effect - The {@linkcode StatusEffect} whose applicability is being checked. + * @param quiet - Whether to suppress in-battle messages for status checks; default `false`. + * @param overrideStatus - Whether to allow overriding the Pokemon's current status with a different one; default `false`. + * @param sourcePokemon - The {@linkcode Pokemon} applying the status effect to the target, + * or `null` if the status is applied from a non-Pokemon source (hazards, etc.); default `null`. + * @param ignoreField - Whether to ignore field effects (weather, terrain, etc.) preventing status application; + * default `false` + * @returns Whether {@linkcode effect} can be applied to this Pokemon. */ // TODO: Review and verify the message order precedence in mainline if multiple status-blocking effects are present at once canSetStatus( @@ -5436,11 +5439,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { ignoreField = false, ): boolean { if (effect !== StatusEffect.FAINT) { - // Status-overriding moves (ie Rest) fail if their respective status already exists + // Status-overriding moves (ie Rest) fail if their respective status already exists; + // all other moves fail if the target already has _any_ status if (overrideStatus ? this.status?.effect === effect : this.status) { this.queueImmuneMessage(quiet, effect); return false; } + if ( this.isGrounded() && !ignoreField && @@ -5453,13 +5458,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const types = this.getTypes(true, true); - // Check for specific immunities for certain statuses + /* Whether the target is immune to the specific status being applied. */ let isImmune = false; + switch (effect) { case StatusEffect.POISON: case StatusEffect.TOXIC: - // Check for type based immunities and/or Corrosion - isImmune = types.some(defType => { + // Check for type based immunities and/or Corrosion from the applier + isImmune = types.some((defType) => { if (defType !== PokemonType.POISON && defType !== PokemonType.STEEL) { return false; } @@ -5469,40 +5475,41 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } const cancelImmunity = new BooleanHolder(false); - applyAbAttrs( - IgnoreTypeStatusEffectImmunityAbAttr, - sourcePokemon, - cancelImmunity, - false, - effect, - defType, - ); - return cancelImmunity.value; - }); + applyAbAttrs( + IgnoreTypeStatusEffectImmunityAbAttr, + sourcePokemon, + cancelImmunity, + false, + effect, + defType, + ); + return cancelImmunity.value; + }); break; case StatusEffect.PARALYSIS: - isImmune = this.isOfType(PokemonType.ELECTRIC) + isImmune = this.isOfType(PokemonType.ELECTRIC); break; case StatusEffect.SLEEP: isImmune = this.isGrounded() && globalScene.arena.terrain?.terrainType === TerrainType.ELECTRIC; break; - case StatusEffect.FREEZE: + case StatusEffect.FREEZE: { + const weatherType = globalScene.arena.weather?.weatherType; isImmune = this.isOfType(PokemonType.ICE) || - !ignoreField && - [WeatherType.SUNNY, WeatherType.HARSH_SUN].includes( - globalScene.arena.weather?.weatherType ?? WeatherType.NONE, - ) + (!ignoreField && + (weatherType === WeatherType.SUNNY || + weatherType === WeatherType.HARSH_SUN)); break; + } case StatusEffect.BURN: - isImmune = this.isOfType(PokemonType.FIRE) + isImmune = this.isOfType(PokemonType.FIRE); break; } if (isImmune) { - this.queueImmuneMessage(quiet, effect) + this.queueImmuneMessage(quiet, effect); return false; } @@ -5525,8 +5532,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { pokemon, effect, cancelled, - quiet, this, sourcePokemon, - ) + quiet, + this, + sourcePokemon, + ); if (cancelled.value) { return false; } @@ -5540,7 +5549,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { ) { if (!quiet) { globalScene.queueMessage( - i18next.t("moveTriggers:safeguard", { targetName: getPokemonNameWithAffix(this)}) + i18next.t("moveTriggers:safeguard", { + targetName: getPokemonNameWithAffix(this), + }), ); } return false; @@ -5549,12 +5560,27 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return true; } - // TODO: Make this take a destructured object as args to condense all these optional args... + /** + * Attempt to set this Pokemon's status to the specified condition. + * @param effect - The {@linkcode StatusEffect} to set. + * @param asPhase - Whether to set the status in a new {@linkcode ObtainStatusEffectPhase} or immediately; default `false`. + * If `false`, will not display most messages associated with status setting. + * @param sourcePokemon - The {@linkcode Pokemon} applying the status effect to the target, + * or `null` if the status is applied from a non-Pokemon source (hazards, etc.); default `null`. + * @param sleepTurnsRemaining - The number of turns to set {@linkcode StatusEffect.SLEEP} for; + * defaults to a random number between 2 and 4 and is unused for non-Sleep statuses. + * @param sourceText - The text to show for the source of the status effect, if any; + * defaults to `null` and is unused if `asPhase=false`. + * @param overrideStatus - Whether to allow overriding the Pokemon's current status with a different one; default `false`. + * @param quiet - Whether to suppress in-battle messages for status checks; default `false`. + * @returns Whether the status effect was successfully applied (or a phase for it) + */ + // TODO: Make this take a destructured object for params and make it same order as `canSetStatus` trySetStatus( effect: StatusEffect, asPhase = false, sourcePokemon: Pokemon | null = null, - turnsRemaining?: number, + sleepTurnsRemaining?: number, sourceText: string | null = null, overrideStatus?: boolean, quiet = true, @@ -5579,22 +5605,22 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } - if (asPhase) { - if (overrideStatus) { - this.resetStatus(false); - } + if (overrideStatus) { + this.resetStatus(false); + } + if (asPhase) { globalScene.unshiftPhase( new ObtainStatusEffectPhase( this.getBattlerIndex(), effect, - turnsRemaining, + sleepTurnsRemaining, sourceText, sourcePokemon, ), ); } else { - this.doSetStatus(effect, turnsRemaining) + this.doSetStatus(effect, sleepTurnsRemaining) } return true; @@ -5611,7 +5637,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * Attempt to give the specified Pokemon the given status effect. * Does **NOT** perform any feasibility checks whatsoever, and should thus never be called directly * unless conditions are known to be met. - * @param effect - StatusEffect.SLEEP + * @param effect - {@linkcode StatusEffect.SLEEP} * @param sleepTurnsRemaining - The number of turns to inflict sleep for; defaults to a random number between 2 and 4. */ doSetStatus(effect: StatusEffect.SLEEP, sleepTurnsRemaining?: number): void; @@ -5620,7 +5646,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * Does **NOT** perform any feasibility checks whatsoever, and should thus never be called directly * unless conditions are known to be met. * @param effect - The {@linkcode StatusEffect} to set - * @param sleepTurnsRemaining - The number of turns to inflict sleep for; defaults to a random number between 2 and 4. + * @param sleepTurnsRemaining - The number of turns to inflict sleep for; defaults to a random number between 2 and 4 + * and is unused for all non-sleep Statuses. */ doSetStatus(effect: StatusEffect, sleepTurnsRemaining?: number): void; /** @@ -5628,13 +5655,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * Does **NOT** perform any feasibility checks whatsoever, and should thus never be called directly * unless conditions are known to be met. * @param effect - The {@linkcode StatusEffect} to set - * @param sleepTurnsRemaining - The number of turns to inflict sleep for; defaults to a random number between 2 and 4. + * @param sleepTurnsRemaining - The number of turns to inflict sleep for; defaults to a random number between 2 and 4 + * and is unused for all non-sleep Statuses. */ - doSetStatus(effect: StatusEffect, sleepTurnsRemaining = this.randBattleSeedIntRange(2, 4)): void { + doSetStatus(effect: StatusEffect, sleepTurnsRemaining = effect !== StatusEffect.SLEEP ? 0 : this.randBattleSeedIntRange(2, 4)): void { if (effect === StatusEffect.SLEEP) { this.setFrameRate(4); - // If the user is invulnerable, remove their invulnerability when they fall asleep + // If the user is semi-invulnerable when put asleep (such as due to Yawm), + // remove their invulnerability and cancel the upcoming move from the queue const invulnTag = [ BattlerTagType.UNDERGROUND, BattlerTagType.UNDERWATER, @@ -5651,8 +5680,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.status = new Status(effect, 0, sleepTurnsRemaining); } - - /** * Resets the status of a pokemon. * @param revive Whether revive should be cured; defaults to true. diff --git a/src/phases/obtain-status-effect-phase.ts b/src/phases/obtain-status-effect-phase.ts index 3e27eb001e7..be8d8c385b5 100644 --- a/src/phases/obtain-status-effect-phase.ts +++ b/src/phases/obtain-status-effect-phase.ts @@ -1,60 +1,65 @@ import { globalScene } from "#app/global-scene"; import type { BattlerIndex } from "#app/battle"; import { CommonBattleAnim, CommonAnim } from "#app/data/battle-anims"; -import { getStatusEffectObtainText, getStatusEffectOverlapText } from "#app/data/status-effect"; +import { getStatusEffectObtainText } from "#app/data/status-effect"; import { StatusEffect } from "#app/enums/status-effect"; import type Pokemon from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { PokemonPhase } from "./pokemon-phase"; import { SpeciesFormChangeStatusEffectTrigger } from "#app/data/pokemon-forms"; import { applyPostSetStatusAbAttrs, PostSetStatusAbAttr } from "#app/data/abilities/ability"; -import { isNullOrUndefined } from "#app/utils/common"; /** The phase where pokemon obtain status effects. */ export class ObtainStatusEffectPhase extends PokemonPhase { private statusEffect: StatusEffect; - private turnsRemaining?: number; + private sleepTurnsRemaining?: number; private sourceText?: string | null; private sourcePokemon?: Pokemon | null; + /** + * Create a new ObtainStatusEffectPhase. + * @param battlerIndex - The {@linkcode BattlerIndex} of the Pokemon obtaining the status effect. + * @param statusEffect - The {@linkcode StatusEffect} being applied. + * @param sleepTurnsRemaining - The number of turns to set {@linkcode StatusEffect.SLEEP} for; + * defaults to a random number between 2 and 4 and is unused for non-Sleep statuses. + * @param sourceText + * @param sourcePokemon + */ constructor( battlerIndex: BattlerIndex, statusEffect: StatusEffect, - turnsRemaining?: number, + sleepTurnsRemaining?: number, sourceText?: string | null, sourcePokemon?: Pokemon | null, ) { super(battlerIndex); this.statusEffect = statusEffect; - this.turnsRemaining = turnsRemaining; + this.sleepTurnsRemaining = sleepTurnsRemaining; this.sourceText = sourceText; this.sourcePokemon = sourcePokemon; } start() { const pokemon = this.getPokemon(); - if (pokemon.status?.effect === this.statusEffect) { - globalScene.queueMessage(getStatusEffectOverlapText(this.statusEffect, getPokemonNameWithAffix(pokemon))); - this.end(); - return; - } - + // No need to override status as the calling `trySetStatus` call will do it for us + // TODO: Consider changing this if (!pokemon.canSetStatus(this.statusEffect, false, false, this.sourcePokemon)) { - // status application fails this.end(); return; } - pokemon.doSetStatus(this.statusEffect, this.turnsRemaining); - + pokemon.doSetStatus(this.statusEffect, this.sleepTurnsRemaining); pokemon.updateInfo(true); + new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect! - 1), pokemon).play(false, () => { globalScene.queueMessage( getStatusEffectObtainText(this.statusEffect, getPokemonNameWithAffix(pokemon), this.sourceText ?? undefined), ); - if (!isNullOrUndefined(this.statusEffect) && this.statusEffect !== StatusEffect.FAINT) { + if (this.statusEffect && this.statusEffect !== StatusEffect.FAINT) { globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeStatusEffectTrigger, true); + // If the status was applied from a move, ensure abilities are not ignored for follow-up triggers. + // (This is fine as this phase only runs after the MoveEffectPhase concludes and all effects have been applied.) globalScene.arena.setIgnoreAbilities(false); applyPostSetStatusAbAttrs(PostSetStatusAbAttr, pokemon, this.statusEffect, this.sourcePokemon); }