From 49f0231982911aa4998182690acf9e314f2f2fcc Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sat, 26 Jul 2025 14:43:17 -0400 Subject: [PATCH] Fixed tests for real --- .../positional-tags/positional-tag-manager.ts | 8 +-- src/data/positional-tags/positional-tag.ts | 9 ++- src/phases/move-effect-phase.ts | 1 + ..._attack.test.ts => delayed-attack.test.ts} | 61 +++++++++++-------- test/moves/wish.test.ts | 6 +- test/test-utils/helpers/field-helper.ts | 12 ++-- 6 files changed, 55 insertions(+), 42 deletions(-) rename test/moves/{delayed_attack.test.ts => delayed-attack.test.ts} (89%) diff --git a/src/data/positional-tags/positional-tag-manager.ts b/src/data/positional-tags/positional-tag-manager.ts index 219186d94de..7b8486e0a6a 100644 --- a/src/data/positional-tags/positional-tag-manager.ts +++ b/src/data/positional-tags/positional-tag-manager.ts @@ -39,17 +39,15 @@ export class PositionalTagManager { const leftoverTags: PositionalTag[] = []; for (const tag of this.tags) { // Check for silent removal, immediately removing invalid tags. - if (tag.shouldDisappear()) { - continue; - } - if (--tag.turnCount > 0) { // tag still cooking leftoverTags.push(tag); continue; } - tag.trigger(); + if (!tag.shouldDisappear()) { + tag.trigger(); + } } this.tags = leftoverTags; } diff --git a/src/data/positional-tags/positional-tag.ts b/src/data/positional-tags/positional-tag.ts index 5ef23a1dc1a..c5636e49ac4 100644 --- a/src/data/positional-tags/positional-tag.ts +++ b/src/data/positional-tags/positional-tag.ts @@ -51,10 +51,8 @@ export abstract class PositionalTag implements PositionalTagBaseArgs { public abstract trigger(): void; /** - * Check whether this tag should be removed without triggering. - * @returns Whether this tag should disappear. - * @privateRemarks - * Silent removal is accomplished by setting the attack's turn count to -1. + * Check whether this tag should be removed without calling {@linkcode trigger} and triggering effects. + * @returns Whether this tag should disappear without triggering. */ abstract shouldDisappear(): boolean; @@ -89,6 +87,7 @@ export class DelayedAttackTag extends PositionalTag implements DelayedAttackArgs } override trigger(): void { + // Bangs are justified as the `shouldDisappear` method will queue the tag for removal if the source or target leave the field const source = globalScene.getPokemonById(this.sourceId)!; const target = this.getTarget()!; @@ -102,7 +101,7 @@ export class DelayedAttackTag extends PositionalTag implements DelayedAttackArgs globalScene.phaseManager.unshiftNew( "MoveEffectPhase", - this.sourceId, + this.sourceId, // TODO: Find an alternate method of passing the source pokemon without a source ID [this.targetIndex], allMoves[this.sourceMove], MoveUseMode.TRANSPARENT, diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 67d41234c69..20c19df5121 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -639,6 +639,7 @@ export class MoveEffectPhase extends PokemonPhase { /** @returns The {@linkcode Pokemon} using this phase's invoked move */ public getUserPokemon(): Pokemon | null { + // TODO: Make this purely a battler index if (this.battlerIndex > BattlerIndex.ENEMY_2) { return globalScene.getPokemonById(this.battlerIndex); } diff --git a/test/moves/delayed_attack.test.ts b/test/moves/delayed-attack.test.ts similarity index 89% rename from test/moves/delayed_attack.test.ts rename to test/moves/delayed-attack.test.ts index b23af3785c9..e8cf2871626 100644 --- a/test/moves/delayed_attack.test.ts +++ b/test/moves/delayed-attack.test.ts @@ -10,7 +10,7 @@ import { PokemonType } from "#enums/pokemon-type"; import { PositionalTagType } from "#enums/positional-tag-type"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; -import { GameManager } from "#test/testUtils/gameManager"; +import { GameManager } from "#test/test-utils/game-manager"; import i18next from "i18next"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -40,9 +40,9 @@ describe("Moves - Delayed Attacks", () => { }); /** - * Wait until a number of turns have passed and a delayed attack has struck. + * Wait until a number of turns have passed. * @param numTurns - Number of turns to pass. - * @param toEndOfTurn - Whether to advance to the `TurnEndPhase` (true) or the `PositionalTagPhase` (`false`); + * @param toEndOfTurn - Whether to advance to the `TurnEndPhase` (`true`) or the `PositionalTagPhase` (`false`); * default `true` * @returns A Promise that resolves once the specified number of turns has elapsed * and the specified phase has been reached. @@ -50,15 +50,15 @@ describe("Moves - Delayed Attacks", () => { async function passTurns(numTurns: number, toEndOfTurn = true): Promise { for (let i = 0; i < numTurns; i++) { game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER); - if (game.scene.getPlayerField()[1]) { + if (game.scene.getPlayerField()[1]?.isActive()) { game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2); } await game.move.forceEnemyMove(MoveId.SPLASH); - if (game.scene.getEnemyField()[1]) { + if (game.scene.getEnemyField()[1]?.isActive()) { await game.move.forceEnemyMove(MoveId.SPLASH); } + await game.phaseInterceptor.to("PositionalTagPhase"); } - await game.phaseInterceptor.to("PositionalTagPhase"); if (toEndOfTurn) { await game.toEndOfTurn(); } @@ -91,9 +91,9 @@ describe("Moves - Delayed Attacks", () => { game.forceEnemyToSwitch(); await game.toNextTurn(); - game.move.use(MoveId.SPLASH); - await game.toEndOfTurn(); + await passTurns(1); + expectFutureSightActive(0); const enemy = game.field.getEnemyPokemon(); expect(enemy.hp).toBeLessThan(enemy.getMaxHp()); expect(game.textInterceptor.logs).toContain( @@ -134,6 +134,7 @@ describe("Moves - Delayed Attacks", () => { await passTurns(2); + expectFutureSightActive(0); expect(enemy.hp).toBeLessThan(enemy.getMaxHp()); }); @@ -163,10 +164,7 @@ describe("Moves - Delayed Attacks", () => { game.override.battleStyle("double"); await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.FEEBAS]); - const [alomomola, blissey, karp1, karp2] = game.scene.getField(); - - vi.spyOn(karp1, "getNameToRender").mockReturnValue("Karp 1"); - vi.spyOn(karp2, "getNameToRender").mockReturnValue("Karp 2"); + const [alomomola, blissey] = game.scene.getField(); const oldOrder = game.field.getSpeedOrder(); @@ -175,8 +173,9 @@ describe("Moves - Delayed Attacks", () => { await game.move.forceEnemyMove(MoveId.FUTURE_SIGHT, BattlerIndex.PLAYER); await game.move.forceEnemyMove(MoveId.FUTURE_SIGHT, BattlerIndex.PLAYER_2); // Ensure that the moves are used deterministically in speed order (for speed ties) - await game.setTurnOrder(oldOrder); + await game.setTurnOrder(oldOrder.map(p => p.getBattlerIndex())); await game.toNextTurn(); + expectFutureSightActive(4); // Lower speed to change turn order @@ -193,30 +192,36 @@ describe("Moves - Delayed Attacks", () => { const MEPs = game.scene.phaseManager.phaseQueue.filter(p => p.is("MoveEffectPhase")); expect(MEPs).toHaveLength(4); - expect(MEPs.map(mep => mep["battlerIndex"])).toEqual(oldOrder); + expect(MEPs.map(mep => mep.getPokemon())).toEqual(oldOrder); }); it("should vanish silently if it would otherwise hit the user", async () => { game.override.battleStyle("double"); - await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.FEEBAS, SpeciesId.MIENFOO]); + await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.FEEBAS, SpeciesId.MILOTIC]); - const [karp, feebas] = game.scene.getPlayerField(); + const [karp, feebas, milotic] = game.scene.getPlayerParty(); game.move.use(MoveId.FUTURE_SIGHT, BattlerIndex.PLAYER, BattlerIndex.PLAYER_2); - // Karp / Feebas / Milotic - game.doSwitchPokemon(2); + game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2); await game.toNextTurn(); expectFutureSightActive(1); // Milotic / Feebas // Karp game.doSwitchPokemon(2); - // Feebas / Karp // Milotic - game.doSwitchPokemon(2); + game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2); await game.toNextTurn(); + expect(game.scene.getPlayerParty()).toEqual([milotic, feebas, karp]); + + // Milotic / Karp // Feebas + game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER); + game.doSwitchPokemon(2); + await passTurns(1); + expect(game.scene.getPlayerParty()).toEqual([milotic, karp, feebas]); + expect(karp.hp).toBe(karp.getMaxHp()); expect(feebas.hp).toBe(feebas.getMaxHp()); expect(game.textInterceptor.logs).not.toContain( @@ -227,7 +232,7 @@ describe("Moves - Delayed Attacks", () => { ); }); - it("should redirect normally if target is fainted when attack is launched", async () => { + it("should redirect normally if target is fainted when move is used", async () => { game.override.battleStyle("double"); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); @@ -238,7 +243,13 @@ describe("Moves - Delayed Attacks", () => { await game.toNextTurn(); expect(enemy2.isFainted()).toBe(true); - expectFutureSightActive(1); + expectFutureSightActive(); + + const attack = game.scene.arena.positionalTagManager.tags.find( + t => t.tagType === PositionalTagType.DELAYED_ATTACK, + )!; + expect(attack).toBeDefined(); + expect(attack.targetIndex).toBe(enemy1.getBattlerIndex()); await passTurns(2); @@ -251,7 +262,7 @@ describe("Moves - Delayed Attacks", () => { ); }); - it("should vanish silently if target is fainted when attack lands", async () => { + it("should vanish silently if slot is vacant when attack lands", async () => { game.override.battleStyle("double"); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); @@ -264,8 +275,10 @@ describe("Moves - Delayed Attacks", () => { game.move.use(MoveId.SPLASH); await game.killPokemon(enemy2); + await game.toNextTurn(); - await passTurns(1); + game.move.use(MoveId.SPLASH); + await game.toNextTurn(); expectFutureSightActive(0); expect(enemy1.hp).toBe(enemy1.getMaxHp()); diff --git a/test/moves/wish.test.ts b/test/moves/wish.test.ts index 397778d720f..147c598106b 100644 --- a/test/moves/wish.test.ts +++ b/test/moves/wish.test.ts @@ -6,7 +6,7 @@ import { MoveResult } from "#enums/move-result"; import { PositionalTagType } from "#enums/positional-tag-type"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; -import { GameManager } from "#test/testUtils/gameManager"; +import { GameManager } from "#test/test-utils/game-manager"; import { toDmgValue } from "#utils/common"; import i18next from "i18next"; import Phaser from "phaser"; @@ -124,7 +124,7 @@ describe("Move - Wish", () => { await game.move.forceEnemyMove(MoveId.WISH); await game.move.forceEnemyMove(MoveId.WISH); // Ensure that the wishes are used deterministically in speed order (for speed ties) - await game.setTurnOrder(oldOrder); + await game.setTurnOrder(oldOrder.map(p => p.getBattlerIndex())); await game.toNextTurn(); expectWishActive(4); @@ -145,7 +145,7 @@ describe("Move - Wish", () => { const healPhases = game.scene.phaseManager.phaseQueue.filter(p => p.is("PokemonHealPhase")); expect(healPhases).toHaveLength(4); - expect.soft(healPhases.map(php => php["battlerIndex"])).toEqual(oldOrder); + expect.soft(healPhases.map(php => php.getPokemon())).toEqual(oldOrder); await game.toEndOfTurn(); diff --git a/test/test-utils/helpers/field-helper.ts b/test/test-utils/helpers/field-helper.ts index 35ca853d049..2d8fd8ee701 100644 --- a/test/test-utils/helpers/field-helper.ts +++ b/test/test-utils/helpers/field-helper.ts @@ -5,7 +5,6 @@ import type { globalScene } from "#app/global-scene"; import type { Ability } from "#abilities/ability"; import { allAbilities } from "#data/data-lists"; import type { AbilityId } from "#enums/ability-id"; -import type { BattlerIndex } from "#enums/battler-index"; import type { PokemonType } from "#enums/pokemon-type"; import { Stat } from "#enums/stat"; import type { EnemyPokemon, PlayerPokemon, Pokemon } from "#field/pokemon"; @@ -45,18 +44,21 @@ export class FieldHelper extends GameManagerHelper { } /** - * @returns The {@linkcode BattlerIndex | indexes} of Pokemon on the field in order of decreasing Speed. + * Helper function to return all on-field {@linkcode Pokemon} in speed order (fastest first). + * @returns An array containing all {@linkcode Pokemon} on the field in order of descending Speed. * Speed ties are returned in increasing order of index. * * @remarks * This does not account for Trick Room as it does not modify the _speed_ of Pokemon on the field, * only their turn order. */ - public getSpeedOrder(): BattlerIndex[] { + public getSpeedOrder(): Pokemon[] { return this.game.scene .getField(true) - .sort((pA, pB) => pB.getEffectiveStat(Stat.SPD) - pA.getEffectiveStat(Stat.SPD)) - .map(p => p.getBattlerIndex()); + .sort( + (pA, pB) => + pB.getEffectiveStat(Stat.SPD) - pA.getEffectiveStat(Stat.SPD) || pA.getBattlerIndex() - pB.getBattlerIndex(), + ); } /**