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/18] 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/18] [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/18] [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/18] [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/18] [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/18] [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 2cf23b7ea7b1662c260b74ee61a42a19dd955e8b Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sun, 7 Sep 2025 10:58:40 -0700 Subject: [PATCH 07/18] [Misc] Fix console log colors --- src/constants/colors.ts | 4 +++- src/phase-manager.ts | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/constants/colors.ts b/src/constants/colors.ts index e4d740addff..717c5fa5f0d 100644 --- a/src/constants/colors.ts +++ b/src/constants/colors.ts @@ -5,8 +5,10 @@ */ // Colors used in prod +/** Color used for "Start Phase " logs */ export const PHASE_START_COLOR = "green" as const; -export const MOVE_COLOR = "RebeccaPurple" as const; +/** Color used for logs in `MovePhase` */ +export const MOVE_COLOR = "orchid" as const; // Colors used for testing code export const NEW_TURN_COLOR = "#ffad00ff" as const; diff --git a/src/phase-manager.ts b/src/phase-manager.ts index 68b7d74293b..2185de559ae 100644 --- a/src/phase-manager.ts +++ b/src/phase-manager.ts @@ -1,3 +1,4 @@ +import { PHASE_START_COLOR } from "#app/constants/colors"; import { globalScene } from "#app/global-scene"; import type { Phase } from "#app/phase"; import { type PhasePriorityQueue, PostSummonPhasePriorityQueue } from "#data/phase-priority-queue"; @@ -392,7 +393,7 @@ export class PhaseManager { * Helper method to start and log the current phase. */ private startCurrentPhase(): void { - console.log(`%cStart Phase ${this.currentPhase.phaseName}`, "color:${PHASE_START_COLOR};"); + console.log(`%cStart Phase ${this.currentPhase.phaseName}`, `color:${PHASE_START_COLOR};`); this.currentPhase.start(); } From 8b95361d616a778c47c2f0da9c9e40317996be9e Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Sun, 7 Sep 2025 14:35:52 -0400 Subject: [PATCH 08/18] [Dev] Added egg move parse script & script type checking (#6116) * Added egg move parse utility script * Update interactive.js Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update interactive.js Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update interactive.js Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Applied kev's reviews * Removed `basePath` from tsconfig the docs literally recommend against using it so yeah * Fixed up configs so that script folder has its own file * Reverted changes to egg move contents * renamed boilerplate so biome doesn't lint it * Fix `jsconfig.json` so that it doesn't typecheck all of `node_modules` See https://github.com/microsoft/TypeScript/issues/50862#issuecomment-1565175938 for more info * Update tsconfig.json Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Updated workflows and fixed issues * Removed eslint from linting workflow * Fixed type error in d.ts file to shut up linters * Reverted test-filters.yml * Update biome.jsonc * Update decrypt-save.js comment * Update interactive.js * Apply Biome * Fixed type errors for scripts * Fixed biome from removing tsdoc linkcodes * Update test/@types/vitest.d.ts --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- .github/workflows/linting.yml | 81 +++++++-- biome.jsonc | 15 +- package.json | 2 + scripts/create-test/create-test.js | 2 +- scripts/decrypt-save.js | 18 +- scripts/jsconfig.json | 17 ++ .../egg-move-template.boilerplate.ts | 10 ++ scripts/parse-egg-moves/help-message.js | 17 ++ scripts/parse-egg-moves/interactive.js | 108 +++++++++++ scripts/parse-egg-moves/main.js | 168 ++++++++++++++++++ scripts/parse-egg-moves/parse.js | 79 ++++++++ scripts/scrape-trainer-names/fetch-names.js | 8 +- src/data/balance/egg-moves.ts | 62 +------ src/init/init.ts | 2 - test/@types/vitest.d.ts | 12 +- .../helpers/challenge-mode-helper.ts | 2 +- tsconfig.json | 74 ++++---- 17 files changed, 550 insertions(+), 127 deletions(-) create mode 100644 scripts/jsconfig.json create mode 100644 scripts/parse-egg-moves/egg-move-template.boilerplate.ts create mode 100644 scripts/parse-egg-moves/help-message.js create mode 100644 scripts/parse-egg-moves/interactive.js create mode 100644 scripts/parse-egg-moves/main.js create mode 100644 scripts/parse-egg-moves/parse.js diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 08327ee3653..edecae64f95 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -18,7 +18,7 @@ on: jobs: run-linters: - name: Run linters + name: Run all linters timeout-minutes: 10 runs-on: ubuntu-latest @@ -26,27 +26,86 @@ jobs: - name: Check out Git repository uses: actions/checkout@v4 with: - submodules: 'recursive' + submodules: "recursive" - name: Install pnpm uses: pnpm/action-setup@v4 with: version: 10 - - name: Set up Node.js + - name: Set up Node uses: actions/setup-node@v4 with: - node-version-file: '.nvmrc' - cache: 'pnpm' + node-version-file: ".nvmrc" + cache: "pnpm" - - name: Install Node.js dependencies + - name: Install Node modules run: pnpm i - - name: Lint with Biome + # Lint files with Biome-Lint - https://biomejs.dev/linter/ + - name: Run Biome-Lint run: pnpm biome-ci + id: biome_lint + continue-on-error: true - - name: Check dependencies with depcruise + # Validate dependencies with dependency-cruiser - https://github.com/sverweij/dependency-cruiser + - name: Run Dependency-Cruise run: pnpm depcruise - - - name: Lint with ls-lint - run: pnpm ls-lint \ No newline at end of file + id: depcruise + continue-on-error: true + + # Validate types with tsc - https://www.typescriptlang.org/docs/handbook/compiler-options.html#using-the-cli + - name: Run Typecheck + run: pnpm typecheck + id: typecheck + continue-on-error: true + + # The exact same thing + - name: Run Typecheck (scripts) + run: pnpm typecheck:scripts + id: typecheck-scripts + continue-on-error: true + + - name: Evaluate for Errors + env: + BIOME_LINT_OUTCOME: ${{ steps.biome_lint.outcome }} + DEPCRUISE_OUTCOME: ${{ steps.depcruise.outcome }} + TYPECHECK_OUTCOME: ${{ steps.typecheck.outcome }} + TYPECHECK_SCRIPTS_OUTCOME: ${{ steps.typecheck-scripts.outcome }} + run: | + # Check for Errors + + # Make text red. + red () { + printf "\e[31m%s\e[0m" "$1" + } + + # Make text green. + green () { + printf "\e[32m%s\e[0m" "$1" + } + + print_result() { + local name=$1 + local outcome=$2 + if [ "$outcome" == "success" ]; then + printf "$(green "✅ $name: $outcome")\n" + else + printf "$(red "❌ $name: $outcome")\n" + fi + } + + print_result "Biome" "$BIOME_LINT_OUTCOME" + print_result "Depcruise" "$DEPCRUISE_OUTCOME" + print_result "Typecheck" "$TYPECHECK_OUTCOME" + print_result "Typecheck scripts" "$TYPECHECK_SCRIPTS_OUTCOME" + + if [[ "$BIOME_LINT_OUTCOME" != "success" || \ + "$DEPCRUISE_OUTCOME" != "success" || \ + "$TYPECHECK_OUTCOME" != "success" || \ + "$TYPECHECK_SCRIPTS_OUTCOME" != "success" ]]; then + printf "$(red "❌ One or more checks failed!")\n" >&2 + exit 1 + fi + + printf "$(green "✅ All checks passed!")\n" diff --git a/biome.jsonc b/biome.jsonc index a63ce0ee07d..e6f9ff5711a 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -175,10 +175,17 @@ } }, - // Overrides to prevent unused import removal inside `overrides.ts` and enums files (for TSDoc linkcodes), - // as well as in all TS files in `scripts/` (which are assumed to be boilerplate templates). + // Overrides to prevent unused import removal inside `overrides.ts`, enums & `.d.ts` files (for TSDoc linkcodes), + // as well as inside script boilerplate files. { - "includes": ["**/src/overrides.ts", "**/src/enums/**/*", "**/scripts/**/*.ts", "**/*.d.ts"], + // TODO: Rename existing boilerplates in the folder and remove this last alias + "includes": [ + "**/src/overrides.ts", + "**/src/enums/**/*", + "**/*.d.ts", + "scripts/**/*.boilerplate.ts", + "**/boilerplates/*.ts" + ], "linter": { "rules": { "correctness": { @@ -188,7 +195,7 @@ } }, { - "includes": ["**/src/overrides.ts", "**/scripts/**/*.ts"], + "includes": ["**/src/overrides.ts"], "linter": { "rules": { "style": { diff --git a/package.json b/package.json index f6097b8ccb9..d33c5e390d6 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,10 @@ "test:watch": "vitest watch --coverage --no-isolate", "test:silent": "vitest run --silent='passed-only' --no-isolate", "test:create": "node scripts/create-test/create-test.js", + "eggMoves:parse": "node scripts/parse-egg-moves/main.js", "scrape-trainers": "node scripts/scrape-trainer-names/main.js", "typecheck": "tsc --noEmit", + "typecheck:scripts": "tsc -p scripts/jsconfig.json", "biome": "biome check --write --changed --no-errors-on-unmatched --diagnostic-level=error", "biome-ci": "biome ci --diagnostic-level=error --reporter=github --no-errors-on-unmatched", "typedoc": "typedoc", diff --git a/scripts/create-test/create-test.js b/scripts/create-test/create-test.js index 765993959d1..5e395783da7 100644 --- a/scripts/create-test/create-test.js +++ b/scripts/create-test/create-test.js @@ -156,7 +156,7 @@ async function runInteractive() { console.log(chalk.green.bold(`✔ File created at: test/${localDir}/${fileName}.test.ts\n`)); console.groupEnd(); } catch (err) { - console.error(chalk.red("✗ Error: ", err.message)); + console.error(chalk.red("✗ Error: ", err)); } } diff --git a/scripts/decrypt-save.js b/scripts/decrypt-save.js index e50f152f159..26b0a311378 100644 --- a/scripts/decrypt-save.js +++ b/scripts/decrypt-save.js @@ -1,7 +1,6 @@ // Usage: node decrypt-save.js [save-file] -// biome-ignore lint/performance/noNamespaceImport: This is how you import fs from node -import * as fs from "node:fs"; +import fs from "node:fs"; import crypto_js from "crypto-js"; const { AES, enc } = crypto_js; @@ -60,6 +59,11 @@ function decryptSave(path) { try { fileData = fs.readFileSync(path, "utf8"); } catch (e) { + if (!(e instanceof Error)) { + console.error(`Unrecognized error: ${e}`); + process.exit(1); + } + // @ts-expect-error - e is usually a SystemError (all of which have codes) switch (e.code) { case "ENOENT": console.error(`File not found: ${path}`); @@ -104,6 +108,13 @@ function writeToFile(filePath, data) { try { fs.writeFileSync(filePath, data); } catch (e) { + if (!(e instanceof Error)) { + console.error("Unknown error detected: ", e); + process.exitCode = 1; + return; + } + + // @ts-expect-error - e is usually a SystemError (all of which have codes) switch (e.code) { case "EACCES": console.error(`Could not open ${filePath}: Permission denied`); @@ -114,7 +125,8 @@ function writeToFile(filePath, data) { default: console.error(`Error writing file: ${e.message}`); } - process.exit(1); + process.exitCode = 1; + return; } } diff --git a/scripts/jsconfig.json b/scripts/jsconfig.json new file mode 100644 index 00000000000..aed71f4f576 --- /dev/null +++ b/scripts/jsconfig.json @@ -0,0 +1,17 @@ +{ + "include": ["**/*.js"], + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "rootDir": ".", + "target": "esnext", + "module": "nodenext", + "moduleResolution": "nodenext", + "erasableSyntaxOnly": true, + "strict": true, + "noEmit": true, + // Forcibly disable `node_modules` recursion to prevent TSC from typechecking random JS files. + // This is disabled by default in `tsconfig.json`, but needs to be explicitly disabled from the default of `2` + "maxNodeModuleJsDepth": 0 + } +} diff --git a/scripts/parse-egg-moves/egg-move-template.boilerplate.ts b/scripts/parse-egg-moves/egg-move-template.boilerplate.ts new file mode 100644 index 00000000000..bfac05f4bde --- /dev/null +++ b/scripts/parse-egg-moves/egg-move-template.boilerplate.ts @@ -0,0 +1,10 @@ +//! DO NOT EDIT THIS FILE - CREATED BY THE `eggMoves:parse` script automatically +import { MoveId } from "#enums/move-id"; +import { SpeciesId } from "#enums/species-id"; + +/** + * An object mapping all base form {@linkcode SpeciesId}s to an array of {@linkcode MoveId}s corresponding + * to their current egg moves. + * Generated by the `eggMoves:parse` script using a CSV sourced from the current Balance Team spreadsheet. + */ +export const speciesEggMoves = "{{table}}"; diff --git a/scripts/parse-egg-moves/help-message.js b/scripts/parse-egg-moves/help-message.js new file mode 100644 index 00000000000..397a28e5011 --- /dev/null +++ b/scripts/parse-egg-moves/help-message.js @@ -0,0 +1,17 @@ +import chalk from "chalk"; + +/** Show help/usage text for the `eggMoves:parse` CLI. */ +export function showHelpText() { + console.log(` +Usage: ${chalk.cyan("pnpm eggMoves:parse [options]")} +If given no options, assumes ${chalk.blue("\`--interactive\`")}. +If given only a file path, assumes ${chalk.blue("\`--file\`")}. + +${chalk.hex("#ffa500")("Options:")} + ${chalk.blue("-h, --help")} Show this help message. + ${chalk.blue("-f, --file[=PATH]")} Specify a path to a CSV file to read, or provide one from stdin. + ${chalk.blue("-t, --text[=TEXT]")} + ${chalk.blue("-c, --console[=TEXT]")} Specify CSV text to read, or provide it from stdin. + ${chalk.blue("-i, --interactive")} Run in interactive mode (default) +`); +} diff --git a/scripts/parse-egg-moves/interactive.js b/scripts/parse-egg-moves/interactive.js new file mode 100644 index 00000000000..68ee41e7900 --- /dev/null +++ b/scripts/parse-egg-moves/interactive.js @@ -0,0 +1,108 @@ +import fs from "fs"; +import chalk from "chalk"; +import inquirer from "inquirer"; +import { showHelpText } from "./help-message.js"; + +/** + * @import { Option } from "./main.js" + */ + +/** + * Prompt the user to interactively select an option (console/file) to retrieve the egg move CSV. + * @returns {Promise