diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 0087e502ce3..ede114236d6 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -1402,6 +1402,12 @@ export class PostDefendMoveDisableAbAttr extends PostDefendAbAttr { export class PostStatStageChangeAbAttr extends AbAttr { private declare readonly _: never; + + override canApply(_params: Closed) { + return true; + } + + override apply(_params: Closed) {} } export interface PostStatStageChangeAbAttrParams extends AbAttrBaseParams { @@ -3415,9 +3421,12 @@ export interface PreStatStageChangeAbAttrParams extends AbAttrBaseParams { stat: BattleStat; /** The amount of stages to change by (negative if the stat is being decreased) */ stages: number; - /** The source of the stat stage drop */ - source: Pokemon; - // Note: `cancelled` already exists in `AbAttrBaseParams`, though we redefine it here to change its tsdoc + /** The source of the stat stage drop. May be omitted if the source of the stat drop is the user itself. + * + * @remarks + * Currently, only used by {@linkcode ReflectStatStageChangeAbAttr} in order to reflect the stat stage change + */ + source?: Pokemon; /** Holder that will be set to true if the stat stage change should be cancelled due to the ability */ cancelled: BooleanHolder; } @@ -3440,10 +3449,17 @@ export class ReflectStatStageChangeAbAttr extends PreStatStageChangeAbAttr { /** {@linkcode BattleStat} to reflect */ private reflectedStat?: BattleStat; + override canApply({ source, cancelled }: PreStatStageChangeAbAttrParams): boolean { + return !!source && !cancelled.value; + } + /** * Apply the {@linkcode ReflectStatStageChangeAbAttr} to an interaction */ override apply({ source, cancelled, stat, simulated, stages }: PreStatStageChangeAbAttrParams): void { + if (!source) { + return; + } this.reflectedStat = stat; if (!simulated) { globalScene.phaseManager.unshiftNew( diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index c8dde455ea4..f33cadd3f53 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -946,7 +946,7 @@ class StealthRockTag extends ArenaTrapTag { override activateTrap(pokemon: Pokemon, simulated: boolean): boolean { const cancelled = new BooleanHolder(false); - applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelled); + applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled }); if (cancelled.value) { return false; } @@ -1003,7 +1003,12 @@ class StickyWebTag extends ArenaTrapTag { override activateTrap(pokemon: Pokemon, simulated: boolean): boolean { if (pokemon.isGrounded()) { const cancelled = new BooleanHolder(false); - applyAbAttrs("ProtectStatAbAttr", pokemon, cancelled); + applyAbAttrs("ProtectStatAbAttr", { + pokemon, + cancelled, + stat: Stat.SPD, + stages: -1, + }); if (simulated) { return !cancelled.value; @@ -1416,7 +1421,9 @@ export class SuppressAbilitiesTag extends ArenaTag { for (const fieldPokemon of globalScene.getField(true)) { if (fieldPokemon && fieldPokemon.id !== pokemon.id) { - [true, false].forEach(passive => applyOnLoseAbAttrs(fieldPokemon, passive)); + // TODO: investigate whether we can just remove the foreach and call `applyAbAttrs` directly, providing + // the appropriate attributes (preLEaveField and IllusionBreak) + [true, false].forEach(passive => applyOnLoseAbAttrs({ pokemon: fieldPokemon, passive })); } } } diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 8405fd1dd4d..3351a908fe6 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -621,7 +621,7 @@ export class FlinchedTag extends BattlerTag { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), }), ); - applyAbAttrs("FlinchEffectAbAttr", pokemon, null); + applyAbAttrs("FlinchEffectAbAttr", { pokemon }); return true; } @@ -916,7 +916,7 @@ export class SeedTag extends BattlerTag { const source = pokemon.getOpponents().find(o => o.getBattlerIndex() === this.sourceIndex); if (source) { const cancelled = new BooleanHolder(false); - applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelled); + applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled }); if (!cancelled.value) { globalScene.phaseManager.unshiftNew( @@ -1006,7 +1006,7 @@ export class PowderTag extends BattlerTag { globalScene.phaseManager.unshiftNew("CommonAnimPhase", idx, idx, CommonAnim.POWDER); const cancelDamage = new BooleanHolder(false); - applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelDamage); + applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled: cancelDamage }); if (!cancelDamage.value) { pokemon.damageAndUpdate(Math.floor(pokemon.getMaxHp() / 4), { result: HitResult.INDIRECT }); } @@ -1056,7 +1056,7 @@ export class NightmareTag extends BattlerTag { phaseManager.unshiftNew("CommonAnimPhase", pokemon.getBattlerIndex(), undefined, CommonAnim.CURSE); // TODO: Update animation type const cancelled = new BooleanHolder(false); - applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelled); + applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled }); if (!cancelled.value) { pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 4), { result: HitResult.INDIRECT }); @@ -1409,7 +1409,7 @@ export abstract class DamagingTrapTag extends TrappedTag { phaseManager.unshiftNew("CommonAnimPhase", pokemon.getBattlerIndex(), undefined, this.commonAnim); const cancelled = new BooleanHolder(false); - applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelled); + applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled }); if (!cancelled.value) { pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8), { result: HitResult.INDIRECT }); @@ -1642,7 +1642,7 @@ export class ContactDamageProtectedTag extends ContactProtectedTag { */ override onContact(attacker: Pokemon, user: Pokemon): void { const cancelled = new BooleanHolder(false); - applyAbAttrs("BlockNonDirectDamageAbAttr", user, cancelled); + applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon: user, cancelled }); if (!cancelled.value) { attacker.damageAndUpdate(toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio)), { result: HitResult.INDIRECT, @@ -2243,7 +2243,7 @@ export class SaltCuredTag extends BattlerTag { ); const cancelled = new BooleanHolder(false); - applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelled); + applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled }); if (!cancelled.value) { const pokemonSteelOrWater = pokemon.isOfType(PokemonType.STEEL) || pokemon.isOfType(PokemonType.WATER); @@ -2297,7 +2297,7 @@ export class CursedTag extends BattlerTag { ); const cancelled = new BooleanHolder(false); - applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelled); + applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled }); if (!cancelled.value) { pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 4), { result: HitResult.INDIRECT }); @@ -2632,7 +2632,7 @@ export class GulpMissileTag extends BattlerTag { } const cancelled = new BooleanHolder(false); - applyAbAttrs("BlockNonDirectDamageAbAttr", attacker, cancelled); + applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon: attacker, cancelled }); if (!cancelled.value) { attacker.damageAndUpdate(Math.max(1, Math.floor(attacker.getMaxHp() / 4)), { result: HitResult.INDIRECT }); @@ -3021,14 +3021,7 @@ export class MysteryEncounterPostSummonTag extends BattlerTag { const ret = super.lapse(pokemon, lapseType); if (lapseType === BattlerTagLapseType.CUSTOM) { - const cancelled = new BooleanHolder(false); - applyAbAttrs("ProtectStatAbAttr", pokemon, cancelled); - applyAbAttrs("ConditionalUserFieldProtectStatAbAttr", pokemon, cancelled, false, pokemon); - if (!cancelled.value) { - if (pokemon.mysteryEncounterBattleEffects) { - pokemon.mysteryEncounterBattleEffects(pokemon); - } - } + pokemon.mysteryEncounterBattleEffects?.(pokemon); } return ret; diff --git a/src/phases/new-biome-encounter-phase.ts b/src/phases/new-biome-encounter-phase.ts index 5aad607764f..74476412401 100644 --- a/src/phases/new-biome-encounter-phase.ts +++ b/src/phases/new-biome-encounter-phase.ts @@ -14,7 +14,7 @@ export class NewBiomeEncounterPhase extends NextEncounterPhase { if (pokemon) { pokemon.resetBattleAndWaveData(); if (pokemon.isOnField()) { - applyAbAttrs("PostBiomeChangeAbAttr", pokemon, null); + applyAbAttrs("PostBiomeChangeAbAttr", { pokemon }); } } } diff --git a/src/phases/obtain-status-effect-phase.ts b/src/phases/obtain-status-effect-phase.ts index dc26d070029..78db8ae0a99 100644 --- a/src/phases/obtain-status-effect-phase.ts +++ b/src/phases/obtain-status-effect-phase.ts @@ -8,7 +8,7 @@ import type Pokemon from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { PokemonPhase } from "./pokemon-phase"; import { SpeciesFormChangeStatusEffectTrigger } from "#app/data/pokemon-forms/form-change-triggers"; -import { applyPostSetStatusAbAttrs } from "#app/data/abilities/apply-ab-attrs"; +import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { isNullOrUndefined } from "#app/utils/common"; export class ObtainStatusEffectPhase extends PokemonPhase { @@ -53,7 +53,11 @@ export class ObtainStatusEffectPhase extends PokemonPhase { globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeStatusEffectTrigger, true); // If mold breaker etc was used to set this status, it shouldn't apply to abilities activated afterwards globalScene.arena.setIgnoreAbilities(false); - applyPostSetStatusAbAttrs("PostSetStatusAbAttr", pokemon, this.statusEffect, this.sourcePokemon); + applyAbAttrs("PostSetStatusAbAttr", { + pokemon, + effect: this.statusEffect, + sourcePokemon: this.sourcePokemon ?? undefined, + }); } this.end(); }); diff --git a/src/phases/post-summon-activate-ability-phase.ts b/src/phases/post-summon-activate-ability-phase.ts index ba6c80d4ee0..b1079a9b3e5 100644 --- a/src/phases/post-summon-activate-ability-phase.ts +++ b/src/phases/post-summon-activate-ability-phase.ts @@ -1,4 +1,4 @@ -import { applyPostSummonAbAttrs } from "#app/data/abilities/apply-ab-attrs"; +import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { PostSummonPhase } from "#app/phases/post-summon-phase"; import type { BattlerIndex } from "#enums/battler-index"; @@ -16,7 +16,8 @@ export class PostSummonActivateAbilityPhase extends PostSummonPhase { } start() { - applyPostSummonAbAttrs("PostSummonAbAttr", this.getPokemon(), this.passive, false); + // TODO: Check with Dean on whether or not passive must be provided to `this.passive` + applyAbAttrs("PostSummonAbAttr", { pokemon: this.getPokemon(), passive: this.passive }); this.end(); } diff --git a/src/phases/post-summon-phase.ts b/src/phases/post-summon-phase.ts index 26fffd1b024..7f22148fdcf 100644 --- a/src/phases/post-summon-phase.ts +++ b/src/phases/post-summon-phase.ts @@ -28,7 +28,7 @@ export class PostSummonPhase extends PokemonPhase { const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); for (const p of field) { - applyAbAttrs("CommanderAbAttr", p, null, false); + applyAbAttrs("CommanderAbAttr", { pokemon: p }); } this.end(); diff --git a/src/phases/post-turn-status-effect-phase.ts b/src/phases/post-turn-status-effect-phase.ts index e0a3bb5c00b..fd7dd6ed419 100644 --- a/src/phases/post-turn-status-effect-phase.ts +++ b/src/phases/post-turn-status-effect-phase.ts @@ -1,6 +1,6 @@ import { globalScene } from "#app/global-scene"; import type { BattlerIndex } from "#enums/battler-index"; -import { applyAbAttrs, applyPostDamageAbAttrs } from "#app/data/abilities/apply-ab-attrs"; +import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { CommonBattleAnim } from "#app/data/battle-anims"; import { CommonAnim } from "#enums/move-anims-common"; import { getStatusEffectActivationText } from "#app/data/status-effect"; @@ -22,8 +22,8 @@ export class PostTurnStatusEffectPhase extends PokemonPhase { if (pokemon?.isActive(true) && pokemon.status && pokemon.status.isPostTurn() && !pokemon.switchOutStatus) { pokemon.status.incrementTurn(); const cancelled = new BooleanHolder(false); - applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelled); - applyAbAttrs("BlockStatusDamageAbAttr", pokemon, cancelled); + applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled }); + applyAbAttrs("BlockStatusDamageAbAttr", { pokemon, cancelled }); if (!cancelled.value) { globalScene.phaseManager.queueMessage( @@ -39,14 +39,14 @@ export class PostTurnStatusEffectPhase extends PokemonPhase { break; case StatusEffect.BURN: damage.value = Math.max(pokemon.getMaxHp() >> 4, 1); - applyAbAttrs("ReduceBurnDamageAbAttr", pokemon, null, false, damage); + applyAbAttrs("ReduceBurnDamageAbAttr", { pokemon, burnDamage: damage }); break; } if (damage.value) { // Set preventEndure flag to avoid pokemon surviving thanks to focus band, sturdy, endure ... globalScene.damageNumberHandler.add(this.getPokemon(), pokemon.damage(damage.value, false, true)); pokemon.updateInfo(); - applyPostDamageAbAttrs("PostDamageAbAttr", pokemon, damage.value, pokemon.hasPassive(), false, []); + applyAbAttrs("PostDamageAbAttr", { pokemon, damage: damage.value }); } new CommonBattleAnim(CommonAnim.POISON + (pokemon.status.effect - 1), pokemon).play(false, () => this.end()); } else { diff --git a/src/phases/quiet-form-change-phase.ts b/src/phases/quiet-form-change-phase.ts index 41b691844bf..9c4a0638b54 100644 --- a/src/phases/quiet-form-change-phase.ts +++ b/src/phases/quiet-form-change-phase.ts @@ -181,9 +181,10 @@ export class QuietFormChangePhase extends BattlePhase { } } if (this.formChange.trigger instanceof SpeciesFormChangeTeraTrigger) { - applyAbAttrs("PostTeraFormChangeStatChangeAbAttr", this.pokemon, null); - applyAbAttrs("ClearWeatherAbAttr", this.pokemon, null); - applyAbAttrs("ClearTerrainAbAttr", this.pokemon, null); + const params = { pokemon: this.pokemon }; + applyAbAttrs("PostTeraFormChangeStatChangeAbAttr", params); + applyAbAttrs("ClearWeatherAbAttr", params); + applyAbAttrs("ClearTerrainAbAttr", params); } super.end(); diff --git a/src/phases/stat-stage-change-phase.ts b/src/phases/stat-stage-change-phase.ts index e73f72f7a63..77fb7b38600 100644 --- a/src/phases/stat-stage-change-phase.ts +++ b/src/phases/stat-stage-change-phase.ts @@ -1,10 +1,6 @@ import { globalScene } from "#app/global-scene"; import type { BattlerIndex } from "#enums/battler-index"; -import { - applyAbAttrs, - applyPostStatStageChangeAbAttrs, - applyPreStatStageChangeAbAttrs, -} from "#app/data/abilities/apply-ab-attrs"; +import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { MistTag } from "#app/data/arena-tag"; import { ArenaTagSide } from "#enums/arena-tag-side"; import type { ArenaTag } from "#app/data/arena-tag"; @@ -18,6 +14,10 @@ import { PokemonPhase } from "./pokemon-phase"; import { Stat, type BattleStat, getStatKey, getStatStageChangeDescriptionKey } from "#enums/stat"; import { OctolockTag } from "#app/data/battler-tags"; import { ArenaTagType } from "#app/enums/arena-tag-type"; +import type { + ConditionalUserFieldProtectStatAbAttrParams, + PreStatStageChangeAbAttrParams, +} from "#app/@types/ability-types"; export type StatStageChangeCallback = ( target: Pokemon | null, @@ -126,7 +126,7 @@ export class StatStageChangePhase extends PokemonPhase { const stages = new NumberHolder(this.stages); if (!this.ignoreAbilities) { - applyAbAttrs("StatStageChangeMultiplierAbAttr", pokemon, null, false, stages); + applyAbAttrs("StatStageChangeMultiplierAbAttr", { pokemon, numStages: stages }); } let simulate = false; @@ -146,42 +146,38 @@ export class StatStageChangePhase extends PokemonPhase { } if (!cancelled.value && !this.selfTarget && stages.value < 0) { - applyPreStatStageChangeAbAttrs("ProtectStatAbAttr", pokemon, stat, cancelled, simulate); - applyPreStatStageChangeAbAttrs( - "ConditionalUserFieldProtectStatAbAttr", + const abAttrParams: PreStatStageChangeAbAttrParams & ConditionalUserFieldProtectStatAbAttrParams = { pokemon, stat, cancelled, - simulate, - pokemon, - ); + simulated: simulate, + target: pokemon, + stages: this.stages, + }; + applyAbAttrs("ProtectStatAbAttr", abAttrParams); + applyAbAttrs("ConditionalUserFieldProtectStatAbAttr", abAttrParams); + // TODO: Consider skipping this call if `cancelled` is false. const ally = pokemon.getAlly(); if (!isNullOrUndefined(ally)) { - applyPreStatStageChangeAbAttrs( - "ConditionalUserFieldProtectStatAbAttr", - ally, - stat, - cancelled, - simulate, - pokemon, - ); + applyAbAttrs("ConditionalUserFieldProtectStatAbAttr", { ...abAttrParams, pokemon: ally }); } /** Potential stat reflection due to Mirror Armor, does not apply to Octolock end of turn effect */ if ( opponentPokemon !== undefined && + // TODO: investigate whether this is stoping mirror armor from applying to non-octolock + // reasons for stat drops if the user has the Octolock tag !pokemon.findTag(t => t instanceof OctolockTag) && !this.comingFromMirrorArmorUser ) { - applyPreStatStageChangeAbAttrs( - "ReflectStatStageChangeAbAttr", + applyAbAttrs("ReflectStatStageChangeAbAttr", { pokemon, stat, cancelled, - simulate, - opponentPokemon, - this.stages, - ); + simulated: simulate, + source: opponentPokemon, + stages: this.stages, + }); } } @@ -222,17 +218,16 @@ export class StatStageChangePhase extends PokemonPhase { if (stages.value > 0 && this.canBeCopied) { for (const opponent of pokemon.getOpponents()) { - applyAbAttrs("StatStageChangeCopyAbAttr", opponent, null, false, this.stats, stages.value); + applyAbAttrs("StatStageChangeCopyAbAttr", { pokemon: opponent, stats: this.stats, numStages: stages.value }); } } - applyPostStatStageChangeAbAttrs( - "PostStatStageChangeAbAttr", + applyAbAttrs("PostStatStageChangeAbAttr", { pokemon, - filteredStats, - this.stages, - this.selfTarget, - ); + stats: filteredStats, + stages: this.stages, + selfTarget: this.selfTarget, + }); // Look for any other stat change phases; if this is the last one, do White Herb check const existingPhase = globalScene.phaseManager.findPhase(