[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>
This commit is contained in:
Dean 2025-04-07 16:32:10 -07:00 committed by GitHub
parent 1b79d1f832
commit 31835e6d53
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 693 additions and 43 deletions

View File

@ -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)

View File

@ -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;
}

View File

@ -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));
}
/**

View File

@ -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();
}
}

View File

@ -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();
}

View File

@ -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;

View File

@ -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();
}
}

View File

@ -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();
});
});

View File

@ -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();
});
});

View File

@ -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();
});
});

View File

@ -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();
});
});

View File

@ -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();
});
});

View File

@ -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();
});
});

View File

@ -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();
});
});

View File

@ -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();
});
});

View File

@ -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();
});
});

View File

@ -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();
});
});