From 808e29f55eb741c224e4c689aa4f77f1f71a1330 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sat, 2 Aug 2025 18:40:28 -0400 Subject: [PATCH] Added `toHaveArenaTagMatcher` --- src/field/arena.ts | 20 +++--- test/@types/vitest.d.ts | 12 ++++ test/moves/tidy-up.test.ts | 62 +++++-------------- test/test-utils/matchers/to-have-arena-tag.ts | 54 ++++++++++++++++ 4 files changed, 91 insertions(+), 57 deletions(-) create mode 100644 test/test-utils/matchers/to-have-arena-tag.ts diff --git a/src/field/arena.ts b/src/field/arena.ts index f7a516c77ce..2bdc01193fb 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -847,23 +847,21 @@ export class Arena { side: ArenaTagSide, quiet = false, ): void { - const indicesToRemove: number[] = []; - for (const [i, tag] of this.tags.entries()) { + const leftoverTags: ArenaTag[] = []; + for (const tag of this.tags) { // Skip tags of different types or on the wrong side of the field - if (!tagTypes.includes(tag.tagType)) { - continue; - } - if (!(side === ArenaTagSide.BOTH || tag.side === ArenaTagSide.BOTH || tag.side === side)) { + if ( + !tagTypes.includes(tag.tagType) || + !(side === ArenaTagSide.BOTH || tag.side === ArenaTagSide.BOTH || tag.side === side) + ) { + leftoverTags.push(tag); continue; } - indicesToRemove.push(i); + tag.onRemove(this, quiet); } - for (const index of indicesToRemove) { - this.tags[index].onRemove(this, quiet); - this.tags.splice(index, 1); - } + this.tags = leftoverTags; } removeAllTags(): void { diff --git a/test/@types/vitest.d.ts b/test/@types/vitest.d.ts index 58b36580727..7e925445119 100644 --- a/test/@types/vitest.d.ts +++ b/test/@types/vitest.d.ts @@ -1,7 +1,11 @@ import type { Pokemon } from "#field/pokemon"; import type { PokemonType } from "#enums/pokemon-type"; import type { expect } from "vitest"; +import type { Arena } from "#field/arena"; +import type { ArenaTag } from "#data/arena-tag"; import type { toHaveTypesOptions } from "#test/test-utils/matchers/to-have-types"; +import { ArenaTagType } from "#enums/arena-tag-type"; +import { toHaveArenaTagOptions } from "#test/test-utils/matchers/to-have-arena-tag"; declare module "vitest" { interface Assertion { @@ -22,5 +26,13 @@ declare module "vitest" { * @param options - The options passed to the matcher. */ toHaveTypes(expected: PokemonType[], options?: toHaveTypesOptions): void; + /** + * Matcher to check if the current {@linkcode Arena} contains the given {@linkcode ArenaTag}. + * + * @param expected - The expected {@linkcode ArenaTagType} + * @param options - The options passed to the matcher + */ + toHaveArenaTag(expected: T, options?: toHaveArenaTagOptions): void; + } } \ No newline at end of file diff --git a/test/moves/tidy-up.test.ts b/test/moves/tidy-up.test.ts index 8dd74e4ab78..05445ca132b 100644 --- a/test/moves/tidy-up.test.ts +++ b/test/moves/tidy-up.test.ts @@ -1,5 +1,6 @@ import { SubstituteTag } from "#data/battler-tags"; import { AbilityId } from "#enums/ability-id"; +import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagType } from "#enums/arena-tag-type"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; @@ -7,6 +8,7 @@ import { Stat } from "#enums/stat"; import { MoveEndPhase } from "#phases/move-end-phase"; import { TurnEndPhase } from "#phases/turn-end-phase"; import { GameManager } from "#test/test-utils/game-manager"; +import type { ArenaTrapTagType } from "#types/arena-tags"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -31,56 +33,24 @@ describe("Moves - Tidy Up", () => { .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH) - .starterSpecies(SpeciesId.FEEBAS) - .ability(AbilityId.BALL_FETCH) - .moveset([MoveId.TIDY_UP]) - .startingLevel(50); + .ability(AbilityId.BALL_FETCH); }); - it("spikes are cleared", async () => { - game.override.moveset([MoveId.SPIKES, MoveId.TIDY_UP]).enemyMoveset(MoveId.SPIKES); - await game.classicMode.startBattle(); + it.each<{ name: string; hazard: ArenaTrapTagType }>([{ name: "Spikes", hazard: ArenaTagType.SPIKES }])( + "should remove $name from both sides of the field", + async ({ hazard }) => { + await game.classicMode.startBattle([SpeciesId.FEEBAS]); - game.move.select(MoveId.SPIKES); - await game.phaseInterceptor.to(TurnEndPhase); - game.move.select(MoveId.TIDY_UP); - await game.phaseInterceptor.to(MoveEndPhase); - expect(game.scene.arena.getTag(ArenaTagType.SPIKES)).toBeUndefined(); - }); + // Add tag to both sides of the field + game.scene.arena.addTag(hazard, 1, undefined, game.field.getPlayerPokemon().id, ArenaTagSide.PLAYER); + game.scene.arena.addTag(hazard, 1, undefined, game.field.getPlayerPokemon().id, ArenaTagSide.ENEMY); - it("stealth rocks are cleared", async () => { - game.override.moveset([MoveId.STEALTH_ROCK, MoveId.TIDY_UP]).enemyMoveset(MoveId.STEALTH_ROCK); - await game.classicMode.startBattle(); - - game.move.select(MoveId.STEALTH_ROCK); - await game.phaseInterceptor.to(TurnEndPhase); - game.move.select(MoveId.TIDY_UP); - await game.phaseInterceptor.to(MoveEndPhase); - expect(game.scene.arena.getTag(ArenaTagType.STEALTH_ROCK)).toBeUndefined(); - }); - - it("toxic spikes are cleared", async () => { - game.override.moveset([MoveId.TOXIC_SPIKES, MoveId.TIDY_UP]).enemyMoveset(MoveId.TOXIC_SPIKES); - await game.classicMode.startBattle(); - - game.move.select(MoveId.TOXIC_SPIKES); - await game.phaseInterceptor.to(TurnEndPhase); - game.move.select(MoveId.TIDY_UP); - await game.phaseInterceptor.to(MoveEndPhase); - expect(game.scene.arena.getTag(ArenaTagType.TOXIC_SPIKES)).toBeUndefined(); - }); - - it("sticky webs are cleared", async () => { - game.override.moveset([MoveId.STICKY_WEB, MoveId.TIDY_UP]).enemyMoveset(MoveId.STICKY_WEB); - - await game.classicMode.startBattle(); - - game.move.select(MoveId.STICKY_WEB); - await game.phaseInterceptor.to(TurnEndPhase); - game.move.select(MoveId.TIDY_UP); - await game.phaseInterceptor.to(MoveEndPhase); - expect(game.scene.arena.getTag(ArenaTagType.STICKY_WEB)).toBeUndefined(); - }); + expect(game.scene.arena.getTag()); + game.move.use(MoveId.TIDY_UP); + await game.toEndOfTurn(); + expect(game.scene.arena.getTag(ArenaTagType.SPIKES)).toBeUndefined(); + }, + ); it("substitutes are cleared", async () => { game.override.moveset([MoveId.SUBSTITUTE, MoveId.TIDY_UP]).enemyMoveset(MoveId.SUBSTITUTE); diff --git a/test/test-utils/matchers/to-have-arena-tag.ts b/test/test-utils/matchers/to-have-arena-tag.ts new file mode 100644 index 00000000000..cc2155ea3b1 --- /dev/null +++ b/test/test-utils/matchers/to-have-arena-tag.ts @@ -0,0 +1,54 @@ +import type { ArenaTagSide } from "#enums/arena-tag-side"; +import { ArenaTagType } from "#enums/arena-tag-type"; +// biome-ignore lint/correctness/noUnusedImports: TSDoc +import type { GameManager } from "#test/test-utils/game-manager"; +import { getEnumStr, stringifyEnumArray } from "#test/test-utils/string-utils"; +import { isGameManagerInstance, receivedStr } from "#test/test-utils/test-utils"; +import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; + +/** + * Matcher to check if the {@linkcode Arena} has a given {@linkcode ArenaTag} active. + * @param received - The object to check. Should be the current {@linkcode GameManager}. + * @param expectedType - The {@linkcode ArenaTagType} of the desired tag + * @param side - The {@linkcode ArenaTagSide | side of the field} the tag should affect, or + * {@linkcode ArenaTagSide.BOTH} to check both sides. + * @param options - The options passed to this matcher + * @returns The result of the matching + */ +export function toHaveArenaTag( + this: MatcherState, + received: unknown, + expectedType: ArenaTagType, + side: ArenaTagSide, +): SyncExpectationResult { + if (!isGameManagerInstance(received)) { + return { + pass: false, + message: () => `Expected to recieve a GameManager, but got ${receivedStr(received)}!`, + }; + } + + if (!received.scene?.arena) { + return { + pass: false, + message: () => `Expected GameManager.${received.scene ? "scene" : "scene.arena"} to be defined!`, + }; + } + + const tag = received.scene.arena.getTagOnSide(expectedType, side); + const pass = !!tag; + const expectedStr = getEnumStr(ArenaTagType, expectedType); + return { + pass, + message: () => + pass + ? `Expected the arena to NOT have a tag matching ${expectedStr}, but it did!` + : // Replace newlines with spaces to preserve one-line ness + `Expected the arena to have a tag matching ${expectedStr}, but it didn't!`, + expected: getEnumStr(ArenaTagType, expectedType), + actual: stringifyEnumArray( + ArenaTagType, + received.scene.arena.tags.map(t => t.tagType), + ), + }; +}