From 50d7ed34d9f6665a432d96df644265f421a448ae Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Thu, 11 Jul 2024 09:12:49 -0700 Subject: [PATCH] [Bug] Fix battler tags lapsing at incorrect times (#2944) * Fix battler tags lapsing at incorrect times * Document FlinchedTag --- src/data/battler-tags.ts | 38 +++++++----- src/field/pokemon.ts | 2 +- src/test/abilities/parental_bond.test.ts | 2 +- src/test/moves/astonish.test.ts | 73 ++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 17 deletions(-) create mode 100644 src/test/moves/astonish.test.ts diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 41b6f73ec28..107947e0c44 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -31,14 +31,14 @@ export enum BattlerTagLapseType { export class BattlerTag { public tagType: BattlerTagType; - public lapseType: BattlerTagLapseType; + public lapseType: BattlerTagLapseType[]; public turnCount: integer; public sourceMove: Moves; public sourceId?: integer; - constructor(tagType: BattlerTagType, lapseType: BattlerTagLapseType, turnCount: integer, sourceMove: Moves, sourceId?: integer) { + constructor(tagType: BattlerTagType, lapseType: BattlerTagLapseType | BattlerTagLapseType[], turnCount: integer, sourceMove: Moves, sourceId?: integer) { this.tagType = tagType; - this.lapseType = lapseType; + this.lapseType = typeof lapseType === "number" ? [ lapseType ] : lapseType; this.turnCount = turnCount; this.sourceMove = sourceMove; this.sourceId = sourceId; @@ -154,9 +154,12 @@ export class TrappedTag extends BattlerTag { } } +/** + * BattlerTag that represents the {@link https://bulbapedia.bulbagarden.net/wiki/Flinch Flinch} status condition + */ export class FlinchedTag extends BattlerTag { constructor(sourceMove: Moves) { - super(BattlerTagType.FLINCHED, BattlerTagLapseType.PRE_MOVE, 0, sourceMove); + super(BattlerTagType.FLINCHED, [ BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END ], 0, sourceMove); } onAdd(pokemon: Pokemon): void { @@ -169,13 +172,19 @@ export class FlinchedTag extends BattlerTag { return !pokemon.isMax(); } + /** + * Cancels the Pokemon's next Move on the turn this tag is applied + * @param pokemon The {@linkcode Pokemon} with this tag + * @param lapseType The {@linkcode BattlerTagLapseType lapse type} used for this function call + * @returns `false` (This tag is always removed after applying its effects) + */ lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { - super.lapse(pokemon, lapseType); + if (lapseType === BattlerTagLapseType.PRE_MOVE) { + (pokemon.scene.getCurrentPhase() as MovePhase).cancel(); + pokemon.scene.queueMessage(i18next.t("battle:battlerTagsFlinchedLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })); + } - (pokemon.scene.getCurrentPhase() as MovePhase).cancel(); - pokemon.scene.queueMessage(i18next.t("battle:battlerTagsFlinchedLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })); - - return true; + return super.lapse(pokemon, lapseType); } getDescriptor(): string { @@ -200,14 +209,13 @@ export class InterruptedTag extends BattlerTag { } lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { - super.lapse(pokemon, lapseType); (pokemon.scene.getCurrentPhase() as MovePhase).cancel(); - return true; + return super.lapse(pokemon, lapseType); } } /** - * BattlerTag that represents the {@link https://bulbapedia.bulbagarden.net/wiki/Confusion_(status_condition)} + * BattlerTag that represents the {@link https://bulbapedia.bulbagarden.net/wiki/Confusion_(status_condition) Confusion} status condition */ export class ConfusedTag extends BattlerTag { constructor(turnCount: integer, sourceMove: Moves) { @@ -883,7 +891,7 @@ export class InfestationTag extends DamagingTrapTag { export class ProtectedTag extends BattlerTag { constructor(sourceMove: Moves, tagType: BattlerTagType = BattlerTagType.PROTECTED) { - super(tagType, BattlerTagLapseType.CUSTOM, 0, sourceMove); + super(tagType, BattlerTagLapseType.TURN_END, 0, sourceMove); } onAdd(pokemon: Pokemon): void { @@ -1477,8 +1485,8 @@ export class CursedTag extends BattlerTag { /** * Battler tag for effects that ground the source, allowing Ground-type moves to hit them. Encompasses two tag types: - * @item IGNORE_FLYING: Persistent grounding effects (i.e. from Smack Down and Thousand Waves) - * @item ROOSTED: One-turn grounding effects (i.e. from Roost) + * @item `IGNORE_FLYING`: Persistent grounding effects (i.e. from Smack Down and Thousand Waves) + * @item `ROOSTED`: One-turn grounding effects (i.e. from Roost) */ export class GroundedTag extends BattlerTag { constructor(tagType: BattlerTagType, lapseType: BattlerTagLapseType, sourceMove: Moves) { diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index dd75951dc56..024d27f9e8f 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2191,7 +2191,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { lapseTags(lapseType: BattlerTagLapseType): void { const tags = this.summonData.tags; - tags.filter(t => lapseType === BattlerTagLapseType.FAINT || ((t.lapseType === lapseType) && !(t.lapse(this, lapseType))) || (lapseType === BattlerTagLapseType.TURN_END && t.turnCount < 1)).forEach(t => { + tags.filter(t => lapseType === BattlerTagLapseType.FAINT || ((t.lapseType.some(lType => lType === lapseType)) && !(t.lapse(this, lapseType)))).forEach(t => { t.onRemove(this); tags.splice(tags.indexOf(t), 1); }); diff --git a/src/test/abilities/parental_bond.test.ts b/src/test/abilities/parental_bond.test.ts index 4401ee0d40a..77877ef529a 100644 --- a/src/test/abilities/parental_bond.test.ts +++ b/src/test/abilities/parental_bond.test.ts @@ -449,7 +449,7 @@ describe("Abilities - Parental Bond", () => { ); /** TODO: Fix TRAPPED tag lapsing incorrectly, then run this test */ - test.skip( + test( "Anchor Shot boosted by this ability should only trap the target after the second hit", async () => { vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.ANCHOR_SHOT]); diff --git a/src/test/moves/astonish.test.ts b/src/test/moves/astonish.test.ts new file mode 100644 index 00000000000..3ca164fedd6 --- /dev/null +++ b/src/test/moves/astonish.test.ts @@ -0,0 +1,73 @@ +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; +import GameManager from "../utils/gameManager"; +import * as Overrides from "#app/overrides"; +import { Species } from "#enums/species"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { getMovePosition } from "../utils/gameManagerUtils"; +import { BerryPhase, CommandPhase, MoveEndPhase, TurnEndPhase } from "#app/phases.js"; +import { BattlerTagType } from "#app/enums/battler-tag-type.js"; +import { allMoves } from "#app/data/move.js"; + +const TIMEOUT = 20 * 1000; + +describe("Moves - Astonish", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + vi.spyOn(Overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.ASTONISH, Moves.SPLASH]); + vi.spyOn(Overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.BLASTOISE); + vi.spyOn(Overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.INSOMNIA); + vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]); + vi.spyOn(Overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(100); + vi.spyOn(Overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(100); + + vi.spyOn(allMoves[Moves.ASTONISH], "chance", "get").mockReturnValue(100); + }); + + test( + "move effect should cancel the target's move on the turn it applies", + async () => { + await game.startBattle([Species.MEOWSCARADA]); + + const leadPokemon = game.scene.getPlayerPokemon(); + expect(leadPokemon).toBeDefined(); + + const enemyPokemon = game.scene.getEnemyPokemon(); + expect(enemyPokemon).toBeDefined(); + + game.doAttack(getMovePosition(game.scene, 0, Moves.ASTONISH)); + + await game.phaseInterceptor.to(MoveEndPhase, false); + + expect(enemyPokemon.getTag(BattlerTagType.FLINCHED)).toBeDefined(); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); + expect(enemyPokemon.getTag(BattlerTagType.FLINCHED)).toBeUndefined(); + + await game.phaseInterceptor.to(CommandPhase, false); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH)); + + await game.phaseInterceptor.to(BerryPhase, false); + + expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp()); + }, TIMEOUT + ); +});