[Bug] Fix battler tags lapsing at incorrect times (#2944)

* Fix battler tags lapsing at incorrect times

* Document FlinchedTag
This commit is contained in:
innerthunder 2024-07-11 09:12:49 -07:00 committed by GitHub
parent e879b3c6a0
commit 50d7ed34d9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 98 additions and 17 deletions

View File

@ -31,14 +31,14 @@ export enum BattlerTagLapseType {
export class BattlerTag { export class BattlerTag {
public tagType: BattlerTagType; public tagType: BattlerTagType;
public lapseType: BattlerTagLapseType; public lapseType: BattlerTagLapseType[];
public turnCount: integer; public turnCount: integer;
public sourceMove: Moves; public sourceMove: Moves;
public sourceId?: integer; 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.tagType = tagType;
this.lapseType = lapseType; this.lapseType = typeof lapseType === "number" ? [ lapseType ] : lapseType;
this.turnCount = turnCount; this.turnCount = turnCount;
this.sourceMove = sourceMove; this.sourceMove = sourceMove;
this.sourceId = sourceId; 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 { export class FlinchedTag extends BattlerTag {
constructor(sourceMove: Moves) { 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 { onAdd(pokemon: Pokemon): void {
@ -169,13 +172,19 @@ export class FlinchedTag extends BattlerTag {
return !pokemon.isMax(); 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 { lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
super.lapse(pokemon, lapseType); if (lapseType === BattlerTagLapseType.PRE_MOVE) {
(pokemon.scene.getCurrentPhase() as MovePhase).cancel(); (pokemon.scene.getCurrentPhase() as MovePhase).cancel();
pokemon.scene.queueMessage(i18next.t("battle:battlerTagsFlinchedLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })); pokemon.scene.queueMessage(i18next.t("battle:battlerTagsFlinchedLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
}
return true; return super.lapse(pokemon, lapseType);
} }
getDescriptor(): string { getDescriptor(): string {
@ -200,14 +209,13 @@ export class InterruptedTag extends BattlerTag {
} }
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
super.lapse(pokemon, lapseType);
(pokemon.scene.getCurrentPhase() as MovePhase).cancel(); (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 { export class ConfusedTag extends BattlerTag {
constructor(turnCount: integer, sourceMove: Moves) { constructor(turnCount: integer, sourceMove: Moves) {
@ -883,7 +891,7 @@ export class InfestationTag extends DamagingTrapTag {
export class ProtectedTag extends BattlerTag { export class ProtectedTag extends BattlerTag {
constructor(sourceMove: Moves, tagType: BattlerTagType = BattlerTagType.PROTECTED) { constructor(sourceMove: Moves, tagType: BattlerTagType = BattlerTagType.PROTECTED) {
super(tagType, BattlerTagLapseType.CUSTOM, 0, sourceMove); super(tagType, BattlerTagLapseType.TURN_END, 0, sourceMove);
} }
onAdd(pokemon: Pokemon): void { 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: * 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 `IGNORE_FLYING`: Persistent grounding effects (i.e. from Smack Down and Thousand Waves)
* @item ROOSTED: One-turn grounding effects (i.e. from Roost) * @item `ROOSTED`: One-turn grounding effects (i.e. from Roost)
*/ */
export class GroundedTag extends BattlerTag { export class GroundedTag extends BattlerTag {
constructor(tagType: BattlerTagType, lapseType: BattlerTagLapseType, sourceMove: Moves) { constructor(tagType: BattlerTagType, lapseType: BattlerTagLapseType, sourceMove: Moves) {

View File

@ -2191,7 +2191,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
lapseTags(lapseType: BattlerTagLapseType): void { lapseTags(lapseType: BattlerTagLapseType): void {
const tags = this.summonData.tags; 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); t.onRemove(this);
tags.splice(tags.indexOf(t), 1); tags.splice(tags.indexOf(t), 1);
}); });

View File

@ -449,7 +449,7 @@ describe("Abilities - Parental Bond", () => {
); );
/** TODO: Fix TRAPPED tag lapsing incorrectly, then run this test */ /** 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", "Anchor Shot boosted by this ability should only trap the target after the second hit",
async () => { async () => {
vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.ANCHOR_SHOT]); vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.ANCHOR_SHOT]);

View File

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