From 6aa2d5132f02501e9b1145c426df921b269787a0 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Tue, 19 Aug 2025 21:00:50 -0400 Subject: [PATCH] Added new matcher for showing text; added splash tests and such --- src/field/pokemon.ts | 2 +- test/@types/vitest.d.ts | 13 +++++ test/abilities/arena-trap.test.ts | 2 +- test/abilities/cud-chew.test.ts | 2 +- test/abilities/truant.test.ts | 2 +- test/matchers.setup.ts | 2 + test/moves/chilly-reception.test.ts | 6 +-- test/moves/delayed-attack.test.ts | 12 ++--- test/moves/laser-focus.test.ts | 52 +++++++++++++++++++ test/moves/splash-celebrate.test.ts | 52 +++++++++++++++++++ test/moves/wish.test.ts | 4 +- test/test-utils/helpers/overrides-helper.ts | 6 ++- .../matchers/to-have-shown-message.ts | 43 +++++++++++++++ 13 files changed, 182 insertions(+), 16 deletions(-) create mode 100644 test/moves/laser-focus.test.ts create mode 100644 test/moves/splash-celebrate.test.ts create mode 100644 test/test-utils/matchers/to-have-shown-message.ts diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index e12485a7272..10dfae91310 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -4016,7 +4016,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { */ getCriticalHitResult(source: Pokemon, move: Move): boolean { if (move.hasAttr("FixedDamageAttr")) { - // fixed damage moves (Dragon Rage, etc.) will nevet crit + // fixed damage moves (Dragon Rage, etc.) will never crit return false; } diff --git a/test/@types/vitest.d.ts b/test/@types/vitest.d.ts index 21cf76ed352..837d8d5433b 100644 --- a/test/@types/vitest.d.ts +++ b/test/@types/vitest.d.ts @@ -27,6 +27,7 @@ import type { expect } from "vitest"; declare module "vitest" { interface Assertion { + // # region Generic Matchers /** * Check whether an array contains EXACTLY the given items (in any order). * @@ -38,6 +39,18 @@ declare module "vitest" { */ toEqualArrayUnsorted(expected: T[]): void; + // # endregion Generic Matchers + + // # region GameManager Matchers + + /** + * Check if the {@linkcode GameManager} has shown the given message at least once in the current battle. + * @param expectedMessage - The expected message + */ + toHaveShownMessage(expectedMessage: string): void; + + // # endregion GameManager Matchers + // #region Arena Matchers /** diff --git a/test/abilities/arena-trap.test.ts b/test/abilities/arena-trap.test.ts index 0090487f49c..8f5d820a145 100644 --- a/test/abilities/arena-trap.test.ts +++ b/test/abilities/arena-trap.test.ts @@ -57,7 +57,7 @@ describe("Abilities - Arena Trap", () => { await game.phaseInterceptor.to("CommandPhase"); - expect(game.textInterceptor.logs).toContain( + expect(game).toHaveShownMessage( i18next.t("abilityTriggers:arenaTrap", { pokemonNameWithAffix: getPokemonNameWithAffix(enemy), abilityName: allAbilities[AbilityId.ARENA_TRAP].name, diff --git a/test/abilities/cud-chew.test.ts b/test/abilities/cud-chew.test.ts index f68141096eb..8d80ba119ca 100644 --- a/test/abilities/cud-chew.test.ts +++ b/test/abilities/cud-chew.test.ts @@ -99,7 +99,7 @@ describe("Abilities - Cud Chew", () => { expect(abDisplaySpy.mock.calls[1][2]).toBe(false); // should display messgae - expect(game.textInterceptor.getLatestMessage()).toBe( + expect(game).toHaveShownMessage( i18next.t("battle:hpIsFull", { pokemonName: getPokemonNameWithAffix(farigiraf), }), diff --git a/test/abilities/truant.test.ts b/test/abilities/truant.test.ts index 0d71cd393b0..31098fa1a85 100644 --- a/test/abilities/truant.test.ts +++ b/test/abilities/truant.test.ts @@ -54,7 +54,7 @@ describe("Ability - Truant", () => { expect(player.getLastXMoves(1)[0]).toEqual(expect.objectContaining({ move: MoveId.NONE, result: MoveResult.FAIL })); expect(enemy.hp).toBe(enemy.getMaxHp()); - expect(game.textInterceptor.logs).toContain( + expect(game).toHaveShownMessage( i18next.t("battlerTags:truantLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(player), }), diff --git a/test/matchers.setup.ts b/test/matchers.setup.ts index f76a9423ab3..5e3686d9ea0 100644 --- a/test/matchers.setup.ts +++ b/test/matchers.setup.ts @@ -7,6 +7,7 @@ import { toHaveFainted } from "#test/test-utils/matchers/to-have-fainted"; import { toHaveFullHp } from "#test/test-utils/matchers/to-have-full-hp"; import { toHaveHp } from "#test/test-utils/matchers/to-have-hp"; import { toHavePositionalTag } from "#test/test-utils/matchers/to-have-positional-tag"; +import { toHaveShownMessage } from "#test/test-utils/matchers/to-have-shown-message"; import { toHaveStatStage } from "#test/test-utils/matchers/to-have-stat-stage"; import { toHaveStatusEffect } from "#test/test-utils/matchers/to-have-status-effect"; import { toHaveTakenDamage } from "#test/test-utils/matchers/to-have-taken-damage"; @@ -24,6 +25,7 @@ import { expect } from "vitest"; expect.extend({ toEqualArrayUnsorted, + toHaveShownMessage, toHaveWeather, toHaveTerrain, toHaveArenaTag, diff --git a/test/moves/chilly-reception.test.ts b/test/moves/chilly-reception.test.ts index 096454132f3..f9caea3d560 100644 --- a/test/moves/chilly-reception.test.ts +++ b/test/moves/chilly-reception.test.ts @@ -47,7 +47,7 @@ describe("Moves - Chilly Reception", () => { expect(game.field.getPlayerPokemon()).toBe(meowth); expect(slowking.isOnField()).toBe(false); expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase"); - expect(game.textInterceptor.logs).toContain( + expect(game).toHaveShownMessage( i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(slowking) }), ); }); @@ -110,7 +110,7 @@ describe("Moves - Chilly Reception", () => { expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase"); expect(game.field.getPlayerPokemon()).toBe(slowking); expect(slowking.getLastXMoves()[0].result).toBe(MoveResult.FAIL); - expect(game.textInterceptor.logs).toContain( + expect(game).toHaveShownMessage( i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(slowking) }), ); }); @@ -129,7 +129,7 @@ describe("Moves - Chilly Reception", () => { expect(game.field.getPlayerPokemon()).toBe(meowth); expect(slowking.isOnField()).toBe(false); expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase"); - expect(game.textInterceptor.logs).not.toContain( + expect(game).not.toHaveShownMessage( i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(slowking) }), ); }); diff --git a/test/moves/delayed-attack.test.ts b/test/moves/delayed-attack.test.ts index e8cf2871626..868aa48d2f4 100644 --- a/test/moves/delayed-attack.test.ts +++ b/test/moves/delayed-attack.test.ts @@ -96,7 +96,7 @@ describe("Moves - Delayed Attacks", () => { expectFutureSightActive(0); const enemy = game.field.getEnemyPokemon(); expect(enemy.hp).toBeLessThan(enemy.getMaxHp()); - expect(game.textInterceptor.logs).toContain( + expect(game).toHaveShownMessage( i18next.t("moveTriggers:tookMoveAttack", { pokemonName: getPokemonNameWithAffix(enemy), moveName: allMoves[move].name, @@ -224,7 +224,7 @@ describe("Moves - Delayed Attacks", () => { expect(karp.hp).toBe(karp.getMaxHp()); expect(feebas.hp).toBe(feebas.getMaxHp()); - expect(game.textInterceptor.logs).not.toContain( + expect(game).not.toHaveShownMessage( i18next.t("moveTriggers:tookMoveAttack", { pokemonName: getPokemonNameWithAffix(karp), moveName: allMoves[MoveId.FUTURE_SIGHT].name, @@ -254,7 +254,7 @@ describe("Moves - Delayed Attacks", () => { await passTurns(2); expect(enemy1.hp).toBeLessThan(enemy1.getMaxHp()); - expect(game.textInterceptor.logs).toContain( + expect(game).toHaveShownMessage( i18next.t("moveTriggers:tookMoveAttack", { pokemonName: getPokemonNameWithAffix(enemy1), moveName: allMoves[MoveId.FUTURE_SIGHT].name, @@ -282,7 +282,7 @@ describe("Moves - Delayed Attacks", () => { expectFutureSightActive(0); expect(enemy1.hp).toBe(enemy1.getMaxHp()); - expect(game.textInterceptor.logs).not.toContain( + expect(game).not.toHaveShownMessage( i18next.t("moveTriggers:tookMoveAttack", { pokemonName: getPokemonNameWithAffix(enemy1), moveName: allMoves[MoveId.FUTURE_SIGHT].name, @@ -319,7 +319,7 @@ describe("Moves - Delayed Attacks", () => { expect(enemy1.hp).toBe(enemy1.getMaxHp()); expect(enemy2.hp).toBeLessThan(enemy2.getMaxHp()); - expect(game.textInterceptor.logs).toContain( + expect(game).toHaveShownMessage( i18next.t("moveTriggers:tookMoveAttack", { pokemonName: getPokemonNameWithAffix(enemy2), moveName: allMoves[MoveId.FUTURE_SIGHT].name, @@ -352,7 +352,7 @@ describe("Moves - Delayed Attacks", () => { // Player Normalize was not applied due to being off field const enemy = game.field.getEnemyPokemon(); expect(enemy.hp).toBeLessThan(enemy.getMaxHp()); - expect(game.textInterceptor.logs).toContain( + expect(game).toHaveShownMessage( i18next.t("moveTriggers:tookMoveAttack", { pokemonName: getPokemonNameWithAffix(enemy), moveName: allMoves[MoveId.DOOM_DESIRE].name, diff --git a/test/moves/laser-focus.test.ts b/test/moves/laser-focus.test.ts new file mode 100644 index 00000000000..41c3f002899 --- /dev/null +++ b/test/moves/laser-focus.test.ts @@ -0,0 +1,52 @@ +import { getPokemonNameWithAffix } from "#app/messages"; +import { AbilityId } from "#enums/ability-id"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { MoveId } from "#enums/move-id"; +import { SpeciesId } from "#enums/species-id"; +import { GameManager } from "#test/test-utils/game-manager"; +import i18next from "i18next"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Move - Laser Focus", () => { + 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 + .ability(AbilityId.BALL_FETCH) + .battleStyle("single") + .criticalHits(false) + .enemySpecies(SpeciesId.MAGIKARP) + .enemyAbility(AbilityId.BALL_FETCH) + .enemyMoveset(MoveId.SPLASH) + .startingLevel(100) + .enemyLevel(100); + }); + + it("should make the user's next move a guaranteed critical hit", async () => { + await game.classicMode.startBattle([SpeciesId.FEEBAS]); + + game.move.use(MoveId.LASER_FOCUS); + await game.toEndOfTurn(); + + const feebas = game.field.getPlayerPokemon(); + expect(feebas).toHaveBattlerTag(BattlerTagType.ALWAYS_CRIT); + expect(game).toHaveShownMessage( + i18next.t("battlerTags:laserFocusOnAdd", { + pokemonNameWithAffix: getPokemonNameWithAffix(feebas), + }), + ); + }); +}); diff --git a/test/moves/splash-celebrate.test.ts b/test/moves/splash-celebrate.test.ts new file mode 100644 index 00000000000..346ffedd12c --- /dev/null +++ b/test/moves/splash-celebrate.test.ts @@ -0,0 +1,52 @@ +import { loggedInUser } from "#app/account"; +import { AbilityId } from "#enums/ability-id"; +import { MoveId } from "#enums/move-id"; +import { SpeciesId } from "#enums/species-id"; +import { GameManager } from "#test/test-utils/game-manager"; +import i18next from "i18next"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe.each<{ name: string; move: MoveId; message: () => string }>([ + { name: "Splash", move: MoveId.SPLASH, message: () => i18next.t("moveTriggers:splash") }, + { + name: "Celebrate", + move: MoveId.CELEBRATE, + message: () => i18next.t("moveTriggers:celebrate", { playerName: loggedInUser?.username }), + }, +])("Move - $name", ({ move, message }) => { + 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 + .ability(AbilityId.BALL_FETCH) + .battleStyle("single") + .criticalHits(false) + .enemySpecies(SpeciesId.MAGIKARP) + .enemyAbility(AbilityId.BALL_FETCH) + .enemyMoveset(MoveId.TACKLE) + .startingLevel(100) + .enemyLevel(100); + }); + + it("should show a message on use", async () => { + await game.classicMode.startBattle([SpeciesId.FEEBAS]); + + game.move.use(move); + await game.toEndOfTurn(); + + expect(game).toHaveShownMessage(message()); + }); +}); diff --git a/test/moves/wish.test.ts b/test/moves/wish.test.ts index 55877edbfd4..1c1f3f3b8ba 100644 --- a/test/moves/wish.test.ts +++ b/test/moves/wish.test.ts @@ -55,7 +55,7 @@ describe("Move - Wish", () => { await game.toEndOfTurn(); expect(game).toHavePositionalTag(PositionalTagType.WISH, 0); - expect(game.textInterceptor.logs).toContain( + expect(game).toHaveShownMessage( i18next.t("arenaTag:wishTagOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(alomomola), }), @@ -165,7 +165,7 @@ describe("Move - Wish", () => { // Wish went away without doing anything expect(game).toHavePositionalTag(PositionalTagType.WISH, 0); - expect(game.textInterceptor.logs).not.toContain( + expect(game).not.toHaveShownMessage( i18next.t("arenaTag:wishTagOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(blissey), }), diff --git a/test/test-utils/helpers/overrides-helper.ts b/test/test-utils/helpers/overrides-helper.ts index 93b89688935..ecefc83b65b 100644 --- a/test/test-utils/helpers/overrides-helper.ts +++ b/test/test-utils/helpers/overrides-helper.ts @@ -338,7 +338,11 @@ export class OverridesHelper extends GameManagerHelper { /** * Force random critical hit rolls to always or never suceed. * @param crits - `true` to guarantee crits on eligible moves, `false` to force rolls to fail, `null` to disable override - * @remarks This does not bypass effects that guarantee or block critical hits; it merely mocks the chance-based rolls. + * @remarks + * This does not change any effects that guarantee or block critical hits; + * it merely mocks any chance-based rolls not already at 100%. + * For instance, a Pokemon at +3 crit stages will still critically hit with the override set to `false`, + * whereas a Pokemon at +2 crit stages (50% chance) will not. * @returns `this` */ public criticalHits(crits: boolean | null): this { diff --git a/test/test-utils/matchers/to-have-shown-message.ts b/test/test-utils/matchers/to-have-shown-message.ts new file mode 100644 index 00000000000..bf5576ee630 --- /dev/null +++ b/test/test-utils/matchers/to-have-shown-message.ts @@ -0,0 +1,43 @@ +// biome-ignore lint/correctness/noUnusedImports: TSDoc +import type { GameManager } from "#test/test-utils/game-manager"; +import { isGameManagerInstance, receivedStr } from "#test/test-utils/test-utils"; +import { truncateString } from "#utils/common"; +import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; + +/** + * Matcher to check if the {@linkcode GameManager} has shown the given message at least once. + * @param received - The object to check. Should be the current {@linkcode GameManager}. + * @param expectedMessage - The expected message + * @returns The result of the matching + */ +export function toHaveShownMessage( + this: MatcherState, + received: unknown, + expectedMessage: string, +): SyncExpectationResult { + if (!isGameManagerInstance(received)) { + return { + pass: this.isNot, + message: () => `Expected to receive a GameManager, but got ${receivedStr(received)}!`, + }; + } + + if (!received.textInterceptor) { + return { + pass: this.isNot, + message: () => "Expected GameManager.TextInterceptor to be defined!", + }; + } + + // Pass if any of the matching tags meet our criteria + const pass = received.textInterceptor.logs.includes(expectedMessage); + return { + pass, + message: () => + pass + ? `Expected the GameManager to NOT have shown the message ${truncateString(expectedMessage, 30)}, but it did!` + : `Expected the GameManager to have shown the message ${truncateString(expectedMessage, 30)}, but it didn't!`, + expected: expectedMessage, + actual: received.textInterceptor.logs, + }; +}