From 848c1f01e0fb3d241fae33e8974a82efb3f7b924 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Tue, 2 Sep 2025 11:14:55 -0500 Subject: [PATCH 01/10] chore: bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 574beb80466..b7aebd879d2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "pokemon-rogue-battle", "private": true, - "version": "1.10.6", + "version": "1.10.7", "type": "module", "scripts": { "start": "vite", From 9fc31350f895a497559a5b1bbfe800156e73386f Mon Sep 17 00:00:00 2001 From: Fabi <192151969+fabske0@users.noreply.github.com> Date: Wed, 3 Sep 2025 04:07:40 +0200 Subject: [PATCH 02/10] [Bug] Fix monotype selector image (#6471) --- src/data/challenge.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/challenge.ts b/src/data/challenge.ts index cea8661e78c..11adec79203 100644 --- a/src/data/challenge.ts +++ b/src/data/challenge.ts @@ -764,7 +764,7 @@ export class SingleTypeChallenge extends Challenge { } getValue(overrideValue: number = this.value): string { - return i18next.t(`pokemonInfo:type.${toCamelCase(PokemonType[overrideValue - 1])}`); + return PokemonType[overrideValue - 1].toLowerCase(); } getDescription(overrideValue: number = this.value): string { From 4a2877392985a84496fcab1b217494750f61fe18 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Tue, 2 Sep 2025 21:44:43 -0500 Subject: [PATCH 03/10] [Bug] [Move] Fix poltergeist crash when no remaining enemies (#6473) * fix: poltergeist crash with no target * fix: adjust move phase history --- src/data/moves/move.ts | 3 ++ src/phases/move-phase.ts | 39 ++++++++++++++++---------- test/moves/poltergeist.test.ts | 50 ++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 15 deletions(-) create mode 100644 test/moves/poltergeist.test.ts diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 5a22b352e73..1c64b28fa75 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -8164,6 +8164,9 @@ const failIfGhostTypeCondition: MoveConditionFunc = (user: Pokemon, target: Poke const failIfNoTargetHeldItemsCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => target.getHeldItems().filter(i => i.isTransferable)?.length > 0; const attackedByItemMessageFunc = (user: Pokemon, target: Pokemon, move: Move) => { + if (isNullOrUndefined(target)) { // Fix bug when used against targets that have both fainted + return ""; + } const heldItems = target.getHeldItems().filter(i => i.isTransferable); if (heldItems.length === 0) { return ""; diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 9a8e509e302..dd73227a4a8 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -24,6 +24,7 @@ import { applyMoveAttrs } from "#moves/apply-attrs"; import { frenzyMissFunc } from "#moves/move-utils"; import type { PokemonMove } from "#moves/pokemon-move"; import { BattlePhase } from "#phases/battle-phase"; +import type { TurnMove } from "#types/turn-move"; import { NumberHolder } from "#utils/common"; import { enumValueToKey } from "#utils/enums"; import i18next from "i18next"; @@ -41,6 +42,13 @@ export class MovePhase extends BattlePhase { /** Whether the current move should fail and retain PP. */ protected cancelled = false; + /** The move history entry object that is pushed to the pokemon's move history + * + * @remarks + * Can be edited _after_ being pushed to the history to adjust the result, targets, etc, for this move phase. + */ + protected moveHistoryEntry: TurnMove; + public get pokemon(): Pokemon { return this._pokemon; } @@ -82,6 +90,11 @@ export class MovePhase extends BattlePhase { this.move = move; this.useMode = useMode; this.forcedLast = forcedLast; + this.moveHistoryEntry = { + move: MoveId.NONE, + targets, + useMode, + }; } /** @@ -410,13 +423,9 @@ export class MovePhase extends BattlePhase { if (showText) { this.showMoveText(); } - - this.pokemon.pushMoveHistory({ - move: this.move.moveId, - targets: this.targets, - result: MoveResult.FAIL, - useMode: this.useMode, - }); + const moveHistoryEntry = this.moveHistoryEntry; + moveHistoryEntry.result = MoveResult.FAIL; + this.pokemon.pushMoveHistory(moveHistoryEntry); // Use move-specific failure messages if present before checking terrain/weather blockage // and falling back to the classic "But it failed!". @@ -630,12 +639,9 @@ export class MovePhase extends BattlePhase { frenzyMissFunc(this.pokemon, this.move.getMove()); } - this.pokemon.pushMoveHistory({ - move: MoveId.NONE, - result: MoveResult.FAIL, - targets: this.targets, - useMode: this.useMode, - }); + const moveHistoryEntry = this.moveHistoryEntry; + moveHistoryEntry.result = MoveResult.FAIL; + this.pokemon.pushMoveHistory(moveHistoryEntry); this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT); this.pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE); @@ -649,13 +655,16 @@ export class MovePhase extends BattlePhase { * Displays the move's usage text to the player as applicable for the move being used. */ public showMoveText(): void { + const moveId = this.move.moveId; if ( - this.move.moveId === MoveId.NONE || + moveId === MoveId.NONE || this.pokemon.getTag(BattlerTagType.RECHARGING) || this.pokemon.getTag(BattlerTagType.INTERRUPTED) ) { return; } + // Showing move text always adjusts the move history entry's move id + this.moveHistoryEntry.move = moveId; // TODO: This should be done by the move... globalScene.phaseManager.queueMessage( @@ -668,7 +677,7 @@ export class MovePhase extends BattlePhase { // Moves with pre-use messages (Magnitude, Chilly Reception, Fickle Beam, etc.) always display their messages even on failure // TODO: This assumes single target for message funcs - is this sustainable? - applyMoveAttrs("PreMoveMessageAttr", this.pokemon, this.pokemon.getOpponents(false)[0], this.move.getMove()); + applyMoveAttrs("PreMoveMessageAttr", this.pokemon, this.getActiveTargetPokemon()[0], this.move.getMove()); } /** diff --git a/test/moves/poltergeist.test.ts b/test/moves/poltergeist.test.ts new file mode 100644 index 00000000000..3e603702416 --- /dev/null +++ b/test/moves/poltergeist.test.ts @@ -0,0 +1,50 @@ +import { AbilityId } from "#enums/ability-id"; +import { BattlerIndex } from "#enums/battler-index"; +import { MoveId } from "#enums/move-id"; +import { MoveResult } from "#enums/move-result"; +import { SpeciesId } from "#enums/species-id"; +import { GameManager } from "#test/test-utils/game-manager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Move - Poltergeist", () => { + 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 not crash when used after both opponents have fainted", async () => { + game.override.battleStyle("double").enemyLevel(5); + await game.classicMode.startBattle([SpeciesId.STARYU, SpeciesId.SLOWPOKE]); + + game.move.use(MoveId.DAZZLING_GLEAM); + game.move.use(MoveId.POLTERGEIST, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY); + const [_, poltergeistUser] = game.scene.getPlayerField(); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]); + await game.toEndOfTurn(); + // Expect poltergeist to have failed + expect(poltergeistUser).toHaveUsedMove({ move: MoveId.POLTERGEIST, result: MoveResult.FAIL }); + // If the test makes it to the end of turn, no crash occurred. Nothing to assert + }); +}); From ddde977a0a485f327a041bedb0d7b249df4770bf Mon Sep 17 00:00:00 2001 From: Fabi <192151969+fabske0@users.noreply.github.com> Date: Thu, 4 Sep 2025 11:31:52 +0200 Subject: [PATCH 04/10] [UI/UX] Auto focus first input field (#6413) --- src/ui/form-modal-ui-handler.ts | 5 +++++ src/ui/pokedex-scan-ui-handler.ts | 4 ---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/ui/form-modal-ui-handler.ts b/src/ui/form-modal-ui-handler.ts index 5c547465de9..c6ef8705aa8 100644 --- a/src/ui/form-modal-ui-handler.ts +++ b/src/ui/form-modal-ui-handler.ts @@ -136,6 +136,11 @@ export abstract class FormModalUiHandler extends ModalUiHandler { this.submitAction = config.buttonActions.length ? config.buttonActions[0] : null; this.cancelAction = config.buttonActions[1] ?? null; + // Auto focus the first input field after a short delay, to prevent accidental inputs + setTimeout(() => { + this.inputs[0].setFocus(); + }, 50); + // #region: Override button pointerDown // Override the pointerDown event for the buttonBgs to call the `submitAction` and `cancelAction` // properties that we set above, allowing their behavior to change after this method terminates diff --git a/src/ui/pokedex-scan-ui-handler.ts b/src/ui/pokedex-scan-ui-handler.ts index 4f606cbcbb0..9be7a903dec 100644 --- a/src/ui/pokedex-scan-ui-handler.ts +++ b/src/ui/pokedex-scan-ui-handler.ts @@ -106,10 +106,6 @@ export class PokedexScanUiHandler extends FormModalUiHandler { this.reduceKeys(); - setTimeout(() => { - input.setFocus(); // Focus after a short delay to avoid unwanted input - }, 50); - input.on("keydown", (inputObject, evt: KeyboardEvent) => { if ( ["escape", "space"].some(v => v === evt.key.toLowerCase() || v === evt.code.toLowerCase()) && From 309e31e196f005911b65eecc3fba8828e3e73b47 Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Fri, 5 Sep 2025 14:38:01 -0400 Subject: [PATCH 05/10] [Bug] Future Sight no longer crashes after catching the user (#6479) --- src/battle-scene.ts | 2 + src/data/battle-anims.ts | 2 +- src/data/positional-tags/positional-tag.ts | 4 +- test/moves/delayed-attack.test.ts | 69 ++++++++++++++++------ test/test-utils/phase-interceptor.ts | 2 + 5 files changed, 58 insertions(+), 21 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 9ac6e385220..3a1de1bcc43 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -863,6 +863,8 @@ export class BattleScene extends SceneBase { * @param pokemonId - The ID whose Pokemon will be retrieved. * @returns The {@linkcode Pokemon} associated with the given id. * Returns `null` if the ID is `undefined` or not present in either party. + * @todo Change the `null` to `undefined` and update callers' signatures - + * this is weird and causes a lot of random jank */ getPokemonById(pokemonId: number | undefined): Pokemon | null { if (isNullOrUndefined(pokemonId)) { diff --git a/src/data/battle-anims.ts b/src/data/battle-anims.ts index 5ff4472d148..85b15c934be 100644 --- a/src/data/battle-anims.ts +++ b/src/data/battle-anims.ts @@ -835,7 +835,7 @@ export abstract class BattleAnim { // biome-ignore lint/complexity/noBannedTypes: callback is used liberally play(onSubstitute?: boolean, callback?: Function) { const isOppAnim = this.isOppAnim(); - const user = !isOppAnim ? this.user! : this.target!; // TODO: are those bangs correct? + const user = !isOppAnim ? this.user! : this.target!; // TODO: These bangs are LITERALLY not correct at all const target = !isOppAnim ? this.target! : this.user!; if (!target?.isOnField() && !this.playRegardlessOfIssues) { diff --git a/src/data/positional-tags/positional-tag.ts b/src/data/positional-tags/positional-tag.ts index 77ca6f0e9eb..a877b45b045 100644 --- a/src/data/positional-tags/positional-tag.ts +++ b/src/data/positional-tags/positional-tag.ts @@ -126,7 +126,9 @@ export class DelayedAttackTag extends PositionalTag implements DelayedAttackArgs // Silently disappear if either source or target are missing or happen to be the same pokemon // (i.e. targeting oneself) // We also need to check for fainted targets as they don't technically leave the field until _after_ the turn ends - return !!source && !!target && source !== target && !target.isFainted(); + // TODO: Figure out a way to store the target's offensive stat if they faint to allow pending attacks to persist + // TODO: Remove the `?.scene` checks once battle anims are cleaned up - needed to avoid catch+release crash + return !!source?.scene && !!target?.scene && source !== target && !target.isFainted(); } } diff --git a/test/moves/delayed-attack.test.ts b/test/moves/delayed-attack.test.ts index e8cf2871626..420ef6d1f00 100644 --- a/test/moves/delayed-attack.test.ts +++ b/test/moves/delayed-attack.test.ts @@ -4,12 +4,15 @@ import { allMoves } from "#data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { BattleType } from "#enums/battle-type"; import { BattlerIndex } from "#enums/battler-index"; +import { Button } from "#enums/buttons"; import { MoveId } from "#enums/move-id"; import { MoveResult } from "#enums/move-result"; +import { PokeballType } from "#enums/pokeball"; 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 { UiMode } from "#enums/ui-mode"; import { GameManager } from "#test/test-utils/game-manager"; import i18next from "i18next"; import Phaser from "phaser"; @@ -95,7 +98,7 @@ describe("Moves - Delayed Attacks", () => { expectFutureSightActive(0); const enemy = game.field.getEnemyPokemon(); - expect(enemy.hp).toBeLessThan(enemy.getMaxHp()); + expect(enemy).not.toHaveFullHp(); expect(game.textInterceptor.logs).toContain( i18next.t("moveTriggers:tookMoveAttack", { pokemonName: getPokemonNameWithAffix(enemy), @@ -130,12 +133,12 @@ describe("Moves - Delayed Attacks", () => { expectFutureSightActive(); const enemy = game.field.getEnemyPokemon(); - expect(enemy.hp).toBe(enemy.getMaxHp()); + expect(enemy).toHaveFullHp(); await passTurns(2); expectFutureSightActive(0); - expect(enemy.hp).toBeLessThan(enemy.getMaxHp()); + expect(enemy).not.toHaveFullHp(); }); it("should work when used against different targets in doubles", async () => { @@ -149,15 +152,15 @@ describe("Moves - Delayed Attacks", () => { await game.toEndOfTurn(); expectFutureSightActive(2); - expect(enemy1.hp).toBe(enemy1.getMaxHp()); - expect(enemy2.hp).toBe(enemy2.getMaxHp()); + expect(enemy1).toHaveFullHp(); + expect(enemy2).toHaveFullHp(); expect(karp.getLastXMoves()[0].result).toBe(MoveResult.OTHER); expect(feebas.getLastXMoves()[0].result).toBe(MoveResult.OTHER); await passTurns(2); - expect(enemy1.hp).toBeLessThan(enemy1.getMaxHp()); - expect(enemy2.hp).toBeLessThan(enemy2.getMaxHp()); + expect(enemy1).not.toHaveFullHp(); + expect(enemy2).not.toHaveFullHp(); }); it("should trigger multiple pending attacks in order of creation, even if that order changes later on", async () => { @@ -222,8 +225,8 @@ describe("Moves - Delayed Attacks", () => { expect(game.scene.getPlayerParty()).toEqual([milotic, karp, feebas]); - expect(karp.hp).toBe(karp.getMaxHp()); - expect(feebas.hp).toBe(feebas.getMaxHp()); + expect(karp).toHaveFullHp(); + expect(feebas).toHaveFullHp(); expect(game.textInterceptor.logs).not.toContain( i18next.t("moveTriggers:tookMoveAttack", { pokemonName: getPokemonNameWithAffix(karp), @@ -245,15 +248,14 @@ describe("Moves - Delayed Attacks", () => { expect(enemy2.isFainted()).toBe(true); expectFutureSightActive(); - const attack = game.scene.arena.positionalTagManager.tags.find( - t => t.tagType === PositionalTagType.DELAYED_ATTACK, - )!; - expect(attack).toBeDefined(); - expect(attack.targetIndex).toBe(enemy1.getBattlerIndex()); + expect(game).toHavePositionalTag({ + tagType: PositionalTagType.DELAYED_ATTACK, + targetIndex: enemy1.getBattlerIndex(), + }); await passTurns(2); - expect(enemy1.hp).toBeLessThan(enemy1.getMaxHp()); + expect(enemy1).not.toHaveFullHp(); expect(game.textInterceptor.logs).toContain( i18next.t("moveTriggers:tookMoveAttack", { pokemonName: getPokemonNameWithAffix(enemy1), @@ -281,7 +283,7 @@ describe("Moves - Delayed Attacks", () => { await game.toNextTurn(); expectFutureSightActive(0); - expect(enemy1.hp).toBe(enemy1.getMaxHp()); + expect(enemy1).toHaveFullHp(); expect(game.textInterceptor.logs).not.toContain( i18next.t("moveTriggers:tookMoveAttack", { pokemonName: getPokemonNameWithAffix(enemy1), @@ -317,8 +319,8 @@ describe("Moves - Delayed Attacks", () => { await game.toEndOfTurn(); - expect(enemy1.hp).toBe(enemy1.getMaxHp()); - expect(enemy2.hp).toBeLessThan(enemy2.getMaxHp()); + expect(enemy1).toHaveFullHp(); + expect(enemy2).not.toHaveFullHp(); expect(game.textInterceptor.logs).toContain( i18next.t("moveTriggers:tookMoveAttack", { pokemonName: getPokemonNameWithAffix(enemy2), @@ -351,7 +353,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(enemy).not.toHaveFullHp(); expect(game.textInterceptor.logs).toContain( i18next.t("moveTriggers:tookMoveAttack", { pokemonName: getPokemonNameWithAffix(enemy), @@ -384,6 +386,35 @@ describe("Moves - Delayed Attacks", () => { expect(typeBoostSpy).not.toHaveBeenCalled(); }); + it("should not crash when catching & releasing a Pokemon on the same turn its delayed attack expires", async () => { + game.override.startingModifier([{ name: "MASTER_BALL", count: 1 }]); + await game.classicMode.startBattle([ + SpeciesId.FEEBAS, + SpeciesId.FEEBAS, + SpeciesId.FEEBAS, + SpeciesId.FEEBAS, + SpeciesId.FEEBAS, + SpeciesId.FEEBAS, + ]); + + game.move.use(MoveId.SPLASH); + await game.move.forceEnemyMove(MoveId.FUTURE_SIGHT); + await game.toNextTurn(); + + expectFutureSightActive(1); + + await passTurns(1); + + // Throw master ball and release the enemy + game.doThrowPokeball(PokeballType.MASTER_BALL); + game.onNextPrompt("AttemptCapturePhase", UiMode.CONFIRM, () => { + game.scene.ui.processInput(Button.CANCEL); + }); + await game.toEndOfTurn(); + + expectFutureSightActive(0); + }); + // TODO: Implement and move to a power spot's test file it.todo("Should activate ally's power spot when switched in during single battles"); }); diff --git a/test/test-utils/phase-interceptor.ts b/test/test-utils/phase-interceptor.ts index 996f00806c6..f2f11db9d12 100644 --- a/test/test-utils/phase-interceptor.ts +++ b/test/test-utils/phase-interceptor.ts @@ -1,6 +1,7 @@ import type { BattleScene } from "#app/battle-scene"; import { Phase } from "#app/phase"; import { UiMode } from "#enums/ui-mode"; +import { AttemptCapturePhase } from "#phases/attempt-capture-phase"; import { AttemptRunPhase } from "#phases/attempt-run-phase"; import { BattleEndPhase } from "#phases/battle-end-phase"; import { BerryPhase } from "#phases/berry-phase"; @@ -183,6 +184,7 @@ export class PhaseInterceptor { PostGameOverPhase, RevivalBlessingPhase, PokemonHealPhase, + AttemptCapturePhase, ]; private endBySetMode = [ From 7001f78beb154767e07d09ef960139a4249ca806 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sat, 6 Sep 2025 02:45:28 -0700 Subject: [PATCH 06/10] [i18n] Update locales submodule --- public/locales | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales b/public/locales index 2686cd3edc0..090bfefaf7e 160000 --- a/public/locales +++ b/public/locales @@ -1 +1 @@ -Subproject commit 2686cd3edc0bd2c7a7f12cc54c00c109e51a48d7 +Subproject commit 090bfefaf7e9d4efcbca61fa78a9cdf5d701830b From 344e9463ccd4fb175804870fef377f5c42d4ece5 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Sun, 7 Sep 2025 17:05:25 -0500 Subject: [PATCH 07/10] [Bug] Fix memory leak in egg hatch (#6511) Co-authored-by: MokaStitcher <54149968+MokaStitcher@users.noreply.github.com> --- src/phases/egg-hatch-phase.ts | 1 + src/phases/egg-summary-phase.ts | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/phases/egg-hatch-phase.ts b/src/phases/egg-hatch-phase.ts index 547ca778c6b..bfcb2136391 100644 --- a/src/phases/egg-hatch-phase.ts +++ b/src/phases/egg-hatch-phase.ts @@ -230,6 +230,7 @@ export class EggHatchPhase extends Phase { } else { globalScene.time.delayedCall(250, () => globalScene.setModifiersVisible(true)); } + this.pokemon?.destroy(); super.end(); } diff --git a/src/phases/egg-summary-phase.ts b/src/phases/egg-summary-phase.ts index c236c5c3abc..d771c8599b4 100644 --- a/src/phases/egg-summary-phase.ts +++ b/src/phases/egg-summary-phase.ts @@ -39,6 +39,10 @@ export class EggSummaryPhase extends Phase { } end() { + this.eggHatchData.forEach(data => { + data.pokemon?.destroy(); + }); + this.eggHatchData = []; globalScene.time.delayedCall(250, () => globalScene.setModifiersVisible(true)); globalScene.ui.setModeForceTransition(UiMode.MESSAGE).then(() => { super.end(); From e175bbfb28be3b9a9e9d7d788d328ba4d5841d3b Mon Sep 17 00:00:00 2001 From: Wlowscha <54003515+Wlowscha@users.noreply.github.com> Date: Mon, 8 Sep 2025 00:35:49 +0200 Subject: [PATCH 08/10] [UI/UX] Use pointer events instead of touch events in `TouchControl` (#6506) * Use pointer events instead of touch events in `TouchControl` * Marked some touch-related tests as todo * Changing test mocker to send pointer events * Also updated the ui to change the position of touch controls Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/touch-controls.ts | 4 +- .../settings/move-touch-controls-handler.ts | 53 +++++++++---------- test/test-utils/inputs-handler.ts | 4 +- 3 files changed, 30 insertions(+), 31 deletions(-) diff --git a/src/touch-controls.ts b/src/touch-controls.ts index faee9ea232e..4d00027e4ef 100644 --- a/src/touch-controls.ts +++ b/src/touch-controls.ts @@ -61,12 +61,12 @@ export class TouchControl { * event, removes the keydown state, and removes the 'active' class from the node and the last touched element. */ bindKey(node: HTMLElement, key: string) { - node.addEventListener("touchstart", event => { + node.addEventListener("pointerdown", event => { event.preventDefault(); this.touchButtonDown(node, key); }); - node.addEventListener("touchend", event => { + node.addEventListener("pointerup", event => { event.preventDefault(); this.touchButtonUp(node, key, event.target?.["id"]); }); diff --git a/src/ui/settings/move-touch-controls-handler.ts b/src/ui/settings/move-touch-controls-handler.ts index 60572529c89..248ee76a850 100644 --- a/src/ui/settings/move-touch-controls-handler.ts +++ b/src/ui/settings/move-touch-controls-handler.ts @@ -9,9 +9,9 @@ export const TOUCH_CONTROL_POSITIONS_PORTRAIT = "touchControlPositionsPortrait"; type ControlPosition = { id: string; x: number; y: number }; type ConfigurationEventListeners = { - touchstart: EventListener[]; - touchmove: EventListener[]; - touchend: EventListener[]; + pointerdown: EventListener[]; + pointermove: EventListener[]; + pointerup: EventListener[]; }; type ToolbarRefs = { @@ -39,9 +39,9 @@ export class MoveTouchControlsHandler { * These are used to remove the event listeners when the configuration mode is disabled. */ private configurationEventListeners: ConfigurationEventListeners = { - touchstart: [], - touchmove: [], - touchend: [], + pointerdown: [], + pointermove: [], + pointerup: [], }; private overlay: Phaser.GameObjects.Container; @@ -165,34 +165,33 @@ export class MoveTouchControlsHandler { /** * Start dragging the given button. * @param controlGroup The button that is being dragged. - * @param touch The touch event that started the drag. + * @param event The pointer event that started the drag. */ private startDrag = (controlGroup: HTMLElement): void => { this.draggingElement = controlGroup; }; /** - * Drags the currently dragged element to the given touch position. - * @param touch The touch event that is currently happening. - * @param isLeft Whether the dragged element is a left button. + * Drags the currently dragged element to the given pointer position. + * @param event The pointer event that is currently happening. */ - private drag = (touch: Touch): void => { + private drag = (event: PointerEvent): void => { if (!this.draggingElement) { return; } const rect = this.draggingElement.getBoundingClientRect(); - // Map the touch position to the center of the dragged element. + // Map the pointer position to the center of the dragged element. const xOffset = this.isLeft(this.draggingElement) - ? touch.clientX - rect.width / 2 - : window.innerWidth - touch.clientX - rect.width / 2; - const yOffset = window.innerHeight - touch.clientY - rect.height / 2; + ? event.clientX - rect.width / 2 + : window.innerWidth - event.clientX - rect.width / 2; + const yOffset = window.innerHeight - event.clientY - rect.height / 2; this.setPosition(this.draggingElement, xOffset, yOffset); }; /** * Stops dragging the currently dragged element. */ - private stopDrag = () => { + private stopDrag = (): void => { this.draggingElement = null; }; @@ -303,19 +302,19 @@ export class MoveTouchControlsHandler { */ private createConfigurationEventListeners(controlGroups: HTMLDivElement[]): ConfigurationEventListeners { return { - touchstart: controlGroups.map((element: HTMLDivElement) => { + pointerdown: controlGroups.map((element: HTMLDivElement) => { const startDrag = () => this.startDrag(element); - element.addEventListener("touchstart", startDrag, { passive: true }); + element.addEventListener("pointerdown", startDrag, { passive: true }); return startDrag; }), - touchmove: controlGroups.map(() => { - const drag = event => this.drag(event.touches[0]); - window.addEventListener("touchmove", drag, { passive: true }); + pointermove: controlGroups.map(() => { + const drag = (event: PointerEvent) => this.drag(event); + window.addEventListener("pointermove", drag, { passive: true }); return drag; }), - touchend: controlGroups.map(() => { + pointerup: controlGroups.map(() => { const stopDrag = () => this.stopDrag(); - window.addEventListener("touchend", stopDrag, { passive: true }); + window.addEventListener("pointerup", stopDrag, { passive: true }); return stopDrag; }), }; @@ -373,12 +372,12 @@ export class MoveTouchControlsHandler { this.draggingElement = null; // Remove event listeners - const { touchstart, touchmove, touchend } = this.configurationEventListeners; + const { pointerdown, pointermove, pointerup } = this.configurationEventListeners; this.getControlGroupElements().forEach((element, index) => - element.removeEventListener("touchstart", touchstart[index]), + element.removeEventListener("pointerdown", pointerdown[index]), ); - touchmove.forEach(listener => window.removeEventListener("touchmove", listener)); - touchend.forEach(listener => window.removeEventListener("touchend", listener)); + pointermove.forEach(listener => window.removeEventListener("pointermove", listener)); + pointerup.forEach(listener => window.removeEventListener("pointerup", listener)); // Remove configuration toolbar const toolbar = document.querySelector("#touchControls #configToolbar"); diff --git a/test/test-utils/inputs-handler.ts b/test/test-utils/inputs-handler.ts index b8b3224c31d..b5638f3694c 100644 --- a/test/test-utils/inputs-handler.ts +++ b/test/test-utils/inputs-handler.ts @@ -111,7 +111,7 @@ class FakeMobile { if (!node) { return; } - const event = new Event("touchstart"); + const event = new Event("pointerdown"); node.dispatchEvent(event); } @@ -120,7 +120,7 @@ class FakeMobile { if (!node) { return; } - const event = new Event("touchend"); + const event = new Event("pointerup"); node.dispatchEvent(event); } } From 8fdd5043c35bb3367febd20d16ba28069b3f40ec Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Sun, 7 Sep 2025 18:39:52 -0500 Subject: [PATCH 09/10] [Bug] [Ability] Prevent message flyout spam for sturdy, damp, and a few other abilities (#6507) * fix: prevent message spam for many abilities * Update src/data/abilities/ability.ts --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/data/abilities/ability.ts | 136 +++++++++++++--------------------- src/data/moves/move.ts | 7 +- src/field/pokemon.ts | 1 + 3 files changed, 58 insertions(+), 86 deletions(-) diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 66d00d950d2..bec3e3f6543 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -396,7 +396,23 @@ export abstract class AbAttr { } } -export class BlockRecoilDamageAttr extends AbAttr { +/** + * Abstract class for ability attributes that simply cancel an interaction + * + * @remarks + * Abilities that have simple cancel interactions (e.g. {@linkcode BlockRecoilDamageAttr}) can extend this class to reuse the `canApply` and `apply` logic + */ +abstract class CancelInteractionAbAttr extends AbAttr { + override canApply({ cancelled }: AbAttrParamsWithCancel): boolean { + return !cancelled.value; + } + + override apply({ cancelled }: AbAttrParamsWithCancel): void { + cancelled.value = true; + } +} + +export class BlockRecoilDamageAttr extends CancelInteractionAbAttr { private declare readonly _: never; constructor() { super(false); @@ -592,11 +608,7 @@ export class PreDefendFullHpEndureAbAttr extends PreDefendAbAttr { } } -export class BlockItemTheftAbAttr extends AbAttr { - override apply({ cancelled }: AbAttrParamsWithCancel): void { - cancelled.value = true; - } - +export class BlockItemTheftAbAttr extends CancelInteractionAbAttr { getTriggerMessage({ pokemon }: AbAttrBaseParams, abilityName: string) { return i18next.t("abilityTriggers:blockItemTheft", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), @@ -869,8 +881,9 @@ export interface FieldPriorityMoveImmunityAbAttrParams extends AugmentMoveIntera } export class FieldPriorityMoveImmunityAbAttr extends PreDefendAbAttr { - override canApply({ move, opponent: attacker }: FieldPriorityMoveImmunityAbAttrParams): boolean { + override canApply({ move, opponent: attacker, cancelled }: FieldPriorityMoveImmunityAbAttrParams): boolean { return ( + !cancelled.value && !(move.moveTarget === MoveTarget.USER || move.moveTarget === MoveTarget.NEAR_ALLY) && move.getPriority(attacker) > 0 && !move.isMultiTarget() @@ -897,10 +910,8 @@ export class MoveImmunityAbAttr extends PreDefendAbAttr { this.immuneCondition = immuneCondition; } - override canApply({ pokemon, opponent: attacker, move }: MoveImmunityAbAttrParams): boolean { - // TODO: Investigate whether this method should be checking against `cancelled`, specifically - // if not checking this results in multiple flyouts showing when multiple abilities block the move. - return this.immuneCondition(pokemon, attacker, move); + override canApply({ pokemon, opponent: attacker, move, cancelled }: MoveImmunityAbAttrParams): boolean { + return !cancelled.value && this.immuneCondition(pokemon, attacker, move); } override apply({ cancelled }: MoveImmunityAbAttrParams): void { @@ -1591,12 +1602,7 @@ export interface FieldPreventExplosiveMovesAbAttrParams extends AbAttrBaseParams cancelled: BooleanHolder; } -export class FieldPreventExplosiveMovesAbAttr extends AbAttr { - // TODO: investigate whether we need to check against `cancelled` in a `canApply` method - override apply({ cancelled }: FieldPreventExplosiveMovesAbAttrParams): void { - cancelled.value = true; - } -} +export class FieldPreventExplosiveMovesAbAttr extends CancelInteractionAbAttr {} export interface FieldMultiplyStatAbAttrParams extends AbAttrBaseParams { /** The kind of stat that is being checked for modification */ @@ -2535,15 +2541,11 @@ export class IgnoreOpponentStatStagesAbAttr extends AbAttr { * Abilities with this attribute prevent the user from being affected by Intimidate. * @sealed */ -export class IntimidateImmunityAbAttr extends AbAttr { +export class IntimidateImmunityAbAttr extends CancelInteractionAbAttr { constructor() { super(false); } - override apply({ cancelled }: AbAttrParamsWithCancel): void { - cancelled.value = true; - } - getTriggerMessage({ pokemon }: AbAttrParamsWithCancel, abilityName: string, ..._args: any[]): string { return i18next.t("abilityTriggers:intimidateImmunity", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), @@ -3577,8 +3579,8 @@ export class ProtectStatAbAttr extends PreStatStageChangeAbAttr { this.protectedStat = protectedStat; } - override canApply({ stat }: PreStatStageChangeAbAttrParams): boolean { - return isNullOrUndefined(this.protectedStat) || stat === this.protectedStat; + override canApply({ stat, cancelled }: PreStatStageChangeAbAttrParams): boolean { + return !cancelled.value && (isNullOrUndefined(this.protectedStat) || stat === this.protectedStat); } /** @@ -3669,8 +3671,11 @@ export class PreSetStatusEffectImmunityAbAttr extends PreSetStatusAbAttr { this.immuneEffects = immuneEffects; } - override canApply({ effect }: PreSetStatusAbAttrParams): boolean { - return (this.immuneEffects.length === 0 && effect !== StatusEffect.FAINT) || this.immuneEffects.includes(effect); + override canApply({ effect, cancelled }: PreSetStatusAbAttrParams): boolean { + return ( + !cancelled.value && + ((this.immuneEffects.length === 0 && effect !== StatusEffect.FAINT) || this.immuneEffects.includes(effect)) + ); } /** @@ -3720,7 +3725,8 @@ export interface UserFieldStatusEffectImmunityAbAttrParams extends AbAttrBasePar /** * Provides immunity to status effects to the user's field. */ -export class UserFieldStatusEffectImmunityAbAttr extends AbAttr { +export class UserFieldStatusEffectImmunityAbAttr extends CancelInteractionAbAttr { + private declare readonly _: never; protected immuneEffects: StatusEffect[]; /** @@ -3740,12 +3746,8 @@ export class UserFieldStatusEffectImmunityAbAttr extends AbAttr { ); } - /** - * Set the `cancelled` value to true, indicating that the status effect is prevented. - */ - override apply({ cancelled }: UserFieldStatusEffectImmunityAbAttrParams): void { - cancelled.value = true; - } + // declare here to allow typescript to allow us to override `canApply` method without adjusting params + declare apply: (params: UserFieldStatusEffectImmunityAbAttrParams) => void; } /** @@ -3776,14 +3778,7 @@ export class ConditionalUserFieldStatusEffectImmunityAbAttr extends UserFieldSta * @returns Whether the ability can be applied to cancel the status effect. */ override canApply(params: UserFieldStatusEffectImmunityAbAttrParams): boolean { - return this.condition(params.target, params.source) && super.canApply(params); - } - - /** - * Set the `cancelled` value to true, indicating that the status effect is prevented. - */ - override apply({ cancelled }: UserFieldStatusEffectImmunityAbAttrParams): void { - cancelled.value = true; + return !params.cancelled.value && this.condition(params.target, params.source) && super.canApply(params); } } @@ -4019,20 +4014,16 @@ export class ConditionalCritAbAttr extends AbAttr { } } -export class BlockNonDirectDamageAbAttr extends AbAttr { +export class BlockNonDirectDamageAbAttr extends CancelInteractionAbAttr { constructor() { super(false); } - - override apply({ cancelled }: AbAttrParamsWithCancel): void { - cancelled.value = true; - } } /** * This attribute will block any status damage that you put in the parameter. */ -export class BlockStatusDamageAbAttr extends AbAttr { +export class BlockStatusDamageAbAttr extends CancelInteractionAbAttr { private effects: StatusEffect[]; /** @@ -4044,20 +4035,12 @@ export class BlockStatusDamageAbAttr extends AbAttr { this.effects = effects; } - override canApply({ pokemon }: AbAttrParamsWithCancel): boolean { - return !!pokemon.status?.effect && this.effects.includes(pokemon.status.effect); - } - - override apply({ cancelled }: AbAttrParamsWithCancel): void { - cancelled.value = true; + override canApply({ pokemon, cancelled }: AbAttrParamsWithCancel): boolean { + return !cancelled.value && !!pokemon.status?.effect && this.effects.includes(pokemon.status.effect); } } -export class BlockOneHitKOAbAttr extends AbAttr { - override apply({ cancelled }: AbAttrParamsWithCancel): void { - cancelled.value = true; - } -} +export class BlockOneHitKOAbAttr extends CancelInteractionAbAttr {} export interface ChangeMovePriorityAbAttrParams extends AbAttrBaseParams { /** The move being used */ @@ -4131,8 +4114,8 @@ export class BlockWeatherDamageAttr extends PreWeatherDamageAbAttr { this.weatherTypes = weatherTypes; } - override canApply({ weather }: PreWeatherEffectAbAttrParams): boolean { - if (!weather) { + override canApply({ weather, cancelled }: PreWeatherEffectAbAttrParams): boolean { + if (!weather || cancelled.value) { return false; } const weatherType = weather.weatherType; @@ -4153,8 +4136,8 @@ export class SuppressWeatherEffectAbAttr extends PreWeatherEffectAbAttr { this.affectsImmutable = affectsImmutable; } - override canApply({ weather }: PreWeatherEffectAbAttrParams): boolean { - if (!weather) { + override canApply({ weather, cancelled }: PreWeatherEffectAbAttrParams): boolean { + if (!weather || cancelled.value) { return false; } return this.affectsImmutable || weather.isImmutable(); @@ -5151,15 +5134,11 @@ export class StatStageChangeCopyAbAttr extends AbAttr { } } -export class BypassBurnDamageReductionAbAttr extends AbAttr { +export class BypassBurnDamageReductionAbAttr extends CancelInteractionAbAttr { private declare readonly _: never; constructor() { super(false); } - - override apply({ cancelled }: AbAttrParamsWithCancel): void { - cancelled.value = true; - } } export interface ReduceBurnDamageAbAttrParams extends AbAttrBaseParams { @@ -5199,14 +5178,7 @@ export class DoubleBerryEffectAbAttr extends AbAttr { * Attribute to prevent opposing berry use while on the field. * Used by {@linkcode AbilityId.UNNERVE}, {@linkcode AbilityId.AS_ONE_GLASTRIER} and {@linkcode AbilityId.AS_ONE_SPECTRIER} */ -export class PreventBerryUseAbAttr extends AbAttr { - /** - * Prevent use of opposing berries. - */ - override apply({ cancelled }: AbAttrParamsWithCancel): void { - cancelled.value = true; - } -} +export class PreventBerryUseAbAttr extends CancelInteractionAbAttr {} /** * A Pokemon with this ability heals by a percentage of their maximum hp after eating a berry @@ -5664,11 +5636,7 @@ export class IncreasePpAbAttr extends AbAttr { } /** @sealed */ -export class ForceSwitchOutImmunityAbAttr extends AbAttr { - override apply({ cancelled }: AbAttrParamsWithCancel): void { - cancelled.value = true; - } -} +export class ForceSwitchOutImmunityAbAttr extends CancelInteractionAbAttr {} export interface ReduceBerryUseThresholdAbAttrParams extends AbAttrBaseParams { /** Holds the hp ratio for the berry to proc, which may be modified by ability application */ @@ -5747,8 +5715,8 @@ export class MoveAbilityBypassAbAttr extends AbAttr { this.moveIgnoreFunc = moveIgnoreFunc || ((_pokemon, _move) => true); } - override canApply({ pokemon, move }: MoveAbilityBypassAbAttrParams): boolean { - return this.moveIgnoreFunc(pokemon, move); + override canApply({ pokemon, move, cancelled }: MoveAbilityBypassAbAttrParams): boolean { + return !cancelled.value && this.moveIgnoreFunc(pokemon, move); } override apply({ cancelled }: MoveAbilityBypassAbAttrParams): void { @@ -5842,8 +5810,8 @@ export class IgnoreTypeImmunityAbAttr extends AbAttr { this.allowedMoveTypes = allowedMoveTypes; } - override canApply({ moveType, defenderType }: IgnoreTypeImmunityAbAttrParams): boolean { - return this.defenderType === defenderType && this.allowedMoveTypes.includes(moveType); + override canApply({ moveType, defenderType, cancelled }: IgnoreTypeImmunityAbAttrParams): boolean { + return !cancelled.value && this.defenderType === defenderType && this.allowedMoveTypes.includes(moveType); } override apply({ cancelled }: IgnoreTypeImmunityAbAttrParams): void { diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 1c64b28fa75..097b7b9d68d 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -8140,9 +8140,12 @@ const failIfSingleBattle: MoveConditionFunc = (user, target, move) => globalScen const failIfDampCondition: MoveConditionFunc = (user, target, move) => { const cancelled = new BooleanHolder(false); - globalScene.getField(true).map(p=>applyAbAttrs("FieldPreventExplosiveMovesAbAttr", {pokemon: p, cancelled})); + // temporary workaround to prevent displaying the message during enemy command phase + // TODO: either move this, or make the move condition func have a `simulated` param + const simulated = globalScene.phaseManager.getCurrentPhase()?.is('EnemyCommandPhase'); + globalScene.getField(true).map(p=>applyAbAttrs("FieldPreventExplosiveMovesAbAttr", {pokemon: p, cancelled, simulated})); // Queue a message if an ability prevented usage of the move - if (cancelled.value) { + if (!simulated && cancelled.value) { globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:cannotUseMove", { pokemonName: getPokemonNameWithAffix(user), moveName: move.name })); } return !cancelled.value; diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index aac2ed55a72..86e63e49953 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -6590,6 +6590,7 @@ export class EnemyPokemon extends Pokemon { ignoreAllyAbility: !p.getAlly()?.waveData.abilityRevealed, ignoreSourceAllyAbility: false, isCritical, + simulated: true, }).damage >= p.hp ); }) From 43f8b78c35e7613f519c4d34a1c62fc8f572647d Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Sun, 7 Sep 2025 20:17:12 -0500 Subject: [PATCH 10/10] [Bug] Fix sessions clearing the wrong slot on save (#6509) * Store session id in session data to prevent somehow deleting wrong slot * Only log session / system if beta or local; fix promise * Fix serialization/deserialization when logging session/system data * Force loadSaveSlot to set the logged in user's last session * No longer add slotId to session data --- src/phases/game-over-phase.ts | 2 +- src/phases/post-game-over-phase.ts | 23 +- src/phases/title-phase.ts | 3 + .../api/pokerogue-session-savedata-api.ts | 1 + src/system/game-data.ts | 407 +++++++++--------- 5 files changed, 230 insertions(+), 206 deletions(-) diff --git a/src/phases/game-over-phase.ts b/src/phases/game-over-phase.ts index 89162e591fc..c4548a54d2f 100644 --- a/src/phases/game-over-phase.ts +++ b/src/phases/game-over-phase.ts @@ -204,7 +204,7 @@ export class GameOverPhase extends BattlePhase { } this.getRunHistoryEntry().then(runHistoryEntry => { globalScene.gameData.saveRunHistory(runHistoryEntry, this.isVictory); - globalScene.phaseManager.pushNew("PostGameOverPhase", endCardPhase); + globalScene.phaseManager.pushNew("PostGameOverPhase", globalScene.sessionSlotId, endCardPhase); this.end(); }); }; diff --git a/src/phases/post-game-over-phase.ts b/src/phases/post-game-over-phase.ts index 3ac112a8a8b..a69ec2d468b 100644 --- a/src/phases/post-game-over-phase.ts +++ b/src/phases/post-game-over-phase.ts @@ -5,10 +5,11 @@ import type { EndCardPhase } from "#phases/end-card-phase"; export class PostGameOverPhase extends Phase { public readonly phaseName = "PostGameOverPhase"; private endCardPhase?: EndCardPhase; + private slotId: number; - constructor(endCardPhase?: EndCardPhase) { + constructor(slotId: number, endCardPhase?: EndCardPhase) { super(); - + this.slotId = slotId; this.endCardPhase = endCardPhase; } @@ -20,16 +21,14 @@ export class PostGameOverPhase extends Phase { if (!success) { return globalScene.reset(true); } - globalScene.gameData - .tryClearSession(globalScene.sessionSlotId) - .then((success: boolean | [boolean, boolean]) => { - if (!success[0]) { - return globalScene.reset(true); - } - globalScene.reset(); - globalScene.phaseManager.unshiftNew("TitlePhase"); - this.end(); - }); + globalScene.gameData.tryClearSession(this.slotId).then((success: boolean | [boolean, boolean]) => { + if (!success[0]) { + return globalScene.reset(true); + } + globalScene.reset(); + globalScene.phaseManager.unshiftNew("TitlePhase"); + this.end(); + }); }); }; diff --git a/src/phases/title-phase.ts b/src/phases/title-phase.ts index 15d92ba2812..a7db5b6a6dd 100644 --- a/src/phases/title-phase.ts +++ b/src/phases/title-phase.ts @@ -177,6 +177,9 @@ export class TitlePhase extends Phase { .then((success: boolean) => { if (success) { this.loaded = true; + if (loggedInUser) { + loggedInUser.lastSessionSlot = slotId; + } globalScene.ui.showText(i18next.t("menu:sessionSuccess"), null, () => this.end()); } else { this.end(); diff --git a/src/plugins/api/pokerogue-session-savedata-api.ts b/src/plugins/api/pokerogue-session-savedata-api.ts index 39fa292f9f1..6438178dfda 100644 --- a/src/plugins/api/pokerogue-session-savedata-api.ts +++ b/src/plugins/api/pokerogue-session-savedata-api.ts @@ -82,6 +82,7 @@ export class PokerogueSessionSavedataApi extends ApiBase { try { const urlSearchParams = this.toUrlSearchParams(params); const response = await this.doGet(`/savedata/session/delete?${urlSearchParams}`); + console.debug("%cSending a request to delete session in slot %d", "color: blue", params.slot); if (response.ok) { return null; diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 3a4dafb2de2..a00701f86f5 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -68,6 +68,7 @@ import { executeIf, fixedInt, isLocal, NumberHolder, randInt, randSeedItem } fro import { decrypt, encrypt } from "#utils/data"; import { getEnumKeys } from "#utils/enums"; import { getPokemonSpecies } from "#utils/pokemon-utils"; +import { isBeta } from "#utils/utility-vars"; import { AES, enc } from "crypto-js"; import i18next from "i18next"; @@ -419,7 +420,15 @@ export class GameData { } } - console.debug(systemData); + if (isLocal || isBeta) { + try { + console.debug( + this.parseSystemData(JSON.stringify(systemData, (_, v: any) => (typeof v === "bigint" ? v.toString() : v))), + ); + } catch (err) { + console.debug("Attempt to log system data failed:", err); + } + } localStorage.setItem(`data_${loggedInUser?.username}`, encrypt(systemDataStr, bypassLogin)); @@ -945,45 +954,46 @@ export class GameData { } as SessionSaveData; } - getSession(slotId: number): Promise { - // biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO: fix this - return new Promise(async (resolve, reject) => { - if (slotId < 0) { - return resolve(null); + async getSession(slotId: number): Promise { + const { promise, resolve, reject } = Promise.withResolvers(); + if (slotId < 0) { + resolve(null); + return promise; + } + const handleSessionData = async (sessionDataStr: string) => { + try { + const sessionData = this.parseSessionData(sessionDataStr); + resolve(sessionData); + } catch (err) { + reject(err); + return; } - const handleSessionData = async (sessionDataStr: string) => { - try { - const sessionData = this.parseSessionData(sessionDataStr); - resolve(sessionData); - } catch (err) { - reject(err); - return; - } - }; + }; - if (!bypassLogin && !localStorage.getItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`)) { - pokerogueApi.savedata.session.get({ slot: slotId, clientSessionId }).then(async response => { - if (!response || response?.length === 0 || response?.[0] !== "{") { - console.error(response); - return resolve(null); - } + if (!bypassLogin && !localStorage.getItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`)) { + const response = await pokerogueApi.savedata.session.get({ slot: slotId, clientSessionId }); - localStorage.setItem( - `sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`, - encrypt(response, bypassLogin), - ); - - await handleSessionData(response); - }); - } else { - const sessionData = localStorage.getItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`); - if (sessionData) { - await handleSessionData(decrypt(sessionData, bypassLogin)); - } else { - return resolve(null); - } + if (!response || response?.length === 0 || response?.[0] !== "{") { + console.error(response); + resolve(null); + return promise; } - }); + + localStorage.setItem( + `sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`, + encrypt(response, bypassLogin), + ); + + await handleSessionData(response); + return promise; + } + const sessionData = localStorage.getItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`); + if (sessionData) { + await handleSessionData(decrypt(sessionData, bypassLogin)); + return promise; + } + resolve(null); + return promise; } async renameSession(slotId: number, newName: string): Promise { @@ -1028,166 +1038,177 @@ export class GameData { return !(success !== null && !success); } - loadSession(slotId: number, sessionData?: SessionSaveData): Promise { - // biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO: fix this - return new Promise(async (resolve, reject) => { - try { - const initSessionFromData = async (sessionData: SessionSaveData) => { - console.debug(sessionData); - - globalScene.gameMode = getGameMode(sessionData.gameMode || GameModes.CLASSIC); - if (sessionData.challenges) { - globalScene.gameMode.challenges = sessionData.challenges.map(c => c.toChallenge()); + async loadSession(slotId: number, sessionData?: SessionSaveData): Promise { + const { promise, resolve, reject } = Promise.withResolvers(); + try { + const initSessionFromData = (fromSession: SessionSaveData) => { + if (isLocal || isBeta) { + try { + console.debug( + this.parseSessionData( + JSON.stringify(fromSession, (_, v: any) => (typeof v === "bigint" ? v.toString() : v)), + ), + ); + } catch (err) { + console.debug("Attempt to log session data failed:", err); } - - globalScene.setSeed(sessionData.seed || globalScene.game.config.seed[0]); - globalScene.resetSeed(); - - console.log("Seed:", globalScene.seed); - - globalScene.sessionPlayTime = sessionData.playTime || 0; - globalScene.lastSavePlayTime = 0; - - const loadPokemonAssets: Promise[] = []; - - const party = globalScene.getPlayerParty(); - party.splice(0, party.length); - - for (const p of sessionData.party) { - const pokemon = p.toPokemon() as PlayerPokemon; - pokemon.setVisible(false); - loadPokemonAssets.push(pokemon.loadAssets(false)); - party.push(pokemon); - } - - Object.keys(globalScene.pokeballCounts).forEach((key: string) => { - globalScene.pokeballCounts[key] = sessionData.pokeballCounts[key] || 0; - }); - if (Overrides.POKEBALL_OVERRIDE.active) { - globalScene.pokeballCounts = Overrides.POKEBALL_OVERRIDE.pokeballs; - } - - globalScene.money = Math.floor(sessionData.money || 0); - globalScene.updateMoneyText(); - - if (globalScene.money > this.gameStats.highestMoney) { - this.gameStats.highestMoney = globalScene.money; - } - - globalScene.score = sessionData.score; - globalScene.updateScoreText(); - - globalScene.mysteryEncounterSaveData = new MysteryEncounterSaveData(sessionData.mysteryEncounterSaveData); - - globalScene.newArena(sessionData.arena.biome, sessionData.playerFaints); - - const battleType = sessionData.battleType || 0; - const trainerConfig = sessionData.trainer ? trainerConfigs[sessionData.trainer.trainerType] : null; - const mysteryEncounterType = - sessionData.mysteryEncounterType !== -1 ? sessionData.mysteryEncounterType : undefined; - const battle = globalScene.newBattle( - sessionData.waveIndex, - battleType, - sessionData.trainer, - battleType === BattleType.TRAINER - ? trainerConfig?.doubleOnly || sessionData.trainer?.variant === TrainerVariant.DOUBLE - : sessionData.enemyParty.length > 1, - mysteryEncounterType, - ); - battle.enemyLevels = sessionData.enemyParty.map(p => p.level); - - globalScene.arena.init(); - - sessionData.enemyParty.forEach((enemyData, e) => { - const enemyPokemon = enemyData.toPokemon( - battleType, - e, - sessionData.trainer?.variant === TrainerVariant.DOUBLE, - ) as EnemyPokemon; - battle.enemyParty[e] = enemyPokemon; - if (battleType === BattleType.WILD) { - battle.seenEnemyPartyMemberIds.add(enemyPokemon.id); - } - - loadPokemonAssets.push(enemyPokemon.loadAssets()); - }); - - globalScene.arena.weather = sessionData.arena.weather; - globalScene.arena.eventTarget.dispatchEvent( - new WeatherChangedEvent( - WeatherType.NONE, - globalScene.arena.weather?.weatherType!, - globalScene.arena.weather?.turnsLeft!, - ), - ); // TODO: is this bang correct? - - globalScene.arena.terrain = sessionData.arena.terrain; - globalScene.arena.eventTarget.dispatchEvent( - new TerrainChangedEvent( - TerrainType.NONE, - globalScene.arena.terrain?.terrainType!, - globalScene.arena.terrain?.turnsLeft!, - ), - ); // TODO: is this bang correct? - - globalScene.arena.playerTerasUsed = sessionData.arena.playerTerasUsed; - - globalScene.arena.tags = sessionData.arena.tags; - if (globalScene.arena.tags) { - for (const tag of globalScene.arena.tags) { - if (tag instanceof EntryHazardTag) { - const { tagType, side, turnCount, layers, maxLayers } = tag as EntryHazardTag; - globalScene.arena.eventTarget.dispatchEvent( - new TagAddedEvent(tagType, side, turnCount, layers, maxLayers), - ); - } else { - globalScene.arena.eventTarget.dispatchEvent(new TagAddedEvent(tag.tagType, tag.side, tag.turnCount)); - } - } - } - - globalScene.arena.positionalTagManager.tags = sessionData.arena.positionalTags.map(tag => - loadPositionalTag(tag), - ); - - if (globalScene.modifiers.length) { - console.warn("Existing modifiers not cleared on session load, deleting..."); - globalScene.modifiers = []; - } - for (const modifierData of sessionData.modifiers) { - const modifier = modifierData.toModifier(Modifier[modifierData.className]); - if (modifier) { - globalScene.addModifier(modifier, true); - } - } - globalScene.updateModifiers(true); - - for (const enemyModifierData of sessionData.enemyModifiers) { - const modifier = enemyModifierData.toModifier(Modifier[enemyModifierData.className]); - if (modifier) { - globalScene.addEnemyModifier(modifier, true); - } - } - - globalScene.updateModifiers(false); - - Promise.all(loadPokemonAssets).then(() => resolve(true)); - }; - if (sessionData) { - initSessionFromData(sessionData); - } else { - this.getSession(slotId) - .then(data => data && initSessionFromData(data)) - .catch(err => { - reject(err); - return; - }); } - } catch (err) { - reject(err); - return; + + globalScene.gameMode = getGameMode(fromSession.gameMode || GameModes.CLASSIC); + if (fromSession.challenges) { + globalScene.gameMode.challenges = fromSession.challenges.map(c => c.toChallenge()); + } + + globalScene.setSeed(fromSession.seed || globalScene.game.config.seed[0]); + globalScene.resetSeed(); + + console.log("Seed:", globalScene.seed); + + globalScene.sessionPlayTime = fromSession.playTime || 0; + globalScene.lastSavePlayTime = 0; + + const loadPokemonAssets: Promise[] = []; + + const party = globalScene.getPlayerParty(); + party.splice(0, party.length); + + for (const p of fromSession.party) { + const pokemon = p.toPokemon() as PlayerPokemon; + pokemon.setVisible(false); + loadPokemonAssets.push(pokemon.loadAssets(false)); + party.push(pokemon); + } + + Object.keys(globalScene.pokeballCounts).forEach((key: string) => { + globalScene.pokeballCounts[key] = fromSession.pokeballCounts[key] || 0; + }); + if (Overrides.POKEBALL_OVERRIDE.active) { + globalScene.pokeballCounts = Overrides.POKEBALL_OVERRIDE.pokeballs; + } + + globalScene.money = Math.floor(fromSession.money || 0); + globalScene.updateMoneyText(); + + if (globalScene.money > this.gameStats.highestMoney) { + this.gameStats.highestMoney = globalScene.money; + } + + globalScene.score = fromSession.score; + globalScene.updateScoreText(); + + globalScene.mysteryEncounterSaveData = new MysteryEncounterSaveData(fromSession.mysteryEncounterSaveData); + + globalScene.newArena(fromSession.arena.biome, fromSession.playerFaints); + + const battleType = fromSession.battleType || 0; + const trainerConfig = fromSession.trainer ? trainerConfigs[fromSession.trainer.trainerType] : null; + const mysteryEncounterType = + fromSession.mysteryEncounterType !== -1 ? fromSession.mysteryEncounterType : undefined; + const battle = globalScene.newBattle( + fromSession.waveIndex, + battleType, + fromSession.trainer, + battleType === BattleType.TRAINER + ? trainerConfig?.doubleOnly || fromSession.trainer?.variant === TrainerVariant.DOUBLE + : fromSession.enemyParty.length > 1, + mysteryEncounterType, + ); + battle.enemyLevels = fromSession.enemyParty.map(p => p.level); + + globalScene.arena.init(); + + fromSession.enemyParty.forEach((enemyData, e) => { + const enemyPokemon = enemyData.toPokemon( + battleType, + e, + fromSession.trainer?.variant === TrainerVariant.DOUBLE, + ) as EnemyPokemon; + battle.enemyParty[e] = enemyPokemon; + if (battleType === BattleType.WILD) { + battle.seenEnemyPartyMemberIds.add(enemyPokemon.id); + } + + loadPokemonAssets.push(enemyPokemon.loadAssets()); + }); + + globalScene.arena.weather = fromSession.arena.weather; + globalScene.arena.eventTarget.dispatchEvent( + new WeatherChangedEvent( + WeatherType.NONE, + globalScene.arena.weather?.weatherType!, + globalScene.arena.weather?.turnsLeft!, + ), + ); // TODO: is this bang correct? + + globalScene.arena.terrain = fromSession.arena.terrain; + globalScene.arena.eventTarget.dispatchEvent( + new TerrainChangedEvent( + TerrainType.NONE, + globalScene.arena.terrain?.terrainType!, + globalScene.arena.terrain?.turnsLeft!, + ), + ); // TODO: is this bang correct? + + globalScene.arena.playerTerasUsed = fromSession.arena.playerTerasUsed; + + globalScene.arena.tags = fromSession.arena.tags; + if (globalScene.arena.tags) { + for (const tag of globalScene.arena.tags) { + if (tag instanceof EntryHazardTag) { + const { tagType, side, turnCount, layers, maxLayers } = tag as EntryHazardTag; + globalScene.arena.eventTarget.dispatchEvent( + new TagAddedEvent(tagType, side, turnCount, layers, maxLayers), + ); + } else { + globalScene.arena.eventTarget.dispatchEvent(new TagAddedEvent(tag.tagType, tag.side, tag.turnCount)); + } + } + } + + globalScene.arena.positionalTagManager.tags = fromSession.arena.positionalTags.map(tag => + loadPositionalTag(tag), + ); + + if (globalScene.modifiers.length) { + console.warn("Existing modifiers not cleared on session load, deleting..."); + globalScene.modifiers = []; + } + for (const modifierData of fromSession.modifiers) { + const modifier = modifierData.toModifier(Modifier[modifierData.className]); + if (modifier) { + globalScene.addModifier(modifier, true); + } + } + globalScene.updateModifiers(true); + + for (const enemyModifierData of fromSession.enemyModifiers) { + const modifier = enemyModifierData.toModifier(Modifier[enemyModifierData.className]); + if (modifier) { + globalScene.addEnemyModifier(modifier, true); + } + } + + globalScene.updateModifiers(false); + + Promise.all(loadPokemonAssets).then(() => resolve(true)); + }; + if (sessionData) { + initSessionFromData(sessionData); + } else { + this.getSession(slotId) + .then(data => { + return data && initSessionFromData(data); + }) + .catch(err => { + reject(err); + return; + }); } - }); + } catch (err) { + reject(err); + } + + return promise; } /**