From 31835e6d534b0a399df01222633f3d5ba8eaa81d Mon Sep 17 00:00:00 2001 From: Dean <69436131+emdeann@users.noreply.github.com> Date: Mon, 7 Apr 2025 16:32:10 -0700 Subject: [PATCH] [Bug] Fix #4972 Status-Prevention Abilities do not Cure Status (#5406) * Add PostSummonHealAbAttr and give it to appropriate abilities * Add attr to insomnia * Remove attr from leaf guard (it does not activate on gain with sun up) * Add tests and remove attr from shields down * Add PostSummonRemoveBattlerTag and give it to oblivious and own tempo * Add tests for oblivious and own tempo * Fix oblivious test sometimes failing * Remove Comatose changes as it doesn't reapply * Remove unused tagRemoved field * Fix tests checking status instead of tag * Fix attr comments * Add PostSetStatusHealStatusAbAttr * Add ResetStatusPhase * Modify pokemon.resetStatus to use ResetStatusPhase * Move post status effects to ObtainStatusEffectPhase * Ensure status overriding (ie rest) works properly * Add PostApplyBattlerTagRemoveTagAbAttr for own tempo and oblivious * Guard removeTag call in PostApplyBattlerTagRemoveTagAbAttr * Commenting * Handle Mold Breaker case in MoveEndPhase * Remove PostSummonHealStatusAbAttr from purifying salt * Fix not passing overrideStatus to canSetStatus * Apply suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Add isNullOrUndefined import * Add canApply to new attrs * Add followup argument back * Remove guard around new MoveEndPhase --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/data/ability.ts | 78 ++++++++++++++++++++++++ src/data/moves/move.ts | 12 ++-- src/field/pokemon.ts | 38 +++--------- src/phases/move-end-phase.ts | 17 +++++- src/phases/move-phase.ts | 10 ++- src/phases/obtain-status-effect-phase.ts | 9 +++ src/phases/reset-status-phase.ts | 44 +++++++++++++ test/abilities/immunity.test.ts | 51 ++++++++++++++++ test/abilities/insomnia.test.ts | 51 ++++++++++++++++ test/abilities/limber.test.ts | 51 ++++++++++++++++ test/abilities/magma_armor.test.ts | 51 ++++++++++++++++ test/abilities/oblivious.test.ts | 69 +++++++++++++++++++++ test/abilities/own_tempo.test.ts | 51 ++++++++++++++++ test/abilities/thermal_exchange.test.ts | 51 ++++++++++++++++ test/abilities/vital_spirit.test.ts | 51 ++++++++++++++++ test/abilities/water_bubble.test.ts | 51 ++++++++++++++++ test/abilities/water_veil.test.ts | 51 ++++++++++++++++ 17 files changed, 693 insertions(+), 43 deletions(-) create mode 100644 src/phases/reset-status-phase.ts create mode 100644 test/abilities/immunity.test.ts create mode 100644 test/abilities/insomnia.test.ts create mode 100644 test/abilities/limber.test.ts create mode 100644 test/abilities/magma_armor.test.ts create mode 100644 test/abilities/oblivious.test.ts create mode 100644 test/abilities/own_tempo.test.ts create mode 100644 test/abilities/thermal_exchange.test.ts create mode 100644 test/abilities/vital_spirit.test.ts create mode 100644 test/abilities/water_bubble.test.ts create mode 100644 test/abilities/water_veil.test.ts diff --git a/src/data/ability.ts b/src/data/ability.ts index a7107ce2e9d..f8c9b4cb8fe 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -2295,6 +2295,11 @@ export class PostSummonAbAttr extends AbAttr { applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): void {} } +/** + * Base class for ability attributes which remove an effect on summon + */ +export class PostSummonRemoveEffectAbAttr extends PostSummonAbAttr {} + /** * Removes specified arena tags when a Pokemon is summoned. */ @@ -2405,6 +2410,31 @@ export class PostSummonAddBattlerTagAbAttr extends PostSummonAbAttr { } } +/** + * Removes Specific battler tags when a Pokemon is summoned + * + * This should realistically only ever activate on gain rather than on summon + */ +export class PostSummonRemoveBattlerTagAbAttr extends PostSummonRemoveEffectAbAttr { + private immuneTags: BattlerTagType[]; + + /** + * @param immuneTags - The {@linkcode BattlerTagType | battler tags} the Pokémon is immune to. + */ + constructor(...immuneTags: BattlerTagType[]) { + super(); + this.immuneTags = immuneTags; + } + + public override canApplyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean { + return this.immuneTags.some(tagType => !!pokemon.getTag(tagType)); + } + + public override applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): void { + this.immuneTags.forEach(tagType => pokemon.removeTag(tagType)); + } +} + export class PostSummonStatStageChangeAbAttr extends PostSummonAbAttr { private stats: BattleStat[]; private stages: number; @@ -2592,6 +2622,43 @@ export class PostSummonTerrainChangeAbAttr extends PostSummonAbAttr { } } +/** + * Heals a status effect if the Pokemon is afflicted with it upon switch in (or gain) + */ +export class PostSummonHealStatusAbAttr extends PostSummonRemoveEffectAbAttr { + private immuneEffects: StatusEffect[]; + private statusHealed: StatusEffect; + + /** + * @param immuneEffects - The {@linkcode StatusEffect}s the Pokémon is immune to. + */ + constructor(...immuneEffects: StatusEffect[]) { + super(); + this.immuneEffects = immuneEffects; + } + + public override canApplyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean { + const status = pokemon.status?.effect; + return !Utils.isNullOrUndefined(status) && (this.immuneEffects.length < 1 || this.immuneEffects.includes(status)) + } + + public override applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): void { + const status = pokemon.status?.effect; + if (!Utils.isNullOrUndefined(status)) { + this.statusHealed = status; + pokemon.resetStatus(false); + pokemon.updateInfo(); + } + } + + public override getTriggerMessage(_pokemon: Pokemon, _abilityName: string, ..._args: any[]): string | null { + if (this.statusHealed) { + return getStatusEffectHealText(this.statusHealed, getPokemonNameWithAffix(_pokemon)); + } + return null; + } +} + export class PostSummonFormChangeAbAttr extends PostSummonAbAttr { private formFunc: (p: Pokemon) => number; @@ -6291,6 +6358,7 @@ export function initAbilities() { .ignorable(), new Ability(Abilities.LIMBER, 3) .attr(StatusEffectImmunityAbAttr, StatusEffect.PARALYSIS) + .attr(PostSummonHealStatusAbAttr, StatusEffect.PARALYSIS) .ignorable(), new Ability(Abilities.SAND_VEIL, 3) .attr(StatMultiplierAbAttr, Stat.EVA, 1.2) @@ -6308,6 +6376,7 @@ export function initAbilities() { .ignorable(), new Ability(Abilities.OBLIVIOUS, 3) .attr(BattlerTagImmunityAbAttr, [ BattlerTagType.INFATUATED, BattlerTagType.TAUNT ]) + .attr(PostSummonRemoveBattlerTagAbAttr, BattlerTagType.INFATUATED, BattlerTagType.TAUNT) .attr(IntimidateImmunityAbAttr) .ignorable(), new Ability(Abilities.CLOUD_NINE, 3) @@ -6320,6 +6389,7 @@ export function initAbilities() { .attr(StatMultiplierAbAttr, Stat.ACC, 1.3), new Ability(Abilities.INSOMNIA, 3) .attr(StatusEffectImmunityAbAttr, StatusEffect.SLEEP) + .attr(PostSummonHealStatusAbAttr, StatusEffect.SLEEP) .attr(BattlerTagImmunityAbAttr, BattlerTagType.DROWSY) .ignorable(), new Ability(Abilities.COLOR_CHANGE, 3) @@ -6327,6 +6397,7 @@ export function initAbilities() { .condition(getSheerForceHitDisableAbCondition()), new Ability(Abilities.IMMUNITY, 3) .attr(StatusEffectImmunityAbAttr, StatusEffect.POISON, StatusEffect.TOXIC) + .attr(PostSummonHealStatusAbAttr, StatusEffect.POISON, StatusEffect.TOXIC) .ignorable(), new Ability(Abilities.FLASH_FIRE, 3) .attr(TypeImmunityAddBattlerTagAbAttr, PokemonType.FIRE, BattlerTagType.FIRE_BOOST, 1) @@ -6336,6 +6407,7 @@ export function initAbilities() { .ignorable(), new Ability(Abilities.OWN_TEMPO, 3) .attr(BattlerTagImmunityAbAttr, BattlerTagType.CONFUSED) + .attr(PostSummonRemoveBattlerTagAbAttr, BattlerTagType.CONFUSED) .attr(IntimidateImmunityAbAttr) .ignorable(), new Ability(Abilities.SUCTION_CUPS, 3) @@ -6401,9 +6473,11 @@ export function initAbilities() { .ignorable(), new Ability(Abilities.MAGMA_ARMOR, 3) .attr(StatusEffectImmunityAbAttr, StatusEffect.FREEZE) + .attr(PostSummonHealStatusAbAttr, StatusEffect.FREEZE) .ignorable(), new Ability(Abilities.WATER_VEIL, 3) .attr(StatusEffectImmunityAbAttr, StatusEffect.BURN) + .attr(PostSummonHealStatusAbAttr, StatusEffect.BURN) .ignorable(), new Ability(Abilities.MAGNET_PULL, 3) .attr(ArenaTrapAbAttr, (user, target) => { @@ -6497,6 +6571,7 @@ export function initAbilities() { .attr(DoubleBattleChanceAbAttr), new Ability(Abilities.VITAL_SPIRIT, 3) .attr(StatusEffectImmunityAbAttr, StatusEffect.SLEEP) + .attr(PostSummonHealStatusAbAttr, StatusEffect.SLEEP) .attr(BattlerTagImmunityAbAttr, BattlerTagType.DROWSY) .ignorable(), new Ability(Abilities.WHITE_SMOKE, 3) @@ -6835,6 +6910,7 @@ export function initAbilities() { .attr(MoveTypeChangeAbAttr, PokemonType.ICE, 1.2, (user, target, move) => move.type === PokemonType.NORMAL && !move.hasAttr(VariableMoveTypeAttr)), new Ability(Abilities.SWEET_VEIL, 6) .attr(UserFieldStatusEffectImmunityAbAttr, StatusEffect.SLEEP) + .attr(PostSummonUserFieldRemoveStatusEffectAbAttr, StatusEffect.SLEEP) .attr(UserFieldBattlerTagImmunityAbAttr, BattlerTagType.DROWSY) .ignorable() .partial(), // Mold Breaker ally should not be affected by Sweet Veil @@ -6919,6 +6995,7 @@ export function initAbilities() { .attr(ReceivedTypeDamageMultiplierAbAttr, PokemonType.FIRE, 0.5) .attr(MoveTypePowerBoostAbAttr, PokemonType.WATER, 2) .attr(StatusEffectImmunityAbAttr, StatusEffect.BURN) + .attr(PostSummonHealStatusAbAttr, StatusEffect.BURN) .ignorable(), new Ability(Abilities.STEELWORKER, 7) .attr(MoveTypePowerBoostAbAttr, PokemonType.STEEL), @@ -7197,6 +7274,7 @@ export function initAbilities() { new Ability(Abilities.THERMAL_EXCHANGE, 9) .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => user.getMoveType(move) === PokemonType.FIRE && move.category !== MoveCategory.STATUS, Stat.ATK, 1) .attr(StatusEffectImmunityAbAttr, StatusEffect.BURN) + .attr(PostSummonHealStatusAbAttr, StatusEffect.BURN) .ignorable(), new Ability(Abilities.ANGER_SHELL, 9) .attr(PostDefendHpGatedStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, 0.5, [ Stat.ATK, Stat.SPATK, Stat.SPD ], 1) diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 7a820d984d0..1af4be4fdf0 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -2443,12 +2443,8 @@ export class StatusEffectAttr extends MoveEffectAttr { const statusCheck = moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance; if (statusCheck) { const pokemon = this.selfTarget ? user : target; - if (pokemon.status) { - if (this.overrideStatus) { - pokemon.resetStatus(); - } else { - return false; - } + if (pokemon.status && !this.overrideStatus) { + return false; } if (user !== target && target.isSafeguarded(user)) { @@ -2457,8 +2453,8 @@ export class StatusEffectAttr extends MoveEffectAttr { } return false; } - if ((!pokemon.status || (pokemon.status.effect === this.effect && moveChance < 0)) - && pokemon.trySetStatus(this.effect, true, user, this.turnsRemaining)) { + if (((!pokemon.status || this.overrideStatus) || (pokemon.status.effect === this.effect && moveChance < 0)) + && pokemon.trySetStatus(this.effect, true, user, this.turnsRemaining, null, this.overrideStatus)) { applyPostAttackAbAttrs(ConfusionOnStatusEffectAbAttr, user, target, move, null, false, this.effect); return true; } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index f3e758e4efd..7d856696188 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -264,6 +264,7 @@ import { StatusEffect } from "#enums/status-effect"; import { doShinySparkleAnim } from "#app/field/anims"; import { MoveFlags } from "#enums/MoveFlags"; import { timedEventManager } from "#app/global-event-manager"; +import { ResetStatusPhase } from "#app/phases/reset-status-phase"; export enum LearnMoveSituation { MISC, @@ -4809,7 +4810,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (newTag.canAdd(this)) { this.summonData.tags.push(newTag); newTag.onAdd(this); - return true; } @@ -5480,8 +5480,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { sourcePokemon: Pokemon | null = null, turnsRemaining = 0, sourceText: string | null = null, + overrideStatus?: boolean ): boolean { - if (!this.canSetStatus(effect, asPhase, false, sourcePokemon)) { + if (!this.canSetStatus(effect, asPhase, overrideStatus, sourcePokemon)) { return false; } if (this.isFainted() && effect !== StatusEffect.FAINT) { @@ -5497,6 +5498,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } if (asPhase) { + if (overrideStatus) { + this.resetStatus(false); + } globalScene.unshiftPhase( new ObtainStatusEffectPhase( this.getBattlerIndex(), @@ -5536,20 +5540,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { effect = effect!; // If `effect` is undefined then `trySetStatus()` will have already returned early via the `canSetStatus()` call this.status = new Status(effect, 0, sleepTurnsRemaining?.value); - if (effect !== StatusEffect.FAINT) { - globalScene.triggerPokemonFormChange( - this, - SpeciesFormChangeStatusEffectTrigger, - true, - ); - applyPostSetStatusAbAttrs( - PostSetStatusAbAttr, - this, - effect, - sourcePokemon, - ); - } - return true; } @@ -5564,21 +5554,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (!revive && lastStatus === StatusEffect.FAINT) { return; } - this.status = null; - if (lastStatus === StatusEffect.SLEEP) { - this.setFrameRate(10); - if (this.getTag(BattlerTagType.NIGHTMARE)) { - this.lapseTag(BattlerTagType.NIGHTMARE); - } - } - if (confusion) { - if (this.getTag(BattlerTagType.CONFUSED)) { - this.lapseTag(BattlerTagType.CONFUSED); - } - } - if (reloadAssets) { - this.loadAssets(false).then(() => this.playAnim()); - } + globalScene.unshiftPhase(new ResetStatusPhase(this, confusion, reloadAssets)); } /** diff --git a/src/phases/move-end-phase.ts b/src/phases/move-end-phase.ts index 53856956401..176abee5e98 100644 --- a/src/phases/move-end-phase.ts +++ b/src/phases/move-end-phase.ts @@ -2,11 +2,18 @@ import { globalScene } from "#app/global-scene"; import { BattlerTagLapseType } from "#app/data/battler-tags"; import { PokemonPhase } from "./pokemon-phase"; import type { BattlerIndex } from "#app/battle"; +import { applyPostSummonAbAttrs, PostSummonRemoveEffectAbAttr } from "#app/data/ability"; +import type Pokemon from "#app/field/pokemon"; export class MoveEndPhase extends PokemonPhase { private wasFollowUp: boolean; - constructor(battlerIndex: BattlerIndex, wasFollowUp = false) { + + /** Targets from the preceding MovePhase */ + private targets: Pokemon[]; + constructor(battlerIndex: BattlerIndex, targets: Pokemon[], wasFollowUp = false) { super(battlerIndex); + + this.targets = targets; this.wasFollowUp = wasFollowUp; } @@ -17,9 +24,15 @@ export class MoveEndPhase extends PokemonPhase { if (!this.wasFollowUp && pokemon?.isActive(true)) { pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE); } - globalScene.arena.setIgnoreAbilities(false); + // Remove effects which were set on a Pokemon which removes them on summon (i.e. via Mold Breaker) + for (const target of this.targets) { + if (target) { + applyPostSummonAbAttrs(PostSummonRemoveEffectAbAttr, target); + } + } + this.end(); } } diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 5232dfee8ba..478229dcae8 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -169,7 +169,11 @@ export class MovePhase extends BattlePhase { // Check move to see if arena.ignoreAbilities should be true. if (!this.followUp || this.reflected) { - if (this.move.getMove().doesFlagEffectApply({ flag: MoveFlags.IGNORE_ABILITIES, user: this.pokemon, isFollowUp: this.followUp })) { + if ( + this.move + .getMove() + .doesFlagEffectApply({ flag: MoveFlags.IGNORE_ABILITIES, user: this.pokemon, isFollowUp: this.followUp }) + ) { globalScene.arena.setIgnoreAbilities(true, this.pokemon.getBattlerIndex()); } } @@ -473,7 +477,9 @@ export class MovePhase extends BattlePhase { * Queues a {@linkcode MoveEndPhase} and then ends the phase */ public end(): void { - globalScene.unshiftPhase(new MoveEndPhase(this.pokemon.getBattlerIndex(), this.followUp)); + globalScene.unshiftPhase( + new MoveEndPhase(this.pokemon.getBattlerIndex(), this.getActiveTargetPokemon(), this.followUp), + ); super.end(); } diff --git a/src/phases/obtain-status-effect-phase.ts b/src/phases/obtain-status-effect-phase.ts index a0c0c14e93f..cba9399b996 100644 --- a/src/phases/obtain-status-effect-phase.ts +++ b/src/phases/obtain-status-effect-phase.ts @@ -6,6 +6,9 @@ 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/ability"; +import { isNullOrUndefined } from "#app/utils"; export class ObtainStatusEffectPhase extends PokemonPhase { private statusEffect?: StatusEffect; @@ -44,6 +47,12 @@ export class ObtainStatusEffectPhase extends PokemonPhase { this.sourceText ?? undefined, ), ); + if (!isNullOrUndefined(this.statusEffect) && this.statusEffect !== StatusEffect.FAINT) { + 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); + } this.end(); }); return; diff --git a/src/phases/reset-status-phase.ts b/src/phases/reset-status-phase.ts new file mode 100644 index 00000000000..0ba3559d9b7 --- /dev/null +++ b/src/phases/reset-status-phase.ts @@ -0,0 +1,44 @@ +import type Pokemon from "#app/field/pokemon"; +import { BattlePhase } from "#app/phases/battle-phase"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { StatusEffect } from "#enums/status-effect"; + +/** + * Phase which handles resetting a Pokemon's status to none + * + * This is necessary to perform in a phase primarly to ensure that the status icon disappears at the correct time in the battle + */ +export class ResetStatusPhase extends BattlePhase { + private readonly pokemon: Pokemon; + private readonly affectConfusion: boolean; + private readonly reloadAssets: boolean; + + constructor(pokemon: Pokemon, affectConfusion: boolean, reloadAssets: boolean) { + super(); + + this.pokemon = pokemon; + this.affectConfusion = affectConfusion; + this.reloadAssets = reloadAssets; + } + + public override start() { + const lastStatus = this.pokemon.status?.effect; + this.pokemon.status = null; + if (lastStatus === StatusEffect.SLEEP) { + this.pokemon.setFrameRate(10); + if (this.pokemon.getTag(BattlerTagType.NIGHTMARE)) { + this.pokemon.lapseTag(BattlerTagType.NIGHTMARE); + } + } + if (this.affectConfusion) { + if (this.pokemon.getTag(BattlerTagType.CONFUSED)) { + this.pokemon.lapseTag(BattlerTagType.CONFUSED); + } + } + if (this.reloadAssets) { + this.pokemon.loadAssets(false).then(() => this.pokemon.playAnim()); + } + this.pokemon.updateInfo(true); + this.end(); + } +} diff --git a/test/abilities/immunity.test.ts b/test/abilities/immunity.test.ts new file mode 100644 index 00000000000..51e9598720b --- /dev/null +++ b/test/abilities/immunity.test.ts @@ -0,0 +1,51 @@ +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; +import GameManager from "#test/testUtils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Abilities - Immunity", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([ Moves.SPLASH ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should remove poison when gained", async () => { + game.override.ability(Abilities.IMMUNITY) + .enemyAbility(Abilities.BALL_FETCH) + .moveset(Moves.SKILL_SWAP) + .enemyMoveset(Moves.SPLASH), + + await game.classicMode.startBattle([ Species.FEEBAS ]); + const enemy = game.scene.getEnemyPokemon(); + enemy?.trySetStatus(StatusEffect.POISON); + expect(enemy?.status?.effect).toBe(StatusEffect.POISON); + + game.move.select(Moves.SKILL_SWAP); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy?.status).toBeNull(); + }); +}); diff --git a/test/abilities/insomnia.test.ts b/test/abilities/insomnia.test.ts new file mode 100644 index 00000000000..91fdc3fc668 --- /dev/null +++ b/test/abilities/insomnia.test.ts @@ -0,0 +1,51 @@ +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; +import GameManager from "#test/testUtils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Abilities - Insomnia", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([ Moves.SPLASH ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should remove sleep when gained", async () => { + game.override.ability(Abilities.INSOMNIA) + .enemyAbility(Abilities.BALL_FETCH) + .moveset(Moves.SKILL_SWAP) + .enemyMoveset(Moves.SPLASH), + + await game.classicMode.startBattle([ Species.FEEBAS ]); + const enemy = game.scene.getEnemyPokemon(); + enemy?.trySetStatus(StatusEffect.SLEEP); + expect(enemy?.status?.effect).toBe(StatusEffect.SLEEP); + + game.move.select(Moves.SKILL_SWAP); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy?.status).toBeNull(); + }); +}); diff --git a/test/abilities/limber.test.ts b/test/abilities/limber.test.ts new file mode 100644 index 00000000000..2b167cc155f --- /dev/null +++ b/test/abilities/limber.test.ts @@ -0,0 +1,51 @@ +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; +import GameManager from "#test/testUtils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Abilities - Limber", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([ Moves.SPLASH ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should remove paralysis when gained", async () => { + game.override.ability(Abilities.LIMBER) + .enemyAbility(Abilities.BALL_FETCH) + .moveset(Moves.SKILL_SWAP) + .enemyMoveset(Moves.SPLASH), + + await game.classicMode.startBattle([ Species.FEEBAS ]); + const enemy = game.scene.getEnemyPokemon(); + enemy?.trySetStatus(StatusEffect.PARALYSIS); + expect(enemy?.status?.effect).toBe(StatusEffect.PARALYSIS); + + game.move.select(Moves.SKILL_SWAP); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy?.status).toBeNull(); + }); +}); diff --git a/test/abilities/magma_armor.test.ts b/test/abilities/magma_armor.test.ts new file mode 100644 index 00000000000..b1d62f948d2 --- /dev/null +++ b/test/abilities/magma_armor.test.ts @@ -0,0 +1,51 @@ +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; +import GameManager from "#test/testUtils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Abilities - Magma Armor", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([ Moves.SPLASH ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should remove freeze when gained", async () => { + game.override.ability(Abilities.MAGMA_ARMOR) + .enemyAbility(Abilities.BALL_FETCH) + .moveset(Moves.SKILL_SWAP) + .enemyMoveset(Moves.SPLASH), + + await game.classicMode.startBattle([ Species.FEEBAS ]); + const enemy = game.scene.getEnemyPokemon(); + enemy?.trySetStatus(StatusEffect.FREEZE); + expect(enemy?.status?.effect).toBe(StatusEffect.FREEZE); + + game.move.select(Moves.SKILL_SWAP); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy?.status).toBeNull(); + }); +}); diff --git a/test/abilities/oblivious.test.ts b/test/abilities/oblivious.test.ts new file mode 100644 index 00000000000..d5089ef6a72 --- /dev/null +++ b/test/abilities/oblivious.test.ts @@ -0,0 +1,69 @@ +import { Abilities } from "#enums/abilities"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/testUtils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Abilities - Oblivious", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([ Moves.SPLASH ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should remove taunt when gained", async () => { + game.override.ability(Abilities.OBLIVIOUS) + .enemyAbility(Abilities.BALL_FETCH) + .moveset(Moves.SKILL_SWAP) + .enemyMoveset(Moves.SPLASH), + + await game.classicMode.startBattle([ Species.FEEBAS ]); + const enemy = game.scene.getEnemyPokemon(); + enemy?.addTag(BattlerTagType.TAUNT); + expect(enemy?.getTag(BattlerTagType.TAUNT)).toBeTruthy(); + + game.move.select(Moves.SKILL_SWAP); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy?.getTag(BattlerTagType.TAUNT)).toBeFalsy(); + }); + + it("should remove infatuation when gained", async () => { + game.override.ability(Abilities.OBLIVIOUS) + .enemyAbility(Abilities.BALL_FETCH) + .moveset(Moves.SKILL_SWAP) + .enemyMoveset(Moves.SPLASH), + + await game.classicMode.startBattle([ Species.FEEBAS ]); + const enemy = game.scene.getEnemyPokemon(); + vi.spyOn(enemy!, "isOppositeGender").mockReturnValue(true); + enemy?.addTag(BattlerTagType.INFATUATED, 5, Moves.JUDGMENT, game.scene.getPlayerPokemon()?.id); // sourceID needs to be defined + expect(enemy?.getTag(BattlerTagType.INFATUATED)).toBeTruthy(); + + game.move.select(Moves.SKILL_SWAP); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy?.getTag(BattlerTagType.INFATUATED)).toBeFalsy(); + }); +}); diff --git a/test/abilities/own_tempo.test.ts b/test/abilities/own_tempo.test.ts new file mode 100644 index 00000000000..936b4311b20 --- /dev/null +++ b/test/abilities/own_tempo.test.ts @@ -0,0 +1,51 @@ +import { Abilities } from "#enums/abilities"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/testUtils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Abilities - Own Tempo", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([ Moves.SPLASH ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should remove confusion when gained", async () => { + game.override.ability(Abilities.OWN_TEMPO) + .enemyAbility(Abilities.BALL_FETCH) + .moveset(Moves.SKILL_SWAP) + .enemyMoveset(Moves.SPLASH), + + await game.classicMode.startBattle([ Species.FEEBAS ]); + const enemy = game.scene.getEnemyPokemon(); + enemy?.addTag(BattlerTagType.CONFUSED); + expect(enemy?.getTag(BattlerTagType.CONFUSED)).toBeTruthy(); + + game.move.select(Moves.SKILL_SWAP); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy?.getTag(BattlerTagType.CONFUSED)).toBeFalsy(); + }); +}); diff --git a/test/abilities/thermal_exchange.test.ts b/test/abilities/thermal_exchange.test.ts new file mode 100644 index 00000000000..124c1dba286 --- /dev/null +++ b/test/abilities/thermal_exchange.test.ts @@ -0,0 +1,51 @@ +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; +import GameManager from "#test/testUtils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Abilities - Thermal Exchange", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([ Moves.SPLASH ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should remove burn when gained", async () => { + game.override.ability(Abilities.THERMAL_EXCHANGE) + .enemyAbility(Abilities.BALL_FETCH) + .moveset(Moves.SKILL_SWAP) + .enemyMoveset(Moves.SPLASH), + + await game.classicMode.startBattle([ Species.FEEBAS ]); + const enemy = game.scene.getEnemyPokemon(); + enemy?.trySetStatus(StatusEffect.BURN); + expect(enemy?.status?.effect).toBe(StatusEffect.BURN); + + game.move.select(Moves.SKILL_SWAP); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy?.status).toBeNull(); + }); +}); diff --git a/test/abilities/vital_spirit.test.ts b/test/abilities/vital_spirit.test.ts new file mode 100644 index 00000000000..3a53c3f520e --- /dev/null +++ b/test/abilities/vital_spirit.test.ts @@ -0,0 +1,51 @@ +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; +import GameManager from "#test/testUtils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Abilities - Vital Spirit", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([ Moves.SPLASH ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should remove sleep when gained", async () => { + game.override.ability(Abilities.INSOMNIA) + .enemyAbility(Abilities.BALL_FETCH) + .moveset(Moves.SKILL_SWAP) + .enemyMoveset(Moves.SPLASH), + + await game.classicMode.startBattle([ Species.FEEBAS ]); + const enemy = game.scene.getEnemyPokemon(); + enemy?.trySetStatus(StatusEffect.SLEEP); + expect(enemy?.status?.effect).toBe(StatusEffect.SLEEP); + + game.move.select(Moves.SKILL_SWAP); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy?.status).toBeNull(); + }); +}); diff --git a/test/abilities/water_bubble.test.ts b/test/abilities/water_bubble.test.ts new file mode 100644 index 00000000000..0b85a5814da --- /dev/null +++ b/test/abilities/water_bubble.test.ts @@ -0,0 +1,51 @@ +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; +import GameManager from "#test/testUtils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Abilities - Water Bubble", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([ Moves.SPLASH ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should remove burn when gained", async () => { + game.override.ability(Abilities.THERMAL_EXCHANGE) + .enemyAbility(Abilities.BALL_FETCH) + .moveset(Moves.SKILL_SWAP) + .enemyMoveset(Moves.SPLASH), + + await game.classicMode.startBattle([ Species.FEEBAS ]); + const enemy = game.scene.getEnemyPokemon(); + enemy?.trySetStatus(StatusEffect.BURN); + expect(enemy?.status?.effect).toBe(StatusEffect.BURN); + + game.move.select(Moves.SKILL_SWAP); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy?.status).toBeNull(); + }); +}); diff --git a/test/abilities/water_veil.test.ts b/test/abilities/water_veil.test.ts new file mode 100644 index 00000000000..38c9a05600b --- /dev/null +++ b/test/abilities/water_veil.test.ts @@ -0,0 +1,51 @@ +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; +import GameManager from "#test/testUtils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Abilities - Water Veil", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([ Moves.SPLASH ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should remove burn when gained", async () => { + game.override.ability(Abilities.THERMAL_EXCHANGE) + .enemyAbility(Abilities.BALL_FETCH) + .moveset(Moves.SKILL_SWAP) + .enemyMoveset(Moves.SPLASH), + + await game.classicMode.startBattle([ Species.FEEBAS ]); + const enemy = game.scene.getEnemyPokemon(); + enemy?.trySetStatus(StatusEffect.BURN); + expect(enemy?.status?.effect).toBe(StatusEffect.BURN); + + game.move.select(Moves.SKILL_SWAP); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy?.status).toBeNull(); + }); +});