[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 {} 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. * 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 { export class PostSummonStatStageChangeAbAttr extends PostSummonAbAttr {
private stats: BattleStat[]; private stats: BattleStat[];
private stages: number; 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 { export class PostSummonFormChangeAbAttr extends PostSummonAbAttr {
private formFunc: (p: Pokemon) => number; private formFunc: (p: Pokemon) => number;
@ -6291,6 +6358,7 @@ export function initAbilities() {
.ignorable(), .ignorable(),
new Ability(Abilities.LIMBER, 3) new Ability(Abilities.LIMBER, 3)
.attr(StatusEffectImmunityAbAttr, StatusEffect.PARALYSIS) .attr(StatusEffectImmunityAbAttr, StatusEffect.PARALYSIS)
.attr(PostSummonHealStatusAbAttr, StatusEffect.PARALYSIS)
.ignorable(), .ignorable(),
new Ability(Abilities.SAND_VEIL, 3) new Ability(Abilities.SAND_VEIL, 3)
.attr(StatMultiplierAbAttr, Stat.EVA, 1.2) .attr(StatMultiplierAbAttr, Stat.EVA, 1.2)
@ -6308,6 +6376,7 @@ export function initAbilities() {
.ignorable(), .ignorable(),
new Ability(Abilities.OBLIVIOUS, 3) new Ability(Abilities.OBLIVIOUS, 3)
.attr(BattlerTagImmunityAbAttr, [ BattlerTagType.INFATUATED, BattlerTagType.TAUNT ]) .attr(BattlerTagImmunityAbAttr, [ BattlerTagType.INFATUATED, BattlerTagType.TAUNT ])
.attr(PostSummonRemoveBattlerTagAbAttr, BattlerTagType.INFATUATED, BattlerTagType.TAUNT)
.attr(IntimidateImmunityAbAttr) .attr(IntimidateImmunityAbAttr)
.ignorable(), .ignorable(),
new Ability(Abilities.CLOUD_NINE, 3) new Ability(Abilities.CLOUD_NINE, 3)
@ -6320,6 +6389,7 @@ export function initAbilities() {
.attr(StatMultiplierAbAttr, Stat.ACC, 1.3), .attr(StatMultiplierAbAttr, Stat.ACC, 1.3),
new Ability(Abilities.INSOMNIA, 3) new Ability(Abilities.INSOMNIA, 3)
.attr(StatusEffectImmunityAbAttr, StatusEffect.SLEEP) .attr(StatusEffectImmunityAbAttr, StatusEffect.SLEEP)
.attr(PostSummonHealStatusAbAttr, StatusEffect.SLEEP)
.attr(BattlerTagImmunityAbAttr, BattlerTagType.DROWSY) .attr(BattlerTagImmunityAbAttr, BattlerTagType.DROWSY)
.ignorable(), .ignorable(),
new Ability(Abilities.COLOR_CHANGE, 3) new Ability(Abilities.COLOR_CHANGE, 3)
@ -6327,6 +6397,7 @@ export function initAbilities() {
.condition(getSheerForceHitDisableAbCondition()), .condition(getSheerForceHitDisableAbCondition()),
new Ability(Abilities.IMMUNITY, 3) new Ability(Abilities.IMMUNITY, 3)
.attr(StatusEffectImmunityAbAttr, StatusEffect.POISON, StatusEffect.TOXIC) .attr(StatusEffectImmunityAbAttr, StatusEffect.POISON, StatusEffect.TOXIC)
.attr(PostSummonHealStatusAbAttr, StatusEffect.POISON, StatusEffect.TOXIC)
.ignorable(), .ignorable(),
new Ability(Abilities.FLASH_FIRE, 3) new Ability(Abilities.FLASH_FIRE, 3)
.attr(TypeImmunityAddBattlerTagAbAttr, PokemonType.FIRE, BattlerTagType.FIRE_BOOST, 1) .attr(TypeImmunityAddBattlerTagAbAttr, PokemonType.FIRE, BattlerTagType.FIRE_BOOST, 1)
@ -6336,6 +6407,7 @@ export function initAbilities() {
.ignorable(), .ignorable(),
new Ability(Abilities.OWN_TEMPO, 3) new Ability(Abilities.OWN_TEMPO, 3)
.attr(BattlerTagImmunityAbAttr, BattlerTagType.CONFUSED) .attr(BattlerTagImmunityAbAttr, BattlerTagType.CONFUSED)
.attr(PostSummonRemoveBattlerTagAbAttr, BattlerTagType.CONFUSED)
.attr(IntimidateImmunityAbAttr) .attr(IntimidateImmunityAbAttr)
.ignorable(), .ignorable(),
new Ability(Abilities.SUCTION_CUPS, 3) new Ability(Abilities.SUCTION_CUPS, 3)
@ -6401,9 +6473,11 @@ export function initAbilities() {
.ignorable(), .ignorable(),
new Ability(Abilities.MAGMA_ARMOR, 3) new Ability(Abilities.MAGMA_ARMOR, 3)
.attr(StatusEffectImmunityAbAttr, StatusEffect.FREEZE) .attr(StatusEffectImmunityAbAttr, StatusEffect.FREEZE)
.attr(PostSummonHealStatusAbAttr, StatusEffect.FREEZE)
.ignorable(), .ignorable(),
new Ability(Abilities.WATER_VEIL, 3) new Ability(Abilities.WATER_VEIL, 3)
.attr(StatusEffectImmunityAbAttr, StatusEffect.BURN) .attr(StatusEffectImmunityAbAttr, StatusEffect.BURN)
.attr(PostSummonHealStatusAbAttr, StatusEffect.BURN)
.ignorable(), .ignorable(),
new Ability(Abilities.MAGNET_PULL, 3) new Ability(Abilities.MAGNET_PULL, 3)
.attr(ArenaTrapAbAttr, (user, target) => { .attr(ArenaTrapAbAttr, (user, target) => {
@ -6497,6 +6571,7 @@ export function initAbilities() {
.attr(DoubleBattleChanceAbAttr), .attr(DoubleBattleChanceAbAttr),
new Ability(Abilities.VITAL_SPIRIT, 3) new Ability(Abilities.VITAL_SPIRIT, 3)
.attr(StatusEffectImmunityAbAttr, StatusEffect.SLEEP) .attr(StatusEffectImmunityAbAttr, StatusEffect.SLEEP)
.attr(PostSummonHealStatusAbAttr, StatusEffect.SLEEP)
.attr(BattlerTagImmunityAbAttr, BattlerTagType.DROWSY) .attr(BattlerTagImmunityAbAttr, BattlerTagType.DROWSY)
.ignorable(), .ignorable(),
new Ability(Abilities.WHITE_SMOKE, 3) 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)), .attr(MoveTypeChangeAbAttr, PokemonType.ICE, 1.2, (user, target, move) => move.type === PokemonType.NORMAL && !move.hasAttr(VariableMoveTypeAttr)),
new Ability(Abilities.SWEET_VEIL, 6) new Ability(Abilities.SWEET_VEIL, 6)
.attr(UserFieldStatusEffectImmunityAbAttr, StatusEffect.SLEEP) .attr(UserFieldStatusEffectImmunityAbAttr, StatusEffect.SLEEP)
.attr(PostSummonUserFieldRemoveStatusEffectAbAttr, StatusEffect.SLEEP)
.attr(UserFieldBattlerTagImmunityAbAttr, BattlerTagType.DROWSY) .attr(UserFieldBattlerTagImmunityAbAttr, BattlerTagType.DROWSY)
.ignorable() .ignorable()
.partial(), // Mold Breaker ally should not be affected by Sweet Veil .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(ReceivedTypeDamageMultiplierAbAttr, PokemonType.FIRE, 0.5)
.attr(MoveTypePowerBoostAbAttr, PokemonType.WATER, 2) .attr(MoveTypePowerBoostAbAttr, PokemonType.WATER, 2)
.attr(StatusEffectImmunityAbAttr, StatusEffect.BURN) .attr(StatusEffectImmunityAbAttr, StatusEffect.BURN)
.attr(PostSummonHealStatusAbAttr, StatusEffect.BURN)
.ignorable(), .ignorable(),
new Ability(Abilities.STEELWORKER, 7) new Ability(Abilities.STEELWORKER, 7)
.attr(MoveTypePowerBoostAbAttr, PokemonType.STEEL), .attr(MoveTypePowerBoostAbAttr, PokemonType.STEEL),
@ -7197,6 +7274,7 @@ export function initAbilities() {
new Ability(Abilities.THERMAL_EXCHANGE, 9) new Ability(Abilities.THERMAL_EXCHANGE, 9)
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => user.getMoveType(move) === PokemonType.FIRE && move.category !== MoveCategory.STATUS, Stat.ATK, 1) .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => user.getMoveType(move) === PokemonType.FIRE && move.category !== MoveCategory.STATUS, Stat.ATK, 1)
.attr(StatusEffectImmunityAbAttr, StatusEffect.BURN) .attr(StatusEffectImmunityAbAttr, StatusEffect.BURN)
.attr(PostSummonHealStatusAbAttr, StatusEffect.BURN)
.ignorable(), .ignorable(),
new Ability(Abilities.ANGER_SHELL, 9) new Ability(Abilities.ANGER_SHELL, 9)
.attr(PostDefendHpGatedStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, 0.5, [ Stat.ATK, Stat.SPATK, Stat.SPD ], 1) .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; const statusCheck = moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance;
if (statusCheck) { if (statusCheck) {
const pokemon = this.selfTarget ? user : target; const pokemon = this.selfTarget ? user : target;
if (pokemon.status) { if (pokemon.status && !this.overrideStatus) {
if (this.overrideStatus) { return false;
pokemon.resetStatus();
} else {
return false;
}
} }
if (user !== target && target.isSafeguarded(user)) { if (user !== target && target.isSafeguarded(user)) {
@ -2457,8 +2453,8 @@ export class StatusEffectAttr extends MoveEffectAttr {
} }
return false; return false;
} }
if ((!pokemon.status || (pokemon.status.effect === this.effect && moveChance < 0)) if (((!pokemon.status || this.overrideStatus) || (pokemon.status.effect === this.effect && moveChance < 0))
&& pokemon.trySetStatus(this.effect, true, user, this.turnsRemaining)) { && pokemon.trySetStatus(this.effect, true, user, this.turnsRemaining, null, this.overrideStatus)) {
applyPostAttackAbAttrs(ConfusionOnStatusEffectAbAttr, user, target, move, null, false, this.effect); applyPostAttackAbAttrs(ConfusionOnStatusEffectAbAttr, user, target, move, null, false, this.effect);
return true; return true;
} }

View File

@ -264,6 +264,7 @@ import { StatusEffect } from "#enums/status-effect";
import { doShinySparkleAnim } from "#app/field/anims"; import { doShinySparkleAnim } from "#app/field/anims";
import { MoveFlags } from "#enums/MoveFlags"; import { MoveFlags } from "#enums/MoveFlags";
import { timedEventManager } from "#app/global-event-manager"; import { timedEventManager } from "#app/global-event-manager";
import { ResetStatusPhase } from "#app/phases/reset-status-phase";
export enum LearnMoveSituation { export enum LearnMoveSituation {
MISC, MISC,
@ -4809,7 +4810,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (newTag.canAdd(this)) { if (newTag.canAdd(this)) {
this.summonData.tags.push(newTag); this.summonData.tags.push(newTag);
newTag.onAdd(this); newTag.onAdd(this);
return true; return true;
} }
@ -5480,8 +5480,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
sourcePokemon: Pokemon | null = null, sourcePokemon: Pokemon | null = null,
turnsRemaining = 0, turnsRemaining = 0,
sourceText: string | null = null, sourceText: string | null = null,
overrideStatus?: boolean
): boolean { ): boolean {
if (!this.canSetStatus(effect, asPhase, false, sourcePokemon)) { if (!this.canSetStatus(effect, asPhase, overrideStatus, sourcePokemon)) {
return false; return false;
} }
if (this.isFainted() && effect !== StatusEffect.FAINT) { if (this.isFainted() && effect !== StatusEffect.FAINT) {
@ -5497,6 +5498,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
if (asPhase) { if (asPhase) {
if (overrideStatus) {
this.resetStatus(false);
}
globalScene.unshiftPhase( globalScene.unshiftPhase(
new ObtainStatusEffectPhase( new ObtainStatusEffectPhase(
this.getBattlerIndex(), 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 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); this.status = new Status(effect, 0, sleepTurnsRemaining?.value);
if (effect !== StatusEffect.FAINT) {
globalScene.triggerPokemonFormChange(
this,
SpeciesFormChangeStatusEffectTrigger,
true,
);
applyPostSetStatusAbAttrs(
PostSetStatusAbAttr,
this,
effect,
sourcePokemon,
);
}
return true; return true;
} }
@ -5564,21 +5554,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (!revive && lastStatus === StatusEffect.FAINT) { if (!revive && lastStatus === StatusEffect.FAINT) {
return; return;
} }
this.status = null; globalScene.unshiftPhase(new ResetStatusPhase(this, confusion, reloadAssets));
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());
}
} }
/** /**

View File

@ -2,11 +2,18 @@ import { globalScene } from "#app/global-scene";
import { BattlerTagLapseType } from "#app/data/battler-tags"; import { BattlerTagLapseType } from "#app/data/battler-tags";
import { PokemonPhase } from "./pokemon-phase"; import { PokemonPhase } from "./pokemon-phase";
import type { BattlerIndex } from "#app/battle"; 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 { export class MoveEndPhase extends PokemonPhase {
private wasFollowUp: boolean; 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); super(battlerIndex);
this.targets = targets;
this.wasFollowUp = wasFollowUp; this.wasFollowUp = wasFollowUp;
} }
@ -17,9 +24,15 @@ export class MoveEndPhase extends PokemonPhase {
if (!this.wasFollowUp && pokemon?.isActive(true)) { if (!this.wasFollowUp && pokemon?.isActive(true)) {
pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE); pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE);
} }
globalScene.arena.setIgnoreAbilities(false); 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(); this.end();
} }
} }

View File

@ -169,7 +169,11 @@ export class MovePhase extends BattlePhase {
// Check move to see if arena.ignoreAbilities should be true. // Check move to see if arena.ignoreAbilities should be true.
if (!this.followUp || this.reflected) { 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()); 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 * Queues a {@linkcode MoveEndPhase} and then ends the phase
*/ */
public end(): void { 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(); super.end();
} }

View File

@ -6,6 +6,9 @@ import { StatusEffect } from "#app/enums/status-effect";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import { PokemonPhase } from "./pokemon-phase"; 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 { export class ObtainStatusEffectPhase extends PokemonPhase {
private statusEffect?: StatusEffect; private statusEffect?: StatusEffect;
@ -44,6 +47,12 @@ export class ObtainStatusEffectPhase extends PokemonPhase {
this.sourceText ?? undefined, 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(); this.end();
}); });
return; 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();
});
});