From da766f364ccae7199c3de5fbd7e21afa0ad3b601 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Thu, 18 Sep 2025 07:52:15 -0500 Subject: [PATCH 1/4] [Tests] Cleanup `getCookie` and add many unit tests (#6562) Cleanup `getCookie` and add many unit tests --- src/utils/cookies.ts | 16 +++++----- test/utils/cookies.test.ts | 62 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 test/utils/cookies.test.ts diff --git a/src/utils/cookies.ts b/src/utils/cookies.ts index e16d9d78556..407bd75da14 100644 --- a/src/utils/cookies.ts +++ b/src/utils/cookies.ts @@ -17,19 +17,17 @@ export function removeCookie(cName: string): void { export function getCookie(cName: string): string { // check if there are multiple cookies with the same name and delete them - if (document.cookie.split(";").filter(c => c.includes(cName)).length > 1) { + if (document.cookie.split(";").filter(c => c.trim().includes(cName)).length > 1) { removeCookie(cName); return ""; } const name = `${cName}=`; - const ca = document.cookie.split(";"); - for (let c of ca) { - // ⚠️ DO NOT REPLACE THIS WITH C = C.TRIM() - IT BREAKS IN NON-CHROMIUM BROWSERS ⚠️ - while (c.charAt(0) === " ") { - c = c.substring(1); - } - if (c.indexOf(name) === 0) { - return c.substring(name.length, c.length); + const cookieArray = document.cookie.split(";"); + // Check all cookies in the document and see if any of them match, grabbing the first one whose value lines up + for (const cookie of cookieArray) { + const cookieTrimmed = cookie.trim(); + if (cookieTrimmed.startsWith(name)) { + return cookieTrimmed.slice(name.length, cookieTrimmed.length); } } return ""; diff --git a/test/utils/cookies.test.ts b/test/utils/cookies.test.ts new file mode 100644 index 00000000000..a5ea248c236 --- /dev/null +++ b/test/utils/cookies.test.ts @@ -0,0 +1,62 @@ +import { getCookie } from "#utils/cookies"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Unit Tests - cookies.ts", () => { + describe("getCookie", () => { + const cookieStart = document.cookie; + beforeEach(() => { + // clear cookie before each test + document.cookie = ""; + }); + + afterEach(() => { + // restore original cookie after each test + document.cookie = cookieStart; + }); + /** + * Spies on `document.cookie` and replaces its value with the provided string. + */ + function setDocumentCookie(value: string) { + vi.spyOn(document, "cookie", "get").mockReturnValue(value); + } + it("returns the value of a single cookie", () => { + setDocumentCookie("foo=bar"); + expect(getCookie("foo")).toBe("bar"); + }); + + it("returns empty string if cookie is not found", () => { + setDocumentCookie("foo=bar"); + expect(getCookie("baz")).toBe(""); + }); + + it("returns the value when multiple cookies exist", () => { + setDocumentCookie("foo=bar; baz=qux"); + expect(getCookie("baz")).toBe("qux"); + }); + + it("trims leading spaces in cookies", () => { + setDocumentCookie("foo=bar; baz=qux"); + expect(getCookie("baz")).toBe("qux"); + }); + + it("returns the value of the first matching cookie if only one exists", () => { + setDocumentCookie("foo=bar; test=val"); + expect(getCookie("foo")).toBe("bar"); + }); + + it("returns empty string if document.cookie is empty", () => { + setDocumentCookie(""); + expect(getCookie("foo")).toBe(""); + }); + + it("handles cookies that aren't separated with a space", () => { + setDocumentCookie("foo=bar;baz=qux;quux=corge;grault=garply"); + expect(getCookie("baz")).toBe("qux"); + }); + + it("handles cookies that may have leading tab characters", () => { + setDocumentCookie("foo=bar;\tbaz=qux"); + expect(getCookie("baz")).toBe("qux"); + }); + }); +}); From 3d9e493e5f550ca933413c015bbb644398b1a8a7 Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Thu, 18 Sep 2025 10:36:08 -0400 Subject: [PATCH 2/4] [Test] Updated more uses of `game.scene.getEnemyField` and `game.scene.getPlayerField` to use updated test utils (#6524) --- test/abilities/ability-duplication.test.ts | 5 +- test/abilities/commander.test.ts | 57 +++++++++---------- test/abilities/dancer.test.ts | 4 +- test/abilities/flower-gift.test.ts | 4 +- test/abilities/flower-veil.test.ts | 2 +- test/abilities/forecast.test.ts | 2 +- test/abilities/magic-bounce.test.ts | 2 +- test/abilities/mirror-armor.test.ts | 3 +- test/abilities/no-guard.test.ts | 2 +- test/abilities/storm-drain.test.ts | 13 ++--- test/abilities/unburden.test.ts | 4 +- test/escape-calculations.test.ts | 1 + .../double-battle-chance-booster.test.ts | 4 +- test/items/grip-claw.test.ts | 4 +- test/items/multi-lens.test.ts | 2 +- test/moves/ability-ignore-moves.test.ts | 4 +- test/moves/defog.test.ts | 20 +++---- test/moves/destiny-bond.test.ts | 6 +- test/moves/dragon-tail.test.ts | 2 +- test/moves/fell-stinger.test.ts | 2 +- test/moves/instruct.test.ts | 6 +- test/moves/jaw-lock.test.ts | 9 +-- test/moves/tailwind.test.ts | 3 +- 23 files changed, 73 insertions(+), 88 deletions(-) diff --git a/test/abilities/ability-duplication.test.ts b/test/abilities/ability-duplication.test.ts index da572d94466..f684500ab90 100644 --- a/test/abilities/ability-duplication.test.ts +++ b/test/abilities/ability-duplication.test.ts @@ -30,12 +30,13 @@ describe("Ability Duplication", () => { .enemyMoveset(MoveId.SPLASH); }); + // TODO: Find a cleaner way of checking ability duplication effects than suppressing the ability it("huge power should only be applied once if both normal and passive", async () => { game.override.passiveAbility(AbilityId.HUGE_POWER); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - const [magikarp] = game.scene.getPlayerField(); + const magikarp = game.field.getPlayerPokemon(); const magikarpAttack = magikarp.getEffectiveStat(Stat.ATK); magikarp.summonData.abilitySuppressed = true; @@ -48,7 +49,7 @@ describe("Ability Duplication", () => { await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - const [magikarp] = game.scene.getPlayerField(); + const magikarp = game.field.getPlayerPokemon(); const magikarpAttack = magikarp.getEffectiveStat(Stat.ATK); magikarp.summonData.abilitySuppressed = true; diff --git a/test/abilities/commander.test.ts b/test/abilities/commander.test.ts index d485cab83a2..8447b2a7d61 100644 --- a/test/abilities/commander.test.ts +++ b/test/abilities/commander.test.ts @@ -5,8 +5,7 @@ import { MoveId } from "#enums/move-id"; import { MoveResult } from "#enums/move-result"; import { PokemonAnimType } from "#enums/pokemon-anim-type"; import { SpeciesId } from "#enums/species-id"; -import type { EffectiveStat } from "#enums/stat"; -import { Stat } from "#enums/stat"; +import { EFFECTIVE_STATS } from "#enums/stat"; import { StatusEffect } from "#enums/status-effect"; import { WeatherType } from "#enums/weather-type"; import { GameManager } from "#test/test-utils/game-manager"; @@ -48,23 +47,24 @@ describe("Abilities - Commander", () => { const [tatsugiri, dondozo] = game.scene.getPlayerField(); - const affectedStats: EffectiveStat[] = [Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD]; - expect(game.scene.triggerPokemonBattleAnim).toHaveBeenLastCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY); - expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeDefined(); - affectedStats.forEach(stat => expect(dondozo.getStatStage(stat)).toBe(2)); - - game.move.select(MoveId.SPLASH, 1); + expect(dondozo).toHaveBattlerTag(BattlerTagType.COMMANDED); + EFFECTIVE_STATS.forEach(stat => { + expect(dondozo).toHaveStatStage(stat, 2); + }); + game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2); expect(game.scene.currentBattle.turnCommands[0]?.skip).toBeTruthy(); // Force both enemies to target the Tatsugiri - await game.move.selectEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER); - await game.move.selectEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER); + await game.move.forceEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER); + await game.move.forceEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER); - await game.phaseInterceptor.to("BerryPhase", false); - game.scene.getEnemyField().forEach(enemy => expect(enemy.getLastXMoves(1)[0].result).toBe(MoveResult.MISS)); - expect(tatsugiri.isFullHp()).toBeTruthy(); + await game.toEndOfTurn(); + const [enemy1, enemy2] = game.scene.getEnemyField(); + expect(enemy1).toHaveUsedMove({ move: MoveId.TACKLE, result: MoveResult.MISS }); + expect(enemy2).toHaveUsedMove({ move: MoveId.TACKLE, result: MoveResult.MISS }); + expect(tatsugiri).toHaveFullHp(); }); it("should activate when a Dondozo switches in and cancel the source's move", async () => { @@ -72,7 +72,7 @@ describe("Abilities - Commander", () => { await game.classicMode.startBattle([SpeciesId.TATSUGIRI, SpeciesId.MAGIKARP, SpeciesId.DONDOZO]); - const tatsugiri = game.scene.getPlayerField()[0]; + const [tatsugiri, _, dondozo] = game.scene.getPlayerParty(); game.move.select(MoveId.LIQUIDATION, 0, BattlerIndex.ENEMY); game.doSwitchPokemon(2); @@ -80,12 +80,11 @@ describe("Abilities - Commander", () => { await game.phaseInterceptor.to("MovePhase", false); expect(game.scene.triggerPokemonBattleAnim).toHaveBeenCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY); - const dondozo = game.scene.getPlayerField()[1]; expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeDefined(); await game.phaseInterceptor.to("BerryPhase", false); expect(tatsugiri.getMoveHistory()).toHaveLength(0); - expect(game.scene.getEnemyField()[0].isFullHp()).toBeTruthy(); + expect(game.field.getEnemyPokemon()).toHaveFullHp(); }); it("source should reenter the field when Dondozo faints", async () => { @@ -192,26 +191,26 @@ describe("Abilities - Commander", () => { }); it("should interrupt the source's semi-invulnerability", async () => { - game.override.moveset([MoveId.SPLASH, MoveId.DIVE]).enemyMoveset(MoveId.SPLASH); - await game.classicMode.startBattle([SpeciesId.TATSUGIRI, SpeciesId.MAGIKARP, SpeciesId.DONDOZO]); - const tatsugiri = game.scene.getPlayerField()[0]; + const [tatsugiri, , dondozo] = game.scene.getPlayerParty(); - game.move.select(MoveId.DIVE, 0, BattlerIndex.ENEMY); - game.move.select(MoveId.SPLASH, 1); + game.move.use(MoveId.DIVE, BattlerIndex.PLAYER, BattlerIndex.ENEMY); + game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2); + await game.move.forceEnemyMove(MoveId.SPLASH); + await game.move.forceEnemyMove(MoveId.SPLASH); await game.toNextTurn(); - expect(tatsugiri.getTag(BattlerTagType.UNDERWATER)).toBeDefined(); + expect(tatsugiri).toHaveBattlerTag(BattlerTagType.UNDERWATER); + game.doSwitchPokemon(2); - await game.phaseInterceptor.to("MovePhase", false); - const dondozo = game.scene.getPlayerField()[1]; - expect(tatsugiri.getTag(BattlerTagType.UNDERWATER)).toBeUndefined(); - expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeDefined(); - await game.toNextTurn(); - const enemy = game.scene.getEnemyField()[0]; - expect(enemy.isFullHp()).toBeTruthy(); + expect(tatsugiri).not.toHaveBattlerTag(BattlerTagType.UNDERWATER); + expect(dondozo).toHaveBattlerTag(BattlerTagType.COMMANDED); + + await game.toEndOfTurn(); + + expect(game.field.getEnemyPokemon()).toHaveFullHp(); }); }); diff --git a/test/abilities/dancer.test.ts b/test/abilities/dancer.test.ts index c651a341c42..e640e326d58 100644 --- a/test/abilities/dancer.test.ts +++ b/test/abilities/dancer.test.ts @@ -74,8 +74,8 @@ describe("Abilities - Dancer", () => { .enemyLevel(10); await game.classicMode.startBattle([SpeciesId.ORICORIO, SpeciesId.FEEBAS]); - const [oricorio] = game.scene.getPlayerField(); - const [, shuckle2] = game.scene.getEnemyField(); + const oricorio = game.field.getPlayerPokemon(); + const shuckle2 = game.scene.getEnemyField()[1]; game.move.select(MoveId.REVELATION_DANCE, BattlerIndex.PLAYER, BattlerIndex.ENEMY_2); game.move.select(MoveId.FIERY_DANCE, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2); diff --git a/test/abilities/flower-gift.test.ts b/test/abilities/flower-gift.test.ts index 6d8641917aa..74be845ffed 100644 --- a/test/abilities/flower-gift.test.ts +++ b/test/abilities/flower-gift.test.ts @@ -58,12 +58,12 @@ describe("Abilities - Flower Gift", () => { const ally_target = allyAttacker ? BattlerIndex.ENEMY : null; await game.classicMode.startBattle([SpeciesId.CHERRIM, SpeciesId.MAGIKARP]); - const target = allyAttacker ? game.scene.getEnemyField()[0] : game.scene.getPlayerField()[1]; + const target = allyAttacker ? game.field.getEnemyPokemon() : game.scene.getPlayerField()[1]; const initialHp = target.getMaxHp(); // Override the ability for the target and attacker only vi.spyOn(game.scene.getPlayerField()[1], "getAbility").mockReturnValue(allAbilities[allyAbility]); - vi.spyOn(game.scene.getEnemyField()[0], "getAbility").mockReturnValue(allAbilities[enemyAbility]); + vi.spyOn(game.field.getEnemyPokemon(), "getAbility").mockReturnValue(allAbilities[enemyAbility]); // turn 1 game.move.select(MoveId.SUNNY_DAY, 0); diff --git a/test/abilities/flower-veil.test.ts b/test/abilities/flower-veil.test.ts index 44274d86a1b..ec34f696bc9 100644 --- a/test/abilities/flower-veil.test.ts +++ b/test/abilities/flower-veil.test.ts @@ -66,7 +66,7 @@ describe("Abilities - Flower Veil", () => { await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.BULBASAUR]); // Clear the ability of the ally to isolate the test - const ally = game.scene.getPlayerField()[1]!; + const ally = game.scene.getPlayerField()[1]; vi.spyOn(ally, "getAbility").mockReturnValue(allAbilities[AbilityId.BALL_FETCH]); game.move.select(MoveId.SPLASH); game.move.select(MoveId.SPLASH); diff --git a/test/abilities/forecast.test.ts b/test/abilities/forecast.test.ts index 87d1d20acdb..9bd40709a94 100644 --- a/test/abilities/forecast.test.ts +++ b/test/abilities/forecast.test.ts @@ -76,7 +76,7 @@ describe("Abilities - Forecast", () => { vi.spyOn(game.scene.getPlayerParty()[5], "getAbility").mockReturnValue(allAbilities[AbilityId.CLOUD_NINE]); - const castform = game.scene.getPlayerField()[0]; + const castform = game.field.getPlayerPokemon(); expect(castform.formIndex).toBe(NORMAL_FORM); game.move.select(MoveId.RAIN_DANCE); diff --git a/test/abilities/magic-bounce.test.ts b/test/abilities/magic-bounce.test.ts index c15690c3f5d..6b7bc7453ed 100644 --- a/test/abilities/magic-bounce.test.ts +++ b/test/abilities/magic-bounce.test.ts @@ -64,7 +64,7 @@ describe("Abilities - Magic Bounce", () => { game.move.use(MoveId.SPLASH, 1); await game.phaseInterceptor.to("BerryPhase"); - const user = game.scene.getPlayerField()[0]; + const user = game.field.getPlayerPokemon(); expect(user.getStatStage(Stat.ATK)).toBe(-2); }); diff --git a/test/abilities/mirror-armor.test.ts b/test/abilities/mirror-armor.test.ts index b2bd9be4755..85d821d0683 100644 --- a/test/abilities/mirror-armor.test.ts +++ b/test/abilities/mirror-armor.test.ts @@ -92,8 +92,7 @@ describe("Ability - Mirror Armor", () => { game.override.battleStyle("double").enemyAbility(AbilityId.MIRROR_ARMOR).ability(AbilityId.INTIMIDATE); await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER]); - const [enemy1, enemy2] = game.scene.getEnemyField(); - const [player1, player2] = game.scene.getPlayerField(); + const [player1, player2, enemy1, enemy2] = game.scene.getField(); // Enemy has intimidate, enemy should lose -1 atk game.move.select(MoveId.SPLASH); diff --git a/test/abilities/no-guard.test.ts b/test/abilities/no-guard.test.ts index 9ce12e710e5..9fc308ab9e3 100644 --- a/test/abilities/no-guard.test.ts +++ b/test/abilities/no-guard.test.ts @@ -58,6 +58,6 @@ describe("Abilities - No Guard", () => { await game.classicMode.startBattle(); - expect(game.scene.getEnemyField().length).toBe(2); + expect(game.scene.getEnemyField()).toHaveLength(2); }); }); diff --git a/test/abilities/storm-drain.test.ts b/test/abilities/storm-drain.test.ts index bc4d4f15cfa..5439459b1dd 100644 --- a/test/abilities/storm-drain.test.ts +++ b/test/abilities/storm-drain.test.ts @@ -37,9 +37,7 @@ describe("Abilities - Storm Drain", () => { it("should redirect water type moves", async () => { await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MAGIKARP]); - const enemy1 = game.scene.getEnemyField()[0]; - const enemy2 = game.scene.getEnemyField()[1]; - + const [enemy1, enemy2] = game.scene.getEnemyField(); game.field.mockAbility(enemy2, AbilityId.STORM_DRAIN); game.move.select(MoveId.WATER_GUN, BattlerIndex.PLAYER, BattlerIndex.ENEMY); @@ -53,8 +51,7 @@ describe("Abilities - Storm Drain", () => { game.override.moveset([MoveId.SPLASH, MoveId.AERIAL_ACE]); await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MAGIKARP]); - const enemy1 = game.scene.getEnemyField()[0]; - const enemy2 = game.scene.getEnemyField()[1]; + const [enemy1, enemy2] = game.scene.getEnemyField(); game.field.mockAbility(enemy2, AbilityId.STORM_DRAIN); @@ -83,8 +80,7 @@ describe("Abilities - Storm Drain", () => { game.override.ability(AbilityId.NORMALIZE); await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MAGIKARP]); - const enemy1 = game.scene.getEnemyField()[0]; - const enemy2 = game.scene.getEnemyField()[1]; + const [enemy1, enemy2] = game.scene.getEnemyField(); game.field.mockAbility(enemy2, AbilityId.STORM_DRAIN); game.move.select(MoveId.WATER_GUN, BattlerIndex.PLAYER, BattlerIndex.ENEMY); @@ -98,8 +94,7 @@ describe("Abilities - Storm Drain", () => { game.override.ability(AbilityId.LIQUID_VOICE); await game.classicMode.startBattle([SpeciesId.FEEBAS]); - const enemy1 = game.scene.getEnemyField()[0]; - const enemy2 = game.scene.getEnemyField()[1]; + const [enemy1, enemy2] = game.scene.getEnemyField(); game.field.mockAbility(enemy2, AbilityId.STORM_DRAIN); diff --git a/test/abilities/unburden.test.ts b/test/abilities/unburden.test.ts index c10dd404ab9..285ea8af32c 100644 --- a/test/abilities/unburden.test.ts +++ b/test/abilities/unburden.test.ts @@ -362,7 +362,7 @@ describe("Abilities - Unburden", () => { .startingHeldItems([{ name: "WIDE_LENS" }]); await game.classicMode.startBattle([SpeciesId.TREECKO, SpeciesId.FEEBAS, SpeciesId.MILOTIC]); - const treecko = game.scene.getPlayerField()[0]; + const treecko = game.field.getPlayerPokemon(); const treeckoInitialHeldItems = getHeldItemCount(treecko); const initialSpeed = treecko.getStat(Stat.SPD); @@ -374,7 +374,7 @@ describe("Abilities - Unburden", () => { game.doSelectPartyPokemon(0, "RevivalBlessingPhase"); await game.toNextTurn(); - expect(game.scene.getPlayerField()[0]).toBe(treecko); + expect(game.field.getPlayerPokemon()).toBe(treecko); expect(getHeldItemCount(treecko)).toBeLessThan(treeckoInitialHeldItems); expect(treecko.getEffectiveStat(Stat.SPD)).toBe(initialSpeed); }); diff --git a/test/escape-calculations.test.ts b/test/escape-calculations.test.ts index fb677e81a45..e1e521f4394 100644 --- a/test/escape-calculations.test.ts +++ b/test/escape-calculations.test.ts @@ -7,6 +7,7 @@ import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +// TODO: These tests are stupid and need to be redone describe("Escape chance calculations", () => { let phaserGame: Phaser.Game; let game: GameManager; diff --git a/test/items/double-battle-chance-booster.test.ts b/test/items/double-battle-chance-booster.test.ts index 2c12b34eba3..ea3c400edb7 100644 --- a/test/items/double-battle-chance-booster.test.ts +++ b/test/items/double-battle-chance-booster.test.ts @@ -31,7 +31,7 @@ describe("Items - Double Battle Chance Boosters", () => { await game.classicMode.startBattle(); - expect(game.scene.getEnemyField().length).toBe(2); + expect(game.scene.getEnemyField()).toHaveLength(2); }); it("should guarantee double boss battle with 3 unique tiers", async () => { @@ -41,7 +41,7 @@ describe("Items - Double Battle Chance Boosters", () => { const enemyField = game.scene.getEnemyField(); - expect(enemyField.length).toBe(2); + expect(enemyField).toHaveLength(2); expect(enemyField[0].isBoss()).toBe(true); expect(enemyField[1].isBoss()).toBe(true); }); diff --git a/test/items/grip-claw.test.ts b/test/items/grip-claw.test.ts index 5ffebd76946..54a40942beb 100644 --- a/test/items/grip-claw.test.ts +++ b/test/items/grip-claw.test.ts @@ -44,7 +44,7 @@ describe("Items - Grip Claw", () => { it("should steal items on contact and only from the attack target", async () => { await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MILOTIC]); - const [playerPokemon] = game.scene.getPlayerField(); + const playerPokemon = game.field.getPlayerPokemon(); const gripClaw = playerPokemon.getHeldItems()[0] as ContactHeldItemTransferChanceModifier; vi.spyOn(gripClaw, "chance", "get").mockReturnValue(100); @@ -73,7 +73,7 @@ describe("Items - Grip Claw", () => { it("should not steal items when using a targetted, non attack move", async () => { await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MILOTIC]); - const [playerPokemon] = game.scene.getPlayerField(); + const playerPokemon = game.field.getPlayerPokemon(); const gripClaw = playerPokemon.getHeldItems()[0] as ContactHeldItemTransferChanceModifier; vi.spyOn(gripClaw, "chance", "get").mockReturnValue(100); diff --git a/test/items/multi-lens.test.ts b/test/items/multi-lens.test.ts index b69a07033c9..3686aff0fcf 100644 --- a/test/items/multi-lens.test.ts +++ b/test/items/multi-lens.test.ts @@ -103,7 +103,7 @@ describe("Items - Multi Lens", () => { await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.FEEBAS]); - const [magikarp] = game.scene.getPlayerField(); + const magikarp = game.field.getPlayerPokemon(); game.move.select(MoveId.SWIFT, 0); game.move.select(MoveId.SPLASH, 1); diff --git a/test/moves/ability-ignore-moves.test.ts b/test/moves/ability-ignore-moves.test.ts index e3a7c7db12f..089af242f87 100644 --- a/test/moves/ability-ignore-moves.test.ts +++ b/test/moves/ability-ignore-moves.test.ts @@ -102,7 +102,7 @@ describe("Moves - Ability-Ignoring Moves", () => { // Both the initial and redirected instruct use ignored sturdy const [enemy1, enemy2] = game.scene.getEnemyField(); - expect(enemy1.isFainted()).toBe(true); - expect(enemy2.isFainted()).toBe(true); + expect(enemy1).toHaveFainted(); + expect(enemy2).toHaveFainted(); }); }); diff --git a/test/moves/defog.test.ts b/test/moves/defog.test.ts index 820dfaa6bcb..4ddb397ee71 100644 --- a/test/moves/defog.test.ts +++ b/test/moves/defog.test.ts @@ -1,4 +1,5 @@ import { AbilityId } from "#enums/ability-id"; +import { ArenaTagType } from "#enums/arena-tag-type"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; @@ -32,26 +33,21 @@ describe("Moves - Defog", () => { .enemyMoveset([MoveId.DEFOG, MoveId.GROWL]); }); + // TODO: Refactor these tests they suck ass it("should not allow Safeguard to be active", async () => { await game.classicMode.startBattle([SpeciesId.REGIELEKI]); - const playerPokemon = game.scene.getPlayerField(); - const enemyPokemon = game.scene.getEnemyField(); + game.scene.arena.addTag(ArenaTagType.SAFEGUARD, 0, 0, 0); - game.move.select(MoveId.SAFEGUARD); - await game.move.selectEnemyMove(MoveId.DEFOG); - await game.phaseInterceptor.to("BerryPhase"); + game.move.use(MoveId.DEFOG); + await game.toEndOfTurn(); - expect(playerPokemon[0].isSafeguarded(enemyPokemon[0])).toBe(false); - - expect(true).toBe(true); + expect(game).not.toHaveArenaTag(ArenaTagType.SAFEGUARD); }); it("should not allow Mist to be active", async () => { await game.classicMode.startBattle([SpeciesId.REGIELEKI]); - const playerPokemon = game.scene.getPlayerField(); - game.move.select(MoveId.MIST); await game.move.selectEnemyMove(MoveId.DEFOG); @@ -62,8 +58,6 @@ describe("Moves - Defog", () => { await game.phaseInterceptor.to("BerryPhase"); - expect(playerPokemon[0].getStatStage(Stat.ATK)).toBe(-1); - - expect(true).toBe(true); + expect(game.field.getPlayerPokemon()).toHaveStatStage(Stat.ATK, -1); }); }); diff --git a/test/moves/destiny-bond.test.ts b/test/moves/destiny-bond.test.ts index 118a45e7682..a5020b83944 100644 --- a/test/moves/destiny-bond.test.ts +++ b/test/moves/destiny-bond.test.ts @@ -160,11 +160,7 @@ describe("Moves - Destiny Bond", () => { game.override.moveset([MoveId.DESTINY_BOND, MoveId.CRUNCH]).battleStyle("double"); await game.classicMode.startBattle([SpeciesId.SHEDINJA, SpeciesId.BULBASAUR, SpeciesId.SQUIRTLE]); - const enemyPokemon0 = game.scene.getEnemyField()[0]; - const enemyPokemon1 = game.scene.getEnemyField()[1]; - const playerPokemon0 = game.scene.getPlayerField()[0]; - const playerPokemon1 = game.scene.getPlayerField()[1]; - + const [playerPokemon0, playerPokemon1, enemyPokemon0, enemyPokemon1] = game.scene.getField(); // Shedinja uses Destiny Bond, then ally Bulbasaur KO's Shedinja with Crunch game.move.select(MoveId.DESTINY_BOND, 0); game.move.select(MoveId.CRUNCH, 1, BattlerIndex.PLAYER); diff --git a/test/moves/dragon-tail.test.ts b/test/moves/dragon-tail.test.ts index e3a5bf459e8..28266465523 100644 --- a/test/moves/dragon-tail.test.ts +++ b/test/moves/dragon-tail.test.ts @@ -171,7 +171,7 @@ describe("Moves - Dragon Tail", () => { const enemy = game.field.getEnemyPokemon(); expect(enemy).toBeDefined(); expect(enemy.hp).toBe(Math.floor(enemy.getMaxHp() / 2)); - expect(game.scene.getEnemyField().length).toBe(1); + expect(game.scene.getEnemyField()).toHaveLength(1); }); it("should not cause a softlock when activating a player's reviver seed", async () => { diff --git a/test/moves/fell-stinger.test.ts b/test/moves/fell-stinger.test.ts index ede70b7af9b..4550cdffa12 100644 --- a/test/moves/fell-stinger.test.ts +++ b/test/moves/fell-stinger.test.ts @@ -107,7 +107,7 @@ describe("Moves - Fell Stinger", () => { await game.classicMode.startBattle([SpeciesId.LEAVANNY]); const leadPokemon = game.field.getPlayerPokemon(); - const leftEnemy = game.scene.getEnemyField()[0]!; + const leftEnemy = game.field.getEnemyPokemon(); // Turn 1: set Salt Cure, enemy splashes and does nothing game.move.select(MoveId.SALT_CURE, 0, leftEnemy.getBattlerIndex()); diff --git a/test/moves/instruct.test.ts b/test/moves/instruct.test.ts index 27318105783..eb3eccff400 100644 --- a/test/moves/instruct.test.ts +++ b/test/moves/instruct.test.ts @@ -498,7 +498,7 @@ describe("Moves - Instruct", () => { .enemyLevel(1); await game.classicMode.startBattle([SpeciesId.KORAIDON, SpeciesId.KLEFKI]); - const koraidon = game.scene.getPlayerField()[0]!; + const koraidon = game.field.getPlayerPokemon(); game.move.select(MoveId.BREAKING_SWIPE); await game.phaseInterceptor.to("TurnEndPhase", false); @@ -527,7 +527,7 @@ describe("Moves - Instruct", () => { .enemyLevel(1); await game.classicMode.startBattle([SpeciesId.KORAIDON, SpeciesId.KLEFKI]); - const koraidon = game.scene.getPlayerField()[0]!; + const koraidon = game.field.getPlayerPokemon(); game.move.select(MoveId.BRUTAL_SWING); await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); @@ -587,7 +587,7 @@ describe("Moves - Instruct", () => { .enemyLevel(5); await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.IVYSAUR]); - const [, ivysaur] = game.scene.getPlayerField(); + const ivysaur = game.scene.getPlayerField()[1]; game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER); game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER_2); diff --git a/test/moves/jaw-lock.test.ts b/test/moves/jaw-lock.test.ts index 441c74c7356..0ea2c0bd8bf 100644 --- a/test/moves/jaw-lock.test.ts +++ b/test/moves/jaw-lock.test.ts @@ -111,7 +111,8 @@ describe("Moves - Jaw Lock", () => { await game.classicMode.startBattle([SpeciesId.CHARMANDER, SpeciesId.BULBASAUR]); - const playerPokemon = game.scene.getPlayerField(); + const playerPokemon = game.field.getPlayerPokemon(); + const enemyPokemon = game.scene.getEnemyField(); game.move.select(MoveId.JAW_LOCK, 0, BattlerIndex.ENEMY); @@ -120,7 +121,7 @@ describe("Moves - Jaw Lock", () => { await game.phaseInterceptor.to(MoveEffectPhase); - expect(playerPokemon[0].getTag(BattlerTagType.TRAPPED)).toBeDefined(); + expect(playerPokemon.getTag(BattlerTagType.TRAPPED)).toBeDefined(); expect(enemyPokemon[0].getTag(BattlerTagType.TRAPPED)).toBeDefined(); await game.toNextTurn(); @@ -131,8 +132,8 @@ describe("Moves - Jaw Lock", () => { await game.phaseInterceptor.to(MoveEffectPhase); expect(enemyPokemon[1].getTag(BattlerTagType.TRAPPED)).toBeUndefined(); - expect(playerPokemon[0].getTag(BattlerTagType.TRAPPED)).toBeDefined(); - expect(playerPokemon[0].getTag(BattlerTagType.TRAPPED)?.sourceId).toBe(enemyPokemon[0].id); + expect(playerPokemon.getTag(BattlerTagType.TRAPPED)).toBeDefined(); + expect(playerPokemon.getTag(BattlerTagType.TRAPPED)?.sourceId).toBe(enemyPokemon[0].id); }); it("should not trap either pokemon if the target is protected", async () => { diff --git a/test/moves/tailwind.test.ts b/test/moves/tailwind.test.ts index 5c91a37f786..d9a0bdeb5f1 100644 --- a/test/moves/tailwind.test.ts +++ b/test/moves/tailwind.test.ts @@ -34,8 +34,7 @@ describe("Moves - Tailwind", () => { it("doubles the Speed stat of the Pokemons on its side", async () => { await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MEOWTH]); - const magikarp = game.scene.getPlayerField()[0]; - const meowth = game.scene.getPlayerField()[1]; + const [magikarp, meowth] = game.scene.getPlayerField(); const magikarpSpd = magikarp.getStat(Stat.SPD); const meowthSpd = meowth.getStat(Stat.SPD); From 0e87391b20c86b596a49e76415264099eeb98950 Mon Sep 17 00:00:00 2001 From: Madmadness65 <59298170+Madmadness65@users.noreply.github.com> Date: Thu, 18 Sep 2025 14:13:52 -0500 Subject: [PATCH 3/4] [Misc] Update biome pools in init functions (#6572) Added new biome entries for Type: Null, Silvally, Poipole, Naganadel, Kubfu, Urshifu, Scientist, and Swimmer to their respective init functions, and reran `outputPools`. --- src/data/balance/biomes.ts | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/data/balance/biomes.ts b/src/data/balance/biomes.ts index b253b0ded6e..9af2dbe221c 100644 --- a/src/data/balance/biomes.ts +++ b/src/data/balance/biomes.ts @@ -1119,7 +1119,7 @@ export const biomePokemonPools: BiomePokemonPools = { }, [BiomePoolTier.RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.HITMONLEE, SpeciesId.HITMONCHAN, SpeciesId.LUCARIO, SpeciesId.THROH, SpeciesId.SAWK, { 1: [ SpeciesId.PANCHAM ], 52: [ SpeciesId.PANGORO ] } ] }, [BiomePoolTier.SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.HITMONTOP, SpeciesId.GALLADE, SpeciesId.GALAR_FARFETCHD ] }, - [BiomePoolTier.ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.TERRAKION, { 1: [ SpeciesId.KUBFU ], 60: [ SpeciesId.URSHIFU] }, SpeciesId.GALAR_ZAPDOS ] }, + [BiomePoolTier.ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.TERRAKION, { 1: [ SpeciesId.KUBFU ], 60: [ SpeciesId.URSHIFU ] }, SpeciesId.GALAR_ZAPDOS ] }, [BiomePoolTier.BOSS]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], @@ -1128,7 +1128,7 @@ export const biomePokemonPools: BiomePokemonPools = { [TimeOfDay.ALL]: [ SpeciesId.HITMONLEE, SpeciesId.HITMONCHAN, SpeciesId.HARIYAMA, SpeciesId.MEDICHAM, SpeciesId.LUCARIO, SpeciesId.TOXICROAK, SpeciesId.THROH, SpeciesId.SAWK, SpeciesId.SCRAFTY, SpeciesId.MIENSHAO, SpeciesId.BEWEAR, SpeciesId.GRAPPLOCT, SpeciesId.ANNIHILAPE ] }, [BiomePoolTier.BOSS_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.HITMONTOP, SpeciesId.GALLADE, SpeciesId.PANGORO, SpeciesId.SIRFETCHD, SpeciesId.HISUI_DECIDUEYE ] }, - [BiomePoolTier.BOSS_SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.TERRAKION, { 1: [ SpeciesId.KUBFU ], 60: [ SpeciesId.URSHIFU] } ] }, + [BiomePoolTier.BOSS_SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.TERRAKION, { 1: [ SpeciesId.KUBFU ], 60: [ SpeciesId.URSHIFU ] } ] }, [BiomePoolTier.BOSS_ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.ZAMAZENTA, SpeciesId.GALAR_ZAPDOS ] } }, [BiomeId.FACTORY]: { @@ -1597,10 +1597,10 @@ export const biomePokemonPools: BiomePokemonPools = { [BiomePoolTier.UNCOMMON]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ { 1: [ SpeciesId.SOLOSIS ], 32: [ SpeciesId.DUOSION ], 41: [ SpeciesId.REUNICLUS ] } ] }, [BiomePoolTier.RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.DITTO, { 1: [ SpeciesId.PORYGON ], 30: [ SpeciesId.PORYGON2 ] } ] }, [BiomePoolTier.SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.ROTOM ] }, - [BiomePoolTier.ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ { 1: [SpeciesId.TYPE_NULL], 60: [ SpeciesId.SILVALLY ] } ] }, + [BiomePoolTier.ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ { 1: [ SpeciesId.TYPE_NULL ], 60: [ SpeciesId.SILVALLY ] } ] }, [BiomePoolTier.BOSS]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.MUK, SpeciesId.ELECTRODE, SpeciesId.BRONZONG, SpeciesId.MAGNEZONE, SpeciesId.PORYGON_Z, SpeciesId.REUNICLUS, SpeciesId.KLINKLANG ] }, [BiomePoolTier.BOSS_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [] }, - [BiomePoolTier.BOSS_SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.ROTOM, SpeciesId.ZYGARDE, { 1: [SpeciesId.TYPE_NULL], 60: [ SpeciesId.SILVALLY ] } ] }, + [BiomePoolTier.BOSS_SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.ROTOM, SpeciesId.ZYGARDE, { 1: [ SpeciesId.TYPE_NULL ], 60: [ SpeciesId.SILVALLY ] } ] }, [BiomePoolTier.BOSS_ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.MEWTWO, SpeciesId.MIRAIDON ] } }, [BiomeId.END]: { @@ -5627,10 +5627,12 @@ export function initBiomes() { ] ], [ SpeciesId.TYPE_NULL, PokemonType.NORMAL, -1, [ - [ BiomeId.LABORATORY, BiomePoolTier.ULTRA_RARE ] + [ BiomeId.LABORATORY, BiomePoolTier.ULTRA_RARE ], + [ BiomeId.LABORATORY, BiomePoolTier.BOSS_SUPER_RARE ] ] ], [ SpeciesId.SILVALLY, PokemonType.NORMAL, -1, [ + [ BiomeId.LABORATORY, BiomePoolTier.ULTRA_RARE ], [ BiomeId.LABORATORY, BiomePoolTier.BOSS_SUPER_RARE ] ] ], @@ -5773,10 +5775,12 @@ export function initBiomes() { ] ], [ SpeciesId.POIPOLE, PokemonType.POISON, -1, [ - [ BiomeId.SWAMP, BiomePoolTier.ULTRA_RARE ] + [ BiomeId.SWAMP, BiomePoolTier.ULTRA_RARE ], + [ BiomeId.SWAMP, BiomePoolTier.BOSS_SUPER_RARE ] ] ], [ SpeciesId.NAGANADEL, PokemonType.POISON, PokemonType.DRAGON, [ + [ BiomeId.SWAMP, BiomePoolTier.ULTRA_RARE ], [ BiomeId.SWAMP, BiomePoolTier.BOSS_SUPER_RARE ] ] ], @@ -6165,10 +6169,12 @@ export function initBiomes() { ] ], [ SpeciesId.KUBFU, PokemonType.FIGHTING, -1, [ - [ BiomeId.DOJO, BiomePoolTier.ULTRA_RARE ] + [ BiomeId.DOJO, BiomePoolTier.ULTRA_RARE ], + [ BiomeId.DOJO, BiomePoolTier.BOSS_SUPER_RARE ] ] ], [ SpeciesId.URSHIFU, PokemonType.FIGHTING, PokemonType.DARK, [ + [ BiomeId.DOJO, BiomePoolTier.ULTRA_RARE ], [ BiomeId.DOJO, BiomePoolTier.BOSS_SUPER_RARE ] ] ], @@ -7209,7 +7215,8 @@ export function initBiomes() { ], [ TrainerType.SCIENTIST, [ [ BiomeId.DESERT, BiomePoolTier.COMMON ], - [ BiomeId.RUINS, BiomePoolTier.COMMON ] + [ BiomeId.RUINS, BiomePoolTier.COMMON ], + [ BiomeId.LABORATORY, BiomePoolTier.COMMON ] ] ], [ TrainerType.SMASHER, []], @@ -7224,7 +7231,8 @@ export function initBiomes() { ] ], [ TrainerType.SWIMMER, [ - [ BiomeId.SEA, BiomePoolTier.COMMON ] + [ BiomeId.SEA, BiomePoolTier.COMMON ], + [ BiomeId.SEABED, BiomePoolTier.COMMON ] ] ], [ TrainerType.TWINS, [ @@ -7590,11 +7598,13 @@ export function initBiomes() { [ TrainerType.ALDER, []], [ TrainerType.IRIS, []], [ TrainerType.DIANTHA, []], + [ TrainerType.KUKUI, []], [ TrainerType.HAU, []], + [ TrainerType.LEON, []], + [ TrainerType.MUSTARD, []], [ TrainerType.GEETA, []], [ TrainerType.NEMONA, []], [ TrainerType.KIERAN, []], - [ TrainerType.LEON, []], [ TrainerType.RIVAL, []] ]; From 7d83a3a24a83b5d3dbebbe46e977c4229d70bf23 Mon Sep 17 00:00:00 2001 From: Fabi <192151969+fabske0@users.noreply.github.com> Date: Thu, 18 Sep 2025 22:35:22 +0200 Subject: [PATCH 4/4] [UI/UX] Show correct max duration in flyout (#6566) * Fix terrain & weather max duration flyout * show correct max duration for tags * maka maxDuration optional in arenaEvent constructor * Apply suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/data/arena-tag.ts | 7 +++++++ src/data/terrain.ts | 6 ++++-- src/data/weather.ts | 6 ++++-- src/events/arena.ts | 16 ++++++++++------ src/field/arena.ts | 10 +++++----- src/system/arena-data.ts | 8 ++++++-- src/system/game-data.ts | 10 +++++++--- src/ui/containers/arena-flyout.ts | 4 ++-- 8 files changed, 45 insertions(+), 22 deletions(-) diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index ff939194bcd..7d78076e06b 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -84,6 +84,10 @@ interface BaseArenaTag { * The tag's remaining duration. Setting to any number `<=0` will make the tag's duration effectively infinite. */ turnCount: number; + /** + * The tag's max duration. + */ + maxDuration: number; /** * The {@linkcode MoveId} that created this tag, or `undefined` if not set by a move. */ @@ -110,12 +114,14 @@ export abstract class ArenaTag implements BaseArenaTag { /** The type of the arena tag */ public abstract readonly tagType: ArenaTagType; public turnCount: number; + public maxDuration: number; public sourceMove?: MoveId; public sourceId: number | undefined; public side: ArenaTagSide; constructor(turnCount: number, sourceMove?: MoveId, sourceId?: number, side: ArenaTagSide = ArenaTagSide.BOTH) { this.turnCount = turnCount; + this.maxDuration = turnCount; this.sourceMove = sourceMove; this.sourceId = sourceId; this.side = side; @@ -164,6 +170,7 @@ export abstract class ArenaTag implements BaseArenaTag { */ loadTag(source: BaseArenaTag & Pick): void { this.turnCount = source.turnCount; + this.maxDuration = source.maxDuration; this.sourceMove = source.sourceMove; this.sourceId = source.sourceId; this.side = source.side; diff --git a/src/data/terrain.ts b/src/data/terrain.ts index 139230605bf..315ed919e03 100644 --- a/src/data/terrain.ts +++ b/src/data/terrain.ts @@ -22,10 +22,12 @@ export interface SerializedTerrain { export class Terrain { public terrainType: TerrainType; public turnsLeft: number; + public maxDuration: number; - constructor(terrainType: TerrainType, turnsLeft?: number) { + constructor(terrainType: TerrainType, turnsLeft = 0, maxDuration: number = turnsLeft) { this.terrainType = terrainType; - this.turnsLeft = turnsLeft || 0; + this.turnsLeft = turnsLeft; + this.maxDuration = maxDuration; } lapse(): boolean { diff --git a/src/data/weather.ts b/src/data/weather.ts index 84a5e1ba4f8..49af505dc62 100644 --- a/src/data/weather.ts +++ b/src/data/weather.ts @@ -19,10 +19,12 @@ export interface SerializedWeather { export class Weather { public weatherType: WeatherType; public turnsLeft: number; + public maxDuration: number; - constructor(weatherType: WeatherType, turnsLeft?: number) { + constructor(weatherType: WeatherType, turnsLeft = 0, maxDuration: number = turnsLeft) { this.weatherType = weatherType; - this.turnsLeft = !this.isImmutable() ? turnsLeft || 0 : 0; + this.turnsLeft = this.isImmutable() ? 0 : turnsLeft; + this.maxDuration = this.isImmutable() ? 0 : maxDuration; } lapse(): boolean { diff --git a/src/events/arena.ts b/src/events/arena.ts index cf287de3176..9f818a36c89 100644 --- a/src/events/arena.ts +++ b/src/events/arena.ts @@ -20,10 +20,13 @@ export enum ArenaEventType { export class ArenaEvent extends Event { /** The total duration of the {@linkcode ArenaEventType} */ public duration: number; - constructor(eventType: ArenaEventType, duration: number) { + /** The maximum duration of the {@linkcode ArenaEventType} */ + public maxDuration: number; + constructor(eventType: ArenaEventType, duration: number, maxDuration: number = duration) { super(eventType); this.duration = duration; + this.maxDuration = maxDuration; } } /** Container class for {@linkcode ArenaEventType.WEATHER_CHANGED} events */ @@ -32,8 +35,8 @@ export class WeatherChangedEvent extends ArenaEvent { public oldWeatherType: WeatherType; /** The {@linkcode WeatherType} being set */ public newWeatherType: WeatherType; - constructor(oldWeatherType: WeatherType, newWeatherType: WeatherType, duration: number) { - super(ArenaEventType.WEATHER_CHANGED, duration); + constructor(oldWeatherType: WeatherType, newWeatherType: WeatherType, duration: number, maxDuration?: number) { + super(ArenaEventType.WEATHER_CHANGED, duration, maxDuration); this.oldWeatherType = oldWeatherType; this.newWeatherType = newWeatherType; @@ -45,8 +48,8 @@ export class TerrainChangedEvent extends ArenaEvent { public oldTerrainType: TerrainType; /** The {@linkcode TerrainType} being set */ public newTerrainType: TerrainType; - constructor(oldTerrainType: TerrainType, newTerrainType: TerrainType, duration: number) { - super(ArenaEventType.TERRAIN_CHANGED, duration); + constructor(oldTerrainType: TerrainType, newTerrainType: TerrainType, duration: number, maxDuration?: number) { + super(ArenaEventType.TERRAIN_CHANGED, duration, maxDuration); this.oldTerrainType = oldTerrainType; this.newTerrainType = newTerrainType; @@ -68,10 +71,11 @@ export class TagAddedEvent extends ArenaEvent { arenaTagType: ArenaTagType, arenaTagSide: ArenaTagSide, duration: number, + maxDuration?: number, arenaTagLayers?: number, arenaTagMaxLayers?: number, ) { - super(ArenaEventType.TAG_ADDED, duration); + super(ArenaEventType.TAG_ADDED, duration, maxDuration); this.arenaTagType = arenaTagType; this.arenaTagSide = arenaTagSide; diff --git a/src/field/arena.ts b/src/field/arena.ts index c4708be1336..5ab50e540ee 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -344,7 +344,7 @@ export class Arena { globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, weatherDuration); } - this.weather = weather ? new Weather(weather, weatherDuration.value) : null; + this.weather = weather ? new Weather(weather, weatherDuration.value, weatherDuration.value) : null; this.eventTarget.dispatchEvent( new WeatherChangedEvent(oldWeatherType, this.weather?.weatherType!, this.weather?.turnsLeft!), ); // TODO: is this bang correct? @@ -425,7 +425,7 @@ export class Arena { globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, terrainDuration); } - this.terrain = terrain ? new Terrain(terrain, terrainDuration.value) : null; + this.terrain = terrain ? new Terrain(terrain, terrainDuration.value, terrainDuration.value) : null; this.eventTarget.dispatchEvent( new TerrainChangedEvent(oldTerrainType, this.terrain?.terrainType!, this.terrain?.turnsLeft!), @@ -705,8 +705,8 @@ export class Arena { existingTag.onOverlap(this, globalScene.getPokemonById(sourceId)); if (existingTag instanceof EntryHazardTag) { - const { tagType, side, turnCount, layers, maxLayers } = existingTag as EntryHazardTag; - this.eventTarget.dispatchEvent(new TagAddedEvent(tagType, side, turnCount, layers, maxLayers)); + const { tagType, side, turnCount, maxDuration, layers, maxLayers } = existingTag as EntryHazardTag; + this.eventTarget.dispatchEvent(new TagAddedEvent(tagType, side, turnCount, maxDuration, layers, maxLayers)); } return false; @@ -721,7 +721,7 @@ export class Arena { const { layers = 0, maxLayers = 0 } = newTag instanceof EntryHazardTag ? newTag : {}; this.eventTarget.dispatchEvent( - new TagAddedEvent(newTag.tagType, newTag.side, newTag.turnCount, layers, maxLayers), + new TagAddedEvent(newTag.tagType, newTag.side, newTag.turnCount, newTag.maxDuration, layers, maxLayers), ); } diff --git a/src/system/arena-data.ts b/src/system/arena-data.ts index 18620e15223..0d40a9c6234 100644 --- a/src/system/arena-data.ts +++ b/src/system/arena-data.ts @@ -47,8 +47,12 @@ export class ArenaData { } this.biome = source.biome; - this.weather = source.weather ? new Weather(source.weather.weatherType, source.weather.turnsLeft) : null; - this.terrain = source.terrain ? new Terrain(source.terrain.terrainType, source.terrain.turnsLeft) : null; + this.weather = source.weather + ? new Weather(source.weather.weatherType, source.weather.turnsLeft, source.weather.maxDuration) + : null; + this.terrain = source.terrain + ? new Terrain(source.terrain.terrainType, source.terrain.turnsLeft, source.terrain.maxDuration) + : null; this.positionalTags = source.positionalTags ?? []; } } diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 8c2a1219245..3ffa7482706 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -1021,6 +1021,7 @@ export class GameData { WeatherType.NONE, globalScene.arena.weather?.weatherType!, globalScene.arena.weather?.turnsLeft!, + globalScene.arena.weather?.maxDuration!, ), ); // TODO: is this bang correct? @@ -1030,6 +1031,7 @@ export class GameData { TerrainType.NONE, globalScene.arena.terrain?.terrainType!, globalScene.arena.terrain?.turnsLeft!, + globalScene.arena.terrain?.maxDuration!, ), ); // TODO: is this bang correct? @@ -1039,12 +1041,14 @@ export class GameData { if (globalScene.arena.tags) { for (const tag of globalScene.arena.tags) { if (tag instanceof EntryHazardTag) { - const { tagType, side, turnCount, layers, maxLayers } = tag as EntryHazardTag; + const { tagType, side, turnCount, maxDuration, layers, maxLayers } = tag as EntryHazardTag; globalScene.arena.eventTarget.dispatchEvent( - new TagAddedEvent(tagType, side, turnCount, layers, maxLayers), + new TagAddedEvent(tagType, side, turnCount, maxDuration, layers, maxLayers), ); } else { - globalScene.arena.eventTarget.dispatchEvent(new TagAddedEvent(tag.tagType, tag.side, tag.turnCount)); + globalScene.arena.eventTarget.dispatchEvent( + new TagAddedEvent(tag.tagType, tag.side, tag.turnCount, tag.maxDuration), + ); } } } diff --git a/src/ui/containers/arena-flyout.ts b/src/ui/containers/arena-flyout.ts index a73846de1ac..ab95d1a3e7a 100644 --- a/src/ui/containers/arena-flyout.ts +++ b/src/ui/containers/arena-flyout.ts @@ -317,7 +317,7 @@ export class ArenaFlyout extends Phaser.GameObjects.Container { this.fieldEffectInfo.push({ name, effectType: arenaEffectType, - maxDuration: tagAddedEvent.duration, + maxDuration: tagAddedEvent.maxDuration, duration: tagAddedEvent.duration, tagType: tagAddedEvent.arenaTagType, }); @@ -353,7 +353,7 @@ export class ArenaFlyout extends Phaser.GameObjects.Container { ), effectType: fieldEffectChangedEvent instanceof WeatherChangedEvent ? ArenaEffectType.WEATHER : ArenaEffectType.TERRAIN, - maxDuration: fieldEffectChangedEvent.duration, + maxDuration: fieldEffectChangedEvent.maxDuration, duration: fieldEffectChangedEvent.duration, };