From e7b8326bb7835515366d1c272cf5aae73cc330ac Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sat, 14 Jun 2025 13:07:39 -0400 Subject: [PATCH 01/59] Refactored healing moves to make more sense + added tests --- src/data/moves/move.ts | 267 +++++++++++++----------------- test/moves/recovery-moves.test.ts | 175 ++++++++++++++++++++ 2 files changed, 291 insertions(+), 151 deletions(-) create mode 100644 test/moves/recovery-moves.test.ts diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index b46f109db9e..c82506b64d5 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -126,10 +126,10 @@ export default abstract class Move implements Localizable { /** * Check if the move is of the given subclass without requiring `instanceof`. - * + * * ⚠️ Does _not_ work for {@linkcode ChargingAttackMove} and {@linkcode ChargingSelfStatusMove} subclasses. For those, * use {@linkcode isChargingMove} instead. - * + * * @param moveKind - The string name of the move to check against * @returns Whether this move is of the provided type. */ @@ -1946,6 +1946,64 @@ export class HealAttr extends MoveEffectAttr { const score = ((1 - (this.selfTarget ? user : target).getHpRatio()) * 20) - this.healRatio * 10; return Math.round(score / (1 - this.healRatio / 2)); } + + override canApply(user: Pokemon, target: Pokemon, _move: Move, _args?: any[]): boolean { + return !(this.selfTarget ? user : target).isFullHp(); + } + + override getFailedText(user: Pokemon, target: Pokemon, _move: Move): string | undefined { + const healedPokemon = this.selfTarget ? user : target; + return healedPokemon.isFullHp() + ? i18next.t("battle:hpIsFull", { + pokemonName: getPokemonNameWithAffix(healedPokemon), + }) + : undefined; + } +} + +/** + * Attribute for moves with variable healing amounts. + * Heals the target by an amount depending on the return value of {@linkcode healFunc}. + * + * Used for {@linkcode MoveId.MOONLIGHT}, {@linkcode MoveId.SHORE_UP}, {@linkcode MoveId.JUNGLE_HEALING}, {@linkcode MoveId.SWALLOW} + */ +export class VariableHealAttr extends HealAttr { + constructor( + /** A function yielding the amount of HP to heal. */ + private healFunc: (user: Pokemon, target: Pokemon, _move: Move) => number, + showAnim = false, + selfTarget = true, + ) { + super(1, showAnim, selfTarget); + this.healFunc = healFunc; + } + + apply(user: Pokemon, target: Pokemon, move: Move, _args: any[]): boolean { + const healRatio = this.healFunc(user, target, move) + this.addHealPhase(target, healRatio); + return true; + } +} + +/** + * Heals the target only if it is an ally. + * Used for {@linkcode MoveId.POLLEN_PUFF}. + */ +export class HealOnAllyAttr extends HealAttr { + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + if (user.getAlly() === target) { + super.apply(user, target, move, args); + return true; + } + + return false; + } + + override canApply(user: Pokemon, target: Pokemon, _move: Move, _args?: any[]): boolean { + return user.getAlly() !== target || super.canApply(user, target, _move, _args); + } +} + } /** @@ -2128,112 +2186,6 @@ export class IgnoreWeatherTypeDebuffAttr extends MoveAttr { } } -export abstract class WeatherHealAttr extends HealAttr { - constructor() { - super(0.5); - } - - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - let healRatio = 0.5; - if (!globalScene.arena.weather?.isEffectSuppressed()) { - const weatherType = globalScene.arena.weather?.weatherType || WeatherType.NONE; - healRatio = this.getWeatherHealRatio(weatherType); - } - this.addHealPhase(user, healRatio); - return true; - } - - abstract getWeatherHealRatio(weatherType: WeatherType): number; -} - -export class PlantHealAttr extends WeatherHealAttr { - getWeatherHealRatio(weatherType: WeatherType): number { - switch (weatherType) { - case WeatherType.SUNNY: - case WeatherType.HARSH_SUN: - return 2 / 3; - case WeatherType.RAIN: - case WeatherType.SANDSTORM: - case WeatherType.HAIL: - case WeatherType.SNOW: - case WeatherType.FOG: - case WeatherType.HEAVY_RAIN: - return 0.25; - default: - return 0.5; - } - } -} - -export class SandHealAttr extends WeatherHealAttr { - getWeatherHealRatio(weatherType: WeatherType): number { - switch (weatherType) { - case WeatherType.SANDSTORM: - return 2 / 3; - default: - return 0.5; - } - } -} - -/** - * Heals the target or the user by either {@linkcode normalHealRatio} or {@linkcode boostedHealRatio} - * depending on the evaluation of {@linkcode condition} - * @extends HealAttr - * @see {@linkcode apply} - */ -export class BoostHealAttr extends HealAttr { - /** Healing received when {@linkcode condition} is false */ - private normalHealRatio: number; - /** Healing received when {@linkcode condition} is true */ - private boostedHealRatio: number; - /** The lambda expression to check against when boosting the healing value */ - private condition?: MoveConditionFunc; - - constructor(normalHealRatio: number = 0.5, boostedHealRatio: number = 2 / 3, showAnim?: boolean, selfTarget?: boolean, condition?: MoveConditionFunc) { - super(normalHealRatio, showAnim, selfTarget); - this.normalHealRatio = normalHealRatio; - this.boostedHealRatio = boostedHealRatio; - this.condition = condition; - } - - /** - * @param user {@linkcode Pokemon} using the move - * @param target {@linkcode Pokemon} target of the move - * @param move {@linkcode Move} with this attribute - * @param args N/A - * @returns true if the move was successful - */ - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - const healRatio: number = (this.condition ? this.condition(user, target, move) : false) ? this.boostedHealRatio : this.normalHealRatio; - this.addHealPhase(target, healRatio); - return true; - } -} - -/** - * Heals the target only if it is the ally - * @extends HealAttr - * @see {@linkcode apply} - */ -export class HealOnAllyAttr extends HealAttr { - /** - * @param user {@linkcode Pokemon} using the move - * @param target {@linkcode Pokemon} target of the move - * @param move {@linkcode Move} with this attribute - * @param args N/A - * @returns true if the function succeeds - */ - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - if (user.getAlly() === target) { - super.apply(user, target, move, args); - return true; - } - - return false; - } -} - /** * Heals user as a side effect of a move that hits a target. * Healing is based on {@linkcode healRatio} * the amount of damage dealt or a stat of the target. @@ -4325,36 +4277,6 @@ export class SpitUpPowerAttr extends VariablePowerAttr { } } -/** - * Attribute used to apply Swallow's healing, which scales with Stockpile stacks. - * Does NOT remove stockpiled stacks. - */ -export class SwallowHealAttr extends HealAttr { - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - const stockpilingTag = user.getTag(StockpilingTag); - - if (stockpilingTag && stockpilingTag.stockpiledCount > 0) { - const stockpiled = stockpilingTag.stockpiledCount; - let healRatio: number; - - if (stockpiled === 1) { - healRatio = 0.25; - } else if (stockpiled === 2) { - healRatio = 0.50; - } else { // stockpiled >= 3 - healRatio = 1.00; - } - - if (healRatio) { - this.addHealPhase(user, healRatio); - return true; - } - } - - return false; - } -} - const hasStockpileStacksCondition: MoveConditionFunc = (user) => { const hasStockpilingTag = user.getTag(StockpilingTag); return !!hasStockpilingTag && hasStockpilingTag.stockpiledCount > 0; @@ -7990,6 +7912,53 @@ const attackedByItemMessageFunc = (user: Pokemon, target: Pokemon, move: Move) = return message; }; +const sunnyHealRatioFunc = (): number => { + if (globalScene.arena.weather?.isEffectSuppressed()) { + return 1 / 2; + } + + switch (globalScene.arena.getWeatherType()) { + case WeatherType.SUNNY: + case WeatherType.HARSH_SUN: + return 2 / 3; + case WeatherType.RAIN: + case WeatherType.SANDSTORM: + case WeatherType.HAIL: + case WeatherType.SNOW: + case WeatherType.HEAVY_RAIN: + case WeatherType.FOG: + return 1 / 4; + case WeatherType.STRONG_WINDS: + default: + return 1 / 2; + } +} + +const shoreUpHealRatioFunc = (): number => { + if (globalScene.arena.weather?.isEffectSuppressed()) { + return 1 / 2; + } + + return globalScene.arena.getWeatherType() === WeatherType.SANDSTORM ? 2 / 3 : 1 / 2; +} + +const swallowHealFunc = (user: Pokemon): number => { + const tag = user.getTag(StockpilingTag); + if (!tag || tag.stockpiledCount <= 0) { + return 0; + } + + switch (tag.stockpiledCount) { + case 1: + return 0.25 + case 2: + return 0.5 + case 3: + default: // in case we ever get more stacks + return 1; + } +} + export class MoveCondition { protected func: MoveConditionFunc; @@ -8200,15 +8169,12 @@ const MoveAttrs = Object.freeze({ SacrificialAttrOnHit, HalfSacrificialAttr, AddSubstituteAttr, - HealAttr, PartyStatusCureAttr, FlameBurstAttr, SacrificialFullRestoreAttr, IgnoreWeatherTypeDebuffAttr, - WeatherHealAttr, - PlantHealAttr, - SandHealAttr, - BoostHealAttr, + HealAttr, + VariableHealAttr, HealOnAllyAttr, HitHealAttr, IncrementMovePriorityAttr, @@ -8273,7 +8239,6 @@ const MoveAttrs = Object.freeze({ PresentPowerAttr, WaterShurikenPowerAttr, SpitUpPowerAttr, - SwallowHealAttr, MultiHitPowerIncrementAttr, LastMoveDoublePowerAttr, CombinedPledgePowerAttr, @@ -9121,13 +9086,13 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.ATK ], 1, true), new AttackMove(MoveId.VITAL_THROW, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 70, -1, 10, -1, -1, 2), new SelfStatusMove(MoveId.MORNING_SUN, PokemonType.NORMAL, -1, 5, -1, 0, 2) - .attr(PlantHealAttr) + .attr(VariableHealAttr, sunnyHealRatioFunc) .triageMove(), new SelfStatusMove(MoveId.SYNTHESIS, PokemonType.GRASS, -1, 5, -1, 0, 2) - .attr(PlantHealAttr) + .attr(VariableHealAttr, sunnyHealRatioFunc) .triageMove(), new SelfStatusMove(MoveId.MOONLIGHT, PokemonType.FAIRY, -1, 5, -1, 0, 2) - .attr(PlantHealAttr) + .attr(VariableHealAttr, sunnyHealRatioFunc) .triageMove(), new AttackMove(MoveId.HIDDEN_POWER, PokemonType.NORMAL, MoveCategory.SPECIAL, 60, 100, 15, -1, 0, 2) .attr(HiddenPowerTypeAttr), @@ -9184,12 +9149,12 @@ export function initMoves() { .condition(user => (user.getTag(StockpilingTag)?.stockpiledCount ?? 0) < 3) .attr(AddBattlerTagAttr, BattlerTagType.STOCKPILING, true), new AttackMove(MoveId.SPIT_UP, PokemonType.NORMAL, MoveCategory.SPECIAL, -1, -1, 10, -1, 0, 3) - .condition(hasStockpileStacksCondition) .attr(SpitUpPowerAttr, 100) + .condition(hasStockpileStacksCondition) .attr(RemoveBattlerTagAttr, [ BattlerTagType.STOCKPILING ], true), new SelfStatusMove(MoveId.SWALLOW, PokemonType.NORMAL, -1, 10, -1, 0, 3) + .attr(VariableHealAttr, swallowHealFunc) .condition(hasStockpileStacksCondition) - .attr(SwallowHealAttr) .attr(RemoveBattlerTagAttr, [ BattlerTagType.STOCKPILING ], true) .triageMove(), new AttackMove(MoveId.HEAT_WAVE, PokemonType.FIRE, MoveCategory.SPECIAL, 95, 90, 10, 10, 0, 3) @@ -10440,7 +10405,7 @@ export function initMoves() { .unimplemented(), /* End Unused */ new SelfStatusMove(MoveId.SHORE_UP, PokemonType.GROUND, -1, 5, -1, 0, 7) - .attr(SandHealAttr) + .attr(VariableHealAttr, shoreUpHealRatioFunc) .triageMove(), new AttackMove(MoveId.FIRST_IMPRESSION, PokemonType.BUG, MoveCategory.PHYSICAL, 90, 100, 10, -1, 2, 7) .condition(new FirstMoveCondition()), @@ -10460,7 +10425,7 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.SPD ], -1, true) .punchingMove(), new StatusMove(MoveId.FLORAL_HEALING, PokemonType.FAIRY, -1, 10, -1, 0, 7) - .attr(BoostHealAttr, 0.5, 2 / 3, true, false, (user, target, move) => globalScene.arena.terrain?.terrainType === TerrainType.GRASSY) + .attr(VariableHealAttr, () => globalScene.arena.terrain?.terrainType === TerrainType.GRASSY ? 2 / 3 : 1 / 2, true, false) .triageMove() .reflectable(), new AttackMove(MoveId.HIGH_HORSEPOWER, PokemonType.GROUND, MoveCategory.PHYSICAL, 95, 95, 10, -1, 0, 7), diff --git a/test/moves/recovery-moves.test.ts b/test/moves/recovery-moves.test.ts new file mode 100644 index 00000000000..89c97437367 --- /dev/null +++ b/test/moves/recovery-moves.test.ts @@ -0,0 +1,175 @@ +import { getPokemonNameWithAffix } from "#app/messages"; +import { getEnumValues, toReadableString } from "#app/utils/common"; +import { AbilityId } from "#enums/ability-id"; +import { MoveId } from "#enums/move-id"; +import { SpeciesId } from "#enums/species-id"; +import { WeatherType } from "#enums/weather-type"; +import GameManager from "#test/testUtils/gameManager"; +import i18next from "i18next"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Recovery Moves", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + describe.each<{ name: string; move: MoveId }>([ + { name: "Recover", move: MoveId.RECOVER }, + { name: "Soft-Boiled", move: MoveId.SOFT_BOILED }, + { name: "Milk Drink", move: MoveId.MILK_DRINK }, + { name: "Slack Off", move: MoveId.SLACK_OFF }, + { name: "Roost", move: MoveId.ROOST }, + ])("Normal Recovery Moves - $name", ({ move }) => { + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .ability(AbilityId.BALL_FETCH) + .battleStyle("single") + .disableCrits() + .enemySpecies(SpeciesId.MAGIKARP) + .enemyAbility(AbilityId.BALL_FETCH) + .enemyMoveset(MoveId.SPLASH) + .startingLevel(100) + .enemyLevel(100); + }); + + it("should heal 50% of the user's maximum HP", async () => { + await game.classicMode.startBattle([SpeciesId.BLISSEY]); + + const blissey = game.field.getPlayerPokemon(); + blissey.hp = 1; + + game.move.use(move); + await game.toEndOfTurn(); + + expect(blissey.getHpRatio()).toBeCloseTo(0.5, 1); + expect(game.phaseInterceptor.log).toContain("PokemonHealPhase"); + }); + + it("should display message and fail if used at full HP", async () => { + await game.classicMode.startBattle([SpeciesId.BLISSEY]); + + game.move.use(move); + await game.toEndOfTurn(); + + const blissey = game.field.getPlayerPokemon(); + expect(blissey.hp).toBe(blissey.getMaxHp()); + expect(game.textInterceptor.logs).toContain( + i18next.t("battle:hpIsFull", { + pokemonName: getPokemonNameWithAffix(blissey), + }), + ); + expect(game.phaseInterceptor.log).not.toContain("PokemonHealPhase"); + }); + }); + + describe("Weather-based Healing moves", () => { + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .ability(AbilityId.BALL_FETCH) + .battleStyle("single") + .disableCrits() + .enemySpecies(SpeciesId.MAGIKARP) + .enemyAbility(AbilityId.BALL_FETCH) + .enemyMoveset(MoveId.SPLASH) + .startingLevel(100) + .enemyLevel(100); + }); + + it.each([ + { name: "Harsh Sunlight", ability: AbilityId.DROUGHT }, + { name: "Extremely Harsh Sunlight", ability: AbilityId.DESOLATE_LAND }, + ])("should heal 66% of the user's maximum HP under $name", async ({ ability }) => { + game.override.passiveAbility(ability); + await game.classicMode.startBattle([SpeciesId.BLISSEY]); + + const blissey = game.field.getPlayerPokemon(); + blissey.hp = 1; + + game.move.use(MoveId.MOONLIGHT); + await game.toEndOfTurn(); + + expect(blissey.getHpRatio()).toBeCloseTo(0.67, 1); + }); + + const nonSunWTs = getEnumValues(WeatherType) + .filter(wt => ![WeatherType.NONE, WeatherType.STRONG_WINDS].includes(wt)) + .map(wt => ({ + name: toReadableString(WeatherType[wt]), + weather: wt, + })); + + it.each(nonSunWTs)("should heal 25% of the user's maximum HP under $name", async ({ weather }) => { + game.override.weather(weather); + await game.classicMode.startBattle([SpeciesId.BLISSEY]); + + const blissey = game.field.getPlayerPokemon(); + blissey.hp = 1; + + game.move.use(MoveId.MOONLIGHT); + await game.toEndOfTurn(); + + expect(blissey.getHpRatio()).toBeCloseTo(0.25, 1); + }); + + it("should heal 50% of the user's maximum HP under strong winds", async () => { + game.override.ability(AbilityId.DELTA_STREAM); + await game.classicMode.startBattle([SpeciesId.BLISSEY]); + + const blissey = game.field.getPlayerPokemon(); + blissey.hp = 1; + + game.move.use(MoveId.MOONLIGHT); + await game.toEndOfTurn(); + + expect(blissey.getHpRatio()).toBeCloseTo(0.5, 1); + }); + }); + + describe("Shore Up", () => { + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .ability(AbilityId.BALL_FETCH) + .battleStyle("single") + .disableCrits() + .enemySpecies(SpeciesId.MAGIKARP) + .enemyAbility(AbilityId.BALL_FETCH) + .enemyMoveset(MoveId.SPLASH) + .startingLevel(100) + .enemyLevel(100); + }); + + it("should heal 66% of the user's maximum HP under sandstorm", async () => { + game.override.ability(AbilityId.SAND_STREAM); + await game.classicMode.startBattle([SpeciesId.BLISSEY]); + + const blissey = game.field.getPlayerPokemon(); + blissey.hp = 1; + + game.move.use(MoveId.SHORE_UP); + await game.toEndOfTurn(); + + expect(blissey.getHpRatio()).toBeCloseTo(0.66, 1); + }); + }); +}); From 5449209f2f7ba5ae64c4b21cc011af8b55d7d7d9 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sat, 14 Jun 2025 13:38:52 -0400 Subject: [PATCH 02/59] Consolidated Swallow/Spit Up tests into 1 file for brevity --- src/data/moves/move.ts | 2 - test/moves/spit_up.test.ts | 203 ---------------------- test/moves/swallow-spit-up.test.ts | 265 +++++++++++++++++++++++++++++ test/moves/swallow.test.ts | 205 ---------------------- 4 files changed, 265 insertions(+), 410 deletions(-) delete mode 100644 test/moves/spit_up.test.ts create mode 100644 test/moves/swallow-spit-up.test.ts delete mode 100644 test/moves/swallow.test.ts diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index c82506b64d5..fc578f130c6 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -2004,8 +2004,6 @@ export class HealOnAllyAttr extends HealAttr { } } -} - /** * Cures the user's party of non-volatile status conditions, ie. Heal Bell, Aromatherapy * @extends MoveEffectAttr diff --git a/test/moves/spit_up.test.ts b/test/moves/spit_up.test.ts deleted file mode 100644 index 83549c28f40..00000000000 --- a/test/moves/spit_up.test.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { Stat } from "#enums/stat"; -import { StockpilingTag } from "#app/data/battler-tags"; -import { allMoves } from "#app/data/data-lists"; -import { BattlerTagType } from "#app/enums/battler-tag-type"; -import type { TurnMove } from "#app/field/pokemon"; -import { MoveResult } from "#enums/move-result"; -import GameManager from "#test/testUtils/gameManager"; -import { AbilityId } from "#enums/ability-id"; -import { MoveId } from "#enums/move-id"; -import type Move from "#app/data/moves/move"; -import { SpeciesId } from "#enums/species-id"; -import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { MovePhase } from "#app/phases/move-phase"; -import { TurnInitPhase } from "#app/phases/turn-init-phase"; - -describe("Moves - Spit Up", () => { - let phaserGame: Phaser.Game; - let game: GameManager; - - let spitUp: Move; - - beforeAll(() => { - phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); - }); - - afterEach(() => { - game.phaseInterceptor.restoreOg(); - }); - - beforeEach(() => { - spitUp = allMoves[MoveId.SPIT_UP]; - game = new GameManager(phaserGame); - - game.override.battleStyle("single"); - - game.override.enemySpecies(SpeciesId.RATTATA); - game.override.enemyMoveset(MoveId.SPLASH); - game.override.enemyAbility(AbilityId.NONE); - game.override.enemyLevel(2000); - - game.override.moveset(new Array(4).fill(spitUp.id)); - game.override.ability(AbilityId.NONE); - - vi.spyOn(spitUp, "calculateBattlePower"); - }); - - describe("consumes all stockpile stacks to deal damage (scaling with stacks)", () => { - it("1 stack -> 100 power", async () => { - const stacksToSetup = 1; - const expectedPower = 100; - - await game.classicMode.startBattle([SpeciesId.ABOMASNOW]); - - const pokemon = game.scene.getPlayerPokemon()!; - pokemon.addTag(BattlerTagType.STOCKPILING); - - const stockpilingTag = pokemon.getTag(StockpilingTag)!; - expect(stockpilingTag).toBeDefined(); - expect(stockpilingTag.stockpiledCount).toBe(stacksToSetup); - - game.move.select(MoveId.SPIT_UP); - await game.phaseInterceptor.to(TurnInitPhase); - - expect(spitUp.calculateBattlePower).toHaveBeenCalledOnce(); - expect(spitUp.calculateBattlePower).toHaveReturnedWith(expectedPower); - - expect(pokemon.getTag(StockpilingTag)).toBeUndefined(); - }); - - it("2 stacks -> 200 power", async () => { - const stacksToSetup = 2; - const expectedPower = 200; - - await game.classicMode.startBattle([SpeciesId.ABOMASNOW]); - - const pokemon = game.scene.getPlayerPokemon()!; - pokemon.addTag(BattlerTagType.STOCKPILING); - pokemon.addTag(BattlerTagType.STOCKPILING); - - const stockpilingTag = pokemon.getTag(StockpilingTag)!; - expect(stockpilingTag).toBeDefined(); - expect(stockpilingTag.stockpiledCount).toBe(stacksToSetup); - - game.move.select(MoveId.SPIT_UP); - await game.phaseInterceptor.to(TurnInitPhase); - - expect(spitUp.calculateBattlePower).toHaveBeenCalledOnce(); - expect(spitUp.calculateBattlePower).toHaveReturnedWith(expectedPower); - - expect(pokemon.getTag(StockpilingTag)).toBeUndefined(); - }); - - it("3 stacks -> 300 power", async () => { - const stacksToSetup = 3; - const expectedPower = 300; - - await game.classicMode.startBattle([SpeciesId.ABOMASNOW]); - - const pokemon = game.scene.getPlayerPokemon()!; - pokemon.addTag(BattlerTagType.STOCKPILING); - pokemon.addTag(BattlerTagType.STOCKPILING); - pokemon.addTag(BattlerTagType.STOCKPILING); - - const stockpilingTag = pokemon.getTag(StockpilingTag)!; - expect(stockpilingTag).toBeDefined(); - expect(stockpilingTag.stockpiledCount).toBe(stacksToSetup); - - game.move.select(MoveId.SPIT_UP); - await game.phaseInterceptor.to(TurnInitPhase); - - expect(spitUp.calculateBattlePower).toHaveBeenCalledOnce(); - expect(spitUp.calculateBattlePower).toHaveReturnedWith(expectedPower); - - expect(pokemon.getTag(StockpilingTag)).toBeUndefined(); - }); - }); - - it("fails without stacks", async () => { - await game.classicMode.startBattle([SpeciesId.ABOMASNOW]); - - const pokemon = game.scene.getPlayerPokemon()!; - - const stockpilingTag = pokemon.getTag(StockpilingTag)!; - expect(stockpilingTag).toBeUndefined(); - - game.move.select(MoveId.SPIT_UP); - await game.phaseInterceptor.to(TurnInitPhase); - - expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ - move: MoveId.SPIT_UP, - result: MoveResult.FAIL, - targets: [game.scene.getEnemyPokemon()!.getBattlerIndex()], - }); - - expect(spitUp.calculateBattlePower).not.toHaveBeenCalled(); - }); - - describe("restores stat boosts granted by stacks", () => { - it("decreases stats based on stored values (both boosts equal)", async () => { - await game.classicMode.startBattle([SpeciesId.ABOMASNOW]); - - const pokemon = game.scene.getPlayerPokemon()!; - pokemon.addTag(BattlerTagType.STOCKPILING); - - const stockpilingTag = pokemon.getTag(StockpilingTag)!; - expect(stockpilingTag).toBeDefined(); - - game.move.select(MoveId.SPIT_UP); - await game.phaseInterceptor.to(MovePhase); - - expect(pokemon.getStatStage(Stat.DEF)).toBe(1); - expect(pokemon.getStatStage(Stat.SPDEF)).toBe(1); - - await game.phaseInterceptor.to(TurnInitPhase); - - expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ - move: MoveId.SPIT_UP, - result: MoveResult.SUCCESS, - targets: [game.scene.getEnemyPokemon()!.getBattlerIndex()], - }); - - expect(spitUp.calculateBattlePower).toHaveBeenCalledOnce(); - - expect(pokemon.getStatStage(Stat.DEF)).toBe(0); - expect(pokemon.getStatStage(Stat.SPDEF)).toBe(0); - - expect(pokemon.getTag(StockpilingTag)).toBeUndefined(); - }); - - it("decreases stats based on stored values (different boosts)", async () => { - await game.classicMode.startBattle([SpeciesId.ABOMASNOW]); - - const pokemon = game.scene.getPlayerPokemon()!; - pokemon.addTag(BattlerTagType.STOCKPILING); - - const stockpilingTag = pokemon.getTag(StockpilingTag)!; - expect(stockpilingTag).toBeDefined(); - - // for the sake of simplicity (and because other tests cover the setup), set boost amounts directly - stockpilingTag.statChangeCounts = { - [Stat.DEF]: -1, - [Stat.SPDEF]: 2, - }; - - game.move.select(MoveId.SPIT_UP); - await game.phaseInterceptor.to(TurnInitPhase); - - expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ - move: MoveId.SPIT_UP, - result: MoveResult.SUCCESS, - targets: [game.scene.getEnemyPokemon()!.getBattlerIndex()], - }); - - expect(spitUp.calculateBattlePower).toHaveBeenCalledOnce(); - - expect(pokemon.getStatStage(Stat.DEF)).toBe(1); - expect(pokemon.getStatStage(Stat.SPDEF)).toBe(-2); - - expect(pokemon.getTag(StockpilingTag)).toBeUndefined(); - }); - }); -}); diff --git a/test/moves/swallow-spit-up.test.ts b/test/moves/swallow-spit-up.test.ts new file mode 100644 index 00000000000..96fd4ebdb07 --- /dev/null +++ b/test/moves/swallow-spit-up.test.ts @@ -0,0 +1,265 @@ +import { Stat } from "#enums/stat"; +import { StockpilingTag } from "#app/data/battler-tags"; +import { BattlerTagType } from "#app/enums/battler-tag-type"; +import { MoveResult } from "#enums/move-result"; +import { AbilityId } from "#enums/ability-id"; +import { MoveId } from "#enums/move-id"; +import { SpeciesId } from "#enums/species-id"; +import GameManager from "#test/testUtils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi, type MockInstance } from "vitest"; +import { BattlerIndex } from "#enums/battler-index"; +import { allMoves } from "#app/data/data-lists"; +import { getPokemonNameWithAffix } from "#app/messages"; +import i18next from "i18next"; + +describe("Swallow & Spit Up", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + describe("Moves - Swallow", () => { + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .battleStyle("single") + .enemySpecies(SpeciesId.RATTATA) + .enemyMoveset(MoveId.SPLASH) + .enemyAbility(AbilityId.BALL_FETCH) + .enemyLevel(100) + .startingLevel(100) + .ability(AbilityId.BALL_FETCH); + }); + + it.each<{ stackCount: number; healPercent: number }>([ + { stackCount: 1, healPercent: 25 }, + { stackCount: 2, healPercent: 50 }, + { stackCount: 3, healPercent: 100 }, + ])( + "should heal the user by $healPercent% max HP when consuming $stackCount stockpile stacks", + async ({ stackCount, healPercent }) => { + await game.classicMode.startBattle([SpeciesId.SWALOT]); + + const swalot = game.field.getPlayerPokemon(); + swalot.hp = 1; + + for (let i = 0; i < stackCount; i++) { + swalot.addTag(BattlerTagType.STOCKPILING); + } + + const stockpilingTag = swalot.getTag(StockpilingTag)!; + expect(stockpilingTag).toBeDefined(); + expect(stockpilingTag.stockpiledCount).toBe(stackCount); + + game.move.use(MoveId.SWALLOW); + await game.toEndOfTurn(); + + expect(swalot.getHpRatio()).toBeCloseTo(healPercent / 100, 1); + expect(swalot.getTag(StockpilingTag)).toBeUndefined(); + }, + ); + + it("should fail without Stockpile stacks", async () => { + await game.classicMode.startBattle([SpeciesId.ABOMASNOW]); + + const player = game.field.getPlayerPokemon(); + player.hp = 1; + + const stockpilingTag = player.getTag(StockpilingTag)!; + expect(stockpilingTag).toBeUndefined(); + + game.move.use(MoveId.SWALLOW); + await game.toEndOfTurn(); + + expect(player.getLastXMoves()[0]).toMatchObject({ + move: MoveId.SWALLOW, + result: MoveResult.FAIL, + }); + }); + + it("should fail and display message at full HP without consuming stacks", async () => { + await game.classicMode.startBattle([SpeciesId.SWALOT]); + + const swalot = game.field.getPlayerPokemon(); + swalot.addTag(BattlerTagType.STOCKPILING); + const stockpilingTag = swalot.getTag(StockpilingTag)!; + expect(stockpilingTag).toBeDefined(); + + game.move.use(MoveId.SWALLOW); + await game.toEndOfTurn(); + + expect(swalot.getLastXMoves()[0]).toMatchObject({ + move: MoveId.SWALLOW, + result: MoveResult.FAIL, + }); + expect(game.textInterceptor.logs).toContain( + i18next.t("battle:hpIsFull", { + pokemonName: getPokemonNameWithAffix(swalot), + }), + ); + expect(stockpilingTag).toBeDefined(); + }); + }); + + describe("Moves - Spit Up", () => { + let spitUpSpy: MockInstance; + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + + game.override + .battleStyle("single") + .enemySpecies(SpeciesId.RATTATA) + .enemyAbility(AbilityId.BALL_FETCH) + .enemyLevel(2000) + .enemyMoveset(MoveId.SPLASH) + .ability(AbilityId.BALL_FETCH); + + spitUpSpy = vi.spyOn(allMoves[MoveId.SPIT_UP], "calculateBattlePower"); + }); + + it.each<{ stackCount: number; power: number }>([ + { stackCount: 1, power: 100 }, + { stackCount: 2, power: 200 }, + { stackCount: 3, power: 300 }, + ])("should have $power base power when consuming $stackCount stockpile stacks", async ({ stackCount, power }) => { + await game.classicMode.startBattle([SpeciesId.SWALOT]); + + const swalot = game.field.getPlayerPokemon(); + + for (let i = 0; i < stackCount; i++) { + swalot.addTag(BattlerTagType.STOCKPILING); + } + + const stockpilingTag = swalot.getTag(StockpilingTag)!; + expect(stockpilingTag).toBeDefined(); + expect(stockpilingTag.stockpiledCount).toBe(stackCount); + + game.move.use(MoveId.SPIT_UP); + await game.toEndOfTurn(); + + expect(spitUpSpy).toHaveReturnedWith(power); + expect(swalot.getTag(StockpilingTag)).toBeUndefined(); + }); + + it("should fail without Stockpile stacks", async () => { + await game.classicMode.startBattle([SpeciesId.ABOMASNOW]); + + const player = game.field.getPlayerPokemon(); + + const stockpilingTag = player.getTag(StockpilingTag)!; + expect(stockpilingTag).toBeUndefined(); + + game.move.use(MoveId.SPIT_UP); + await game.toEndOfTurn(); + + expect(player.getLastXMoves()[0]).toMatchObject({ + move: MoveId.SPIT_UP, + result: MoveResult.FAIL, + }); + }); + }); + + describe("Stockpile stack removal", () => { + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + + game.override + .battleStyle("single") + .enemySpecies(SpeciesId.RATTATA) + .enemyMoveset(MoveId.SPLASH) + .enemyAbility(AbilityId.BALL_FETCH) + .enemyLevel(100) + .startingLevel(100) + .ability(AbilityId.BALL_FETCH); + }); + + it("should undo stat boosts when losing stacks", async () => { + await game.classicMode.startBattle([SpeciesId.ABOMASNOW]); + + const player = game.field.getPlayerPokemon(); + player.hp = 1; + + player.addTag(BattlerTagType.STOCKPILING); + const stockpilingTag = player.getTag(StockpilingTag)!; + expect(stockpilingTag).toBeDefined(); + expect(player.getStatStage(Stat.DEF)).toBe(1); + expect(player.getStatStage(Stat.SPDEF)).toBe(1); + + // remove the prior stat boosts from the log + game.phaseInterceptor.clearLogs(); + game.move.use(MoveId.SWALLOW); + await game.move.forceEnemyMove(MoveId.ACID_SPRAY); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.toEndOfTurn(); + + expect(player.getStatStage(Stat.DEF)).toBe(0); + expect(player.getStatStage(Stat.SPDEF)).toBe(-2); // +1 --> -1 --> -2 + expect(game.phaseInterceptor.log.filter(l => l === "StatStageChangePhase")).toHaveLength(3); + }); + + it("should double stat drops when gaining Simple", async () => { + await game.classicMode.startBattle([SpeciesId.ABOMASNOW]); + + const player = game.field.getPlayerPokemon(); + + game.move.use(MoveId.STOCKPILE); + await game.move.forceEnemyMove(MoveId.SIMPLE_BEAM); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.toNextTurn(); + + expect(player.getStatStage(Stat.DEF)).toBe(1); + expect(player.getStatStage(Stat.SPDEF)).toBe(1); + expect(player.hasAbility(AbilityId.SIMPLE)).toBe(true); + + game.move.use(MoveId.SPIT_UP); + await game.move.forceEnemyMove(MoveId.SPLASH); + await game.toEndOfTurn(); + + // should have fallen by 2 stages from Simple + expect(player.getStatStage(Stat.DEF)).toBe(-1); + expect(player.getStatStage(Stat.SPDEF)).toBe(-1); + expect(player.waveData.abilityRevealed).toBe(true); + }); + + it("should invert stat drops when gaining Contrary", async () => { + game.override.enemyAbility(AbilityId.CONTRARY); + await game.classicMode.startBattle([SpeciesId.ABOMASNOW]); + + const player = game.field.getPlayerPokemon(); + + game.move.use(MoveId.STOCKPILE); + await game.move.forceEnemyMove(MoveId.ENTRAINMENT); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.toEndOfTurn(); + + expect(player.getStatStage(Stat.DEF)).toBe(1); + expect(player.getStatStage(Stat.SPDEF)).toBe(1); + expect(player.hasAbility(AbilityId.CONTRARY)).toBe(true); + + game.move.use(MoveId.SPIT_UP); + await game.move.forceEnemyMove(MoveId.SPLASH); + await game.toEndOfTurn(); + + // should have risen 1 stage from Contrary + expect(player.getStatStage(Stat.DEF)).toBe(2); + expect(player.getStatStage(Stat.SPDEF)).toBe(2); + expect(player.waveData.abilityRevealed).toBe(true); + }); + }); +}); diff --git a/test/moves/swallow.test.ts b/test/moves/swallow.test.ts deleted file mode 100644 index bb95c2c593d..00000000000 --- a/test/moves/swallow.test.ts +++ /dev/null @@ -1,205 +0,0 @@ -import { Stat } from "#enums/stat"; -import { StockpilingTag } from "#app/data/battler-tags"; -import { BattlerTagType } from "#app/enums/battler-tag-type"; -import type { TurnMove } from "#app/field/pokemon"; -import { MoveResult } from "#enums/move-result"; -import { MovePhase } from "#app/phases/move-phase"; -import { TurnInitPhase } from "#app/phases/turn-init-phase"; -import { AbilityId } from "#enums/ability-id"; -import { MoveId } from "#enums/move-id"; -import { SpeciesId } from "#enums/species-id"; -import GameManager from "#test/testUtils/gameManager"; -import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; - -describe("Moves - Swallow", () => { - 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 - .battleStyle("single") - .enemySpecies(SpeciesId.RATTATA) - .enemyMoveset(MoveId.SPLASH) - .enemyAbility(AbilityId.NONE) - .enemyLevel(2000) - .moveset(MoveId.SWALLOW) - .ability(AbilityId.NONE); - }); - - describe("consumes all stockpile stacks to heal (scaling with stacks)", () => { - it("1 stack -> 25% heal", async () => { - const stacksToSetup = 1; - const expectedHeal = 25; - - await game.classicMode.startBattle([SpeciesId.ABOMASNOW]); - - const pokemon = game.scene.getPlayerPokemon()!; - vi.spyOn(pokemon, "getMaxHp").mockReturnValue(100); - pokemon["hp"] = 1; - - pokemon.addTag(BattlerTagType.STOCKPILING); - - const stockpilingTag = pokemon.getTag(StockpilingTag)!; - expect(stockpilingTag).toBeDefined(); - expect(stockpilingTag.stockpiledCount).toBe(stacksToSetup); - - vi.spyOn(pokemon, "heal"); - - game.move.select(MoveId.SWALLOW); - await game.phaseInterceptor.to(TurnInitPhase); - - expect(pokemon.heal).toHaveBeenCalledOnce(); - expect(pokemon.heal).toHaveReturnedWith(expectedHeal); - - expect(pokemon.getTag(StockpilingTag)).toBeUndefined(); - }); - - it("2 stacks -> 50% heal", async () => { - const stacksToSetup = 2; - const expectedHeal = 50; - - await game.classicMode.startBattle([SpeciesId.ABOMASNOW]); - - const pokemon = game.scene.getPlayerPokemon()!; - vi.spyOn(pokemon, "getMaxHp").mockReturnValue(100); - pokemon["hp"] = 1; - - pokemon.addTag(BattlerTagType.STOCKPILING); - pokemon.addTag(BattlerTagType.STOCKPILING); - - const stockpilingTag = pokemon.getTag(StockpilingTag)!; - expect(stockpilingTag).toBeDefined(); - expect(stockpilingTag.stockpiledCount).toBe(stacksToSetup); - - vi.spyOn(pokemon, "heal"); - - game.move.select(MoveId.SWALLOW); - await game.phaseInterceptor.to(TurnInitPhase); - - expect(pokemon.heal).toHaveBeenCalledOnce(); - expect(pokemon.heal).toHaveReturnedWith(expectedHeal); - - expect(pokemon.getTag(StockpilingTag)).toBeUndefined(); - }); - - it("3 stacks -> 100% heal", async () => { - const stacksToSetup = 3; - const expectedHeal = 100; - - await game.classicMode.startBattle([SpeciesId.ABOMASNOW]); - - const pokemon = game.scene.getPlayerPokemon()!; - vi.spyOn(pokemon, "getMaxHp").mockReturnValue(100); - pokemon["hp"] = 0.0001; - - pokemon.addTag(BattlerTagType.STOCKPILING); - pokemon.addTag(BattlerTagType.STOCKPILING); - pokemon.addTag(BattlerTagType.STOCKPILING); - - const stockpilingTag = pokemon.getTag(StockpilingTag)!; - expect(stockpilingTag).toBeDefined(); - expect(stockpilingTag.stockpiledCount).toBe(stacksToSetup); - - vi.spyOn(pokemon, "heal"); - - game.move.select(MoveId.SWALLOW); - await game.phaseInterceptor.to(TurnInitPhase); - - expect(pokemon.heal).toHaveBeenCalledOnce(); - expect(pokemon.heal).toHaveReturnedWith(expect.closeTo(expectedHeal)); - - expect(pokemon.getTag(StockpilingTag)).toBeUndefined(); - }); - }); - - it("fails without stacks", async () => { - await game.classicMode.startBattle([SpeciesId.ABOMASNOW]); - - const pokemon = game.scene.getPlayerPokemon()!; - - const stockpilingTag = pokemon.getTag(StockpilingTag)!; - expect(stockpilingTag).toBeUndefined(); - - game.move.select(MoveId.SWALLOW); - await game.phaseInterceptor.to(TurnInitPhase); - - expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ - move: MoveId.SWALLOW, - result: MoveResult.FAIL, - targets: [pokemon.getBattlerIndex()], - }); - }); - - describe("restores stat stage boosts granted by stacks", () => { - it("decreases stats based on stored values (both boosts equal)", async () => { - await game.classicMode.startBattle([SpeciesId.ABOMASNOW]); - - const pokemon = game.scene.getPlayerPokemon()!; - pokemon.addTag(BattlerTagType.STOCKPILING); - - const stockpilingTag = pokemon.getTag(StockpilingTag)!; - expect(stockpilingTag).toBeDefined(); - - game.move.select(MoveId.SWALLOW); - await game.phaseInterceptor.to(MovePhase); - - expect(pokemon.getStatStage(Stat.DEF)).toBe(1); - expect(pokemon.getStatStage(Stat.SPDEF)).toBe(1); - - await game.phaseInterceptor.to(TurnInitPhase); - - expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ - move: MoveId.SWALLOW, - result: MoveResult.SUCCESS, - targets: [pokemon.getBattlerIndex()], - }); - - expect(pokemon.getStatStage(Stat.DEF)).toBe(0); - expect(pokemon.getStatStage(Stat.SPDEF)).toBe(0); - - expect(pokemon.getTag(StockpilingTag)).toBeUndefined(); - }); - - it("lower stat stages based on stored values (different boosts)", async () => { - await game.classicMode.startBattle([SpeciesId.ABOMASNOW]); - - const pokemon = game.scene.getPlayerPokemon()!; - pokemon.addTag(BattlerTagType.STOCKPILING); - - const stockpilingTag = pokemon.getTag(StockpilingTag)!; - expect(stockpilingTag).toBeDefined(); - - // for the sake of simplicity (and because other tests cover the setup), set boost amounts directly - stockpilingTag.statChangeCounts = { - [Stat.DEF]: -1, - [Stat.SPDEF]: 2, - }; - - game.move.select(MoveId.SWALLOW); - - await game.phaseInterceptor.to(TurnInitPhase); - - expect(pokemon.getMoveHistory().at(-1)).toMatchObject({ - move: MoveId.SWALLOW, - result: MoveResult.SUCCESS, - targets: [pokemon.getBattlerIndex()], - }); - - expect(pokemon.getStatStage(Stat.DEF)).toBe(1); - expect(pokemon.getStatStage(Stat.SPDEF)).toBe(-2); - - expect(pokemon.getTag(StockpilingTag)).toBeUndefined(); - }); - }); -}); From 501fb0b594064ee29c2d0cd5eed49bf7603a2018 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sat, 14 Jun 2025 14:45:16 -0400 Subject: [PATCH 03/59] fixed tets a bit --- test/moves/swallow-spit-up.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/moves/swallow-spit-up.test.ts b/test/moves/swallow-spit-up.test.ts index 96fd4ebdb07..80b817723f5 100644 --- a/test/moves/swallow-spit-up.test.ts +++ b/test/moves/swallow-spit-up.test.ts @@ -84,7 +84,8 @@ describe("Swallow & Spit Up", () => { }); }); - it("should fail and display message at full HP without consuming stacks", async () => { + // TODO: Does this consume stacks or not? + it.todo("should fail and display message at full HP, consuming stacks", async () => { await game.classicMode.startBattle([SpeciesId.SWALOT]); const swalot = game.field.getPlayerPokemon(); @@ -234,7 +235,6 @@ describe("Swallow & Spit Up", () => { // should have fallen by 2 stages from Simple expect(player.getStatStage(Stat.DEF)).toBe(-1); expect(player.getStatStage(Stat.SPDEF)).toBe(-1); - expect(player.waveData.abilityRevealed).toBe(true); }); it("should invert stat drops when gaining Contrary", async () => { @@ -259,7 +259,6 @@ describe("Swallow & Spit Up", () => { // should have risen 1 stage from Contrary expect(player.getStatStage(Stat.DEF)).toBe(2); expect(player.getStatStage(Stat.SPDEF)).toBe(2); - expect(player.waveData.abilityRevealed).toBe(true); }); }); }); From 07eb03f9815f261c1398ca12b8f6f827011ddb20 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sat, 14 Jun 2025 15:06:17 -0400 Subject: [PATCH 04/59] hhhh --- src/data/moves/move.ts | 16 +++++++++------- test/moves/recovery-moves.test.ts | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index fc578f130c6..a4c43623d96 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -1948,16 +1948,18 @@ export class HealAttr extends MoveEffectAttr { } override canApply(user: Pokemon, target: Pokemon, _move: Move, _args?: any[]): boolean { - return !(this.selfTarget ? user : target).isFullHp(); - } + if (!super.canApply(user, target, _move, _args)) { + return false; + } - override getFailedText(user: Pokemon, target: Pokemon, _move: Move): string | undefined { const healedPokemon = this.selfTarget ? user : target; - return healedPokemon.isFullHp() - ? i18next.t("battle:hpIsFull", { + if (healedPokemon.isFullHp()) { + globalScene.phaseManager.queueMessage(i18next.t("battle:hpIsFull", { pokemonName: getPokemonNameWithAffix(healedPokemon), - }) - : undefined; + })) + return false; + } + return true; } } diff --git a/test/moves/recovery-moves.test.ts b/test/moves/recovery-moves.test.ts index 89c97437367..92a481f7a7e 100644 --- a/test/moves/recovery-moves.test.ts +++ b/test/moves/recovery-moves.test.ts @@ -104,7 +104,7 @@ describe("Moves - Recovery Moves", () => { game.move.use(MoveId.MOONLIGHT); await game.toEndOfTurn(); - expect(blissey.getHpRatio()).toBeCloseTo(0.67, 1); + expect(blissey.getHpRatio()).toBeCloseTo(0.66, 1); }); const nonSunWTs = getEnumValues(WeatherType) From 3d20235fa5e44b05eb299c034091dc35c0d0530b Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Tue, 17 Jun 2025 15:51:49 -0400 Subject: [PATCH 05/59] ssss --- src/data/moves/move.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 2bb672e4a93..7df53f0c861 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -1929,16 +1929,17 @@ export class HealAttr extends MoveEffectAttr { * Creates a new {@linkcode PokemonHealPhase}. * This heals the target and shows the appropriate message. */ - addHealPhase(target: Pokemon, healRatio: number) { + protected addHealPhase(target: Pokemon, healRatio: number) { globalScene.phaseManager.unshiftNew("PokemonHealPhase", target.getBattlerIndex(), toDmgValue(target.getMaxHp() * healRatio), i18next.t("moveTriggers:healHp", { pokemonName: getPokemonNameWithAffix(target) }), true, !this.showAnim); } - getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { + override getTargetBenefitScore(user: Pokemon, target: Pokemon, _move: Move): number { const score = ((1 - (this.selfTarget ? user : target).getHpRatio()) * 20) - this.healRatio * 10; return Math.round(score / (1 - this.healRatio / 2)); } + // TODO: Change post move failure rework override canApply(user: Pokemon, target: Pokemon, _move: Move, _args?: any[]): boolean { if (!super.canApply(user, target, _move, _args)) { return false; From aee33fe625f2864e7f9c072d1e350996b01b10f9 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Tue, 17 Jun 2025 16:07:53 -0400 Subject: [PATCH 06/59] Fixed up pollen puff tests --- test/moves/pollen_puff.test.ts | 75 +++++++++++++++++++++++------- test/testUtils/phaseInterceptor.ts | 2 + 2 files changed, 60 insertions(+), 17 deletions(-) diff --git a/test/moves/pollen_puff.test.ts b/test/moves/pollen_puff.test.ts index cd0a138e96c..0a9b6d9cf24 100644 --- a/test/moves/pollen_puff.test.ts +++ b/test/moves/pollen_puff.test.ts @@ -5,8 +5,11 @@ import { SpeciesId } from "#enums/species-id"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { MoveResult } from "#enums/move-result"; +import { getPokemonNameWithAffix } from "#app/messages"; +import i18next from "i18next"; -describe("Moves - Pollen Puff", () => { +describe("Move - Pollen Puff", () => { let phaserGame: Phaser.Game; let game: GameManager; @@ -23,42 +26,80 @@ describe("Moves - Pollen Puff", () => { beforeEach(() => { game = new GameManager(phaserGame); game.override - .moveset([MoveId.POLLEN_PUFF]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") .criticalHits(false) + .enemyLevel(100) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); }); - it("should not heal more than once when the user has a source of multi-hit", async () => { - game.override.battleStyle("double").moveset([MoveId.POLLEN_PUFF, MoveId.ENDURE]).ability(AbilityId.PARENTAL_BOND); + it("should damage an enemy when used, or heal an ally for 50% max HP", async () => { + game.override.battleStyle("double").ability(AbilityId.PARENTAL_BOND); await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.OMANYTE]); - const [_, rightPokemon] = game.scene.getPlayerField(); + const [_, omantye, karp1] = game.scene.getField(); + omantye.hp = 1; - rightPokemon.damageAndUpdate(rightPokemon.hp - 1); + game.move.use(MoveId.POLLEN_PUFF, BattlerIndex.PLAYER, BattlerIndex.PLAYER_2); + game.move.use(MoveId.POLLEN_PUFF, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY); + await game.toNextTurn(); - game.move.select(MoveId.POLLEN_PUFF, 0, BattlerIndex.PLAYER_2); - game.move.select(MoveId.ENDURE, 1); + expect(karp1.hp).toBeLessThan(karp1.getMaxHp()); + expect(omantye.hp).toBeCloseTo(0.5 * omantye.getMaxHp() + 1, 1); + expect(game.phaseInterceptor.log).toContain("PokemonHealPhase"); + }); - await game.phaseInterceptor.to("BerryPhase"); + it.todo("should display message & count as failed when hitting a full HP ally", async () => { + game.override.battleStyle("double").ability(AbilityId.PARENTAL_BOND); + await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.OMANYTE]); - // Pollen Puff heals with a ratio of 0.5, as long as Pollen Puff triggers only once the pokemon will always be <= (0.5 * Max HP) + 1 - expect(rightPokemon.hp).toBeLessThanOrEqual(0.5 * rightPokemon.getMaxHp() + 1); + const [bulbasaur, omantye] = game.scene.getPlayerField(); + + game.move.use(MoveId.POLLEN_PUFF, BattlerIndex.PLAYER, BattlerIndex.PLAYER_2); + game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2); + await game.toEndOfTurn(); + + // move failed without unshifting a phase + expect(omantye.hp).toBe(omantye.getMaxHp()); + expect(bulbasaur.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + expect(game.textInterceptor.logs).toContain( + i18next.t("battle:hpIsFull", { + pokemonName: getPokemonNameWithAffix(omantye), + }), + ); + expect(game.phaseInterceptor.log).not.toContain("PokemonHealPhase"); + }); + + it("should not heal more than once if the user has a source of multi-hit", async () => { + game.override.battleStyle("double").ability(AbilityId.PARENTAL_BOND); + await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.OMANYTE]); + + const [bulbasaur, omantye] = game.scene.getPlayerField(); + + omantye.hp = 1; + + game.move.use(MoveId.POLLEN_PUFF, BattlerIndex.PLAYER, BattlerIndex.PLAYER_2); + game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2); + await game.toEndOfTurn(); + + expect(bulbasaur.turnData.hitCount).toBe(1); + expect(omantye.hp).toBeLessThanOrEqual(0.5 * omantye.getMaxHp() + 1); + expect( + game.phaseInterceptor.log.filter(l => l === "PokemonHealPhase"), + game.phaseInterceptor.log.join("\n"), + ).toHaveLength(1); }); it("should damage an enemy multiple times when the user has a source of multi-hit", async () => { - game.override.moveset([MoveId.POLLEN_PUFF]).ability(AbilityId.PARENTAL_BOND).enemyLevel(100); + game.override.ability(AbilityId.PARENTAL_BOND); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - const target = game.scene.getEnemyPokemon()!; - - game.move.select(MoveId.POLLEN_PUFF); - - await game.phaseInterceptor.to("BerryPhase"); + game.move.use(MoveId.POLLEN_PUFF); + await game.toEndOfTurn(); + const target = game.field.getEnemyPokemon(); expect(target.battleData.hitCount).toBe(2); }); }); diff --git a/test/testUtils/phaseInterceptor.ts b/test/testUtils/phaseInterceptor.ts index 9d046fc85ba..e1033f87b15 100644 --- a/test/testUtils/phaseInterceptor.ts +++ b/test/testUtils/phaseInterceptor.ts @@ -64,6 +64,7 @@ import { PostGameOverPhase } from "#app/phases/post-game-over-phase"; import { RevivalBlessingPhase } from "#app/phases/revival-blessing-phase"; import type { PhaseClass, PhaseString } from "#app/@types/phase-types"; +import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase"; export interface PromptHandler { phaseTarget?: string; @@ -143,6 +144,7 @@ export default class PhaseInterceptor { [AttemptRunPhase, this.startPhase], [SelectBiomePhase, this.startPhase], [MysteryEncounterPhase, this.startPhase], + [PokemonHealPhase, this.startPhase], [MysteryEncounterOptionSelectedPhase, this.startPhase], [MysteryEncounterBattlePhase, this.startPhase], [MysteryEncounterRewardsPhase, this.startPhase], From 1529fd932906b4df065442400de1eaee38e33b5f Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Tue, 17 Jun 2025 16:10:43 -0400 Subject: [PATCH 07/59] Fixed tests --- test/moves/recovery-moves.test.ts | 6 +++--- test/moves/swallow-spit-up.test.ts | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/test/moves/recovery-moves.test.ts b/test/moves/recovery-moves.test.ts index 92a481f7a7e..b9a7bb4523d 100644 --- a/test/moves/recovery-moves.test.ts +++ b/test/moves/recovery-moves.test.ts @@ -35,7 +35,7 @@ describe("Moves - Recovery Moves", () => { game.override .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH) @@ -83,7 +83,7 @@ describe("Moves - Recovery Moves", () => { game.override .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH) @@ -151,7 +151,7 @@ describe("Moves - Recovery Moves", () => { game.override .ability(AbilityId.BALL_FETCH) .battleStyle("single") - .disableCrits() + .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH) diff --git a/test/moves/swallow-spit-up.test.ts b/test/moves/swallow-spit-up.test.ts index 80b817723f5..4c73d23e766 100644 --- a/test/moves/swallow-spit-up.test.ts +++ b/test/moves/swallow-spit-up.test.ts @@ -196,7 +196,9 @@ describe("Swallow & Spit Up", () => { const player = game.field.getPlayerPokemon(); player.hp = 1; - player.addTag(BattlerTagType.STOCKPILING); + game.move.use(MoveId.STOCKPILE); + await game.toNextTurn(); + const stockpilingTag = player.getTag(StockpilingTag)!; expect(stockpilingTag).toBeDefined(); expect(player.getStatStage(Stat.DEF)).toBe(1); From b001496035914d00003267bb55b1f57c595ae575 Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Tue, 24 Jun 2025 12:38:33 -0400 Subject: [PATCH 08/59] Fix leftovers.test.ts Thing was waiting till end of turn but heal happened after --- test/items/leftovers.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/items/leftovers.test.ts b/test/items/leftovers.test.ts index 21319d2c9d7..96e0d391279 100644 --- a/test/items/leftovers.test.ts +++ b/test/items/leftovers.test.ts @@ -54,7 +54,7 @@ describe("Items - Leftovers", () => { const leadHpAfterDamage = leadPokemon.hp; // Check if leftovers heal us - await game.phaseInterceptor.to(TurnEndPhase); + await game.toNextTurn(); expect(leadPokemon.hp).toBeGreaterThan(leadHpAfterDamage); }); }); From c06c1bfbaf6e92f4be67908e4ccc8deb9e478f27 Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Tue, 24 Jun 2025 12:41:42 -0400 Subject: [PATCH 09/59] Update leftovers.test.ts --- test/items/leftovers.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/items/leftovers.test.ts b/test/items/leftovers.test.ts index 96e0d391279..99d2a16ff75 100644 --- a/test/items/leftovers.test.ts +++ b/test/items/leftovers.test.ts @@ -1,5 +1,4 @@ import { DamageAnimPhase } from "#app/phases/damage-anim-phase"; -import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; From 4cd9399681a6c47594d6720494a25c44cabce8c8 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sat, 21 Jun 2025 22:12:59 -0400 Subject: [PATCH 10/59] Fixed test --- test/moves/recovery-moves.test.ts | 98 +++++++++++-------------------- 1 file changed, 35 insertions(+), 63 deletions(-) diff --git a/test/moves/recovery-moves.test.ts b/test/moves/recovery-moves.test.ts index b9a7bb4523d..6b3b84c5539 100644 --- a/test/moves/recovery-moves.test.ts +++ b/test/moves/recovery-moves.test.ts @@ -2,6 +2,7 @@ import { getPokemonNameWithAffix } from "#app/messages"; import { getEnumValues, toReadableString } from "#app/utils/common"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; +import { MoveResult } from "#enums/move-result"; import { SpeciesId } from "#enums/species-id"; import { WeatherType } from "#enums/weather-type"; import GameManager from "#test/testUtils/gameManager"; @@ -9,7 +10,7 @@ import i18next from "i18next"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -describe("Moves - Recovery Moves", () => { +describe("Moves - Recovery Moves - ", () => { let phaserGame: Phaser.Game; let game: GameManager; @@ -19,30 +20,33 @@ describe("Moves - Recovery Moves", () => { }); }); + 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); + }); + + describe.each<{ name: string; move: MoveId }>([ { name: "Recover", move: MoveId.RECOVER }, { name: "Soft-Boiled", move: MoveId.SOFT_BOILED }, { name: "Milk Drink", move: MoveId.MILK_DRINK }, { name: "Slack Off", move: MoveId.SLACK_OFF }, { name: "Roost", move: MoveId.ROOST }, - ])("Normal Recovery Moves - $name", ({ move }) => { - 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); - }); - + { name: "Weather-based Healing Moves", move: MoveId.SYNTHESIS }, + { name: "Shore Up", move: MoveId.SHORE_UP }, + ])("$name", ({ move }) => { it("should heal 50% of the user's maximum HP", async () => { await game.classicMode.startBattle([SpeciesId.BLISSEY]); @@ -56,7 +60,7 @@ describe("Moves - Recovery Moves", () => { expect(game.phaseInterceptor.log).toContain("PokemonHealPhase"); }); - it("should display message and fail if used at full HP", async () => { + it("should display message without adding phase if used at full HP", async () => { await game.classicMode.startBattle([SpeciesId.BLISSEY]); game.move.use(move); @@ -73,29 +77,12 @@ describe("Moves - Recovery Moves", () => { }); }); - describe("Weather-based Healing moves", () => { - 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); - }); - + describe("Weather-based Healing Moves", () => { it.each([ - { name: "Harsh Sunlight", ability: AbilityId.DROUGHT }, - { name: "Extremely Harsh Sunlight", ability: AbilityId.DESOLATE_LAND }, - ])("should heal 66% of the user's maximum HP under $name", async ({ ability }) => { - game.override.passiveAbility(ability); + { name: "Harsh Sunlight", weather: WeatherType.SUNNY }, + { name: "Extremely Harsh Sunlight", weather: WeatherType.HARSH_SUN }, + ])("should heal 66% of the user's maximum HP under $name", async ({ weather }) => { + game.override.weather(weather); await game.classicMode.startBattle([SpeciesId.BLISSEY]); const blissey = game.field.getPlayerPokemon(); @@ -107,8 +94,10 @@ describe("Moves - Recovery Moves", () => { expect(blissey.getHpRatio()).toBeCloseTo(0.66, 1); }); - const nonSunWTs = getEnumValues(WeatherType) - .filter(wt => ![WeatherType.NONE, WeatherType.STRONG_WINDS].includes(wt)) + const nonSunWTs = (getEnumValues(WeatherType) as WeatherType[]) + .filter( + wt => ![WeatherType.SUNNY, WeatherType.HARSH_SUN, WeatherType.NONE, WeatherType.STRONG_WINDS].includes(wt), + ) .map(wt => ({ name: toReadableString(WeatherType[wt]), weather: wt, @@ -142,25 +131,8 @@ describe("Moves - Recovery Moves", () => { }); describe("Shore Up", () => { - 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 heal 66% of the user's maximum HP under sandstorm", async () => { - game.override.ability(AbilityId.SAND_STREAM); + it("should heal 66% of the user's maximum HP in a sandstorm", async () => { + game.override.weather(WeatherType.SANDSTORM); await game.classicMode.startBattle([SpeciesId.BLISSEY]); const blissey = game.field.getPlayerPokemon(); From 41f066cb4b6c826853ae654db6c40add52c5d9c7 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Tue, 24 Jun 2025 18:59:44 -0400 Subject: [PATCH 11/59] Fixed bug and such --- src/data/moves/move.ts | 27 ++++++++++++++------------- src/field/pokemon.ts | 1 + test/moves/recovery-moves.test.ts | 4 +--- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 42596a47204..26ffa0c0656 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -1932,19 +1932,21 @@ export class AddSubstituteAttr extends MoveEffectAttr { * @see {@linkcode apply} */ export class HealAttr extends MoveEffectAttr { - /** The percentage of {@linkcode Stat.HP} to heal */ - private healRatio: number; - /** Should an animation be shown? */ - private showAnim: boolean; - - constructor(healRatio?: number, showAnim?: boolean, selfTarget?: boolean) { - super(selfTarget === undefined || selfTarget); - - this.healRatio = healRatio || 1; - this.showAnim = !!showAnim; + constructor( + /** The percentage of {@linkcode Stat.HP} to heal; default `1` */ + protected healRatio = 1, + /** Whether to display a healing animation upon healing the target; default `false` */ + private showAnim = false, + selfTarget = true + ) { + super(selfTarget); } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + if (!super.apply(user, target, move, args)) { + return false; + } + this.addHealPhase(this.selfTarget ? user : target, this.healRatio); return true; } @@ -1998,9 +2000,8 @@ export class VariableHealAttr extends HealAttr { } apply(user: Pokemon, target: Pokemon, move: Move, _args: any[]): boolean { - const healRatio = this.healFunc(user, target, move) - this.addHealPhase(target, healRatio); - return true; + this.healRatio = this.healFunc(user, target, move) + return super.apply(user, target, move, args); } } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index eee6c309859..f62542fcaac 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -1601,6 +1601,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return this.getMaxHp() - this.hp; } + // TODO: Why does this default to `false`? getHpRatio(precise = false): number { return precise ? this.hp / this.getMaxHp() : Math.round((this.hp / this.getMaxHp()) * 100) / 100; } diff --git a/test/moves/recovery-moves.test.ts b/test/moves/recovery-moves.test.ts index 6b3b84c5539..0a6daaa209c 100644 --- a/test/moves/recovery-moves.test.ts +++ b/test/moves/recovery-moves.test.ts @@ -2,7 +2,6 @@ import { getPokemonNameWithAffix } from "#app/messages"; import { getEnumValues, toReadableString } from "#app/utils/common"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; -import { MoveResult } from "#enums/move-result"; import { SpeciesId } from "#enums/species-id"; import { WeatherType } from "#enums/weather-type"; import GameManager from "#test/testUtils/gameManager"; @@ -27,7 +26,7 @@ describe("Moves - Recovery Moves - ", () => { beforeEach(() => { game = new GameManager(phaserGame); game.override - .ability(AbilityId.BALL_FETCH) + .ability(AbilityId.MAGIC_GUARD) // prevents passive weather damage .battleStyle("single") .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) @@ -37,7 +36,6 @@ describe("Moves - Recovery Moves - ", () => { .enemyLevel(100); }); - describe.each<{ name: string; move: MoveId }>([ { name: "Recover", move: MoveId.RECOVER }, { name: "Soft-Boiled", move: MoveId.SOFT_BOILED }, From 19cca2664b825eae0afb9c088aa6b6bd233f75f2 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Tue, 24 Jun 2025 19:06:48 -0400 Subject: [PATCH 12/59] Added TODO test case for Roost --- test/moves/roost.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/moves/roost.test.ts b/test/moves/roost.test.ts index 3707e0ead45..0b65d36f82a 100644 --- a/test/moves/roost.test.ts +++ b/test/moves/roost.test.ts @@ -7,7 +7,7 @@ import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, test, it } from "vitest"; describe("Moves - Roost", () => { let phaserGame: Phaser.Game; @@ -47,6 +47,9 @@ describe("Moves - Roost", () => { * 6. Non flying types using roost (such as dunsparce) are already grounded, so this move will only heal and have no other effects. */ + // TODO: This test currently is meaningless since we don't fail ineffective moves + it.todo("should still ground the user without failing when used at full HP"); + test("Non flying type uses roost -> no type change, took damage", async () => { await game.classicMode.startBattle([SpeciesId.DUNSPARCE]); const playerPokemon = game.scene.getPlayerPokemon()!; From 5903aab4b1607a39b05095358a976fa40d7d4fb8 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sat, 19 Jul 2025 09:23:13 -0400 Subject: [PATCH 13/59] fixed roost test --- src/data/moves/move.ts | 11 +- test/moves/recovery-moves.test.ts | 4 +- test/moves/roost.test.ts | 284 +++++++++--------------------- test/testUtils/gameManager.ts | 9 +- 4 files changed, 98 insertions(+), 210 deletions(-) diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 26ffa0c0656..db69e61ef53 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -1966,11 +1966,8 @@ export class HealAttr extends MoveEffectAttr { } // TODO: Change post move failure rework - override canApply(user: Pokemon, target: Pokemon, _move: Move, _args?: any[]): boolean { - if (!super.canApply(user, target, _move, _args)) { - return false; - } - + override getCondition(): MoveConditionFunc { + return (user, target) => { const healedPokemon = this.selfTarget ? user : target; if (healedPokemon.isFullHp()) { globalScene.phaseManager.queueMessage(i18next.t("battle:hpIsFull", { @@ -2019,8 +2016,8 @@ export class HealOnAllyAttr extends HealAttr { return false; } - override canApply(user: Pokemon, target: Pokemon, _move: Move, _args?: any[]): boolean { - return user.getAlly() !== target || super.canApply(user, target, _move, _args); + override getCondition(user: Pokemon, target: Pokemon, _move: Move, _args?: any[]): boolean { + return user.getAlly() !== target || super.getCondition()(user, target, _move); } } diff --git a/test/moves/recovery-moves.test.ts b/test/moves/recovery-moves.test.ts index 0a6daaa209c..9fb21fc5507 100644 --- a/test/moves/recovery-moves.test.ts +++ b/test/moves/recovery-moves.test.ts @@ -2,6 +2,7 @@ import { getPokemonNameWithAffix } from "#app/messages"; import { getEnumValues, toReadableString } from "#app/utils/common"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; +import { MoveResult } from "#enums/move-result"; import { SpeciesId } from "#enums/species-id"; import { WeatherType } from "#enums/weather-type"; import GameManager from "#test/testUtils/gameManager"; @@ -58,7 +59,7 @@ describe("Moves - Recovery Moves - ", () => { expect(game.phaseInterceptor.log).toContain("PokemonHealPhase"); }); - it("should display message without adding phase if used at full HP", async () => { + it("should fail if used at full HP", async () => { await game.classicMode.startBattle([SpeciesId.BLISSEY]); game.move.use(move); @@ -72,6 +73,7 @@ describe("Moves - Recovery Moves - ", () => { }), ); expect(game.phaseInterceptor.log).not.toContain("PokemonHealPhase"); + expect(blissey.getLastXMoves()[0].result).toBe(MoveResult.FAIL); }); }); diff --git a/test/moves/roost.test.ts b/test/moves/roost.test.ts index 0b65d36f82a..7fa52b8f2f7 100644 --- a/test/moves/roost.test.ts +++ b/test/moves/roost.test.ts @@ -1,13 +1,11 @@ -import { BattlerIndex } from "#enums/battler-index"; import { PokemonType } from "#enums/pokemon-type"; import { BattlerTagType } from "#app/enums/battler-tag-type"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; -import { MoveEffectPhase } from "#app/phases/move-effect-phase"; -import { TurnEndPhase } from "#app/phases/turn-end-phase"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, test, it } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { AbilityId } from "#enums/ability-id"; describe("Moves - Roost", () => { let phaserGame: Phaser.Game; @@ -28,221 +26,109 @@ describe("Moves - Roost", () => { game.override .battleStyle("single") .enemySpecies(SpeciesId.RELICANTH) + .ability(AbilityId.TRIAGE) .startingLevel(100) .enemyLevel(100) - .enemyMoveset(MoveId.EARTHQUAKE) - .moveset([MoveId.ROOST, MoveId.BURN_UP, MoveId.DOUBLE_SHOCK]); + .enemyMoveset(MoveId.SPLASH) }); - /** - * Roost's behavior should be defined as: - * The pokemon loses its flying type for a turn. If the pokemon was ungroundd solely due to being a flying type, it will be grounded until end of turn. - * 1. Pure Flying type pokemon -> become normal type until end of turn - * 2. Dual Flying/X type pokemon -> become type X until end of turn - * 3. Pokemon that use burn up into roost (ex. Moltres) -> become flying due to burn up, then typeless until end of turn after using roost - * 4. If a pokemon is afflicted with Forest's Curse or Trick or treat, dual type pokemon will become 3 type pokemon after the flying type is regained - * Pure flying types become (Grass or Ghost) and then back to flying/ (Grass or Ghost), - * and pokemon post Burn up become () - * 5. If a pokemon is also ungrounded due to other reasons (such as levitate), it will stay ungrounded post roost, despite not being flying type. - * 6. Non flying types using roost (such as dunsparce) are already grounded, so this move will only heal and have no other effects. - */ - - // TODO: This test currently is meaningless since we don't fail ineffective moves - it.todo("should still ground the user without failing when used at full HP"); - - test("Non flying type uses roost -> no type change, took damage", async () => { - await game.classicMode.startBattle([SpeciesId.DUNSPARCE]); - const playerPokemon = game.scene.getPlayerPokemon()!; - const playerPokemonStartingHP = playerPokemon.hp; - game.move.select(MoveId.ROOST); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); - await game.phaseInterceptor.to(MoveEffectPhase); - - // Should only be normal type, and NOT flying type - let playerPokemonTypes = playerPokemon.getTypes(); - expect(playerPokemonTypes[0] === PokemonType.NORMAL).toBeTruthy(); - expect(playerPokemonTypes.length === 1).toBeTruthy(); - expect(playerPokemon.isGrounded()).toBeTruthy(); - - await game.phaseInterceptor.to(TurnEndPhase); - - // Lose HP, still normal type - playerPokemonTypes = playerPokemon.getTypes(); - expect(playerPokemon.hp).toBeLessThan(playerPokemonStartingHP); - expect(playerPokemonTypes[0] === PokemonType.NORMAL).toBeTruthy(); - expect(playerPokemonTypes.length === 1).toBeTruthy(); - expect(playerPokemon.isGrounded()).toBeTruthy(); - }); - - test("Pure flying type -> becomes normal after roost and takes damage from ground moves -> regains flying", async () => { - await game.classicMode.startBattle([SpeciesId.TORNADUS]); - const playerPokemon = game.scene.getPlayerPokemon()!; - const playerPokemonStartingHP = playerPokemon.hp; - game.move.select(MoveId.ROOST); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); - await game.phaseInterceptor.to(MoveEffectPhase); - - // Should only be normal type, and NOT flying type - let playerPokemonTypes = playerPokemon.getTypes(); - expect(playerPokemonTypes[0] === PokemonType.NORMAL).toBeTruthy(); - expect(playerPokemonTypes[0] === PokemonType.FLYING).toBeFalsy(); - expect(playerPokemon.isGrounded()).toBeTruthy(); - - await game.phaseInterceptor.to(TurnEndPhase); - - // Should have lost HP and is now back to being pure flying - playerPokemonTypes = playerPokemon.getTypes(); - expect(playerPokemon.hp).toBeLessThan(playerPokemonStartingHP); - expect(playerPokemonTypes[0] === PokemonType.NORMAL).toBeFalsy(); - expect(playerPokemonTypes[0] === PokemonType.FLYING).toBeTruthy(); - expect(playerPokemon.isGrounded()).toBeFalsy(); - }); - - test("Dual X/flying type -> becomes type X after roost and takes damage from ground moves -> regains flying", async () => { + it("should remove the user's Flying type until end of turn", async () => { await game.classicMode.startBattle([SpeciesId.HAWLUCHA]); - const playerPokemon = game.scene.getPlayerPokemon()!; - const playerPokemonStartingHP = playerPokemon.hp; - game.move.select(MoveId.ROOST); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); - await game.phaseInterceptor.to(MoveEffectPhase); - // Should only be pure fighting type and grounded - let playerPokemonTypes = playerPokemon.getTypes(); - expect(playerPokemonTypes[0] === PokemonType.FIGHTING).toBeTruthy(); - expect(playerPokemonTypes.length === 1).toBeTruthy(); - expect(playerPokemon.isGrounded()).toBeTruthy(); + const hawlucha = game.field.getPlayerPokemon(); + hawlucha.hp = 1; - await game.phaseInterceptor.to(TurnEndPhase); + game.move.use(MoveId.ROOST); + await game.phaseInterceptor.to("MoveEffectPhase"); - // Should have lost HP and is now back to being fighting/flying - playerPokemonTypes = playerPokemon.getTypes(); - expect(playerPokemon.hp).toBeLessThan(playerPokemonStartingHP); - expect(playerPokemonTypes[0] === PokemonType.FIGHTING).toBeTruthy(); - expect(playerPokemonTypes[1] === PokemonType.FLYING).toBeTruthy(); - expect(playerPokemon.isGrounded()).toBeFalsy(); + + // Should lose flying type temporarily + expect(hawlucha.getTag(BattlerTagType.ROOSTED)).toBeDefined(); + expect(hawlucha.getTypes()).toEqual([PokemonType.FIGHTING]) + expect(hawlucha.isGrounded()).toBe(true); + + await game.toEndOfTurn(); + + // Should have changed back to fighting/flying + expect(hawlucha.getTypes()).toEqual([PokemonType.FIGHTING, PokemonType.FLYING]) + expect(hawlucha.isGrounded()).toBe(false); }); - test("Pokemon with levitate after using roost should lose flying type but still be unaffected by ground moves", async () => { - game.override.starterForms({ [SpeciesId.ROTOM]: 4 }); - await game.classicMode.startBattle([SpeciesId.ROTOM]); - const playerPokemon = game.scene.getPlayerPokemon()!; - const playerPokemonStartingHP = playerPokemon.hp; - game.move.select(MoveId.ROOST); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); - await game.phaseInterceptor.to(MoveEffectPhase); + it("should preserve types of non-Flying type Pokemon", async () => { + await game.classicMode.startBattle([SpeciesId.MEW]); - // Should only be pure eletric type and grounded - let playerPokemonTypes = playerPokemon.getTypes(); - expect(playerPokemonTypes[0] === PokemonType.ELECTRIC).toBeTruthy(); - expect(playerPokemonTypes.length === 1).toBeTruthy(); - expect(playerPokemon.isGrounded()).toBeFalsy(); + const mew = game.field.getPlayerPokemon(); + mew.hp = 1; - await game.phaseInterceptor.to(TurnEndPhase); + game.move.use(MoveId.ROOST); + await game.toEndOfTurn(false); - // Should have lost HP and is now back to being electric/flying - playerPokemonTypes = playerPokemon.getTypes(); - expect(playerPokemon.hp).toBe(playerPokemonStartingHP); - expect(playerPokemonTypes[0] === PokemonType.ELECTRIC).toBeTruthy(); - expect(playerPokemonTypes[1] === PokemonType.FLYING).toBeTruthy(); - expect(playerPokemon.isGrounded()).toBeFalsy(); + // Should remain psychic type + expect(mew.getTypes()).toEqual([PokemonType.PSYCHIC]); + expect(mew.isGrounded()).toBe(true); }); - test("A fire/flying type that uses burn up, then roost should be typeless until end of turn", async () => { + it("should convert pure Flying types into normal types", async () => { + await game.classicMode.startBattle([SpeciesId.TORNADUS]); + + const tornadus = game.field.getPlayerPokemon(); + tornadus.hp = 1; + + game.move.use(MoveId.ROOST); + await game.toEndOfTurn(true); + + // Should only be normal type, and NOT flying type + expect(tornadus.getTypes()).toEqual([PokemonType.NORMAL]); + expect(tornadus.isGrounded()).toBe(true); + }); + + it.each<{ name: string, move: MoveId, species: SpeciesId }>([ + { name: "Burn Up", move: MoveId.BURN_UP, species: SpeciesId.MOLTRES }, + { name: "Double Shock", move: MoveId.DOUBLE_SHOCK, species: SpeciesId.ZAPDOS } + ])("should render user typeless when roosting after using $name", async ({ move, species }) => { + await game.classicMode.startBattle([species]); + + const player = game.field.getPlayerPokemon()!; + player.hp = 1; + + game.move.use(move); + await game.toNextTurn(); + + // Should be pure flying type + expect(player.getTypes()).toEqual([PokemonType.FLYING]); + expect(player.isGrounded()).toBe(false); + + game.move.use(move); + await game.phaseInterceptor.to("MoveEffectPhase"); + + // Should be typeless + expect(player.getTag(BattlerTagType.ROOSTED)).toBeDefined(); + expect(player.getTypes()).toEqual([PokemonType.UNKNOWN]); + expect(player.isGrounded()).toBe(true); + + await game.toEndOfTurn(); + + // Should go back to being pure flying + expect(player.getTypes()).toEqual([PokemonType.FLYING]); + expect(player.isGrounded()).toBe(false); + }); + + it("should revert to 3 types when affected by Forest's Curse or Trick-or-Treat", async () => { await game.classicMode.startBattle([SpeciesId.MOLTRES]); - const playerPokemon = game.scene.getPlayerPokemon()!; - const playerPokemonStartingHP = playerPokemon.hp; - game.move.select(MoveId.BURN_UP); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); - await game.phaseInterceptor.to(MoveEffectPhase); - // Should only be pure flying type after burn up - let playerPokemonTypes = playerPokemon.getTypes(); - expect(playerPokemonTypes[0] === PokemonType.FLYING).toBeTruthy(); - expect(playerPokemonTypes.length === 1).toBeTruthy(); + const moltres = game.field.getPlayerPokemon(); + moltres.hp = 1; - await game.phaseInterceptor.to(TurnEndPhase); - game.move.select(MoveId.ROOST); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); - await game.phaseInterceptor.to(MoveEffectPhase); + game.move.use(MoveId.ROOST); + await game.move.forceEnemyMove(MoveId.TRICK_OR_TREAT) + await game.toEndOfTurn(false); - // Should only be typeless type after roost and is grounded - playerPokemonTypes = playerPokemon.getTypes(); - expect(playerPokemon.getTag(BattlerTagType.ROOSTED)).toBeDefined(); - expect(playerPokemonTypes[0] === PokemonType.UNKNOWN).toBeTruthy(); - expect(playerPokemonTypes.length === 1).toBeTruthy(); - expect(playerPokemon.isGrounded()).toBeTruthy(); + expect(moltres.getTypes()).toEqual([PokemonType.FIRE, PokemonType.GHOST]); + expect(moltres.isGrounded()).toBe(true); - await game.phaseInterceptor.to(TurnEndPhase); + await game.toEndOfTurn(); - // Should go back to being pure flying and have taken damage from earthquake, and is ungrounded again - playerPokemonTypes = playerPokemon.getTypes(); - expect(playerPokemon.hp).toBeLessThan(playerPokemonStartingHP); - expect(playerPokemonTypes[0] === PokemonType.FLYING).toBeTruthy(); - expect(playerPokemonTypes.length === 1).toBeTruthy(); - expect(playerPokemon.isGrounded()).toBeFalsy(); - }); - - test("An electric/flying type that uses double shock, then roost should be typeless until end of turn", async () => { - game.override.enemySpecies(SpeciesId.ZEKROM); - await game.classicMode.startBattle([SpeciesId.ZAPDOS]); - const playerPokemon = game.scene.getPlayerPokemon()!; - const playerPokemonStartingHP = playerPokemon.hp; - game.move.select(MoveId.DOUBLE_SHOCK); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); - await game.phaseInterceptor.to(MoveEffectPhase); - - // Should only be pure flying type after burn up - let playerPokemonTypes = playerPokemon.getTypes(); - expect(playerPokemonTypes[0] === PokemonType.FLYING).toBeTruthy(); - expect(playerPokemonTypes.length === 1).toBeTruthy(); - - await game.phaseInterceptor.to(TurnEndPhase); - game.move.select(MoveId.ROOST); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); - await game.phaseInterceptor.to(MoveEffectPhase); - - // Should only be typeless type after roost and is grounded - playerPokemonTypes = playerPokemon.getTypes(); - expect(playerPokemon.getTag(BattlerTagType.ROOSTED)).toBeDefined(); - expect(playerPokemonTypes[0] === PokemonType.UNKNOWN).toBeTruthy(); - expect(playerPokemonTypes.length === 1).toBeTruthy(); - expect(playerPokemon.isGrounded()).toBeTruthy(); - - await game.phaseInterceptor.to(TurnEndPhase); - - // Should go back to being pure flying and have taken damage from earthquake, and is ungrounded again - playerPokemonTypes = playerPokemon.getTypes(); - expect(playerPokemon.hp).toBeLessThan(playerPokemonStartingHP); - expect(playerPokemonTypes[0] === PokemonType.FLYING).toBeTruthy(); - expect(playerPokemonTypes.length === 1).toBeTruthy(); - expect(playerPokemon.isGrounded()).toBeFalsy(); - }); - - test("Dual Type Pokemon afflicted with Forests Curse/Trick or Treat and post roost will become dual type and then become 3 type at end of turn", async () => { - game.override.enemyMoveset([ - MoveId.TRICK_OR_TREAT, - MoveId.TRICK_OR_TREAT, - MoveId.TRICK_OR_TREAT, - MoveId.TRICK_OR_TREAT, - ]); - await game.classicMode.startBattle([SpeciesId.MOLTRES]); - const playerPokemon = game.scene.getPlayerPokemon()!; - game.move.select(MoveId.ROOST); - await game.phaseInterceptor.to(MoveEffectPhase); - - let playerPokemonTypes = playerPokemon.getTypes(); - expect(playerPokemonTypes[0] === PokemonType.FIRE).toBeTruthy(); - expect(playerPokemonTypes.length === 1).toBeTruthy(); - expect(playerPokemon.isGrounded()).toBeTruthy(); - - await game.phaseInterceptor.to(TurnEndPhase); - - // Should be fire/flying/ghost - playerPokemonTypes = playerPokemon.getTypes(); - expect(playerPokemonTypes.filter(type => type === PokemonType.FLYING)).toHaveLength(1); - expect(playerPokemonTypes.filter(type => type === PokemonType.FIRE)).toHaveLength(1); - expect(playerPokemonTypes.filter(type => type === PokemonType.GHOST)).toHaveLength(1); - expect(playerPokemonTypes.length === 3).toBeTruthy(); - expect(playerPokemon.isGrounded()).toBeFalsy(); + expect(moltres.getTypes()).toEqual([PokemonType.FIRE, PokemonType.FLYING, PokemonType.GHOST]); + expect(moltres.isGrounded()).toBe(false); }); }); diff --git a/test/testUtils/gameManager.ts b/test/testUtils/gameManager.ts index b9f499d4e0c..334266d3bfa 100644 --- a/test/testUtils/gameManager.ts +++ b/test/testUtils/gameManager.ts @@ -363,9 +363,12 @@ export default class GameManager { console.log("==================[New Turn]=================="); } - /** Transition to the {@linkcode TurnEndPhase | end of the current turn}. */ - async toEndOfTurn() { - await this.phaseInterceptor.to("TurnEndPhase"); + /** + * Transition to the {@linkcode TurnEndPhase | end of the current turn}. + * @param endTurn - Whether to run the TurnEndPhase or not; default `true` + */ + async toEndOfTurn(endTurn = true) { + await this.phaseInterceptor.to("TurnEndPhase", endTurn); console.log("==================[End of Turn]=================="); } From 06290774c29981f06aa8a8f3caa947d4869964c0 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Wed, 23 Jul 2025 22:57:47 -0400 Subject: [PATCH 14/59] Fixed import issues i think --- src/data/moves/move.ts | 26 +++++---- test/moves/recovery-moves.test.ts | 7 ++- test/moves/swallow-spit-up.test.ts | 89 ++++++++++-------------------- 3 files changed, 49 insertions(+), 73 deletions(-) diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 6d6496a91c1..14944f9fd8d 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -1972,17 +1972,17 @@ export class HealAttr extends MoveEffectAttr { return Math.round(score / (1 - this.healRatio / 2)); } - // TODO: Change post move failure rework override getCondition(): MoveConditionFunc { - return (user, target) => { - const healedPokemon = this.selfTarget ? user : target; + return (user, target) => !(this.selfTarget ? user : target).isFullHp(); + } + + override getFailedText(user: Pokemon, target: Pokemon): string | undefined { + const healedPokemon = (this.selfTarget ? user : target); if (healedPokemon.isFullHp()) { - globalScene.phaseManager.queueMessage(i18next.t("battle:hpIsFull", { + return i18next.t("battle:hpIsFull", { pokemonName: getPokemonNameWithAffix(healedPokemon), - })) - return false; + }) } - return true; } } @@ -1990,7 +1990,11 @@ export class HealAttr extends MoveEffectAttr { * Attribute for moves with variable healing amounts. * Heals the target by an amount depending on the return value of {@linkcode healFunc}. * - * Used for {@linkcode MoveId.MOONLIGHT}, {@linkcode MoveId.SHORE_UP}, {@linkcode MoveId.JUNGLE_HEALING}, {@linkcode MoveId.SWALLOW} + * Used for: + * - {@linkcode MoveId.MOONLIGHT} and variants + * - {@linkcode MoveId.SHORE_UP} + * - {@linkcode MoveId.JUNGLE_HEALING} + * - {@linkcode MoveId.SWALLOW} */ export class VariableHealAttr extends HealAttr { constructor( @@ -2005,7 +2009,7 @@ export class VariableHealAttr extends HealAttr { apply(user: Pokemon, target: Pokemon, move: Move, _args: any[]): boolean { this.healRatio = this.healFunc(user, target, move) - return super.apply(user, target, move, args); + return super.apply(user, target, move, _args); } } @@ -2023,8 +2027,8 @@ export class HealOnAllyAttr extends HealAttr { return false; } - override getCondition(user: Pokemon, target: Pokemon, _move: Move, _args?: any[]): boolean { - return user.getAlly() !== target || super.getCondition()(user, target, _move); + override getCondition(): MoveConditionFunc { + return (user, target, _move) => user.getAlly() !== target || super.getCondition()(user, target, _move) } } diff --git a/test/moves/recovery-moves.test.ts b/test/moves/recovery-moves.test.ts index 9fb21fc5507..cbed18f7453 100644 --- a/test/moves/recovery-moves.test.ts +++ b/test/moves/recovery-moves.test.ts @@ -1,11 +1,12 @@ import { getPokemonNameWithAffix } from "#app/messages"; -import { getEnumValues, toReadableString } from "#app/utils/common"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { MoveResult } from "#enums/move-result"; import { SpeciesId } from "#enums/species-id"; import { WeatherType } from "#enums/weather-type"; -import GameManager from "#test/testUtils/gameManager"; +import { GameManager } from "#test/testUtils/gameManager"; +import { toReadableString } from "#utils/common"; +import { getEnumValues } from "#utils/enums"; import i18next from "i18next"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -94,7 +95,7 @@ describe("Moves - Recovery Moves - ", () => { expect(blissey.getHpRatio()).toBeCloseTo(0.66, 1); }); - const nonSunWTs = (getEnumValues(WeatherType) as WeatherType[]) + const nonSunWTs = getEnumValues(WeatherType) .filter( wt => ![WeatherType.SUNNY, WeatherType.HARSH_SUN, WeatherType.NONE, WeatherType.STRONG_WINDS].includes(wt), ) diff --git a/test/moves/swallow-spit-up.test.ts b/test/moves/swallow-spit-up.test.ts index 4c73d23e766..429ed36cb3d 100644 --- a/test/moves/swallow-spit-up.test.ts +++ b/test/moves/swallow-spit-up.test.ts @@ -1,43 +1,45 @@ -import { Stat } from "#enums/stat"; import { StockpilingTag } from "#app/data/battler-tags"; -import { BattlerTagType } from "#app/enums/battler-tag-type"; -import { MoveResult } from "#enums/move-result"; -import { AbilityId } from "#enums/ability-id"; -import { MoveId } from "#enums/move-id"; -import { SpeciesId } from "#enums/species-id"; -import GameManager from "#test/testUtils/gameManager"; -import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi, type MockInstance } from "vitest"; -import { BattlerIndex } from "#enums/battler-index"; import { allMoves } from "#app/data/data-lists"; +import { BattlerTagType } from "#app/enums/battler-tag-type"; import { getPokemonNameWithAffix } from "#app/messages"; +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 { Stat } from "#enums/stat"; +import { GameManager } from "#test/testUtils/gameManager"; import i18next from "i18next"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, type MockInstance, vi } from "vitest"; -describe("Swallow & Spit Up", () => { +describe("Moves - Swallow & Spit Up - ", () => { let phaserGame: Phaser.Game; let game: GameManager; beforeAll(() => { - phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); }); - describe("Moves - Swallow", () => { - afterEach(() => { - game.phaseInterceptor.restoreOg(); - }); + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); - beforeEach(() => { - game = new GameManager(phaserGame); - game.override - .battleStyle("single") - .enemySpecies(SpeciesId.RATTATA) - .enemyMoveset(MoveId.SPLASH) - .enemyAbility(AbilityId.BALL_FETCH) - .enemyLevel(100) - .startingLevel(100) - .ability(AbilityId.BALL_FETCH); - }); + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .battleStyle("single") + .enemySpecies(SpeciesId.RATTATA) + .enemyMoveset(MoveId.SPLASH) + .enemyAbility(AbilityId.BALL_FETCH) + .enemyLevel(100) + .startingLevel(100) + .ability(AbilityId.BALL_FETCH); + }); + describe("Swallow", () => { it.each<{ stackCount: number; healPercent: number }>([ { stackCount: 1, healPercent: 25 }, { stackCount: 2, healPercent: 50 }, @@ -109,24 +111,10 @@ describe("Swallow & Spit Up", () => { }); }); - describe("Moves - Spit Up", () => { + describe("Spit Up", () => { let spitUpSpy: MockInstance; - afterEach(() => { - game.phaseInterceptor.restoreOg(); - }); - beforeEach(() => { - game = new GameManager(phaserGame); - - game.override - .battleStyle("single") - .enemySpecies(SpeciesId.RATTATA) - .enemyAbility(AbilityId.BALL_FETCH) - .enemyLevel(2000) - .enemyMoveset(MoveId.SPLASH) - .ability(AbilityId.BALL_FETCH); - spitUpSpy = vi.spyOn(allMoves[MoveId.SPIT_UP], "calculateBattlePower"); }); @@ -173,23 +161,6 @@ describe("Swallow & Spit Up", () => { }); describe("Stockpile stack removal", () => { - afterEach(() => { - game.phaseInterceptor.restoreOg(); - }); - - beforeEach(() => { - game = new GameManager(phaserGame); - - game.override - .battleStyle("single") - .enemySpecies(SpeciesId.RATTATA) - .enemyMoveset(MoveId.SPLASH) - .enemyAbility(AbilityId.BALL_FETCH) - .enemyLevel(100) - .startingLevel(100) - .ability(AbilityId.BALL_FETCH); - }); - it("should undo stat boosts when losing stacks", async () => { await game.classicMode.startBattle([SpeciesId.ABOMASNOW]); From 3a255e59b52257dbe731121ac34a67ac766a554a Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sat, 26 Jul 2025 15:02:13 -0400 Subject: [PATCH 15/59] Fxixed test --- test/moves/roost.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/moves/roost.test.ts b/test/moves/roost.test.ts index 49d53769f11..d59c4d93903 100644 --- a/test/moves/roost.test.ts +++ b/test/moves/roost.test.ts @@ -74,7 +74,7 @@ describe("Moves - Roost", () => { tornadus.hp = 1; game.move.use(MoveId.ROOST); - await game.toEndOfTurn(true); + await game.toEndOfTurn(false); // Should only be normal type, and NOT flying type expect(tornadus.getTypes()).toEqual([PokemonType.NORMAL]); From 6d7d2a5c6a827659bd086aab8b906c02e0c5488f Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sat, 26 Jul 2025 17:39:05 -0400 Subject: [PATCH 16/59] Added roost tests --- test/moves/roost.test.ts | 69 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 3 deletions(-) diff --git a/test/moves/roost.test.ts b/test/moves/roost.test.ts index d59c4d93903..e1507228c94 100644 --- a/test/moves/roost.test.ts +++ b/test/moves/roost.test.ts @@ -1,4 +1,5 @@ import { AbilityId } from "#enums/ability-id"; +import { BattlerIndex } from "#enums/battler-index"; import { BattlerTagType } from "#enums/battler-tag-type"; import { MoveId } from "#enums/move-id"; import { PokemonType } from "#enums/pokemon-type"; @@ -67,6 +68,21 @@ describe("Moves - Roost", () => { expect(mew.isGrounded()).toBe(true); }); + it("should not override the user's Tera Type", async () => { + await game.classicMode.startBattle([SpeciesId.PIDGEOT]); + + const pidgeot = game.field.getPlayerPokemon(); + pidgeot.hp = 1; + pidgeot.teraType = PokemonType.FIGHTING; + + game.move.use(MoveId.ROOST, BattlerIndex.PLAYER, undefined, true); + await game.toEndOfTurn(false); + + // Should remain flying type + expect(pidgeot.getTypes(true)).toEqual([PokemonType.FLYING]); + expect(pidgeot.isGrounded()).toBe(true); + }); + it("should convert pure Flying types into normal types", async () => { await game.classicMode.startBattle([SpeciesId.TORNADUS]); @@ -87,7 +103,7 @@ describe("Moves - Roost", () => { ])("should render user typeless when roosting after using $name", async ({ move, species }) => { await game.classicMode.startBattle([species]); - const player = game.field.getPlayerPokemon()!; + const player = game.field.getPlayerPokemon(); player.hp = 1; game.move.use(move); @@ -97,7 +113,7 @@ describe("Moves - Roost", () => { expect(player.getTypes()).toEqual([PokemonType.FLYING]); expect(player.isGrounded()).toBe(false); - game.move.use(move); + game.move.use(MoveId.ROOST); await game.phaseInterceptor.to("MoveEffectPhase"); // Should be typeless @@ -112,6 +128,53 @@ describe("Moves - Roost", () => { expect(player.isGrounded()).toBe(false); }); + it("should consider the user's current types when transforming", async () => { + game.override.ability(AbilityId.IMPOSTER).enemySpecies(SpeciesId.CORVIKNIGHT); + await game.classicMode.startBattle([SpeciesId.DITTO]); + + const ditto = game.field.getPlayerPokemon(); + ditto.hp = 1; + + expect(ditto.summonData.speciesForm).toBe(SpeciesId.CORVIKNIGHT); + expect(ditto.getTypes()).toEqual([PokemonType.STEEL, PokemonType.FLYING]); + + game.move.use(MoveId.ROOST); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.phaseInterceptor.to("MoveEffectPhase"); // Roost + + expect(ditto.getTypes()).toEqual([PokemonType.STEEL]); + + await game.toEndOfTurn(); + + expect(ditto.getTypes()).toEqual([PokemonType.STEEL, PokemonType.FLYING, PokemonType.GHOST]); + }); + + it("should not change normally-Flying type Pokemon with 3 types into Normal-types", async () => { + game.override.ability(AbilityId.IMPOSTER).enemySpecies(SpeciesId.CORVIKNIGHT); + await game.classicMode.startBattle([SpeciesId.TORNADUS]); + + const tornadus = game.field.getPlayerPokemon(); + tornadus.hp = 1; + + expect(tornadus.summonData.speciesForm).toBe(SpeciesId.CORVIKNIGHT); + expect(tornadus.getTypes()).toEqual([PokemonType.STEEL, PokemonType.FLYING]); + + game.move.use(MoveId.ROOST); + await game.move.forceEnemyMove(MoveId.TRICK_OR_TREAT); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.phaseInterceptor.to("MoveEffectPhase"); // Trick or treat + + expect(tornadus.getTypes()).toEqual([PokemonType.STEEL, PokemonType.FLYING, PokemonType.GHOST]); + + await game.phaseInterceptor.to("MoveEffectPhase"); // Roost + + expect(tornadus.getTypes()).toEqual([PokemonType.STEEL, PokemonType.GHOST]); + + await game.toEndOfTurn(); + + expect(tornadus.getTypes()).toEqual([PokemonType.STEEL, PokemonType.FLYING, PokemonType.GHOST]); + }); + it("should revert to 3 types when affected by Forest's Curse or Trick-or-Treat", async () => { await game.classicMode.startBattle([SpeciesId.MOLTRES]); @@ -125,7 +188,7 @@ describe("Moves - Roost", () => { expect(moltres.getTypes()).toEqual([PokemonType.FIRE, PokemonType.GHOST]); expect(moltres.isGrounded()).toBe(true); - await game.toEndOfTurn(); + await game.toNextTurn(); expect(moltres.getTypes()).toEqual([PokemonType.FIRE, PokemonType.FLYING, PokemonType.GHOST]); expect(moltres.isGrounded()).toBe(false); From 89cbf7a01a5bd85fef2da9bfa7c043130f1cbcfa Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Sat, 26 Jul 2025 16:07:16 -0600 Subject: [PATCH 17/59] [Refactor][Bug] Illusion no longer overwrites data of original Pokemon https://github.com/pagefaultgames/pokerogue/pull/6140 --- src/@types/illusion-data.ts | 26 ++-- src/data/abilities/ability.ts | 10 +- src/field/pokemon.ts | 251 ++++++++++++++++---------------- src/phases/evolution-phase.ts | 2 +- src/system/pokemon-data.ts | 10 +- src/ui/party-ui-handler.ts | 13 +- src/ui/summary-ui-handler.ts | 23 +-- test/abilities/illusion.test.ts | 4 +- 8 files changed, 164 insertions(+), 175 deletions(-) diff --git a/src/@types/illusion-data.ts b/src/@types/illusion-data.ts index 854c98c8cc9..b152e3fdf59 100644 --- a/src/@types/illusion-data.ts +++ b/src/@types/illusion-data.ts @@ -8,20 +8,14 @@ import type { Variant } from "#sprites/variant"; * Data pertaining to a Pokemon's Illusion. */ export interface IllusionData { - basePokemon: { - /** The actual name of the Pokemon */ - name: string; - /** The actual nickname of the Pokemon */ - nickname: string; - /** Whether the base pokemon is shiny or not */ - shiny: boolean; - /** The shiny variant of the base pokemon */ - variant: Variant; - /** Whether the fusion species of the base pokemon is shiny or not */ - fusionShiny: boolean; - /** The variant of the fusion species of the base pokemon */ - fusionVariant: Variant; - }; + /** The name of pokemon featured in the illusion */ + name: string; + /** The nickname of the pokemon featured in the illusion */ + nickname: string; + /** Whether the pokemon featured in the illusion is shiny or not */ + shiny: boolean; + /** The variant of the pokemon featured in the illusion */ + variant: Variant; /** The species of the illusion */ species: SpeciesId; /** The formIndex of the illusion */ @@ -34,6 +28,10 @@ export interface IllusionData { fusionSpecies?: PokemonSpecies; /** The fusionFormIndex of the illusion */ fusionFormIndex?: number; + /** Whether the fusion species of the pokemon featured in the illusion is shiny or not */ + fusionShiny?: boolean; + /** The variant of the fusion species of the pokemon featured in the illusion */ + fusionVariant?: Variant; /** The fusionGender of the illusion if it's a fusion */ fusionGender?: Gender; /** The level of the illusion (not used currently) */ diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 62d6974d3a2..0ee1a51a78e 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -15,6 +15,7 @@ import { SpeciesFormChangeAbilityTrigger, SpeciesFormChangeWeatherTrigger } from import { Gender } from "#data/gender"; import { getPokeballName } from "#data/pokeball"; import { pokemonFormChanges } from "#data/pokemon-forms"; +import type { PokemonSpecies } from "#data/pokemon-species"; import { getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "#data/status-effect"; import { TerrainType } from "#data/terrain"; import type { Weather } from "#data/weather"; @@ -6001,8 +6002,13 @@ export class IllusionPreSummonAbAttr extends PreSummonAbAttr { const party: Pokemon[] = (pokemon.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty()).filter( p => p.isAllowedInBattle(), ); - const lastPokemon: Pokemon = party.filter(p => p !== pokemon).at(-1) || pokemon; - pokemon.setIllusion(lastPokemon); + let illusionPokemon: Pokemon | PokemonSpecies; + if (pokemon.hasTrainer()) { + illusionPokemon = party.filter(p => p !== pokemon).at(-1) || pokemon; + } else { + illusionPokemon = globalScene.arena.randomSpecies(globalScene.currentBattle.waveIndex, pokemon.level); + } + pokemon.setIllusion(illusionPokemon); } /** @returns Whether the illusion can be applied. */ diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 751e261442d..72d4870eda4 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -442,10 +442,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @returns The name to render for this {@linkcode Pokemon}. */ getNameToRender(useIllusion = true) { - const name: string = - !useIllusion && this.summonData.illusion ? this.summonData.illusion.basePokemon.name : this.name; - const nickname: string = - !useIllusion && this.summonData.illusion ? this.summonData.illusion.basePokemon.nickname : this.nickname; + const illusion = this.summonData.illusion; + const name = useIllusion ? (illusion?.name ?? this.name) : this.name; + const nickname: string = useIllusion ? (illusion?.nickname ?? this.nickname) : this.nickname; try { if (nickname) { return decodeURIComponent(escape(atob(nickname))); // TODO: Remove `atob` and `escape`... eventually... @@ -463,7 +462,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @returns The {@linkcode PokeballType} that will be shown when this Pokemon is sent out into battle. */ getPokeball(useIllusion = false): PokeballType { - return useIllusion && this.summonData.illusion ? this.summonData.illusion.pokeball : this.pokeball; + return useIllusion ? (this.summonData.illusion?.pokeball ?? this.pokeball) : this.pokeball; } init(): void { @@ -609,24 +608,33 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Generate an illusion of the last pokemon in the party, as other wild pokemon in the area. + * Set this pokemon's illusion to the data of the given pokemon. + * + * @remarks + * When setting the illusion of a wild pokemon, a {@linkcode PokemonSpecies} is generally passed. + * When setting the illusion of a pokemon in this way, the fields required by illusion data + * but missing from `PokemonSpecies` are set as follows + * - `pokeball` and `nickname` are both inherited from this pokemon + * - `shiny` will always be set if this pokemon OR its fusion is shiny + * - `variant` will always be 0 + * - Fields related to fusion will be set to `undefined` or `0` as appropriate + * - The gender is set to be the same as this pokemon, if it is compatible with the provided pokemon. + * - If the provided pokemon can only ever exist as one gender, it is always that gender + * - If this pokemon is genderless but the provided pokemon isn't, then a gender roll is done based on this + * pokemon's ID */ - setIllusion(pokemon: Pokemon): boolean { - if (this.summonData.illusion) { - this.breakIllusion(); - } - if (this.hasTrainer()) { + setIllusion(pokemon: Pokemon | PokemonSpecies): boolean { + this.breakIllusion(); + if (pokemon instanceof Pokemon) { const speciesId = pokemon.species.speciesId; this.summonData.illusion = { - basePokemon: { - name: this.name, - nickname: this.nickname, - shiny: this.shiny, - variant: this.variant, - fusionShiny: this.fusionShiny, - fusionVariant: this.fusionVariant, - }, + name: pokemon.name, + nickname: pokemon.nickname, + shiny: pokemon.shiny, + variant: pokemon.variant, + fusionShiny: pokemon.fusionShiny, + fusionVariant: pokemon.fusionVariant, species: speciesId, formIndex: pokemon.formIndex, gender: pokemon.gender, @@ -636,54 +644,61 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { fusionGender: pokemon.fusionGender, }; - this.name = pokemon.name; - this.nickname = pokemon.nickname; - this.shiny = pokemon.shiny; - this.variant = pokemon.variant; - this.fusionVariant = pokemon.fusionVariant; - this.fusionShiny = pokemon.fusionShiny; - if (this.shiny) { + if (pokemon.shiny || pokemon.fusionShiny) { this.initShinySparkle(); } - this.loadAssets(false, true).then(() => this.playAnim()); - this.updateInfo(); } else { - const randomIllusion: PokemonSpecies = globalScene.arena.randomSpecies( - globalScene.currentBattle.waveIndex, - this.level, - ); - + // Correct the gender in case the illusioned species has a gender incompatible with this pokemon + let gender = this.gender; + switch (pokemon.malePercent) { + case null: + gender = Gender.GENDERLESS; + break; + case 0: + gender = Gender.FEMALE; + break; + case 100: + gender = Gender.MALE; + break; + default: + gender = (this.id % 256) * 0.390625 < pokemon.malePercent ? Gender.MALE : Gender.FEMALE; + } + /* + TODO: Allow setting `variant` to something other than 0, which would require first loading the + assets for the provided species, as its entry would otherwise not + be guaranteed to exist in the `variantData` map. But this would prevent `summonData` from being populated + until the assets are loaded, which would cause issues as this method cannot be easily promisified. + */ this.summonData.illusion = { - basePokemon: { - name: this.name, - nickname: this.nickname, - shiny: this.shiny, - variant: this.variant, - fusionShiny: this.fusionShiny, - fusionVariant: this.fusionVariant, - }, - species: randomIllusion.speciesId, - formIndex: randomIllusion.formIndex, - gender: this.gender, + fusionShiny: false, + fusionVariant: 0, + shiny: this.shiny || this.fusionShiny, + variant: 0, + nickname: this.nickname, + name: pokemon.name, + species: pokemon.speciesId, + formIndex: pokemon.formIndex, + gender, pokeball: this.pokeball, }; - this.name = randomIllusion.name; - this.loadAssets(false, true).then(() => this.playAnim()); + if (this.shiny || this.fusionShiny) { + this.initShinySparkle(); + } } + this.loadAssets(false, true).then(() => this.playAnim()); + this.updateInfo(); return true; } + /** + * Break the illusion of this pokemon, if it has an active illusion. + * @returns Whether an illusion was broken. + */ breakIllusion(): boolean { if (!this.summonData.illusion) { return false; } - this.name = this.summonData.illusion.basePokemon.name; - this.nickname = this.summonData.illusion.basePokemon.nickname; - this.shiny = this.summonData.illusion.basePokemon.shiny; - this.variant = this.summonData.illusion.basePokemon.variant; - this.fusionVariant = this.summonData.illusion.basePokemon.fusionVariant; - this.fusionShiny = this.summonData.illusion.basePokemon.fusionShiny; this.summonData.illusion = null; if (this.isOnField()) { globalScene.playSound("PRSFX- Transform"); @@ -718,8 +733,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { // Assets for moves loadPromises.push(loadMoveAnimations(this.getMoveset().map(m => m.getMove().id))); + /** alias for `this.summonData.illusion`; bangs on this are safe when guarded with `useIllusion` being true */ + const illusion = this.summonData.illusion; + useIllusion = useIllusion && !!illusion; + // Load the assets for the species form - const formIndex = useIllusion && this.summonData.illusion ? this.summonData.illusion.formIndex : this.formIndex; + const formIndex = useIllusion ? illusion!.formIndex : this.formIndex; loadPromises.push( this.getSpeciesForm(false, useIllusion).loadAssets( this.getGender(useIllusion) === Gender.FEMALE, @@ -736,16 +755,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { ); } if (this.getFusionSpeciesForm()) { - const fusionFormIndex = - useIllusion && this.summonData.illusion ? this.summonData.illusion.fusionFormIndex : this.fusionFormIndex; - const fusionShiny = - !useIllusion && this.summonData.illusion?.basePokemon - ? this.summonData.illusion.basePokemon.fusionShiny - : this.fusionShiny; - const fusionVariant = - !useIllusion && this.summonData.illusion?.basePokemon - ? this.summonData.illusion.basePokemon.fusionVariant - : this.fusionVariant; + const { fusionFormIndex, fusionShiny, fusionVariant } = useIllusion ? illusion! : this; loadPromises.push( this.getFusionSpeciesForm(false, useIllusion).loadAssets( this.getFusionGender(false, useIllusion) === Gender.FEMALE, @@ -933,8 +943,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return this.getSpeciesForm(ignoreOverride, false).getSpriteKey( this.getGender(ignoreOverride) === Gender.FEMALE, this.formIndex, - this.summonData.illusion?.basePokemon.shiny ?? this.shiny, - this.summonData.illusion?.basePokemon.variant ?? this.variant, + this.isShiny(false), + this.getVariant(false), ); } @@ -977,11 +987,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } getIconAtlasKey(ignoreOverride = false, useIllusion = true): string { - // TODO: confirm the correct behavior here (is it intentional that the check fails if `illusion.formIndex` is `0`?) - const formIndex = - useIllusion && this.summonData.illusion?.formIndex ? this.summonData.illusion.formIndex : this.formIndex; - const variant = - !useIllusion && this.summonData.illusion ? this.summonData.illusion.basePokemon.variant : this.variant; + const illusion = this.summonData.illusion; + const { formIndex, variant } = useIllusion && illusion ? illusion : this; return this.getSpeciesForm(ignoreOverride, useIllusion).getIconAtlasKey( formIndex, this.isBaseShiny(useIllusion), @@ -990,15 +997,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } getFusionIconAtlasKey(ignoreOverride = false, useIllusion = true): string { - // TODO: confirm the correct behavior here (is it intentional that the check fails if `illusion.fusionFormIndex` is `0`?) - const fusionFormIndex = - useIllusion && this.summonData.illusion?.fusionFormIndex - ? this.summonData.illusion.fusionFormIndex - : this.fusionFormIndex; - const fusionVariant = - !useIllusion && this.summonData.illusion - ? this.summonData.illusion.basePokemon.fusionVariant - : this.fusionVariant; + const illusion = this.summonData.illusion; + const { fusionFormIndex, fusionVariant } = useIllusion && illusion ? illusion : this; return this.getFusionSpeciesForm(ignoreOverride, useIllusion).getIconAtlasKey( fusionFormIndex, this.isFusionShiny(), @@ -1006,11 +1006,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { ); } - getIconId(ignoreOverride?: boolean, useIllusion = true): string { - const formIndex = - useIllusion && this.summonData.illusion?.formIndex ? this.summonData.illusion?.formIndex : this.formIndex; - const variant = - !useIllusion && !!this.summonData.illusion ? this.summonData.illusion?.basePokemon.variant : this.variant; + getIconId(ignoreOverride?: boolean, useIllusion = false): string { + const illusion = this.summonData.illusion; + const { formIndex, variant } = useIllusion && illusion ? illusion : this; return this.getSpeciesForm(ignoreOverride, useIllusion).getIconId( this.getGender(ignoreOverride, useIllusion) === Gender.FEMALE, formIndex, @@ -1020,14 +1018,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } getFusionIconId(ignoreOverride?: boolean, useIllusion = true): string { - const fusionFormIndex = - useIllusion && this.summonData.illusion?.fusionFormIndex - ? this.summonData.illusion?.fusionFormIndex - : this.fusionFormIndex; - const fusionVariant = - !useIllusion && !!this.summonData.illusion - ? this.summonData.illusion?.basePokemon.fusionVariant - : this.fusionVariant; + const illusion = this.summonData.illusion; + const { fusionFormIndex, fusionVariant } = useIllusion && illusion ? illusion : this; return this.getFusionSpeciesForm(ignoreOverride, useIllusion).getIconId( this.getFusionGender(ignoreOverride, useIllusion) === Gender.FEMALE, fusionFormIndex, @@ -1703,29 +1695,23 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @returns Whether this Pokemon is shiny */ isShiny(useIllusion = false): boolean { - if (!useIllusion && this.summonData.illusion) { - return ( - this.summonData.illusion.basePokemon?.shiny || - (this.summonData.illusion.fusionSpecies && this.summonData.illusion.basePokemon?.fusionShiny) || - false - ); + if (useIllusion) { + const illusion = this.summonData.illusion; + return illusion?.shiny || (!!illusion?.fusionSpecies && !!illusion.fusionShiny); } return this.shiny || (this.isFusion(useIllusion) && this.fusionShiny); } isBaseShiny(useIllusion = false) { - if (!useIllusion && this.summonData.illusion) { - return !!this.summonData.illusion.basePokemon?.shiny; - } - return this.shiny; + return useIllusion ? (this.summonData.illusion?.shiny ?? this.shiny) : this.shiny; } isFusionShiny(useIllusion = false) { - if (!useIllusion && this.summonData.illusion) { - return !!this.summonData.illusion.basePokemon?.fusionShiny; + if (!this.isFusion(useIllusion)) { + return false; } - return this.isFusion(useIllusion) && this.fusionShiny; + return useIllusion ? (this.summonData.illusion?.fusionShiny ?? this.fusionShiny) : this.fusionShiny; } /** @@ -1734,39 +1720,48 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @returns Whether this pokemon's base and fusion counterparts are both shiny. */ isDoubleShiny(useIllusion = false): boolean { - if (!useIllusion && this.summonData.illusion?.basePokemon) { - return ( - this.isFusion(false) && - this.summonData.illusion.basePokemon.shiny && - this.summonData.illusion.basePokemon.fusionShiny - ); - } - - return this.isFusion(useIllusion) && this.shiny && this.fusionShiny; + return this.isFusion(useIllusion) && this.isBaseShiny(useIllusion) && this.isFusionShiny(useIllusion); } /** * Return this Pokemon's {@linkcode Variant | shiny variant}. + * If a fusion, returns the maximum of the two variants. * Only meaningful if this pokemon is actually shiny. * @param useIllusion - Whether to consider this pokemon's illusion if present; default `false` * @returns The shiny variant of this Pokemon. */ getVariant(useIllusion = false): Variant { - if (!useIllusion && this.summonData.illusion) { - return !this.isFusion(false) - ? this.summonData.illusion.basePokemon!.variant - : (Math.max(this.variant, this.fusionVariant) as Variant); + const illusion = this.summonData.illusion; + const baseVariant = useIllusion ? (illusion?.variant ?? this.variant) : this.variant; + if (!this.isFusion(useIllusion)) { + return baseVariant; } - - return !this.isFusion(true) ? this.variant : (Math.max(this.variant, this.fusionVariant) as Variant); + const fusionVariant = useIllusion ? (illusion?.fusionVariant ?? this.fusionVariant) : this.fusionVariant; + return Math.max(baseVariant, fusionVariant) as Variant; } - // TODO: Clarify how this differs from `getVariant` - getBaseVariant(doubleShiny: boolean): Variant { - if (doubleShiny) { - return this.summonData.illusion?.basePokemon?.variant ?? this.variant; + /** + * Return the base pokemon's variant. Equivalent to {@linkcode getVariant} if this pokemon is not a fusion. + * @returns The shiny variant of this Pokemon's base species. + */ + getBaseVariant(useIllusion = false): Variant { + const illusion = this.summonData.illusion; + return useIllusion && illusion ? (illusion.variant ?? this.variant) : this.variant; + } + + /** + * Return the fused pokemon's variant. + * + * @remarks + * Always returns `0` if the pokemon is not a fusion. + * @returns The shiny variant of this pokemon's fusion species. + */ + getFusionVariant(useIllusion = false): Variant { + if (!this.isFusion(useIllusion)) { + return 0; } - return this.getVariant(); + const illusion = this.summonData.illusion; + return illusion ? (illusion.fusionVariant ?? this.fusionVariant) : this.fusionVariant; } /** @@ -1783,7 +1778,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @returns Whether this Pokemon is currently fused with another species. */ isFusion(useIllusion = false): boolean { - return useIllusion && this.summonData.illusion ? !!this.summonData.illusion.fusionSpecies : !!this.fusionSpecies; + return useIllusion ? !!this.summonData.illusion?.fusionSpecies : !!this.fusionSpecies; } /** @@ -1793,9 +1788,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @see {@linkcode getNameToRender} - gets this Pokemon's display name. */ getName(useIllusion = false): string { - return !useIllusion && this.summonData.illusion?.basePokemon - ? this.summonData.illusion.basePokemon.name - : this.name; + return useIllusion ? (this.summonData.illusion?.name ?? this.name) : this.name; } /** diff --git a/src/phases/evolution-phase.ts b/src/phases/evolution-phase.ts index f8bee8371f2..cad79455af3 100644 --- a/src/phases/evolution-phase.ts +++ b/src/phases/evolution-phase.ts @@ -135,7 +135,7 @@ export class EvolutionPhase extends Phase { sprite .setPipelineData("ignoreTimeTint", true) - .setPipelineData("spriteKey", pokemon.getSpriteKey()) + .setPipelineData("spriteKey", spriteKey) .setPipelineData("shiny", pokemon.shiny) .setPipelineData("variant", pokemon.variant); diff --git a/src/system/pokemon-data.ts b/src/system/pokemon-data.ts index 9cea08bfb13..69c1539d944 100644 --- a/src/system/pokemon-data.ts +++ b/src/system/pokemon-data.ts @@ -88,12 +88,12 @@ export class PokemonData { this.id = source.id; this.player = sourcePokemon?.isPlayer() ?? source.player; this.species = sourcePokemon?.species.speciesId ?? source.species; - this.nickname = sourcePokemon?.summonData.illusion?.basePokemon.nickname ?? source.nickname; + this.nickname = source.nickname; this.formIndex = Math.max(Math.min(source.formIndex, getPokemonSpecies(this.species).forms.length - 1), 0); this.abilityIndex = source.abilityIndex; this.passive = source.passive; - this.shiny = sourcePokemon?.summonData.illusion?.basePokemon.shiny ?? source.shiny; - this.variant = sourcePokemon?.summonData.illusion?.basePokemon.variant ?? source.variant; + this.shiny = source.shiny; + this.variant = source.variant; this.pokeball = source.pokeball ?? PokeballType.POKEBALL; this.level = source.level; this.exp = source.exp; @@ -134,8 +134,8 @@ export class PokemonData { this.fusionSpecies = sourcePokemon?.fusionSpecies?.speciesId ?? source.fusionSpecies; this.fusionFormIndex = source.fusionFormIndex; this.fusionAbilityIndex = source.fusionAbilityIndex; - this.fusionShiny = sourcePokemon?.summonData.illusion?.basePokemon.fusionShiny ?? source.fusionShiny; - this.fusionVariant = sourcePokemon?.summonData.illusion?.basePokemon.fusionVariant ?? source.fusionVariant; + this.fusionShiny = source.fusionShiny; + this.fusionVariant = source.fusionVariant; this.fusionGender = source.fusionGender; this.fusionLuck = source.fusionLuck ?? (source.fusionShiny ? source.fusionVariant + 1 : 0); this.fusionTeraType = (source.fusionTeraType ?? 0) as PokemonType; diff --git a/src/ui/party-ui-handler.ts b/src/ui/party-ui-handler.ts index ce5f60813c7..956e4c52e39 100644 --- a/src/ui/party-ui-handler.ts +++ b/src/ui/party-ui-handler.ts @@ -1791,17 +1791,16 @@ class PartySlot extends Phaser.GameObjects.Container { const shinyStar = globalScene.add.image(0, 0, `shiny_star_small${doubleShiny ? "_1" : ""}`); shinyStar.setOrigin(0, 0); shinyStar.setPositionRelative(this.slotName, -9, 3); - shinyStar.setTint(getVariantTint(this.pokemon.getBaseVariant(doubleShiny))); + shinyStar.setTint(getVariantTint(this.pokemon.getBaseVariant())); slotInfoContainer.add(shinyStar); if (doubleShiny) { - const fusionShinyStar = globalScene.add.image(0, 0, "shiny_star_small_2"); - fusionShinyStar.setOrigin(0, 0); - fusionShinyStar.setPosition(shinyStar.x, shinyStar.y); - fusionShinyStar.setTint( - getVariantTint(this.pokemon.summonData.illusion?.basePokemon.fusionVariant ?? this.pokemon.fusionVariant), - ); + const fusionShinyStar = globalScene.add + .image(0, 0, "shiny_star_small_2") + .setOrigin(0) + .setPosition(shinyStar.x, shinyStar.y) + .setTint(getVariantTint(this.pokemon.fusionVariant)); slotInfoContainer.add(fusionShinyStar); } diff --git a/src/ui/summary-ui-handler.ts b/src/ui/summary-ui-handler.ts index b4df4612546..770bb1d9d29 100644 --- a/src/ui/summary-ui-handler.ts +++ b/src/ui/summary-ui-handler.ts @@ -354,18 +354,13 @@ export class SummaryUiHandler extends UiHandler { } catch (err: unknown) { console.error(`Failed to play animation for ${spriteKey}`, err); } - this.pokemonSprite.setPipelineData("teraColor", getTypeRgb(this.pokemon.getTeraType())); - this.pokemonSprite.setPipelineData("isTerastallized", this.pokemon.isTerastallized); - this.pokemonSprite.setPipelineData("ignoreTimeTint", true); - this.pokemonSprite.setPipelineData("spriteKey", this.pokemon.getSpriteKey()); - this.pokemonSprite.setPipelineData( - "shiny", - this.pokemon.summonData.illusion?.basePokemon.shiny ?? this.pokemon.shiny, - ); - this.pokemonSprite.setPipelineData( - "variant", - this.pokemon.summonData.illusion?.basePokemon.variant ?? this.pokemon.variant, - ); + this.pokemonSprite + .setPipelineData("teraColor", getTypeRgb(this.pokemon.getTeraType())) + .setPipelineData("isTerastallized", this.pokemon.isTerastallized) + .setPipelineData("ignoreTimeTint", true) + .setPipelineData("spriteKey", this.pokemon.getSpriteKey()) + .setPipelineData("shiny", this.pokemon.shiny) + .setPipelineData("variant", this.pokemon.variant); ["spriteColors", "fusionSpriteColors"].map(k => { delete this.pokemonSprite.pipelineData[`${k}Base`]; if (this.pokemon?.summonData.speciesForm) { @@ -463,9 +458,7 @@ export class SummaryUiHandler extends UiHandler { this.fusionShinyIcon.setPosition(this.shinyIcon.x, this.shinyIcon.y); this.fusionShinyIcon.setVisible(doubleShiny); if (isFusion) { - this.fusionShinyIcon.setTint( - getVariantTint(this.pokemon.summonData.illusion?.basePokemon.fusionVariant ?? this.pokemon.fusionVariant), - ); + this.fusionShinyIcon.setTint(getVariantTint(this.pokemon.fusionVariant)); } this.pokeball.setFrame(getPokeballAtlasKey(this.pokemon.pokeball)); diff --git a/test/abilities/illusion.test.ts b/test/abilities/illusion.test.ts index 17a1fa8dd3d..e48cd9e9b78 100644 --- a/test/abilities/illusion.test.ts +++ b/test/abilities/illusion.test.ts @@ -145,8 +145,8 @@ describe("Abilities - Illusion", () => { const zoroark = game.scene.getPlayerPokemon()!; - expect(zoroark.name).equals("Axew"); - expect(zoroark.getNameToRender()).equals("axew nickname"); + expect(zoroark.summonData.illusion?.name).equals("Axew"); + expect(zoroark.getNameToRender(true)).equals("axew nickname"); expect(zoroark.getGender(false, true)).equals(Gender.FEMALE); expect(zoroark.isShiny(true)).equals(true); expect(zoroark.getPokeball(true)).equals(PokeballType.GREAT_BALL); From 54d9740ae46916389862f31d36e54f6b294e288f Mon Sep 17 00:00:00 2001 From: SmhMyHead <191356399+SmhMyHead@users.noreply.github.com> Date: Sun, 27 Jul 2025 00:34:21 +0200 Subject: [PATCH 18/59] [UI/UX] Added "Hide Username" Setting (#6105) * [UI/UX] Added "Hide Username" Setting * Mask tid with asterisk instead of hiding completely --------- Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> --- src/battle-scene.ts | 1 + src/system/settings/settings.ts | 11 +++++++++++ src/ui/summary-ui-handler.ts | 22 ++++++++++++++++------ 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 4c846a019a1..7e9360270ad 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -236,6 +236,7 @@ export class BattleScene extends SceneBase { public enableTouchControls = false; public enableVibration = false; public showBgmBar = true; + public hideUsername = false; /** Determines the selected battle style. */ public battleStyle: BattleStyle = BattleStyle.SWITCH; /** diff --git a/src/system/settings/settings.ts b/src/system/settings/settings.ts index 19d10baedfd..33087f2509e 100644 --- a/src/system/settings/settings.ts +++ b/src/system/settings/settings.ts @@ -171,6 +171,7 @@ export const SettingKeys = { UI_Volume: "UI_SOUND_EFFECTS", Battle_Music: "BATTLE_MUSIC", Show_BGM_Bar: "SHOW_BGM_BAR", + Hide_Username: "HIDE_USERNAME", Move_Touch_Controls: "MOVE_TOUCH_CONTROLS", Shop_Overlay_Opacity: "SHOP_OVERLAY_OPACITY", }; @@ -625,6 +626,13 @@ export const Setting: Array = [ default: 1, type: SettingType.DISPLAY, }, + { + key: SettingKeys.Hide_Username, + label: i18next.t("settings:hideUsername"), + options: OFF_ON, + default: 0, + type: SettingType.DISPLAY, + }, { key: SettingKeys.Master_Volume, label: i18next.t("settings:masterVolume"), @@ -792,6 +800,9 @@ export function setSetting(setting: string, value: number): boolean { case SettingKeys.Show_BGM_Bar: globalScene.showBgmBar = Setting[index].options[value].value === "On"; break; + case SettingKeys.Hide_Username: + globalScene.hideUsername = Setting[index].options[value].value === "On"; + break; case SettingKeys.Candy_Upgrade_Notification: if (globalScene.candyUpgradeNotification === value) { break; diff --git a/src/ui/summary-ui-handler.ts b/src/ui/summary-ui-handler.ts index 770bb1d9d29..7661dab068e 100644 --- a/src/ui/summary-ui-handler.ts +++ b/src/ui/summary-ui-handler.ts @@ -803,24 +803,34 @@ export class SummaryUiHandler extends UiHandler { case Page.PROFILE: { const profileContainer = globalScene.add.container(0, -pageBg.height); pageContainer.add(profileContainer); + const otColor = + globalScene.gameData.gender === PlayerGender.FEMALE ? TextStyle.SUMMARY_PINK : TextStyle.SUMMARY_BLUE; + const usernameReplacement = + globalScene.gameData.gender === PlayerGender.FEMALE + ? i18next.t("trainerNames:player_f") + : i18next.t("trainerNames:player_m"); // TODO: should add field for original trainer name to Pokemon object, to support gift/traded Pokemon from MEs const trainerText = addBBCodeTextObject( 7, 12, - `${i18next.t("pokemonSummary:ot")}/${getBBCodeFrag(loggedInUser?.username || i18next.t("pokemonSummary:unknown"), globalScene.gameData.gender === PlayerGender.FEMALE ? TextStyle.SUMMARY_PINK : TextStyle.SUMMARY_BLUE)}`, + `${i18next.t("pokemonSummary:ot")}/${getBBCodeFrag( + !globalScene.hideUsername + ? loggedInUser?.username || i18next.t("pokemonSummary:unknown") + : usernameReplacement, + otColor, + )}`, TextStyle.SUMMARY_ALT, - ); - trainerText.setOrigin(0, 0); + ).setOrigin(0); profileContainer.add(trainerText); + const idToDisplay = globalScene.hideUsername ? "*****" : globalScene.gameData.trainerId.toString(); const trainerIdText = addTextObject( 141, 12, - `${i18next.t("pokemonSummary:idNo")}${globalScene.gameData.trainerId.toString()}`, + `${i18next.t("pokemonSummary:idNo")}${idToDisplay}`, TextStyle.SUMMARY_ALT, - ); - trainerIdText.setOrigin(0, 0); + ).setOrigin(0); profileContainer.add(trainerIdText); const typeLabel = addTextObject(7, 28, `${i18next.t("pokemonSummary:type")}/`, TextStyle.WINDOW_ALT); From 5a2c9d1712b9ecdd152939fb8e4e1b1b406780ec Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sat, 26 Jul 2025 19:23:18 -0700 Subject: [PATCH 19/59] [Dev] Add `dist/` to `ls-lint` ignore list --- .ls-lint.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.ls-lint.yml b/.ls-lint.yml index 09d626af624..610218f8ed2 100644 --- a/.ls-lint.yml +++ b/.ls-lint.yml @@ -25,3 +25,4 @@ ignore: - .github - .git - public + - dist From c622e26fd1c438b5a54b8fbb822b1260bfc30f2b Mon Sep 17 00:00:00 2001 From: damocleas Date: Sat, 26 Jul 2025 22:31:55 -0400 Subject: [PATCH 20/59] [i18n] Update Locales --- public/locales | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales b/public/locales index 362b2c4fcc2..e2fbba17ea7 160000 --- a/public/locales +++ b/public/locales @@ -1 +1 @@ -Subproject commit 362b2c4fcc20b31a7be6c2dab537055fbaeb247f +Subproject commit e2fbba17ea7a96068970ea98a8a84ed3e25b6f07 From 9a89f48fd96e6b834c36e20a4d5e23845c948d4e Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Sat, 26 Jul 2025 20:35:39 -0600 Subject: [PATCH 21/59] [Beta] [Bug] Fix shiny display issues (#6154) Fix shininess check --- src/field/pokemon.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 72d4870eda4..703e890e555 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -1695,12 +1695,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @returns Whether this Pokemon is shiny */ isShiny(useIllusion = false): boolean { - if (useIllusion) { - const illusion = this.summonData.illusion; - return illusion?.shiny || (!!illusion?.fusionSpecies && !!illusion.fusionShiny); - } - - return this.shiny || (this.isFusion(useIllusion) && this.fusionShiny); + return this.isBaseShiny(useIllusion) || this.isFusionShiny(useIllusion); } isBaseShiny(useIllusion = false) { From 3c651e11b706a1d5ff56b8e5f02ca18768121f0c Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Sat, 26 Jul 2025 20:58:06 -0600 Subject: [PATCH 22/59] [Bug][Beta] Make bounce delay use fixed int (#6156) Make bounce delay use fixed int --- src/ui/starter-select-ui-handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index 18a3fbc30a3..fcd8167a519 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -1476,7 +1476,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { loop: -1, // Make the initial bounce a little randomly delayed delay: randIntRange(0, 50) * 5, - loopDelay: 1000, + loopDelay: fixedInt(1000), tweens: [ { targets: icon, From 3de9a495809f7c782a930a49ec0b62a853a46f81 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Sun, 27 Jul 2025 11:46:56 -0600 Subject: [PATCH 23/59] [Refactor] Refactor UI text ts (#5946) * Add destroy method to pokemon-sprite-sparkle-handler * Move TextStyle to enums, convert into const object * Cleanup text.ts file * Add necessary explicit types for TextStyle let vars * Fix locales submodule commit * Fix merge issue --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/@types/ui.ts | 10 +++ src/battle-scene.ts | 3 +- .../mystery-encounter-dialogue.ts | 2 +- .../utils/encounter-dialogue-utils.ts | 2 +- src/data/nature.ts | 3 +- src/enums/text-style.ts | 59 +++++++++++++ src/field/damage-number-handler.ts | 3 +- src/field/pokemon-sprite-sparkle-handler.ts | 13 ++- src/modifier/modifier.ts | 3 +- src/phases/end-card-phase.ts | 3 +- src/phases/scan-ivs-phase.ts | 3 +- src/timed-event-manager.ts | 3 +- src/ui/ability-bar.ts | 3 +- src/ui/abstact-option-select-ui-handler.ts | 3 +- src/ui/achv-bar.ts | 3 +- src/ui/achvs-ui-handler.ts | 3 +- src/ui/admin-ui-handler.ts | 2 +- src/ui/arena-flyout.ts | 3 +- src/ui/ball-ui-handler.ts | 3 +- src/ui/base-stats-overlay.ts | 3 +- src/ui/battle-flyout.ts | 3 +- src/ui/battle-info/battle-info.ts | 3 +- src/ui/battle-info/enemy-battle-info.ts | 3 +- src/ui/battle-message-ui-handler.ts | 3 +- src/ui/bgm-bar.ts | 3 +- src/ui/candy-bar.ts | 3 +- src/ui/challenges-select-ui-handler.ts | 3 +- src/ui/command-ui-handler.ts | 3 +- src/ui/daily-run-scoreboard.ts | 3 +- src/ui/dropdown.ts | 3 +- src/ui/egg-counter-container.ts | 3 +- src/ui/egg-gacha-ui-handler.ts | 5 +- src/ui/egg-list-ui-handler.ts | 3 +- src/ui/evolution-scene-handler.ts | 3 +- src/ui/fight-ui-handler.ts | 5 +- src/ui/filter-bar.ts | 3 +- src/ui/filter-text.ts | 3 +- src/ui/form-modal-ui-handler.ts | 3 +- src/ui/game-stats-ui-handler.ts | 3 +- src/ui/loading-modal-ui-handler.ts | 3 +- src/ui/login-form-ui-handler.ts | 3 +- src/ui/menu-ui-handler.ts | 3 +- src/ui/modal-ui-handler.ts | 3 +- src/ui/modifier-select-ui-handler.ts | 3 +- src/ui/move-info-overlay.ts | 3 +- src/ui/mystery-encounter-ui-handler.ts | 3 +- src/ui/party-exp-bar.ts | 3 +- src/ui/party-ui-handler.ts | 3 +- src/ui/pokedex-info-overlay.ts | 3 +- src/ui/pokedex-mon-container.ts | 3 +- src/ui/pokedex-page-ui-handler.ts | 3 +- src/ui/pokedex-ui-handler.ts | 3 +- src/ui/pokemon-hatch-info-container.ts | 3 +- src/ui/pokemon-info-container.ts | 3 +- src/ui/registration-form-ui-handler.ts | 3 +- src/ui/run-history-ui-handler.ts | 3 +- src/ui/run-info-ui-handler.ts | 3 +- src/ui/save-slot-select-ui-handler.ts | 3 +- src/ui/session-reload-modal-ui-handler.ts | 3 +- .../settings/abstract-binding-ui-handler.ts | 3 +- .../abstract-control-settings-ui-handler.ts | 3 +- .../settings/abstract-settings-ui-handler.ts | 3 +- src/ui/settings/gamepad-binding-ui-handler.ts | 3 +- .../settings/keyboard-binding-ui-handler.ts | 3 +- src/ui/settings/navigation-menu.ts | 3 +- .../settings/settings-gamepad-ui-handler.ts | 3 +- .../settings/settings-keyboard-ui-handler.ts | 3 +- src/ui/starter-container.ts | 3 +- src/ui/starter-select-ui-handler.ts | 3 +- src/ui/stats-container.ts | 3 +- src/ui/summary-ui-handler.ts | 3 +- src/ui/text.ts | 82 ++----------------- src/ui/title-ui-handler.ts | 3 +- src/ui/ui-handler.ts | 2 +- src/ui/ui.ts | 3 +- src/ui/unavailable-modal-ui-handler.ts | 3 +- 76 files changed, 230 insertions(+), 150 deletions(-) create mode 100644 src/@types/ui.ts create mode 100644 src/enums/text-style.ts diff --git a/src/@types/ui.ts b/src/@types/ui.ts new file mode 100644 index 00000000000..10dab01c616 --- /dev/null +++ b/src/@types/ui.ts @@ -0,0 +1,10 @@ +import type Phaser from "phaser"; +import type InputText from "phaser3-rex-plugins/plugins/gameobjects/dom/inputtext/InputText"; + +export interface TextStyleOptions { + scale: number; + styleOptions: Phaser.Types.GameObjects.Text.TextStyle | InputText.IConfig; + shadowColor: string; + shadowXpos: number; + shadowYpos: number; +} diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 7e9360270ad..275f129a63a 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -67,6 +67,7 @@ import { PokemonType } from "#enums/pokemon-type"; import { ShopCursorTarget } from "#enums/shop-cursor-target"; import { SpeciesId } from "#enums/species-id"; import { StatusEffect } from "#enums/status-effect"; +import { TextStyle } from "#enums/text-style"; import type { TrainerSlot } from "#enums/trainer-slot"; import { TrainerType } from "#enums/trainer-type"; import { TrainerVariant } from "#enums/trainer-variant"; @@ -132,7 +133,7 @@ import { CharSprite } from "#ui/char-sprite"; import { PartyExpBar } from "#ui/party-exp-bar"; import { PokeballTray } from "#ui/pokeball-tray"; import { PokemonInfoContainer } from "#ui/pokemon-info-container"; -import { addTextObject, getTextColor, TextStyle } from "#ui/text"; +import { addTextObject, getTextColor } from "#ui/text"; import { UI } from "#ui/ui"; import { addUiThemeOverrides } from "#ui/ui-theme"; import { diff --git a/src/data/mystery-encounters/mystery-encounter-dialogue.ts b/src/data/mystery-encounters/mystery-encounter-dialogue.ts index 42383940755..385ccb5c246 100644 --- a/src/data/mystery-encounters/mystery-encounter-dialogue.ts +++ b/src/data/mystery-encounters/mystery-encounter-dialogue.ts @@ -1,4 +1,4 @@ -import type { TextStyle } from "#ui/text"; +import type { TextStyle } from "#enums/text-style"; export class TextDisplay { speaker?: string; diff --git a/src/data/mystery-encounters/utils/encounter-dialogue-utils.ts b/src/data/mystery-encounters/utils/encounter-dialogue-utils.ts index 4f4af94a88d..1ae0659b29e 100644 --- a/src/data/mystery-encounters/utils/encounter-dialogue-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-dialogue-utils.ts @@ -1,6 +1,6 @@ import { globalScene } from "#app/global-scene"; +import type { TextStyle } from "#enums/text-style"; import { UiTheme } from "#enums/ui-theme"; -import type { TextStyle } from "#ui/text"; import { getTextWithColors } from "#ui/text"; import { isNullOrUndefined } from "#utils/common"; import i18next from "i18next"; diff --git a/src/data/nature.ts b/src/data/nature.ts index 4f4e627daf3..41764ec4276 100644 --- a/src/data/nature.ts +++ b/src/data/nature.ts @@ -1,7 +1,8 @@ import { Nature } from "#enums/nature"; import { EFFECTIVE_STATS, getShortenedStatKey, Stat } from "#enums/stat"; +import { TextStyle } from "#enums/text-style"; import { UiTheme } from "#enums/ui-theme"; -import { getBBCodeFrag, TextStyle } from "#ui/text"; +import { getBBCodeFrag } from "#ui/text"; import { toReadableString } from "#utils/common"; import i18next from "i18next"; diff --git a/src/enums/text-style.ts b/src/enums/text-style.ts new file mode 100644 index 00000000000..964a985cdd6 --- /dev/null +++ b/src/enums/text-style.ts @@ -0,0 +1,59 @@ +export const TextStyle = Object.freeze({ + MESSAGE: 1, + WINDOW: 2, + WINDOW_ALT: 3, + WINDOW_BATTLE_COMMAND: 4, + BATTLE_INFO: 5, + PARTY: 6, + PARTY_RED: 7, + PARTY_CANCEL_BUTTON: 8, + INSTRUCTIONS_TEXT: 9, + MOVE_LABEL: 10, + SUMMARY: 11, + SUMMARY_DEX_NUM: 12, + SUMMARY_DEX_NUM_GOLD: 13, + SUMMARY_ALT: 14, + SUMMARY_HEADER: 15, + SUMMARY_RED: 16, + SUMMARY_BLUE: 17, + SUMMARY_PINK: 18, + SUMMARY_GOLD: 19, + SUMMARY_GRAY: 20, + SUMMARY_GREEN: 21, + SUMMARY_STATS: 22, + SUMMARY_STATS_BLUE: 23, + SUMMARY_STATS_PINK: 24, + SUMMARY_STATS_GOLD: 25, + LUCK_VALUE: 26, + STATS_HEXAGON: 27, + GROWTH_RATE_TYPE: 28, + MONEY: 29, // Money default styling (pale yellow) + MONEY_WINDOW: 30, // Money displayed in Windows (needs different colors based on theme) + HEADER_LABEL: 31, + STATS_LABEL: 32, + STATS_VALUE: 33, + SETTINGS_VALUE: 34, + SETTINGS_LABEL: 35, + SETTINGS_LABEL_NAVBAR: 36, + SETTINGS_SELECTED: 37, + SETTINGS_LOCKED: 38, + EGG_LIST: 39, + EGG_SUMMARY_NAME: 40, + EGG_SUMMARY_DEX: 41, + STARTER_VALUE_LIMIT: 42, + TOOLTIP_TITLE: 43, + TOOLTIP_CONTENT: 44, + FILTER_BAR_MAIN: 45, + MOVE_INFO_CONTENT: 46, + MOVE_PP_FULL: 47, + MOVE_PP_HALF_FULL: 48, + MOVE_PP_NEAR_EMPTY: 49, + MOVE_PP_EMPTY: 50, + SMALLER_WINDOW_ALT: 51, + BGM_BAR: 52, + PERFECT_IV: 53, + ME_OPTION_DEFAULT: 54, // Default style for choices in ME + ME_OPTION_SPECIAL: 55, // Style for choices with special requirements in ME + SHADOW_TEXT: 56 // to obscure unavailable options +}) +export type TextStyle = typeof TextStyle[keyof typeof TextStyle]; \ No newline at end of file diff --git a/src/field/damage-number-handler.ts b/src/field/damage-number-handler.ts index acb279a17a0..1bbacc19566 100644 --- a/src/field/damage-number-handler.ts +++ b/src/field/damage-number-handler.ts @@ -1,9 +1,10 @@ import { globalScene } from "#app/global-scene"; import type { BattlerIndex } from "#enums/battler-index"; import { HitResult } from "#enums/hit-result"; +import { TextStyle } from "#enums/text-style"; import type { Pokemon } from "#field/pokemon"; import type { DamageResult } from "#types/damage-result"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import { fixedInt, formatStat } from "#utils/common"; type TextAndShadowArr = [string | null, string | null]; diff --git a/src/field/pokemon-sprite-sparkle-handler.ts b/src/field/pokemon-sprite-sparkle-handler.ts index 725229ce723..bd44dc03330 100644 --- a/src/field/pokemon-sprite-sparkle-handler.ts +++ b/src/field/pokemon-sprite-sparkle-handler.ts @@ -5,10 +5,11 @@ import { coerceArray, fixedInt, randInt } from "#utils/common"; export class PokemonSpriteSparkleHandler { private sprites: Set; + private counterTween?: Phaser.Tweens.Tween; + setup(): void { this.sprites = new Set(); - - globalScene.tweens.addCounter({ + this.counterTween = globalScene.tweens.addCounter({ duration: fixedInt(200), from: 0, to: 1, @@ -78,4 +79,12 @@ export class PokemonSpriteSparkleHandler { this.sprites.delete(s); } } + + destroy(): void { + this.removeAll(); + if (this.counterTween) { + this.counterTween.destroy(); + this.counterTween = undefined; + } + } } diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index f8c35b3e8f9..f6a9ed48847 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -23,6 +23,7 @@ import type { PokemonType } from "#enums/pokemon-type"; import { SpeciesId } from "#enums/species-id"; import { BATTLE_STATS, type PermanentStat, Stat, TEMP_BATTLE_STATS, type TempBattleStat } from "#enums/stat"; import { StatusEffect } from "#enums/status-effect"; +import { TextStyle } from "#enums/text-style"; import type { PlayerPokemon, Pokemon } from "#field/pokemon"; import type { DoubleBattleChanceBoosterModifierType, @@ -40,7 +41,7 @@ import type { } from "#modifiers/modifier-type"; import type { VoucherType } from "#system/voucher"; import type { ModifierInstanceMap, ModifierString } from "#types/modifier-types"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import { BooleanHolder, hslToHex, isNullOrUndefined, NumberHolder, randSeedFloat, toDmgValue } from "#utils/common"; import { getModifierType } from "#utils/modifier-utils"; import i18next from "i18next"; diff --git a/src/phases/end-card-phase.ts b/src/phases/end-card-phase.ts index 5c3f6e1bf9b..b9b383db13d 100644 --- a/src/phases/end-card-phase.ts +++ b/src/phases/end-card-phase.ts @@ -1,7 +1,8 @@ import { globalScene } from "#app/global-scene"; import { Phase } from "#app/phase"; import { PlayerGender } from "#enums/player-gender"; -import { addTextObject, TextStyle } from "#ui/text"; +import { TextStyle } from "#enums/text-style"; +import { addTextObject } from "#ui/text"; import i18next from "i18next"; export class EndCardPhase extends Phase { diff --git a/src/phases/scan-ivs-phase.ts b/src/phases/scan-ivs-phase.ts index e0865feb7ca..eebee28bfbb 100644 --- a/src/phases/scan-ivs-phase.ts +++ b/src/phases/scan-ivs-phase.ts @@ -2,9 +2,10 @@ import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; import type { BattlerIndex } from "#enums/battler-index"; import { PERMANENT_STATS, Stat } from "#enums/stat"; +import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; import { PokemonPhase } from "#phases/pokemon-phase"; -import { getTextColor, TextStyle } from "#ui/text"; +import { getTextColor } from "#ui/text"; import i18next from "i18next"; export class ScanIvsPhase extends PokemonPhase { diff --git a/src/timed-event-manager.ts b/src/timed-event-manager.ts index 02cb7fe8e0d..9877f298404 100644 --- a/src/timed-event-manager.ts +++ b/src/timed-event-manager.ts @@ -5,8 +5,9 @@ import { Challenges } from "#enums/challenges"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; +import { TextStyle } from "#enums/text-style"; import { WeatherType } from "#enums/weather-type"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import type { nil } from "#utils/common"; import { isNullOrUndefined } from "#utils/common"; import i18next from "i18next"; diff --git a/src/ui/ability-bar.ts b/src/ui/ability-bar.ts index 79a68e9dce7..4b868d4e66c 100644 --- a/src/ui/ability-bar.ts +++ b/src/ui/ability-bar.ts @@ -1,5 +1,6 @@ import { globalScene } from "#app/global-scene"; -import { addTextObject, TextStyle } from "#ui/text"; +import { TextStyle } from "#enums/text-style"; +import { addTextObject } from "#ui/text"; import i18next from "i18next"; const barWidth = 118; diff --git a/src/ui/abstact-option-select-ui-handler.ts b/src/ui/abstact-option-select-ui-handler.ts index d93ad8b7665..2fb0159b6ef 100644 --- a/src/ui/abstact-option-select-ui-handler.ts +++ b/src/ui/abstact-option-select-ui-handler.ts @@ -1,7 +1,8 @@ import { globalScene } from "#app/global-scene"; import { Button } from "#enums/buttons"; +import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; -import { addBBCodeTextObject, getTextColor, getTextStyleOptions, TextStyle } from "#ui/text"; +import { addBBCodeTextObject, getTextColor, getTextStyleOptions } from "#ui/text"; import { UiHandler } from "#ui/ui-handler"; import { addWindow } from "#ui/ui-theme"; import { fixedInt, rgbHexToRgba } from "#utils/common"; diff --git a/src/ui/achv-bar.ts b/src/ui/achv-bar.ts index 8e0f2a9404b..bb1ef95c9de 100644 --- a/src/ui/achv-bar.ts +++ b/src/ui/achv-bar.ts @@ -1,8 +1,9 @@ import { globalScene } from "#app/global-scene"; import type { PlayerGender } from "#enums/player-gender"; +import { TextStyle } from "#enums/text-style"; import { Achv, getAchievementDescription } from "#system/achv"; import { Voucher } from "#system/voucher"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; export class AchvBar extends Phaser.GameObjects.Container { private defaultWidth: number; diff --git a/src/ui/achvs-ui-handler.ts b/src/ui/achvs-ui-handler.ts index 6b247f6da96..01fd1d45a61 100644 --- a/src/ui/achvs-ui-handler.ts +++ b/src/ui/achvs-ui-handler.ts @@ -1,6 +1,7 @@ import { globalScene } from "#app/global-scene"; import { Button } from "#enums/buttons"; import { PlayerGender } from "#enums/player-gender"; +import { TextStyle } from "#enums/text-style"; import type { UiMode } from "#enums/ui-mode"; import type { Achv } from "#system/achv"; import { achvs, getAchievementDescription } from "#system/achv"; @@ -9,7 +10,7 @@ import type { Voucher } from "#system/voucher"; import { getVoucherTypeIcon, getVoucherTypeName, vouchers } from "#system/voucher"; import { MessageUiHandler } from "#ui/message-ui-handler"; import { ScrollBar } from "#ui/scroll-bar"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; import i18next from "i18next"; diff --git a/src/ui/admin-ui-handler.ts b/src/ui/admin-ui-handler.ts index 3d0a1153127..f136aeb5008 100644 --- a/src/ui/admin-ui-handler.ts +++ b/src/ui/admin-ui-handler.ts @@ -1,11 +1,11 @@ import { pokerogueApi } from "#api/pokerogue-api"; import { globalScene } from "#app/global-scene"; import { Button } from "#enums/buttons"; +import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; import type { InputFieldConfig } from "#ui/form-modal-ui-handler"; import { FormModalUiHandler } from "#ui/form-modal-ui-handler"; import type { ModalConfig } from "#ui/modal-ui-handler"; -import { TextStyle } from "#ui/text"; import { formatText } from "#utils/common"; type AdminUiHandlerService = "discord" | "google"; diff --git a/src/ui/arena-flyout.ts b/src/ui/arena-flyout.ts index 970c65c8978..9a674719202 100644 --- a/src/ui/arena-flyout.ts +++ b/src/ui/arena-flyout.ts @@ -3,6 +3,7 @@ import { ArenaTrapTag } from "#data/arena-tag"; import { TerrainType } from "#data/terrain"; import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagType } from "#enums/arena-tag-type"; +import { TextStyle } from "#enums/text-style"; import { WeatherType } from "#enums/weather-type"; import type { ArenaEvent } from "#events/arena"; import { @@ -14,7 +15,7 @@ import { } from "#events/arena"; import type { TurnEndEvent } from "#events/battle-scene"; import { BattleSceneEventType } from "#events/battle-scene"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import { TimeOfDayWidget } from "#ui/time-of-day-widget"; import { addWindow, WindowVariant } from "#ui/ui-theme"; import { fixedInt, formatText, toCamelCaseString } from "#utils/common"; diff --git a/src/ui/ball-ui-handler.ts b/src/ui/ball-ui-handler.ts index bde340e3cf7..67beb0eba84 100644 --- a/src/ui/ball-ui-handler.ts +++ b/src/ui/ball-ui-handler.ts @@ -2,9 +2,10 @@ import { globalScene } from "#app/global-scene"; import { getPokeballName } from "#data/pokeball"; import { Button } from "#enums/buttons"; import { Command } from "#enums/command"; +import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; import type { CommandPhase } from "#phases/command-phase"; -import { addTextObject, getTextStyleOptions, TextStyle } from "#ui/text"; +import { addTextObject, getTextStyleOptions } from "#ui/text"; import { UiHandler } from "#ui/ui-handler"; import { addWindow } from "#ui/ui-theme"; import i18next from "i18next"; diff --git a/src/ui/base-stats-overlay.ts b/src/ui/base-stats-overlay.ts index 888b87a8d11..e3ba472475a 100644 --- a/src/ui/base-stats-overlay.ts +++ b/src/ui/base-stats-overlay.ts @@ -1,6 +1,7 @@ import type { InfoToggle } from "#app/battle-scene"; import { globalScene } from "#app/global-scene"; -import { addTextObject, TextStyle } from "#ui/text"; +import { TextStyle } from "#enums/text-style"; +import { addTextObject } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; import { fixedInt } from "#utils/common"; import i18next from "i18next"; diff --git a/src/ui/battle-flyout.ts b/src/ui/battle-flyout.ts index 083dc7bbf19..0a67dc9ad37 100644 --- a/src/ui/battle-flyout.ts +++ b/src/ui/battle-flyout.ts @@ -2,12 +2,13 @@ import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; import { BerryType } from "#enums/berry-type"; import { MoveId } from "#enums/move-id"; +import { TextStyle } from "#enums/text-style"; import { UiTheme } from "#enums/ui-theme"; import type { BerryUsedEvent, MoveUsedEvent } from "#events/battle-scene"; import { BattleSceneEventType } from "#events/battle-scene"; import type { EnemyPokemon, Pokemon } from "#field/pokemon"; import type { Move } from "#moves/move"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import { fixedInt } from "#utils/common"; /** Container for info about a {@linkcode Move} */ diff --git a/src/ui/battle-info/battle-info.ts b/src/ui/battle-info/battle-info.ts index 4a2a6d1804d..0aedfbdf5e7 100644 --- a/src/ui/battle-info/battle-info.ts +++ b/src/ui/battle-info/battle-info.ts @@ -4,9 +4,10 @@ import { getTypeRgb } from "#data/type"; import { PokemonType } from "#enums/pokemon-type"; import { Stat } from "#enums/stat"; import { StatusEffect } from "#enums/status-effect"; +import { TextStyle } from "#enums/text-style"; import type { Pokemon } from "#field/pokemon"; import { getVariantTint } from "#sprites/variant"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import { fixedInt, getLocalizedSpriteKey, getShinyDescriptor } from "#utils/common"; import i18next from "i18next"; diff --git a/src/ui/battle-info/enemy-battle-info.ts b/src/ui/battle-info/enemy-battle-info.ts index 5799fb476ef..d426a49df5c 100644 --- a/src/ui/battle-info/enemy-battle-info.ts +++ b/src/ui/battle-info/enemy-battle-info.ts @@ -1,10 +1,11 @@ import { globalScene } from "#app/global-scene"; import { Stat } from "#enums/stat"; +import { TextStyle } from "#enums/text-style"; import type { EnemyPokemon } from "#field/pokemon"; import { BattleFlyout } from "#ui/battle-flyout"; import type { BattleInfoParamList } from "#ui/battle-info"; import { BattleInfo } from "#ui/battle-info"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import { addWindow, WindowVariant } from "#ui/ui-theme"; import i18next from "i18next"; import type { GameObjects } from "phaser"; diff --git a/src/ui/battle-message-ui-handler.ts b/src/ui/battle-message-ui-handler.ts index bd524f0bb43..b58897b9022 100644 --- a/src/ui/battle-message-ui-handler.ts +++ b/src/ui/battle-message-ui-handler.ts @@ -1,9 +1,10 @@ import { globalScene } from "#app/global-scene"; import { Button } from "#enums/buttons"; import { getStatKey, PERMANENT_STATS } from "#enums/stat"; +import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; import { MessageUiHandler } from "#ui/message-ui-handler"; -import { addBBCodeTextObject, addTextObject, getTextColor, TextStyle } from "#ui/text"; +import { addBBCodeTextObject, addTextObject, getTextColor } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; import i18next from "i18next"; import type BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext"; diff --git a/src/ui/bgm-bar.ts b/src/ui/bgm-bar.ts index d8b6bbe8b8a..fb50e444aa5 100644 --- a/src/ui/bgm-bar.ts +++ b/src/ui/bgm-bar.ts @@ -1,5 +1,6 @@ import { globalScene } from "#app/global-scene"; -import { addTextObject, TextStyle } from "#ui/text"; +import { TextStyle } from "#enums/text-style"; +import { addTextObject } from "#ui/text"; import { formatText } from "#utils/common"; import i18next from "i18next"; diff --git a/src/ui/candy-bar.ts b/src/ui/candy-bar.ts index ea3500d6c4c..239b963227b 100644 --- a/src/ui/candy-bar.ts +++ b/src/ui/candy-bar.ts @@ -1,7 +1,8 @@ import { globalScene } from "#app/global-scene"; import { starterColors } from "#app/global-vars/starter-colors"; import type { SpeciesId } from "#enums/species-id"; -import { addTextObject, TextStyle } from "#ui/text"; +import { TextStyle } from "#enums/text-style"; +import { addTextObject } from "#ui/text"; import { rgbHexToRgba } from "#utils/common"; import { argbFromRgba } from "@material/material-color-utilities"; diff --git a/src/ui/challenges-select-ui-handler.ts b/src/ui/challenges-select-ui-handler.ts index a827cddc9a7..4f205d59de8 100644 --- a/src/ui/challenges-select-ui-handler.ts +++ b/src/ui/challenges-select-ui-handler.ts @@ -3,8 +3,9 @@ import type { Challenge } from "#data/challenge"; import { Button } from "#enums/buttons"; import { Challenges } from "#enums/challenges"; import { Color, ShadowColor } from "#enums/color"; +import { TextStyle } from "#enums/text-style"; import type { UiMode } from "#enums/ui-mode"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import { UiHandler } from "#ui/ui-handler"; import { addWindow } from "#ui/ui-theme"; import { getLocalizedSpriteKey } from "#utils/common"; diff --git a/src/ui/command-ui-handler.ts b/src/ui/command-ui-handler.ts index 2e4acfb7c42..41ff559062a 100644 --- a/src/ui/command-ui-handler.ts +++ b/src/ui/command-ui-handler.ts @@ -5,11 +5,12 @@ import { Button } from "#enums/buttons"; import { Command } from "#enums/command"; import { PokemonType } from "#enums/pokemon-type"; import { SpeciesId } from "#enums/species-id"; +import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; import { TerastallizeAccessModifier } from "#modifiers/modifier"; import type { CommandPhase } from "#phases/command-phase"; import { PartyUiHandler, PartyUiMode } from "#ui/party-ui-handler"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import { UiHandler } from "#ui/ui-handler"; import i18next from "i18next"; diff --git a/src/ui/daily-run-scoreboard.ts b/src/ui/daily-run-scoreboard.ts index dcd45b40390..9391d02859c 100644 --- a/src/ui/daily-run-scoreboard.ts +++ b/src/ui/daily-run-scoreboard.ts @@ -1,6 +1,7 @@ import { pokerogueApi } from "#api/pokerogue-api"; import { globalScene } from "#app/global-scene"; -import { addTextObject, TextStyle } from "#ui/text"; +import { TextStyle } from "#enums/text-style"; +import { addTextObject } from "#ui/text"; import { addWindow, WindowVariant } from "#ui/ui-theme"; import { executeIf } from "#utils/common"; import { getEnumKeys } from "#utils/enums"; diff --git a/src/ui/dropdown.ts b/src/ui/dropdown.ts index 2a100ddbe59..c13d1ab6482 100644 --- a/src/ui/dropdown.ts +++ b/src/ui/dropdown.ts @@ -1,6 +1,7 @@ import { globalScene } from "#app/global-scene"; +import { TextStyle } from "#enums/text-style"; import { ScrollBar } from "#ui/scroll-bar"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import { addWindow, WindowVariant } from "#ui/ui-theme"; import i18next from "i18next"; diff --git a/src/ui/egg-counter-container.ts b/src/ui/egg-counter-container.ts index ff536228fde..da394e73b28 100644 --- a/src/ui/egg-counter-container.ts +++ b/src/ui/egg-counter-container.ts @@ -1,8 +1,9 @@ import { globalScene } from "#app/global-scene"; +import { TextStyle } from "#enums/text-style"; import type { EggCountChangedEvent } from "#events/egg"; import { EggEventType } from "#events/egg"; import type { EggHatchSceneHandler } from "#ui/egg-hatch-scene-handler"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; /** diff --git a/src/ui/egg-gacha-ui-handler.ts b/src/ui/egg-gacha-ui-handler.ts index 19d1efa75dd..5dcf05e2606 100644 --- a/src/ui/egg-gacha-ui-handler.ts +++ b/src/ui/egg-gacha-ui-handler.ts @@ -6,10 +6,11 @@ import { Egg, getLegendaryGachaSpeciesForTimestamp } from "#data/egg"; import { Button } from "#enums/buttons"; import { EggTier } from "#enums/egg-type"; import { GachaType } from "#enums/gacha-types"; +import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; import { getVoucherTypeIcon, VoucherType } from "#system/voucher"; import { MessageUiHandler } from "#ui/message-ui-handler"; -import { addTextObject, getEggTierTextTint, getTextStyleOptions, TextStyle } from "#ui/text"; +import { addTextObject, getEggTierTextTint, getTextStyleOptions } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; import { fixedInt, randSeedShuffle } from "#utils/common"; import { getEnumValues } from "#utils/enums"; @@ -74,7 +75,7 @@ export class EggGachaUiHandler extends MessageUiHandler { const gachaInfoContainer = globalScene.add.container(160, 46); const currentLanguage = i18next.resolvedLanguage ?? "en"; - let gachaTextStyle = TextStyle.WINDOW_ALT; + let gachaTextStyle: TextStyle = TextStyle.WINDOW_ALT; let gachaX = 4; let gachaY = 0; let pokemonIconX = -20; diff --git a/src/ui/egg-list-ui-handler.ts b/src/ui/egg-list-ui-handler.ts index 94d6889ed48..42f969b9d38 100644 --- a/src/ui/egg-list-ui-handler.ts +++ b/src/ui/egg-list-ui-handler.ts @@ -1,11 +1,12 @@ import { globalScene } from "#app/global-scene"; import { Button } from "#enums/buttons"; +import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; import { MessageUiHandler } from "#ui/message-ui-handler"; import { PokemonIconAnimHandler, PokemonIconAnimMode } from "#ui/pokemon-icon-anim-handler"; import { ScrollBar } from "#ui/scroll-bar"; import { ScrollableGridUiHandler } from "#ui/scrollable-grid-handler"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; import i18next from "i18next"; diff --git a/src/ui/evolution-scene-handler.ts b/src/ui/evolution-scene-handler.ts index 5ad4fc6fdf5..c22cf31faaa 100644 --- a/src/ui/evolution-scene-handler.ts +++ b/src/ui/evolution-scene-handler.ts @@ -1,8 +1,9 @@ import { globalScene } from "#app/global-scene"; import { Button } from "#enums/buttons"; +import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; import { MessageUiHandler } from "#ui/message-ui-handler"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; export class EvolutionSceneHandler extends MessageUiHandler { public evolutionContainer: Phaser.GameObjects.Container; diff --git a/src/ui/fight-ui-handler.ts b/src/ui/fight-ui-handler.ts index 286199b99e2..42f8cba5df4 100644 --- a/src/ui/fight-ui-handler.ts +++ b/src/ui/fight-ui-handler.ts @@ -7,12 +7,13 @@ import { Command } from "#enums/command"; import { MoveCategory } from "#enums/move-category"; import { MoveUseMode } from "#enums/move-use-mode"; import { PokemonType } from "#enums/pokemon-type"; +import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; import type { EnemyPokemon, Pokemon } from "#field/pokemon"; import type { PokemonMove } from "#moves/pokemon-move"; import type { CommandPhase } from "#phases/command-phase"; import { MoveInfoOverlay } from "#ui/move-info-overlay"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import { UiHandler } from "#ui/ui-handler"; import { fixedInt, getLocalizedSpriteKey, padInt } from "#utils/common"; import i18next from "i18next"; @@ -284,7 +285,7 @@ export class FightUiHandler extends UiHandler implements InfoToggle { const ppColorStyle = FightUiHandler.ppRatioToColor(pp / maxPP); - //** Changes the text color and shadow according to the determined TextStyle */ + // Changes the text color and shadow according to the determined TextStyle this.ppText.setColor(this.getTextColor(ppColorStyle, false)).setShadowColor(this.getTextColor(ppColorStyle, true)); this.moveInfoOverlay.show(pokemonMove.getMove()); diff --git a/src/ui/filter-bar.ts b/src/ui/filter-bar.ts index 3961ae3415c..ea227655a97 100644 --- a/src/ui/filter-bar.ts +++ b/src/ui/filter-bar.ts @@ -1,10 +1,11 @@ import { globalScene } from "#app/global-scene"; import type { DropDownColumn } from "#enums/drop-down-column"; +import { TextStyle } from "#enums/text-style"; import type { UiTheme } from "#enums/ui-theme"; import type { DropDown } from "#ui/dropdown"; import { DropDownType } from "#ui/dropdown"; import type { StarterContainer } from "#ui/starter-container"; -import { addTextObject, getTextColor, TextStyle } from "#ui/text"; +import { addTextObject, getTextColor } from "#ui/text"; import { addWindow, WindowVariant } from "#ui/ui-theme"; export class FilterBar extends Phaser.GameObjects.Container { diff --git a/src/ui/filter-text.ts b/src/ui/filter-text.ts index 4a9012e44fc..ff7119dd778 100644 --- a/src/ui/filter-text.ts +++ b/src/ui/filter-text.ts @@ -1,9 +1,10 @@ import { globalScene } from "#app/global-scene"; +import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; import type { UiTheme } from "#enums/ui-theme"; import type { AwaitableUiHandler } from "#ui/awaitable-ui-handler"; import type { StarterContainer } from "#ui/starter-container"; -import { addTextObject, getTextColor, TextStyle } from "#ui/text"; +import { addTextObject, getTextColor } from "#ui/text"; import type { UI } from "#ui/ui"; import { addWindow, WindowVariant } from "#ui/ui-theme"; import i18next from "i18next"; diff --git a/src/ui/form-modal-ui-handler.ts b/src/ui/form-modal-ui-handler.ts index 35965b09b80..7ae545a7292 100644 --- a/src/ui/form-modal-ui-handler.ts +++ b/src/ui/form-modal-ui-handler.ts @@ -1,9 +1,10 @@ import { globalScene } from "#app/global-scene"; import { Button } from "#enums/buttons"; +import { TextStyle } from "#enums/text-style"; import type { UiMode } from "#enums/ui-mode"; import type { ModalConfig } from "#ui/modal-ui-handler"; import { ModalUiHandler } from "#ui/modal-ui-handler"; -import { addTextInputObject, addTextObject, TextStyle } from "#ui/text"; +import { addTextInputObject, addTextObject } from "#ui/text"; import { addWindow, WindowVariant } from "#ui/ui-theme"; import { fixedInt } from "#utils/common"; import type InputText from "phaser3-rex-plugins/plugins/inputtext"; diff --git a/src/ui/game-stats-ui-handler.ts b/src/ui/game-stats-ui-handler.ts index 759792b122f..fa5cd0a989a 100644 --- a/src/ui/game-stats-ui-handler.ts +++ b/src/ui/game-stats-ui-handler.ts @@ -2,9 +2,10 @@ import { globalScene } from "#app/global-scene"; import { speciesStarterCosts } from "#balance/starters"; import { Button } from "#enums/buttons"; import { DexAttr } from "#enums/dex-attr"; +import { TextStyle } from "#enums/text-style"; import { UiTheme } from "#enums/ui-theme"; import type { GameData } from "#system/game-data"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import { UiHandler } from "#ui/ui-handler"; import { addWindow } from "#ui/ui-theme"; import { formatFancyLargeNumber, getPlayTimeString, toReadableString } from "#utils/common"; diff --git a/src/ui/loading-modal-ui-handler.ts b/src/ui/loading-modal-ui-handler.ts index 585d70d51db..de00d911c47 100644 --- a/src/ui/loading-modal-ui-handler.ts +++ b/src/ui/loading-modal-ui-handler.ts @@ -1,6 +1,7 @@ +import { TextStyle } from "#enums/text-style"; import type { UiMode } from "#enums/ui-mode"; import { ModalUiHandler } from "#ui/modal-ui-handler"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import i18next from "i18next"; export class LoadingModalUiHandler extends ModalUiHandler { diff --git a/src/ui/login-form-ui-handler.ts b/src/ui/login-form-ui-handler.ts index 417a9031bf7..524eaeece86 100644 --- a/src/ui/login-form-ui-handler.ts +++ b/src/ui/login-form-ui-handler.ts @@ -1,11 +1,12 @@ import { pokerogueApi } from "#api/pokerogue-api"; import { globalScene } from "#app/global-scene"; +import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; import type { OptionSelectItem } from "#ui/abstact-option-select-ui-handler"; import type { InputFieldConfig } from "#ui/form-modal-ui-handler"; import { FormModalUiHandler } from "#ui/form-modal-ui-handler"; import type { ModalConfig } from "#ui/modal-ui-handler"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; import { fixedInt } from "#utils/common"; import i18next from "i18next"; diff --git a/src/ui/menu-ui-handler.ts b/src/ui/menu-ui-handler.ts index 4e45dfedcb3..fa65cccab2f 100644 --- a/src/ui/menu-ui-handler.ts +++ b/src/ui/menu-ui-handler.ts @@ -5,13 +5,14 @@ import { bypassLogin } from "#app/global-vars/bypass-login"; import { handleTutorial, Tutorial } from "#app/tutorial"; import { Button } from "#enums/buttons"; import { GameDataType } from "#enums/game-data-type"; +import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; import type { OptionSelectConfig, OptionSelectItem } from "#ui/abstact-option-select-ui-handler"; import { AdminMode, getAdminModeName } from "#ui/admin-ui-handler"; import type { AwaitableUiHandler } from "#ui/awaitable-ui-handler"; import { BgmBar } from "#ui/bgm-bar"; import { MessageUiHandler } from "#ui/message-ui-handler"; -import { addTextObject, getTextStyleOptions, TextStyle } from "#ui/text"; +import { addTextObject, getTextStyleOptions } from "#ui/text"; import { addWindow, WindowVariant } from "#ui/ui-theme"; import { fixedInt, isLocal, sessionIdKey } from "#utils/common"; import { getCookie } from "#utils/cookies"; diff --git a/src/ui/modal-ui-handler.ts b/src/ui/modal-ui-handler.ts index 844f7f43930..c93b713831e 100644 --- a/src/ui/modal-ui-handler.ts +++ b/src/ui/modal-ui-handler.ts @@ -1,7 +1,8 @@ import { globalScene } from "#app/global-scene"; import type { Button } from "#enums/buttons"; +import { TextStyle } from "#enums/text-style"; import type { UiMode } from "#enums/ui-mode"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import { UiHandler } from "#ui/ui-handler"; import { addWindow, WindowVariant } from "#ui/ui-theme"; diff --git a/src/ui/modifier-select-ui-handler.ts b/src/ui/modifier-select-ui-handler.ts index 3e3487d4ffb..50d88738d32 100644 --- a/src/ui/modifier-select-ui-handler.ts +++ b/src/ui/modifier-select-ui-handler.ts @@ -6,13 +6,14 @@ import { getPokeballAtlasKey } from "#data/pokeball"; import { Button } from "#enums/buttons"; import type { PokeballType } from "#enums/pokeball"; import { ShopCursorTarget } from "#enums/shop-cursor-target"; +import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; import { HealShopCostModifier, LockModifierTiersModifier, PokemonHeldItemModifier } from "#modifiers/modifier"; import type { ModifierTypeOption } from "#modifiers/modifier-type"; import { getPlayerShopModifierTypeOptionsForWave, TmModifierType } from "#modifiers/modifier-type"; import { AwaitableUiHandler } from "#ui/awaitable-ui-handler"; import { MoveInfoOverlay } from "#ui/move-info-overlay"; -import { addTextObject, getModifierTierTextTint, getTextColor, getTextStyleOptions, TextStyle } from "#ui/text"; +import { addTextObject, getModifierTierTextTint, getTextColor, getTextStyleOptions } from "#ui/text"; import { formatMoney, NumberHolder } from "#utils/common"; import i18next from "i18next"; import Phaser from "phaser"; diff --git a/src/ui/move-info-overlay.ts b/src/ui/move-info-overlay.ts index 7720354a5b3..f8632eb244e 100644 --- a/src/ui/move-info-overlay.ts +++ b/src/ui/move-info-overlay.ts @@ -2,8 +2,9 @@ import type { InfoToggle } from "#app/battle-scene"; import { globalScene } from "#app/global-scene"; import { MoveCategory } from "#enums/move-category"; import { PokemonType } from "#enums/pokemon-type"; +import { TextStyle } from "#enums/text-style"; import type { Move } from "#moves/move"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; import { fixedInt, getLocalizedSpriteKey } from "#utils/common"; import i18next from "i18next"; diff --git a/src/ui/mystery-encounter-ui-handler.ts b/src/ui/mystery-encounter-ui-handler.ts index 37f0efb50e4..b6bc464855c 100644 --- a/src/ui/mystery-encounter-ui-handler.ts +++ b/src/ui/mystery-encounter-ui-handler.ts @@ -3,13 +3,14 @@ import { getPokeballAtlasKey } from "#data/pokeball"; import { Button } from "#enums/buttons"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; import { getEncounterText } from "#mystery-encounters/encounter-dialogue-utils"; import type { OptionSelectSettings } from "#mystery-encounters/encounter-phase-utils"; import type { MysteryEncounterOption } from "#mystery-encounters/mystery-encounter-option"; import type { MysteryEncounterPhase } from "#phases/mystery-encounter-phases"; import { PartyUiMode } from "#ui/party-ui-handler"; -import { addBBCodeTextObject, getBBCodeFrag, TextStyle } from "#ui/text"; +import { addBBCodeTextObject, getBBCodeFrag } from "#ui/text"; import { UiHandler } from "#ui/ui-handler"; import { addWindow, WindowVariant } from "#ui/ui-theme"; import { fixedInt, isNullOrUndefined } from "#utils/common"; diff --git a/src/ui/party-exp-bar.ts b/src/ui/party-exp-bar.ts index 0d6ec936a92..952a1f8227a 100644 --- a/src/ui/party-exp-bar.ts +++ b/src/ui/party-exp-bar.ts @@ -1,6 +1,7 @@ import { globalScene } from "#app/global-scene"; +import { TextStyle } from "#enums/text-style"; import type { Pokemon } from "#field/pokemon"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import i18next from "i18next"; export class PartyExpBar extends Phaser.GameObjects.Container { diff --git a/src/ui/party-ui-handler.ts b/src/ui/party-ui-handler.ts index 956e4c52e39..2b29a564f9f 100644 --- a/src/ui/party-ui-handler.ts +++ b/src/ui/party-ui-handler.ts @@ -13,6 +13,7 @@ import { MoveId } from "#enums/move-id"; import { MoveResult } from "#enums/move-result"; import { SpeciesId } from "#enums/species-id"; import { StatusEffect } from "#enums/status-effect"; +import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; import type { PlayerPokemon, Pokemon } from "#field/pokemon"; import type { PokemonFormChangeItemModifier, PokemonHeldItemModifier } from "#modifiers/modifier"; @@ -23,7 +24,7 @@ import type { TurnMove } from "#types/turn-move"; import { MessageUiHandler } from "#ui/message-ui-handler"; import { MoveInfoOverlay } from "#ui/move-info-overlay"; import { PokemonIconAnimHandler, PokemonIconAnimMode } from "#ui/pokemon-icon-anim-handler"; -import { addBBCodeTextObject, addTextObject, getTextColor, TextStyle } from "#ui/text"; +import { addBBCodeTextObject, addTextObject, getTextColor } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; import { BooleanHolder, getLocalizedSpriteKey, randInt, toReadableString } from "#utils/common"; import i18next from "i18next"; diff --git a/src/ui/pokedex-info-overlay.ts b/src/ui/pokedex-info-overlay.ts index 6d3b8f1009f..0f2f5fa3dde 100644 --- a/src/ui/pokedex-info-overlay.ts +++ b/src/ui/pokedex-info-overlay.ts @@ -1,6 +1,7 @@ import type { InfoToggle } from "#app/battle-scene"; import { globalScene } from "#app/global-scene"; -import { addTextObject, TextStyle } from "#ui/text"; +import { TextStyle } from "#enums/text-style"; +import { addTextObject } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; import { fixedInt } from "#utils/common"; diff --git a/src/ui/pokedex-mon-container.ts b/src/ui/pokedex-mon-container.ts index 73799870e6b..cfb8555e6c9 100644 --- a/src/ui/pokedex-mon-container.ts +++ b/src/ui/pokedex-mon-container.ts @@ -1,7 +1,8 @@ import { globalScene } from "#app/global-scene"; import type { PokemonSpecies } from "#data/pokemon-species"; +import { TextStyle } from "#enums/text-style"; import type { Variant } from "#sprites/variant"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import { isNullOrUndefined } from "#utils/common"; interface SpeciesDetails { diff --git a/src/ui/pokedex-page-ui-handler.ts b/src/ui/pokedex-page-ui-handler.ts index ff96aa55772..b3eb8de0f50 100644 --- a/src/ui/pokedex-page-ui-handler.ts +++ b/src/ui/pokedex-page-ui-handler.ts @@ -38,6 +38,7 @@ import type { Nature } from "#enums/nature"; import { Passive as PassiveAttr } from "#enums/passive"; import { PokemonType } from "#enums/pokemon-type"; import { SpeciesId } from "#enums/species-id"; +import { TextStyle } from "#enums/text-style"; import { TimeOfDay } from "#enums/time-of-day"; import { UiMode } from "#enums/ui-mode"; import type { Variant } from "#sprites/variant"; @@ -51,7 +52,7 @@ import { MessageUiHandler } from "#ui/message-ui-handler"; import { MoveInfoOverlay } from "#ui/move-info-overlay"; import { PokedexInfoOverlay } from "#ui/pokedex-info-overlay"; import { StatsContainer } from "#ui/stats-container"; -import { addBBCodeTextObject, addTextObject, getTextColor, getTextStyleOptions, TextStyle } from "#ui/text"; +import { addBBCodeTextObject, addTextObject, getTextColor, getTextStyleOptions } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; import { BooleanHolder, diff --git a/src/ui/pokedex-ui-handler.ts b/src/ui/pokedex-ui-handler.ts index c2f595cb190..5d49e867b59 100644 --- a/src/ui/pokedex-ui-handler.ts +++ b/src/ui/pokedex-ui-handler.ts @@ -26,6 +26,7 @@ import type { Nature } from "#enums/nature"; import { Passive as PassiveAttr } from "#enums/passive"; import { PokemonType } from "#enums/pokemon-type"; import type { SpeciesId } from "#enums/species-id"; +import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; import type { Variant } from "#sprites/variant"; import { getVariantIcon, getVariantTint } from "#sprites/variant"; @@ -40,7 +41,7 @@ import { MessageUiHandler } from "#ui/message-ui-handler"; import { PokedexMonContainer } from "#ui/pokedex-mon-container"; import { PokemonIconAnimHandler, PokemonIconAnimMode } from "#ui/pokemon-icon-anim-handler"; import { ScrollBar } from "#ui/scroll-bar"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; import { BooleanHolder, fixedInt, getLocalizedSpriteKey, padInt, randIntRange, rgbHexToRgba } from "#utils/common"; import type { StarterPreferences } from "#utils/data"; diff --git a/src/ui/pokemon-hatch-info-container.ts b/src/ui/pokemon-hatch-info-container.ts index 8bcd62316cd..9c223adf837 100644 --- a/src/ui/pokemon-hatch-info-container.ts +++ b/src/ui/pokemon-hatch-info-container.ts @@ -8,9 +8,10 @@ import { Gender } from "#data/gender"; import { getPokemonSpeciesForm } from "#data/pokemon-species"; import { PokemonType } from "#enums/pokemon-type"; import { SpeciesId } from "#enums/species-id"; +import { TextStyle } from "#enums/text-style"; import type { PlayerPokemon } from "#field/pokemon"; import { PokemonInfoContainer } from "#ui/pokemon-info-container"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import { padInt, rgbHexToRgba } from "#utils/common"; import { argbFromRgba } from "@material/material-color-utilities"; diff --git a/src/ui/pokemon-info-container.ts b/src/ui/pokemon-info-container.ts index c95f412c834..3b2349348a8 100644 --- a/src/ui/pokemon-info-container.ts +++ b/src/ui/pokemon-info-container.ts @@ -3,13 +3,14 @@ import { Gender, getGenderColor, getGenderSymbol } from "#data/gender"; import { getNatureName } from "#data/nature"; import { DexAttr } from "#enums/dex-attr"; import { PokemonType } from "#enums/pokemon-type"; +import { TextStyle } from "#enums/text-style"; import type { Pokemon } from "#field/pokemon"; import { getVariantTint } from "#sprites/variant"; import type { StarterDataEntry } from "#system/game-data"; import type { DexEntry } from "#types/dex-data"; import { ConfirmUiHandler } from "#ui/confirm-ui-handler"; import { StatsContainer } from "#ui/stats-container"; -import { addBBCodeTextObject, addTextObject, getTextColor, TextStyle } from "#ui/text"; +import { addBBCodeTextObject, addTextObject, getTextColor } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; import { fixedInt, getShinyDescriptor } from "#utils/common"; import i18next from "i18next"; diff --git a/src/ui/registration-form-ui-handler.ts b/src/ui/registration-form-ui-handler.ts index 2466603af71..60f8d1d987f 100644 --- a/src/ui/registration-form-ui-handler.ts +++ b/src/ui/registration-form-ui-handler.ts @@ -1,10 +1,11 @@ import { pokerogueApi } from "#api/pokerogue-api"; import { globalScene } from "#app/global-scene"; +import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; import type { InputFieldConfig } from "#ui/form-modal-ui-handler"; import { FormModalUiHandler } from "#ui/form-modal-ui-handler"; import type { ModalConfig } from "#ui/modal-ui-handler"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import i18next from "i18next"; interface LanguageSetting { diff --git a/src/ui/run-history-ui-handler.ts b/src/ui/run-history-ui-handler.ts index f810468aea1..00aa47ae65d 100644 --- a/src/ui/run-history-ui-handler.ts +++ b/src/ui/run-history-ui-handler.ts @@ -3,13 +3,14 @@ import { BattleType } from "#enums/battle-type"; import { Button } from "#enums/buttons"; import { GameModes } from "#enums/game-modes"; import { PlayerGender } from "#enums/player-gender"; +import { TextStyle } from "#enums/text-style"; import { TrainerVariant } from "#enums/trainer-variant"; import { UiMode } from "#enums/ui-mode"; import type { RunEntry } from "#system/game-data"; import type { PokemonData } from "#system/pokemon-data"; import { MessageUiHandler } from "#ui/message-ui-handler"; import { RunDisplayMode } from "#ui/run-info-ui-handler"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; import { fixedInt, formatLargeNumber } from "#utils/common"; import i18next from "i18next"; diff --git a/src/ui/run-info-ui-handler.ts b/src/ui/run-info-ui-handler.ts index 29f95c4e4c8..465e48a45ad 100644 --- a/src/ui/run-info-ui-handler.ts +++ b/src/ui/run-info-ui-handler.ts @@ -12,6 +12,7 @@ import type { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { PlayerGender } from "#enums/player-gender"; import { PokemonType } from "#enums/pokemon-type"; import type { SpeciesId } from "#enums/species-id"; +import { TextStyle } from "#enums/text-style"; import { TrainerVariant } from "#enums/trainer-variant"; import { UiMode } from "#enums/ui-mode"; // biome-ignore lint/performance/noNamespaceImport: See `src/system/game-data.ts` @@ -21,7 +22,7 @@ import { getVariantTint } from "#sprites/variant"; import type { SessionSaveData } from "#system/game-data"; import type { PokemonData } from "#system/pokemon-data"; import { SettingKeyboard } from "#system/settings-keyboard"; -import { addBBCodeTextObject, addTextObject, getTextColor, TextStyle } from "#ui/text"; +import { addBBCodeTextObject, addTextObject, getTextColor } from "#ui/text"; import { UiHandler } from "#ui/ui-handler"; import { addWindow } from "#ui/ui-theme"; import { formatFancyLargeNumber, formatLargeNumber, formatMoney, getPlayTimeString } from "#utils/common"; diff --git a/src/ui/save-slot-select-ui-handler.ts b/src/ui/save-slot-select-ui-handler.ts index bcbe60265cd..9da34e672f1 100644 --- a/src/ui/save-slot-select-ui-handler.ts +++ b/src/ui/save-slot-select-ui-handler.ts @@ -1,6 +1,7 @@ import { GameMode } from "#app/game-mode"; import { globalScene } from "#app/global-scene"; import { Button } from "#enums/buttons"; +import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; // biome-ignore lint/performance/noNamespaceImport: See `src/system/game-data.ts` import * as Modifier from "#modifiers/modifier"; @@ -8,7 +9,7 @@ import type { SessionSaveData } from "#system/game-data"; import type { PokemonData } from "#system/pokemon-data"; import { MessageUiHandler } from "#ui/message-ui-handler"; import { RunDisplayMode } from "#ui/run-info-ui-handler"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; import { fixedInt, formatLargeNumber, getPlayTimeString, isNullOrUndefined } from "#utils/common"; import i18next from "i18next"; diff --git a/src/ui/session-reload-modal-ui-handler.ts b/src/ui/session-reload-modal-ui-handler.ts index ab1197324a6..1f5a205f990 100644 --- a/src/ui/session-reload-modal-ui-handler.ts +++ b/src/ui/session-reload-modal-ui-handler.ts @@ -1,7 +1,8 @@ +import { TextStyle } from "#enums/text-style"; import type { UiMode } from "#enums/ui-mode"; import type { ModalConfig } from "#ui/modal-ui-handler"; import { ModalUiHandler } from "#ui/modal-ui-handler"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; export class SessionReloadModalUiHandler extends ModalUiHandler { constructor(mode: UiMode | null = null) { diff --git a/src/ui/settings/abstract-binding-ui-handler.ts b/src/ui/settings/abstract-binding-ui-handler.ts index 7004af8c4ed..eb68456a69d 100644 --- a/src/ui/settings/abstract-binding-ui-handler.ts +++ b/src/ui/settings/abstract-binding-ui-handler.ts @@ -1,8 +1,9 @@ import { globalScene } from "#app/global-scene"; import { Button } from "#enums/buttons"; +import { TextStyle } from "#enums/text-style"; import type { UiMode } from "#enums/ui-mode"; import { NavigationManager } from "#ui/navigation-menu"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import { UiHandler } from "#ui/ui-handler"; import { addWindow } from "#ui/ui-theme"; import i18next from "i18next"; diff --git a/src/ui/settings/abstract-control-settings-ui-handler.ts b/src/ui/settings/abstract-control-settings-ui-handler.ts index 64786849abc..8f7de42e4db 100644 --- a/src/ui/settings/abstract-control-settings-ui-handler.ts +++ b/src/ui/settings/abstract-control-settings-ui-handler.ts @@ -2,11 +2,12 @@ import { globalScene } from "#app/global-scene"; import type { InterfaceConfig } from "#app/inputs-controller"; import { Button } from "#enums/buttons"; import type { Device } from "#enums/devices"; +import { TextStyle } from "#enums/text-style"; import type { UiMode } from "#enums/ui-mode"; import { getIconWithSettingName } from "#inputs/config-handler"; import { NavigationManager, NavigationMenu } from "#ui/navigation-menu"; import { ScrollBar } from "#ui/scroll-bar"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import { UiHandler } from "#ui/ui-handler"; import { addWindow } from "#ui/ui-theme"; import i18next from "i18next"; diff --git a/src/ui/settings/abstract-settings-ui-handler.ts b/src/ui/settings/abstract-settings-ui-handler.ts index 9e56ae80b14..81d733220fc 100644 --- a/src/ui/settings/abstract-settings-ui-handler.ts +++ b/src/ui/settings/abstract-settings-ui-handler.ts @@ -1,5 +1,6 @@ import { globalScene } from "#app/global-scene"; import { Button } from "#enums/buttons"; +import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; import type { SettingType } from "#system/settings"; import { Setting, SettingKeys } from "#system/settings"; @@ -7,7 +8,7 @@ import type { InputsIcons } from "#ui/abstract-control-settings-ui-handler"; import { MessageUiHandler } from "#ui/message-ui-handler"; import { NavigationManager, NavigationMenu } from "#ui/navigation-menu"; import { ScrollBar } from "#ui/scroll-bar"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; import i18next from "i18next"; diff --git a/src/ui/settings/gamepad-binding-ui-handler.ts b/src/ui/settings/gamepad-binding-ui-handler.ts index e97fc56d7c0..53d606b6f84 100644 --- a/src/ui/settings/gamepad-binding-ui-handler.ts +++ b/src/ui/settings/gamepad-binding-ui-handler.ts @@ -1,9 +1,10 @@ import { globalScene } from "#app/global-scene"; import { Device } from "#enums/devices"; +import { TextStyle } from "#enums/text-style"; import type { UiMode } from "#enums/ui-mode"; import { getIconWithSettingName, getKeyWithKeycode } from "#inputs/config-handler"; import { AbstractBindingUiHandler } from "#ui/abstract-binding-ui-handler"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import i18next from "i18next"; export class GamepadBindingUiHandler extends AbstractBindingUiHandler { diff --git a/src/ui/settings/keyboard-binding-ui-handler.ts b/src/ui/settings/keyboard-binding-ui-handler.ts index e43184795d1..b339ac16188 100644 --- a/src/ui/settings/keyboard-binding-ui-handler.ts +++ b/src/ui/settings/keyboard-binding-ui-handler.ts @@ -1,9 +1,10 @@ import { globalScene } from "#app/global-scene"; import { Device } from "#enums/devices"; +import { TextStyle } from "#enums/text-style"; import type { UiMode } from "#enums/ui-mode"; import { getKeyWithKeycode } from "#inputs/config-handler"; import { AbstractBindingUiHandler } from "#ui/abstract-binding-ui-handler"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import i18next from "i18next"; export class KeyboardBindingUiHandler extends AbstractBindingUiHandler { diff --git a/src/ui/settings/navigation-menu.ts b/src/ui/settings/navigation-menu.ts index 1303c32d3a5..2f3aa50f7f3 100644 --- a/src/ui/settings/navigation-menu.ts +++ b/src/ui/settings/navigation-menu.ts @@ -1,8 +1,9 @@ import { globalScene } from "#app/global-scene"; import { Button } from "#enums/buttons"; +import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; import type { InputsIcons } from "#ui/abstract-control-settings-ui-handler"; -import { addTextObject, setTextStyle, TextStyle } from "#ui/text"; +import { addTextObject, setTextStyle } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; import i18next from "i18next"; diff --git a/src/ui/settings/settings-gamepad-ui-handler.ts b/src/ui/settings/settings-gamepad-ui-handler.ts index ea2e18a2ce6..57a70411f4c 100644 --- a/src/ui/settings/settings-gamepad-ui-handler.ts +++ b/src/ui/settings/settings-gamepad-ui-handler.ts @@ -1,6 +1,7 @@ import { globalScene } from "#app/global-scene"; import type { InterfaceConfig } from "#app/inputs-controller"; import { Device } from "#enums/devices"; +import { TextStyle } from "#enums/text-style"; import type { UiMode } from "#enums/ui-mode"; import pad_dualshock from "#inputs/pad-dualshock"; import pad_unlicensedSNES from "#inputs/pad-unlicensed-snes"; @@ -13,7 +14,7 @@ import { settingGamepadOptions, } from "#system/settings-gamepad"; import { AbstractControlSettingsUiHandler } from "#ui/abstract-control-settings-ui-handler"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import { truncateString } from "#utils/common"; import i18next from "i18next"; diff --git a/src/ui/settings/settings-keyboard-ui-handler.ts b/src/ui/settings/settings-keyboard-ui-handler.ts index 2c2e0dbd7cd..ad4bf47f673 100644 --- a/src/ui/settings/settings-keyboard-ui-handler.ts +++ b/src/ui/settings/settings-keyboard-ui-handler.ts @@ -1,6 +1,7 @@ import { globalScene } from "#app/global-scene"; import type { InterfaceConfig } from "#app/inputs-controller"; import { Device } from "#enums/devices"; +import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; import cfg_keyboard_qwerty from "#inputs/cfg-keyboard-qwerty"; import { deleteBind } from "#inputs/config-handler"; @@ -13,7 +14,7 @@ import { } from "#system/settings-keyboard"; import { AbstractControlSettingsUiHandler } from "#ui/abstract-control-settings-ui-handler"; import { NavigationManager } from "#ui/navigation-menu"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import { reverseValueToKeySetting, truncateString } from "#utils/common"; import i18next from "i18next"; diff --git a/src/ui/starter-container.ts b/src/ui/starter-container.ts index 4c174dc5955..f81ac8e5bfb 100644 --- a/src/ui/starter-container.ts +++ b/src/ui/starter-container.ts @@ -1,6 +1,7 @@ import { globalScene } from "#app/global-scene"; import type { PokemonSpecies } from "#data/pokemon-species"; -import { addTextObject, TextStyle } from "#ui/text"; +import { TextStyle } from "#enums/text-style"; +import { addTextObject } from "#ui/text"; export class StarterContainer extends Phaser.GameObjects.Container { public species: PokemonSpecies; diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index fcd8167a519..82e19d857c5 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -39,6 +39,7 @@ import type { Nature } from "#enums/nature"; import { Passive as PassiveAttr } from "#enums/passive"; import { PokemonType } from "#enums/pokemon-type"; import { SpeciesId } from "#enums/species-id"; +import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; import type { CandyUpgradeNotificationChangedEvent } from "#events/battle-scene"; import { BattleSceneEventType } from "#events/battle-scene"; @@ -57,7 +58,7 @@ import { PokemonIconAnimHandler, PokemonIconAnimMode } from "#ui/pokemon-icon-an import { ScrollBar } from "#ui/scroll-bar"; import { StarterContainer } from "#ui/starter-container"; import { StatsContainer } from "#ui/stats-container"; -import { addBBCodeTextObject, addTextObject, TextStyle } from "#ui/text"; +import { addBBCodeTextObject, addTextObject } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; import { BooleanHolder, diff --git a/src/ui/stats-container.ts b/src/ui/stats-container.ts index 6b89e80b80a..e9af5eed3e3 100644 --- a/src/ui/stats-container.ts +++ b/src/ui/stats-container.ts @@ -1,6 +1,7 @@ import { globalScene } from "#app/global-scene"; import { getStatKey, PERMANENT_STATS } from "#enums/stat"; -import { addBBCodeTextObject, addTextObject, getTextColor, TextStyle } from "#ui/text"; +import { TextStyle } from "#enums/text-style"; +import { addBBCodeTextObject, addTextObject, getTextColor } from "#ui/text"; import i18next from "i18next"; import type BBCodeText from "phaser3-rex-plugins/plugins/gameobjects/tagtext/bbcodetext/BBCodeText"; diff --git a/src/ui/summary-ui-handler.ts b/src/ui/summary-ui-handler.ts index 7661dab068e..c5c714f0b34 100644 --- a/src/ui/summary-ui-handler.ts +++ b/src/ui/summary-ui-handler.ts @@ -16,6 +16,7 @@ import { PlayerGender } from "#enums/player-gender"; import { PokemonType } from "#enums/pokemon-type"; import { getStatKey, PERMANENT_STATS, Stat } from "#enums/stat"; import { StatusEffect } from "#enums/status-effect"; +import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; import type { PlayerPokemon } from "#field/pokemon"; import { modifierSortFunc, PokemonHeldItemModifier } from "#modifiers/modifier"; @@ -24,7 +25,7 @@ import type { PokemonMove } from "#moves/pokemon-move"; import type { Variant } from "#sprites/variant"; import { getVariantTint } from "#sprites/variant"; import { achvs } from "#system/achv"; -import { addBBCodeTextObject, addTextObject, getBBCodeFrag, TextStyle } from "#ui/text"; +import { addBBCodeTextObject, addTextObject, getBBCodeFrag } from "#ui/text"; import { UiHandler } from "#ui/ui-handler"; import { fixedInt, diff --git a/src/ui/text.ts b/src/ui/text.ts index b2a1894c85c..8aa50983874 100644 --- a/src/ui/text.ts +++ b/src/ui/text.ts @@ -1,79 +1,14 @@ import { globalScene } from "#app/global-scene"; import { EggTier } from "#enums/egg-type"; import { ModifierTier } from "#enums/modifier-tier"; +import { TextStyle } from "#enums/text-style"; import { UiTheme } from "#enums/ui-theme"; import i18next from "#plugins/i18n"; +import type { TextStyleOptions } from "#types/ui"; import type Phaser from "phaser"; import BBCodeText from "phaser3-rex-plugins/plugins/gameobjects/tagtext/bbcodetext/BBCodeText"; import type InputText from "phaser3-rex-plugins/plugins/inputtext"; -export enum TextStyle { - MESSAGE, - WINDOW, - WINDOW_ALT, - WINDOW_BATTLE_COMMAND, - BATTLE_INFO, - PARTY, - PARTY_RED, - PARTY_CANCEL_BUTTON, - INSTRUCTIONS_TEXT, - MOVE_LABEL, - SUMMARY, - SUMMARY_DEX_NUM, - SUMMARY_DEX_NUM_GOLD, - SUMMARY_ALT, - SUMMARY_HEADER, - SUMMARY_RED, - SUMMARY_BLUE, - SUMMARY_PINK, - SUMMARY_GOLD, - SUMMARY_GRAY, - SUMMARY_GREEN, - SUMMARY_STATS, - SUMMARY_STATS_BLUE, - SUMMARY_STATS_PINK, - SUMMARY_STATS_GOLD, - LUCK_VALUE, - STATS_HEXAGON, - GROWTH_RATE_TYPE, - MONEY, // Money default styling (pale yellow) - MONEY_WINDOW, // Money displayed in Windows (needs different colors based on theme) - HEADER_LABEL, - STATS_LABEL, - STATS_VALUE, - SETTINGS_VALUE, - SETTINGS_LABEL, - SETTINGS_LABEL_NAVBAR, - SETTINGS_SELECTED, - SETTINGS_LOCKED, - EGG_LIST, - EGG_SUMMARY_NAME, - EGG_SUMMARY_DEX, - STARTER_VALUE_LIMIT, - TOOLTIP_TITLE, - TOOLTIP_CONTENT, - FILTER_BAR_MAIN, - MOVE_INFO_CONTENT, - MOVE_PP_FULL, - MOVE_PP_HALF_FULL, - MOVE_PP_NEAR_EMPTY, - MOVE_PP_EMPTY, - SMALLER_WINDOW_ALT, - BGM_BAR, - PERFECT_IV, - ME_OPTION_DEFAULT, // Default style for choices in ME - ME_OPTION_SPECIAL, // Style for choices with special requirements in ME - SHADOW_TEXT, // To obscure unavailable options -} - -export interface TextStyleOptions { - scale: number; - styleOptions: Phaser.Types.GameObjects.Text.TextStyle | InputText.IConfig; - shadowColor: string; - shadowXpos: number; - shadowYpos: number; -} - export function addTextObject( x: number, y: number, @@ -87,9 +22,10 @@ export function addTextObject( extraStyleOptions, ); - const ret = globalScene.add.text(x, y, content, styleOptions); - ret.setScale(scale); - ret.setShadow(shadowXpos, shadowYpos, shadowColor); + const ret = globalScene.add + .text(x, y, content, styleOptions) + .setScale(scale) + .setShadow(shadowXpos, shadowYpos, shadowColor); if (!(styleOptions as Phaser.Types.GameObjects.Text.TextStyle).lineSpacing) { ret.setLineSpacing(scale * 30); } @@ -107,8 +43,7 @@ export function setTextStyle( globalScene.uiTheme, extraStyleOptions, ); - obj.setScale(scale); - obj.setShadow(shadowXpos, shadowYpos, shadowColor); + obj.setScale(scale).setShadow(shadowXpos, shadowYpos, shadowColor); if (!(styleOptions as Phaser.Types.GameObjects.Text.TextStyle).lineSpacing) { obj.setLineSpacing(scale * 30); } @@ -133,8 +68,7 @@ export function addBBCodeTextObject( const ret = new BBCodeText(globalScene, x, y, content, styleOptions as BBCodeText.TextStyle); globalScene.add.existing(ret); - ret.setScale(scale); - ret.setShadow(shadowXpos, shadowYpos, shadowColor); + ret.setScale(scale).setShadow(shadowXpos, shadowYpos, shadowColor); if (!(styleOptions as BBCodeText.TextStyle).lineSpacing) { ret.setLineSpacing(scale * 60); } diff --git a/src/ui/title-ui-handler.ts b/src/ui/title-ui-handler.ts index b7c37538a3e..66cb69f6a26 100644 --- a/src/ui/title-ui-handler.ts +++ b/src/ui/title-ui-handler.ts @@ -5,10 +5,11 @@ import { TimedEventDisplay } from "#app/timed-event-manager"; import { getSplashMessages } from "#data/splash-messages"; import { PlayerGender } from "#enums/player-gender"; import type { SpeciesId } from "#enums/species-id"; +import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; import { version } from "#package.json"; import { OptionSelectUiHandler } from "#ui/option-select-ui-handler"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import { fixedInt, randInt, randItem } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import i18next from "i18next"; diff --git a/src/ui/ui-handler.ts b/src/ui/ui-handler.ts index c7b25c90205..7dde6b22dcd 100644 --- a/src/ui/ui-handler.ts +++ b/src/ui/ui-handler.ts @@ -1,7 +1,7 @@ import { globalScene } from "#app/global-scene"; import type { Button } from "#enums/buttons"; +import type { TextStyle } from "#enums/text-style"; import type { UiMode } from "#enums/ui-mode"; -import type { TextStyle } from "#ui/text"; import { getTextColor } from "#ui/text"; /** diff --git a/src/ui/ui.ts b/src/ui/ui.ts index e9798e6350d..4c8f0613122 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -2,6 +2,7 @@ import { globalScene } from "#app/global-scene"; import type { Button } from "#enums/buttons"; import { Device } from "#enums/devices"; import { PlayerGender } from "#enums/player-gender"; +import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; import { AchvBar } from "#ui/achv-bar"; import { AchvsUiHandler } from "#ui/achvs-ui-handler"; @@ -51,7 +52,7 @@ import { StarterSelectUiHandler } from "#ui/starter-select-ui-handler"; import { SummaryUiHandler } from "#ui/summary-ui-handler"; import { TargetSelectUiHandler } from "#ui/target-select-ui-handler"; import { TestDialogueUiHandler } from "#ui/test-dialogue-ui-handler"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import { TitleUiHandler } from "#ui/title-ui-handler"; import type { UiHandler } from "#ui/ui-handler"; import { addWindow } from "#ui/ui-theme"; diff --git a/src/ui/unavailable-modal-ui-handler.ts b/src/ui/unavailable-modal-ui-handler.ts index 420a47664a7..5c3dc513473 100644 --- a/src/ui/unavailable-modal-ui-handler.ts +++ b/src/ui/unavailable-modal-ui-handler.ts @@ -1,9 +1,10 @@ import { updateUserInfo } from "#app/account"; import { globalScene } from "#app/global-scene"; +import { TextStyle } from "#enums/text-style"; import type { UiMode } from "#enums/ui-mode"; import type { ModalConfig } from "#ui/modal-ui-handler"; import { ModalUiHandler } from "#ui/modal-ui-handler"; -import { addTextObject, TextStyle } from "#ui/text"; +import { addTextObject } from "#ui/text"; import { sessionIdKey } from "#utils/common"; import { removeCookie } from "#utils/cookies"; import i18next from "i18next"; From dc8cce53c8d7554a70ad08d03e70122d937485ee Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Sun, 27 Jul 2025 16:16:04 -0400 Subject: [PATCH 24/59] [Test] Added custom equality matchers (#6157) * Added rudimentary test matchers for `toEqualArrayUnsorted` and `toHaveTypes` Might port the rest at a later date * Actually run the effing setup matchers file + fixed ls lint to not be angy * added dev dep * Update .ls-lint.yml * Update .ls-lint.yml --------- Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Co-authored-by: Amani H. <109637146+xsn34kzx@users.noreply.github.com> --- .ls-lint.yml | 4 +- package.json | 1 + pnpm-lock.yaml | 3 + test/@types/vitest.d.ts | 26 ++++++++ test/matchers.setup.ts | 14 ++++ .../matchers/to-equal-array-unsorted.ts | 43 +++++++++++++ test/test-utils/matchers/to-have-types.ts | 64 +++++++++++++++++++ vitest.config.ts | 2 +- 8 files changed, 154 insertions(+), 3 deletions(-) create mode 100644 test/@types/vitest.d.ts create mode 100644 test/matchers.setup.ts create mode 100644 test/test-utils/matchers/to-equal-array-unsorted.ts create mode 100644 test/test-utils/matchers/to-have-types.ts diff --git a/.ls-lint.yml b/.ls-lint.yml index 610218f8ed2..22f08f72938 100644 --- a/.ls-lint.yml +++ b/.ls-lint.yml @@ -11,14 +11,14 @@ _cfg: &cfg ls: <<: *cfg - src: + src: &src <<: *cfg .dir: kebab-case | regex:@types .js: exists:0 src/system/version-migration/versions: .ts: snake_case <<: *cfg - + test: *src ignore: - node_modules - .vscode diff --git a/package.json b/package.json index 9bba5e56f89..02d063389b7 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@types/jsdom": "^21.1.7", "@types/node": "^22.16.3", "@vitest/coverage-istanbul": "^3.2.4", + "@vitest/expect": "^3.2.4", "chalk": "^5.4.1", "dependency-cruiser": "^16.10.4", "inquirer": "^12.7.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e77bf065fd5..86a7bb904a1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,6 +57,9 @@ importers: '@vitest/coverage-istanbul': specifier: ^3.2.4 version: 3.2.4(vitest@3.2.4(@types/node@22.16.3)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.3)(typescript@5.8.3))(yaml@2.8.0)) + '@vitest/expect': + specifier: ^3.2.4 + version: 3.2.4 chalk: specifier: ^5.4.1 version: 5.4.1 diff --git a/test/@types/vitest.d.ts b/test/@types/vitest.d.ts new file mode 100644 index 00000000000..3a0a0fec9cf --- /dev/null +++ b/test/@types/vitest.d.ts @@ -0,0 +1,26 @@ +import type { Pokemon } from "#field/pokemon"; +import type { PokemonType } from "#enums/pokemon-type"; +import type { expect } from "vitest"; +import { toHaveTypesOptions } from "#test/test-utils/matchers/to-have-types"; + +declare module "vitest" { + interface Assertion { + /** + * Matcher to check if an array contains EXACTLY the given items (in any order). + * + * Different from {@linkcode expect.arrayContaining} as the latter only requires the array contain + * _at least_ the listed items. + * + * @param expected - The expected contents of the array, in any order. + * @see {@linkcode expect.arrayContaining} + */ + toEqualArrayUnsorted(expected: E[]): void; + /** + * Matcher to check if a {@linkcode Pokemon}'s current typing includes the given types. + * + * @param expected - The expected types (in any order). + * @param options - The options passed to the matcher. + */ + toHaveTypes(expected: PokemonType[], options?: toHaveTypesOptions): void; + } +} \ No newline at end of file diff --git a/test/matchers.setup.ts b/test/matchers.setup.ts new file mode 100644 index 00000000000..7fd5fcb463e --- /dev/null +++ b/test/matchers.setup.ts @@ -0,0 +1,14 @@ +import { toEqualArrayUnsorted } from "#test/test-utils/matchers/to-equal-array-unsorted"; +import { toHaveTypes } from "#test/test-utils/matchers/to-have-types"; +import { expect } from "vitest"; + +/** + * @module + * Setup file for custom matchers. + * Make sure to define the call signatures in `test/@types/vitest.d.ts` too! + */ + +expect.extend({ + toEqualArrayUnsorted, + toHaveTypes, +}); diff --git a/test/test-utils/matchers/to-equal-array-unsorted.ts b/test/test-utils/matchers/to-equal-array-unsorted.ts new file mode 100644 index 00000000000..0627623bbd9 --- /dev/null +++ b/test/test-utils/matchers/to-equal-array-unsorted.ts @@ -0,0 +1,43 @@ +import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; + +/** + * Matcher to check if an array contains exactly the given items, disregarding order. + * @param received - The object to check. Should be an array of elements. + * @returns The result of the matching + */ +export function toEqualArrayUnsorted(this: MatcherState, received: unknown, expected: unknown): SyncExpectationResult { + if (!Array.isArray(received)) { + return { + pass: this.isNot, + message: () => `Expected an array, but got ${this.utils.stringify(received)}!`, + }; + } + + if (!Array.isArray(expected)) { + return { + pass: this.isNot, + message: () => `Expected to recieve an array, but got ${this.utils.stringify(expected)}!`, + }; + } + + if (received.length !== expected.length) { + return { + pass: this.isNot, + message: () => `Expected to recieve array of length ${received.length}, but got ${expected.length}!`, + actual: received, + expected, + }; + } + + const gotSorted = received.slice().sort(); + const wantSorted = expected.slice().sort(); + const pass = this.equals(gotSorted, wantSorted, [...this.customTesters, this.utils.iterableEquality]); + + return { + pass: this.isNot !== pass, + message: () => + `Expected ${this.utils.stringify(received)} to exactly equal ${this.utils.stringify(expected)} without order!`, + actual: gotSorted, + expected: wantSorted, + }; +} diff --git a/test/test-utils/matchers/to-have-types.ts b/test/test-utils/matchers/to-have-types.ts new file mode 100644 index 00000000000..d09f4fc5f76 --- /dev/null +++ b/test/test-utils/matchers/to-have-types.ts @@ -0,0 +1,64 @@ +import { PokemonType } from "#enums/pokemon-type"; +import { Pokemon } from "#field/pokemon"; +import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; + +export interface toHaveTypesOptions { + /** + * Whether to enforce exact matches (`true`) or superset matches (`false`). + * @defaultValue `true` + */ + exact?: boolean; + /** + * Optional arguments to pass to {@linkcode Pokemon.getTypes}. + */ + args?: Parameters<(typeof Pokemon.prototype)["getTypes"]>; +} + +/** + * Matcher to check if an array contains exactly the given items, disregarding order. + * @param received - The object to check. Should be an array of one or more {@linkcode PokemonType}s. + * @param options - The {@linkcode toHaveTypesOptions | options} for this matcher + * @returns The result of the matching + */ +export function toHaveTypes( + this: MatcherState, + received: unknown, + expected: unknown, + options: toHaveTypesOptions = {}, +): SyncExpectationResult { + if (!(received instanceof Pokemon)) { + return { + pass: this.isNot, + message: () => `Expected a Pokemon, but got ${this.utils.stringify(received)}!`, + }; + } + + if (!Array.isArray(expected) || expected.length === 0) { + return { + pass: this.isNot, + message: () => `Expected to recieve an array with length >=1, but got ${this.utils.stringify(expected)}!`, + }; + } + + if (!expected.every((t): t is PokemonType => t in PokemonType)) { + return { + pass: this.isNot, + message: () => `Expected to recieve array of PokemonTypes but got ${this.utils.stringify(expected)}!`, + }; + } + + const gotSorted = pkmnTypeToStr(received.getTypes(...(options.args ?? []))); + const wantSorted = pkmnTypeToStr(expected.slice()); + const pass = this.equals(gotSorted, wantSorted, [...this.customTesters, this.utils.iterableEquality]); + + return { + pass: this.isNot !== pass, + message: () => `Expected ${received.name} to have types ${this.utils.stringify(wantSorted)}, but got ${gotSorted}!`, + actual: gotSorted, + expected: wantSorted, + }; +} + +function pkmnTypeToStr(p: PokemonType[]): string[] { + return p.sort().map(type => PokemonType[type]); +} diff --git a/vitest.config.ts b/vitest.config.ts index e788302857b..65c5427e591 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -9,7 +9,7 @@ export default defineProject(({ mode }) => ({ TZ: "UTC", }, testTimeout: 20000, - setupFiles: ["./test/font-face.setup.ts", "./test/vitest.setup.ts"], + setupFiles: ["./test/font-face.setup.ts", "./test/vitest.setup.ts", "./test/matchers.setup.ts"], sequence: { sequencer: MySequencer, }, From dc5ad5450673a8409ef493925cfd55615c5f497c Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sun, 27 Jul 2025 13:32:47 -0700 Subject: [PATCH 25/59] [Misc] Fix import in `vitest.d.ts` --- test/@types/vitest.d.ts | 2 +- test/matchers.setup.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/test/@types/vitest.d.ts b/test/@types/vitest.d.ts index 3a0a0fec9cf..58b36580727 100644 --- a/test/@types/vitest.d.ts +++ b/test/@types/vitest.d.ts @@ -1,7 +1,7 @@ import type { Pokemon } from "#field/pokemon"; import type { PokemonType } from "#enums/pokemon-type"; import type { expect } from "vitest"; -import { toHaveTypesOptions } from "#test/test-utils/matchers/to-have-types"; +import type { toHaveTypesOptions } from "#test/test-utils/matchers/to-have-types"; declare module "vitest" { interface Assertion { diff --git a/test/matchers.setup.ts b/test/matchers.setup.ts index 7fd5fcb463e..03d9dd342e4 100644 --- a/test/matchers.setup.ts +++ b/test/matchers.setup.ts @@ -2,8 +2,7 @@ import { toEqualArrayUnsorted } from "#test/test-utils/matchers/to-equal-array-u import { toHaveTypes } from "#test/test-utils/matchers/to-have-types"; import { expect } from "vitest"; -/** - * @module +/* * Setup file for custom matchers. * Make sure to define the call signatures in `test/@types/vitest.d.ts` too! */ From 379bd51ed3e3f0bc1a46a572469bebec0949a26d Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Sun, 27 Jul 2025 16:42:57 -0400 Subject: [PATCH 26/59] [Dev] Turned on `checkJs` in TSConfig; fixed script type errors (#6115) --- biome.jsonc | 7 ++++--- scripts/create-test/create-test.js | 24 +++++++++++++----------- tsconfig.json | 1 + 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/biome.jsonc b/biome.jsonc index d4cb67d33a6..470885a543d 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -177,9 +177,10 @@ } }, - // Overrides to prevent unused import removal inside `overrides.ts` and enums files (for TSDoc linkcodes) + // 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). { - "includes": ["**/src/overrides.ts", "**/src/enums/**/*"], + "includes": ["**/src/overrides.ts", "**/src/enums/**/*", "**/scripts/**/*.ts"], "linter": { "rules": { "correctness": { @@ -189,7 +190,7 @@ } }, { - "includes": ["**/src/overrides.ts"], + "includes": ["**/src/overrides.ts", "**/scripts/**/*.ts"], "linter": { "rules": { "style": { diff --git a/scripts/create-test/create-test.js b/scripts/create-test/create-test.js index f24aac548fc..c0f27f8891e 100644 --- a/scripts/create-test/create-test.js +++ b/scripts/create-test/create-test.js @@ -44,20 +44,22 @@ function getTestFolderPath(...folders) { * @returns {Promise<{selectedOption: {label: string, dir: string}}>} the selected type */ async function promptTestType() { - const typeAnswer = await inquirer.prompt([ - { - type: "list", - name: "selectedOption", - message: "What type of test would you like to create?", - choices: [...choices.map(choice => ({ name: choice.label, value: choice })), "EXIT"], - }, - ]); + const typeAnswer = await inquirer + .prompt([ + { + type: "list", + name: "selectedOption", + message: "What type of test would you like to create?", + choices: [...choices.map(choice => ({ name: choice.label, value: choice })), { name: "EXIT", value: "N/A" }], + }, + ]) + .then(ans => ans.selectedOption); - if (typeAnswer.selectedOption === "EXIT") { + if (typeAnswer.name === "EXIT") { console.log("Exiting..."); - return process.exit(); + return process.exit(0); } - if (!choices.some(choice => choice.dir === typeAnswer.selectedOption.dir)) { + if (!choices.some(choice => choice.dir === typeAnswer.dir)) { console.error(`Please provide a valid type: (${choices.map(choice => choice.label).join(", ")})!`); return await promptTestType(); } diff --git a/tsconfig.json b/tsconfig.json index 8c53625ce55..1e067dcff03 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,6 +18,7 @@ "esModuleInterop": true, "strictNullChecks": true, "sourceMap": false, + "checkJs": true, "strict": false, // TODO: Enable this eventually "rootDir": ".", "baseUrl": "./src", From bc8d4947baf6a3be8c60bfcd77a0f83fc29907ac Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Sun, 27 Jul 2025 16:59:03 -0400 Subject: [PATCH 27/59] [Misc] Moved + cleaned up string manipulation functions (#6112) * Added string utility package to replace util functions * Changed string manipulation functions fully over to `change-case` * Fixed missing comma in package.json trailing commas when :( * fixed lockfile * Hopefully re-added all the string utils * fixed package json * Fixed remaining cases of regex + code dupliation * Fixed more bugs and errors * Moved around functions and hopefully fixed the regex issues * Minor renaming * Fixed incorrect casing on setting strings pascal snake case :skull: * ran biome --- src/data/balance/egg-moves.ts | 4 +- src/data/battle-anims.ts | 15 +- src/data/challenge.ts | 99 +++------- src/data/dialogue.ts | 4 +- src/data/moves/move.ts | 5 +- .../mystery-encounters/mystery-encounter.ts | 3 +- src/data/nature.ts | 4 +- src/data/pokemon-species.ts | 21 +- src/data/trainer-names.ts | 4 +- src/data/trainers/trainer-config.ts | 14 +- src/field/trainer.ts | 7 +- src/plugins/i18n.ts | 8 +- src/system/game-data.ts | 4 +- src/ui/admin-ui-handler.ts | 6 +- src/ui/arena-flyout.ts | 7 +- src/ui/bgm-bar.ts | 4 +- src/ui/game-stats-ui-handler.ts | 9 +- src/ui/party-ui-handler.ts | 5 +- src/ui/pokedex-page-ui-handler.ts | 12 +- .../abstract-control-settings-ui-handler.ts | 14 +- .../settings/settings-keyboard-ui-handler.ts | 5 +- src/ui/starter-select-ui-handler.ts | 4 +- src/ui/summary-ui-handler.ts | 6 +- src/utils/common.ts | 105 +--------- src/utils/strings.ts | 179 ++++++++++++++++++ test/setting-menu/helpers/in-game-manip.ts | 14 +- test/setting-menu/helpers/menu-manip.ts | 1 + test/test-utils/helpers/move-helper.ts | 15 +- test/utils/strings.test.ts | 47 +++++ 29 files changed, 342 insertions(+), 283 deletions(-) create mode 100644 src/utils/strings.ts create mode 100644 test/utils/strings.test.ts diff --git a/src/data/balance/egg-moves.ts b/src/data/balance/egg-moves.ts index f5026abe2ef..3475fe4fdea 100644 --- a/src/data/balance/egg-moves.ts +++ b/src/data/balance/egg-moves.ts @@ -1,8 +1,8 @@ import { allMoves } from "#data/data-lists"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; -import { toReadableString } from "#utils/common"; import { getEnumKeys, getEnumValues } from "#utils/enums"; +import { toTitleCase } from "#utils/strings"; export const speciesEggMoves = { [SpeciesId.BULBASAUR]: [ MoveId.SAPPY_SEED, MoveId.MALIGNANT_CHAIN, MoveId.EARTH_POWER, MoveId.MATCHA_GOTCHA ], @@ -617,7 +617,7 @@ function parseEggMoves(content: string): void { } if (eggMoves.every(m => m === MoveId.NONE)) { - console.warn(`Species ${toReadableString(SpeciesId[species])} could not be parsed, excluding from output...`) + console.warn(`Species ${toTitleCase(SpeciesId[species])} could not be parsed, excluding from output...`) } else { output += `[SpeciesId.${SpeciesId[species]}]: [ ${eggMoves.map(m => `MoveId.${MoveId[m]}`).join(", ")} ],\n`; } diff --git a/src/data/battle-anims.ts b/src/data/battle-anims.ts index 0dc8bf4850c..55a3cc4e916 100644 --- a/src/data/battle-anims.ts +++ b/src/data/battle-anims.ts @@ -7,8 +7,9 @@ import { AnimBlendType, AnimFocus, AnimFrameTarget, ChargeAnim, CommonAnim } fro import { MoveFlags } from "#enums/move-flags"; import { MoveId } from "#enums/move-id"; import type { Pokemon } from "#field/pokemon"; -import { animationFileName, coerceArray, getFrameMs, isNullOrUndefined, type nil } from "#utils/common"; +import { coerceArray, getFrameMs, isNullOrUndefined, type nil } from "#utils/common"; import { getEnumKeys, getEnumValues } from "#utils/enums"; +import { toKebabCase } from "#utils/strings"; import Phaser from "phaser"; export class AnimConfig { @@ -412,7 +413,7 @@ export function initCommonAnims(): Promise { const commonAnimId = commonAnimIds[ca]; commonAnimFetches.push( globalScene - .cachedFetch(`./battle-anims/common-${commonAnimNames[ca].toLowerCase().replace(/_/g, "-")}.json`) + .cachedFetch(`./battle-anims/common-${toKebabCase(commonAnimNames[ca])}.json`) .then(response => response.json()) .then(cas => commonAnims.set(commonAnimId, new AnimConfig(cas))), ); @@ -450,7 +451,7 @@ export function initMoveAnim(move: MoveId): Promise { const fetchAnimAndResolve = (move: MoveId) => { globalScene - .cachedFetch(`./battle-anims/${animationFileName(move)}.json`) + .cachedFetch(`./battle-anims/${toKebabCase(MoveId[move])}.json`) .then(response => { const contentType = response.headers.get("content-type"); if (!response.ok || contentType?.indexOf("application/json") === -1) { @@ -506,7 +507,7 @@ function useDefaultAnim(move: MoveId, defaultMoveAnim: MoveId) { * @remarks use {@linkcode useDefaultAnim} to use a default animation */ function logMissingMoveAnim(move: MoveId, ...optionalParams: any[]) { - const moveName = animationFileName(move); + const moveName = toKebabCase(MoveId[move]); console.warn(`Could not load animation file for move '${moveName}'`, ...optionalParams); } @@ -524,7 +525,7 @@ export async function initEncounterAnims(encounterAnim: EncounterAnim | Encounte } encounterAnimFetches.push( globalScene - .cachedFetch(`./battle-anims/encounter-${encounterAnimNames[anim].toLowerCase().replace(/_/g, "-")}.json`) + .cachedFetch(`./battle-anims/encounter-${toKebabCase(encounterAnimNames[anim])}.json`) .then(response => response.json()) .then(cas => encounterAnims.set(anim, new AnimConfig(cas))), ); @@ -548,7 +549,7 @@ export function initMoveChargeAnim(chargeAnim: ChargeAnim): Promise { } else { chargeAnims.set(chargeAnim, null); globalScene - .cachedFetch(`./battle-anims/${ChargeAnim[chargeAnim].toLowerCase().replace(/_/g, "-")}.json`) + .cachedFetch(`./battle-anims/${toKebabCase(ChargeAnim[chargeAnim])}.json`) .then(response => response.json()) .then(ca => { if (Array.isArray(ca)) { @@ -1405,7 +1406,9 @@ export async function populateAnims() { const chargeAnimIds = getEnumValues(ChargeAnim); const commonNamePattern = /name: (?:Common:)?(Opp )?(.*)/; const moveNameToId = {}; + // Exclude MoveId.NONE; for (const move of getEnumValues(MoveId).slice(1)) { + // KARATE_CHOP => KARATECHOP const moveName = MoveId[move].toUpperCase().replace(/_/g, ""); moveNameToId[moveName] = move; } diff --git a/src/data/challenge.ts b/src/data/challenge.ts index 938ee482d01..db4ec931bc2 100644 --- a/src/data/challenge.ts +++ b/src/data/challenge.ts @@ -27,6 +27,7 @@ import type { DexAttrProps, GameData } from "#system/game-data"; import { BooleanHolder, type NumberHolder, randSeedItem } from "#utils/common"; import { deepCopy } from "#utils/data"; import { getPokemonSpecies } from "#utils/pokemon-utils"; +import { toCamelCase, toSnakeCase } from "#utils/strings"; import i18next from "i18next"; /** A constant for the default max cost of the starting party before a run */ @@ -67,14 +68,11 @@ export abstract class Challenge { } /** - * Gets the localisation key for the challenge - * @returns {@link string} The i18n key for this challenge + * Gets the localization key for the challenge + * @returns The i18n key for this challenge as camel case. */ geti18nKey(): string { - return Challenges[this.id] - .split("_") - .map((f, i) => (i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase())) - .join(""); + return toCamelCase(Challenges[this.id]); } /** @@ -105,23 +103,22 @@ export abstract class Challenge { } /** - * Returns the textual representation of a challenge's current value. - * @param overrideValue {@link number} The value to check for. If undefined, gets the current value. - * @returns {@link string} The localised name for the current value. + * Return the textual representation of a challenge's current value. + * @param overrideValue - The value to check for; default {@linkcode this.value} + * @returns The localised text for the current value. */ - getValue(overrideValue?: number): string { - const value = overrideValue ?? this.value; - return i18next.t(`challenges:${this.geti18nKey()}.value.${value}`); + getValue(overrideValue: number = this.value): string { + return i18next.t(`challenges:${this.geti18nKey()}.value.${overrideValue}`); } /** - * Returns the description of a challenge's current value. - * @param overrideValue {@link number} The value to check for. If undefined, gets the current value. - * @returns {@link string} The localised description for the current value. + * Return the description of a challenge's current value. + * @param overrideValue - The value to check for; default {@linkcode this.value} + * @returns The localised description for the current value. */ - getDescription(overrideValue?: number): string { - const value = overrideValue ?? this.value; - return `${i18next.t([`challenges:${this.geti18nKey()}.desc.${value}`, `challenges:${this.geti18nKey()}.desc`])}`; + // TODO: Do we need an override value here? it's currently unused + getDescription(overrideValue: number = this.value): string { + return `${i18next.t([`challenges:${this.geti18nKey()}.desc.${overrideValue}`, `challenges:${this.geti18nKey()}.desc`])}`; } /** @@ -579,31 +576,19 @@ export class SingleGenerationChallenge extends Challenge { return this.value > 0 ? 1 : 0; } - /** - * Returns the textual representation of a challenge's current value. - * @param {value} overrideValue The value to check for. If undefined, gets the current value. - * @returns {string} The localised name for the current value. - */ - getValue(overrideValue?: number): string { - const value = overrideValue ?? this.value; - if (value === 0) { + getValue(overrideValue: number = this.value): string { + if (overrideValue === 0) { return i18next.t("settings:off"); } - return i18next.t(`starterSelectUiHandler:gen${value}`); + return i18next.t(`starterSelectUiHandler:gen${overrideValue}`); } - /** - * Returns the description of a challenge's current value. - * @param {value} overrideValue The value to check for. If undefined, gets the current value. - * @returns {string} The localised description for the current value. - */ - getDescription(overrideValue?: number): string { - const value = overrideValue ?? this.value; - if (value === 0) { + getDescription(overrideValue: number = this.value): string { + if (overrideValue === 0) { return i18next.t("challenges:singleGeneration.desc_default"); } return i18next.t("challenges:singleGeneration.desc", { - gen: i18next.t(`challenges:singleGeneration.gen_${value}`), + gen: i18next.t(`challenges:singleGeneration.gen_${overrideValue}`), }); } @@ -671,29 +656,13 @@ export class SingleTypeChallenge extends Challenge { return this.value > 0 ? 1 : 0; } - /** - * Returns the textual representation of a challenge's current value. - * @param {value} overrideValue The value to check for. If undefined, gets the current value. - * @returns {string} The localised name for the current value. - */ - getValue(overrideValue?: number): string { - if (overrideValue === undefined) { - overrideValue = this.value; - } - return PokemonType[this.value - 1].toLowerCase(); + getValue(overrideValue: number = this.value): string { + return toSnakeCase(PokemonType[overrideValue - 1]); } - /** - * Returns the description of a challenge's current value. - * @param {value} overrideValue The value to check for. If undefined, gets the current value. - * @returns {string} The localised description for the current value. - */ - getDescription(overrideValue?: number): string { - if (overrideValue === undefined) { - overrideValue = this.value; - } - const type = i18next.t(`pokemonInfo:Type.${PokemonType[this.value - 1]}`); - const typeColor = `[color=${TypeColor[PokemonType[this.value - 1]]}][shadow=${TypeShadow[PokemonType[this.value - 1]]}]${type}[/shadow][/color]`; + getDescription(overrideValue: number = this.value): string { + const type = i18next.t(`pokemonInfo:Type.${PokemonType[overrideValue - 1]}`); + const typeColor = `[color=${TypeColor[PokemonType[overrideValue - 1]]}][shadow=${TypeShadow[PokemonType[this.value - 1]]}]${type}[/shadow][/color]`; const defaultDesc = i18next.t("challenges:singleType.desc_default"); const typeDesc = i18next.t("challenges:singleType.desc", { type: typeColor, @@ -832,13 +801,7 @@ export class LowerStarterMaxCostChallenge extends Challenge { super(Challenges.LOWER_MAX_STARTER_COST, 9); } - /** - * @override - */ - getValue(overrideValue?: number): string { - if (overrideValue === undefined) { - overrideValue = this.value; - } + getValue(overrideValue: number = this.value): string { return (DEFAULT_PARTY_MAX_COST - overrideValue).toString(); } @@ -866,13 +829,7 @@ export class LowerStarterPointsChallenge extends Challenge { super(Challenges.LOWER_STARTER_POINTS, 9); } - /** - * @override - */ - getValue(overrideValue?: number): string { - if (overrideValue === undefined) { - overrideValue = this.value; - } + getValue(overrideValue: number = this.value): string { return (DEFAULT_PARTY_MAX_COST - overrideValue).toString(); } diff --git a/src/data/dialogue.ts b/src/data/dialogue.ts index 406e72ee82b..361d005e83b 100644 --- a/src/data/dialogue.ts +++ b/src/data/dialogue.ts @@ -1,6 +1,7 @@ import { BattleSpec } from "#enums/battle-spec"; import { TrainerType } from "#enums/trainer-type"; import { trainerConfigs } from "#trainers/trainer-config"; +import { capitalizeFirstLetter } from "#utils/strings"; export interface TrainerTypeMessages { encounter?: string | string[]; @@ -1755,8 +1756,7 @@ export function initTrainerTypeDialogue(): void { trainerConfigs[trainerType][`${messageType}Messages`] = messages[0][messageType]; } if (messages.length > 1) { - trainerConfigs[trainerType][`female${messageType.slice(0, 1).toUpperCase()}${messageType.slice(1)}Messages`] = - messages[1][messageType]; + trainerConfigs[trainerType][`female${capitalizeFirstLetter(messageType)}Messages`] = messages[1][messageType]; } } else { trainerConfigs[trainerType][`${messageType}Messages`] = messages[messageType]; diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 459566ada2f..870a32b6e2c 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -87,8 +87,9 @@ import type { AttackMoveResult } from "#types/attack-move-result"; import type { Localizable } from "#types/locales"; import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString } from "#types/move-types"; import type { TurnMove } from "#types/turn-move"; -import { BooleanHolder, type Constructor, isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue, toReadableString } from "#utils/common"; +import { BooleanHolder, type Constructor, isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common"; import { getEnumValues } from "#utils/enums"; +import { toTitleCase } from "#utils/strings"; import i18next from "i18next"; /** @@ -8109,7 +8110,7 @@ export class ResistLastMoveTypeAttr extends MoveEffectAttr { } const type = validTypes[user.randBattleSeedInt(validTypes.length)]; user.summonData.types = [ type ]; - globalScene.phaseManager.queueMessage(i18next.t("battle:transformedIntoType", { pokemonName: getPokemonNameWithAffix(user), type: toReadableString(PokemonType[type]) })); + globalScene.phaseManager.queueMessage(i18next.t("battle:transformedIntoType", { pokemonName: getPokemonNameWithAffix(user), type: toTitleCase(PokemonType[type]) })); user.updateInfo(); return true; diff --git a/src/data/mystery-encounters/mystery-encounter.ts b/src/data/mystery-encounters/mystery-encounter.ts index a2ca2b20ce7..47dfe58cace 100644 --- a/src/data/mystery-encounters/mystery-encounter.ts +++ b/src/data/mystery-encounters/mystery-encounter.ts @@ -25,7 +25,8 @@ import { StatusEffectRequirement, WaveRangeRequirement, } from "#mystery-encounters/mystery-encounter-requirements"; -import { capitalizeFirstLetter, coerceArray, isNullOrUndefined, randSeedInt } from "#utils/common"; +import { coerceArray, isNullOrUndefined, randSeedInt } from "#utils/common"; +import { capitalizeFirstLetter } from "#utils/strings"; export interface EncounterStartOfBattleEffect { sourcePokemon?: Pokemon; diff --git a/src/data/nature.ts b/src/data/nature.ts index 41764ec4276..b085faebb80 100644 --- a/src/data/nature.ts +++ b/src/data/nature.ts @@ -3,7 +3,7 @@ import { EFFECTIVE_STATS, getShortenedStatKey, Stat } from "#enums/stat"; import { TextStyle } from "#enums/text-style"; import { UiTheme } from "#enums/ui-theme"; import { getBBCodeFrag } from "#ui/text"; -import { toReadableString } from "#utils/common"; +import { toTitleCase } from "#utils/strings"; import i18next from "i18next"; export function getNatureName( @@ -13,7 +13,7 @@ export function getNatureName( ignoreBBCode = false, uiTheme: UiTheme = UiTheme.DEFAULT, ): string { - let ret = toReadableString(Nature[nature]); + let ret = toTitleCase(Nature[nature]); //Translating nature if (i18next.exists(`nature:${ret}`)) { ret = i18next.t(`nature:${ret}` as any); diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index 4e1c01642cb..dfaa6425ef1 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -29,15 +29,9 @@ import type { Variant, VariantSet } from "#sprites/variant"; import { populateVariantColorCache, variantColorCache, variantData } from "#sprites/variant"; import type { StarterMoveset } from "#system/game-data"; import type { Localizable } from "#types/locales"; -import { - capitalizeString, - isNullOrUndefined, - randSeedFloat, - randSeedGauss, - randSeedInt, - randSeedItem, -} from "#utils/common"; +import { isNullOrUndefined, randSeedFloat, randSeedGauss, randSeedInt, randSeedItem } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; +import { toCamelCase, toPascalCase } from "#utils/strings"; import { argbFromRgba, QuantizerCelebi, rgbaFromArgb } from "@material/material-color-utilities"; import i18next from "i18next"; @@ -91,6 +85,7 @@ export function getPokemonSpeciesForm(species: SpeciesId, formIndex: number): Po return retSpecies; } +// TODO: Clean this up and seriously review alternate means of fusion naming export function getFusedSpeciesName(speciesAName: string, speciesBName: string): string { const fragAPattern = /([a-z]{2}.*?[aeiou(?:y$)\-']+)(.*?)$/i; const fragBPattern = /([a-z]{2}.*?[aeiou(?:y$)\-'])(.*?)$/i; @@ -904,14 +899,14 @@ export class PokemonSpecies extends PokemonSpeciesForm implements Localizable { * @returns the pokemon-form locale key for the single form name ("Alolan Form", "Eternal Flower" etc) */ getFormNameToDisplay(formIndex = 0, append = false): string { - const formKey = this.forms?.[formIndex!]?.formKey; - const formText = capitalizeString(formKey, "-", false, false) || ""; - const speciesName = capitalizeString(SpeciesId[this.speciesId], "_", true, false); + const formKey = this.forms[formIndex]?.formKey ?? ""; + const formText = toPascalCase(formKey); + const speciesName = toCamelCase(SpeciesId[this.speciesId]); let ret = ""; const region = this.getRegion(); if (this.speciesId === SpeciesId.ARCEUS) { - ret = i18next.t(`pokemonInfo:Type.${formText?.toUpperCase()}`); + ret = i18next.t(`pokemonInfo:Type.${formText.toUpperCase()}`); } else if ( [ SpeciesFormKey.MEGA, @@ -937,7 +932,7 @@ export class PokemonSpecies extends PokemonSpeciesForm implements Localizable { if (i18next.exists(i18key)) { ret = i18next.t(i18key); } else { - const rootSpeciesName = capitalizeString(SpeciesId[this.getRootSpeciesId()], "_", true, false); + const rootSpeciesName = toCamelCase(SpeciesId[this.getRootSpeciesId()]); const i18RootKey = `pokemonForm:${rootSpeciesName}${formText}`; ret = i18next.exists(i18RootKey) ? i18next.t(i18RootKey) : formText; } diff --git a/src/data/trainer-names.ts b/src/data/trainer-names.ts index 6b882d1ddc1..8eafd9f6404 100644 --- a/src/data/trainer-names.ts +++ b/src/data/trainer-names.ts @@ -1,12 +1,12 @@ import { TrainerType } from "#enums/trainer-type"; -import { toReadableString } from "#utils/common"; +import { toPascalSnakeCase } from "#utils/strings"; class TrainerNameConfig { public urls: string[]; public femaleUrls: string[] | null; constructor(type: TrainerType, ...urls: string[]) { - this.urls = urls.length ? urls : [toReadableString(TrainerType[type]).replace(/ /g, "_")]; + this.urls = urls.length ? urls : [toPascalSnakeCase(TrainerType[type])]; } hasGenderVariant(...femaleUrls: string[]): TrainerNameConfig { diff --git a/src/data/trainers/trainer-config.ts b/src/data/trainers/trainer-config.ts index 4e88399bec3..6b3fcf70f80 100644 --- a/src/data/trainers/trainer-config.ts +++ b/src/data/trainers/trainer-config.ts @@ -41,15 +41,9 @@ import type { TrainerConfigs, TrainerTierPools, } from "#types/trainer-funcs"; -import { - coerceArray, - isNullOrUndefined, - randSeedInt, - randSeedIntRange, - randSeedItem, - toReadableString, -} from "#utils/common"; +import { coerceArray, isNullOrUndefined, randSeedInt, randSeedIntRange, randSeedItem } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; +import { toSnakeCase, toTitleCase } from "#utils/strings"; import i18next from "i18next"; /** Minimum BST for Pokemon generated onto the Elite Four's teams */ @@ -140,7 +134,7 @@ export class TrainerConfig { constructor(trainerType: TrainerType, allowLegendaries?: boolean) { this.trainerType = trainerType; this.trainerAI = new TrainerAI(); - this.name = toReadableString(TrainerType[this.getDerivedType()]); + this.name = toTitleCase(TrainerType[this.getDerivedType()]); this.battleBgm = "battle_trainer"; this.mixedBattleBgm = "battle_trainer"; this.victoryBgm = "victory_trainer"; @@ -734,7 +728,7 @@ export class TrainerConfig { } // Localize the trainer's name by converting it to lowercase and replacing spaces with underscores. - const nameForCall = this.name.toLowerCase().replace(/\s/g, "_"); + const nameForCall = toSnakeCase(this.name); this.name = i18next.t(`trainerNames:${nameForCall}`); // Set the title to "elite_four". (this is the key in the i18n file) diff --git a/src/field/trainer.ts b/src/field/trainer.ts index 7186cc4e928..a8af298e1cf 100644 --- a/src/field/trainer.ts +++ b/src/field/trainer.ts @@ -23,6 +23,7 @@ import { } from "#trainers/trainer-party-template"; import { randSeedInt, randSeedItem, randSeedWeightedItem } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; +import { toSnakeCase } from "#utils/strings"; import i18next from "i18next"; export class Trainer extends Phaser.GameObjects.Container { @@ -170,7 +171,7 @@ export class Trainer extends Phaser.GameObjects.Container { const evilTeamTitles = ["grunt"]; if (this.name === "" && evilTeamTitles.some(t => name.toLocaleLowerCase().includes(t))) { // This is a evil team grunt so we localize it by only using the "name" as the title - title = i18next.t(`trainerClasses:${name.toLowerCase().replace(/\s/g, "_")}`); + title = i18next.t(`trainerClasses:${toSnakeCase(name)}`); console.log("Localized grunt name: " + title); // Since grunts are not named we can just return the title return title; @@ -187,7 +188,7 @@ export class Trainer extends Phaser.GameObjects.Container { } // Get the localized trainer class name from the i18n file and set it as the title. // This is used for trainer class names, not titles like "Elite Four, Champion, etc." - title = i18next.t(`trainerClasses:${name.toLowerCase().replace(/\s/g, "_")}`); + title = i18next.t(`trainerClasses:${toSnakeCase(name)}`); } // If no specific trainer slot is set. @@ -208,7 +209,7 @@ export class Trainer extends Phaser.GameObjects.Container { if (this.config.titleDouble && this.variant === TrainerVariant.DOUBLE && !this.config.doubleOnly) { title = this.config.titleDouble; - name = i18next.t(`trainerNames:${this.config.nameDouble.toLowerCase().replace(/\s/g, "_")}`); + name = i18next.t(`trainerNames:${toSnakeCase(this.config.nameDouble)}`); } console.log(title ? `${title} ${name}` : name); diff --git a/src/plugins/i18n.ts b/src/plugins/i18n.ts index 4ee4aa730fb..89946b2691b 100644 --- a/src/plugins/i18n.ts +++ b/src/plugins/i18n.ts @@ -1,5 +1,5 @@ import pkg from "#package.json"; -import { camelCaseToKebabCase } from "#utils/common"; +import { toKebabCase } from "#utils/strings"; import i18next from "i18next"; import LanguageDetector from "i18next-browser-languagedetector"; import HttpBackend from "i18next-http-backend"; @@ -194,14 +194,16 @@ export async function initI18n(): Promise { ], backend: { loadPath(lng: string, [ns]: string[]) { + // Use namespace maps where required let fileName: string; if (namespaceMap[ns]) { fileName = namespaceMap[ns]; } else if (ns.startsWith("mysteryEncounters/")) { - fileName = camelCaseToKebabCase(ns + "Dialogue"); + fileName = toKebabCase(ns + "-dialogue"); // mystery-encounters/a-trainers-test-dialogue } else { - fileName = camelCaseToKebabCase(ns); + fileName = toKebabCase(ns); } + // ex: "./locales/en/move-anims" return `./locales/${lng}/${fileName}.json?v=${pkg.version}`; }, }, diff --git a/src/system/game-data.ts b/src/system/game-data.ts index c52a5155fb4..b148ed4e2ba 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -1454,11 +1454,10 @@ export class GameData { reader.onload = (_ => { return e => { - let dataName: string; + let dataName = GameDataType[dataType].toLowerCase(); let dataStr = AES.decrypt(e.target?.result?.toString()!, saveKey).toString(enc.Utf8); // TODO: is this bang correct? let valid = false; try { - dataName = GameDataType[dataType].toLowerCase(); switch (dataType) { case GameDataType.SYSTEM: { dataStr = this.convertSystemDataStr(dataStr); @@ -1493,7 +1492,6 @@ export class GameData { const displayError = (error: string) => globalScene.ui.showText(error, null, () => globalScene.ui.showText("", 0), fixedInt(1500)); - dataName = dataName!; // tell TS compiler that dataName is defined! if (!valid) { return globalScene.ui.showText( diff --git a/src/ui/admin-ui-handler.ts b/src/ui/admin-ui-handler.ts index f136aeb5008..e577368363d 100644 --- a/src/ui/admin-ui-handler.ts +++ b/src/ui/admin-ui-handler.ts @@ -6,7 +6,7 @@ import { UiMode } from "#enums/ui-mode"; import type { InputFieldConfig } from "#ui/form-modal-ui-handler"; import { FormModalUiHandler } from "#ui/form-modal-ui-handler"; import type { ModalConfig } from "#ui/modal-ui-handler"; -import { formatText } from "#utils/common"; +import { toTitleCase } from "#utils/strings"; type AdminUiHandlerService = "discord" | "google"; type AdminUiHandlerServiceMode = "Link" | "Unlink"; @@ -21,9 +21,9 @@ export class AdminUiHandler extends FormModalUiHandler { private readonly httpUserNotFoundErrorCode: number = 404; private readonly ERR_REQUIRED_FIELD = (field: string) => { if (field === "username") { - return `${formatText(field)} is required`; + return `${toTitleCase(field)} is required`; } - return `${formatText(field)} Id is required`; + return `${toTitleCase(field)} Id is required`; }; // returns a string saying whether a username has been successfully linked/unlinked to discord/google private readonly SUCCESS_SERVICE_MODE = (service: string, mode: string) => { diff --git a/src/ui/arena-flyout.ts b/src/ui/arena-flyout.ts index 9a674719202..d2a45646690 100644 --- a/src/ui/arena-flyout.ts +++ b/src/ui/arena-flyout.ts @@ -18,7 +18,8 @@ import { BattleSceneEventType } from "#events/battle-scene"; import { addTextObject } from "#ui/text"; import { TimeOfDayWidget } from "#ui/time-of-day-widget"; import { addWindow, WindowVariant } from "#ui/ui-theme"; -import { fixedInt, formatText, toCamelCaseString } from "#utils/common"; +import { fixedInt } from "#utils/common"; +import { toCamelCase, toTitleCase } from "#utils/strings"; import type { ParseKeys } from "i18next"; import i18next from "i18next"; @@ -49,10 +50,10 @@ export function getFieldEffectText(arenaTagType: string): string { if (!arenaTagType || arenaTagType === ArenaTagType.NONE) { return arenaTagType; } - const effectName = toCamelCaseString(arenaTagType); + const effectName = toCamelCase(arenaTagType); const i18nKey = `arenaFlyout:${effectName}` as ParseKeys; const resultName = i18next.t(i18nKey); - return !resultName || resultName === i18nKey ? formatText(arenaTagType) : resultName; + return !resultName || resultName === i18nKey ? toTitleCase(arenaTagType) : resultName; } export class ArenaFlyout extends Phaser.GameObjects.Container { diff --git a/src/ui/bgm-bar.ts b/src/ui/bgm-bar.ts index fb50e444aa5..e2c6925ec30 100644 --- a/src/ui/bgm-bar.ts +++ b/src/ui/bgm-bar.ts @@ -1,7 +1,7 @@ import { globalScene } from "#app/global-scene"; import { TextStyle } from "#enums/text-style"; import { addTextObject } from "#ui/text"; -import { formatText } from "#utils/common"; +import { toTitleCase } from "#utils/strings"; import i18next from "i18next"; const hiddenX = -150; @@ -101,7 +101,7 @@ export class BgmBar extends Phaser.GameObjects.Container { getRealBgmName(bgmName: string): string { return i18next.t([`bgmName:${bgmName}`, "bgmName:missing_entries"], { - name: formatText(bgmName), + name: toTitleCase(bgmName), }); } } diff --git a/src/ui/game-stats-ui-handler.ts b/src/ui/game-stats-ui-handler.ts index fa5cd0a989a..ed66230bed7 100644 --- a/src/ui/game-stats-ui-handler.ts +++ b/src/ui/game-stats-ui-handler.ts @@ -8,7 +8,8 @@ import type { GameData } from "#system/game-data"; import { addTextObject } from "#ui/text"; import { UiHandler } from "#ui/ui-handler"; import { addWindow } from "#ui/ui-theme"; -import { formatFancyLargeNumber, getPlayTimeString, toReadableString } from "#utils/common"; +import { formatFancyLargeNumber, getPlayTimeString } from "#utils/common"; +import { toTitleCase } from "#utils/strings"; import i18next from "i18next"; import Phaser from "phaser"; @@ -502,11 +503,9 @@ export function initStatsKeys() { sourceFunc: gameData => gameData.gameStats[key].toString(), }; } - if (!(displayStats[key] as DisplayStat).label_key) { + if (!displayStats[key].label_key) { const splittableKey = key.replace(/([a-z]{2,})([A-Z]{1}(?:[^A-Z]|$))/g, "$1_$2"); - (displayStats[key] as DisplayStat).label_key = toReadableString( - `${splittableKey[0].toUpperCase()}${splittableKey.slice(1)}`, - ); + displayStats[key].label_key = toTitleCase(splittableKey); } } } diff --git a/src/ui/party-ui-handler.ts b/src/ui/party-ui-handler.ts index 2b29a564f9f..915cc76fd73 100644 --- a/src/ui/party-ui-handler.ts +++ b/src/ui/party-ui-handler.ts @@ -26,7 +26,8 @@ import { MoveInfoOverlay } from "#ui/move-info-overlay"; import { PokemonIconAnimHandler, PokemonIconAnimMode } from "#ui/pokemon-icon-anim-handler"; import { addBBCodeTextObject, addTextObject, getTextColor } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; -import { BooleanHolder, getLocalizedSpriteKey, randInt, toReadableString } from "#utils/common"; +import { BooleanHolder, getLocalizedSpriteKey, randInt } from "#utils/common"; +import { toTitleCase } from "#utils/strings"; import i18next from "i18next"; import type BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext"; @@ -1409,7 +1410,7 @@ export class PartyUiHandler extends MessageUiHandler { if (this.localizedOptions.includes(option)) { optionName = i18next.t(`partyUiHandler:${PartyOption[option]}`); } else { - optionName = toReadableString(PartyOption[option]); + optionName = toTitleCase(PartyOption[option]); } } break; diff --git a/src/ui/pokedex-page-ui-handler.ts b/src/ui/pokedex-page-ui-handler.ts index b3eb8de0f50..49ce5b64d9f 100644 --- a/src/ui/pokedex-page-ui-handler.ts +++ b/src/ui/pokedex-page-ui-handler.ts @@ -54,16 +54,10 @@ import { PokedexInfoOverlay } from "#ui/pokedex-info-overlay"; import { StatsContainer } from "#ui/stats-container"; import { addBBCodeTextObject, addTextObject, getTextColor, getTextStyleOptions } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; -import { - BooleanHolder, - getLocalizedSpriteKey, - isNullOrUndefined, - padInt, - rgbHexToRgba, - toReadableString, -} from "#utils/common"; +import { BooleanHolder, getLocalizedSpriteKey, isNullOrUndefined, padInt, rgbHexToRgba } from "#utils/common"; import { getEnumValues } from "#utils/enums"; import { getPokemonSpecies } from "#utils/pokemon-utils"; +import { toTitleCase } from "#utils/strings"; import { argbFromRgba } from "@material/material-color-utilities"; import i18next from "i18next"; import type BBCodeText from "phaser3-rex-plugins/plugins/gameobjects/tagtext/bbcodetext/BBCodeText"; @@ -2620,7 +2614,7 @@ export class PokedexPageUiHandler extends MessageUiHandler { // Setting growth rate text if (isFormCaught) { - let growthReadable = toReadableString(GrowthRate[species.growthRate]); + let growthReadable = toTitleCase(GrowthRate[species.growthRate]); const growthAux = growthReadable.replace(" ", "_"); if (i18next.exists("growth:" + growthAux)) { growthReadable = i18next.t(("growth:" + growthAux) as any); diff --git a/src/ui/settings/abstract-control-settings-ui-handler.ts b/src/ui/settings/abstract-control-settings-ui-handler.ts index 8f7de42e4db..ee9e990ee2a 100644 --- a/src/ui/settings/abstract-control-settings-ui-handler.ts +++ b/src/ui/settings/abstract-control-settings-ui-handler.ts @@ -10,6 +10,7 @@ import { ScrollBar } from "#ui/scroll-bar"; import { addTextObject } from "#ui/text"; import { UiHandler } from "#ui/ui-handler"; import { addWindow } from "#ui/ui-theme"; +import { toCamelCase } from "#utils/strings"; import i18next from "i18next"; export interface InputsIcons { @@ -88,12 +89,6 @@ export abstract class AbstractControlSettingsUiHandler extends UiHandler { return settings; } - private camelize(string: string): string { - return string - .replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => (index === 0 ? word.toLowerCase() : word.toUpperCase())) - .replace(/\s+/g, ""); - } - /** * Setup UI elements. */ @@ -210,14 +205,15 @@ export abstract class AbstractControlSettingsUiHandler extends UiHandler { settingFiltered.forEach((setting, s) => { // Convert the setting key from format 'Key_Name' to 'Key name' for display. - const settingName = setting.replace(/_/g, " "); + // TODO: IDK if this can be followed by both an underscore and a space, so leaving it as a regex matching both for now + const i18nKey = toCamelCase(setting.replace(/Alt(_| )/, "")); // Create and add a text object for the setting name to the scene. const isLock = this.settingBlacklisted.includes(this.setting[setting]); const labelStyle = isLock ? TextStyle.SETTINGS_LOCKED : TextStyle.SETTINGS_LABEL; + const isAlt = setting.includes("Alt"); let labelText: string; - const i18nKey = this.camelize(settingName.replace("Alt ", "")); - if (settingName.toLowerCase().includes("alt")) { + if (isAlt) { labelText = `${i18next.t(`settings:${i18nKey}`)}${i18next.t("settings:alt")}`; } else { labelText = i18next.t(`settings:${i18nKey}`); diff --git a/src/ui/settings/settings-keyboard-ui-handler.ts b/src/ui/settings/settings-keyboard-ui-handler.ts index ad4bf47f673..295a71abe36 100644 --- a/src/ui/settings/settings-keyboard-ui-handler.ts +++ b/src/ui/settings/settings-keyboard-ui-handler.ts @@ -15,7 +15,8 @@ import { import { AbstractControlSettingsUiHandler } from "#ui/abstract-control-settings-ui-handler"; import { NavigationManager } from "#ui/navigation-menu"; import { addTextObject } from "#ui/text"; -import { reverseValueToKeySetting, truncateString } from "#utils/common"; +import { truncateString } from "#utils/common"; +import { toPascalSnakeCase } from "#utils/strings"; import i18next from "i18next"; /** @@ -101,7 +102,7 @@ export class SettingsKeyboardUiHandler extends AbstractControlSettingsUiHandler } const cursor = this.cursor + this.scrollCursor; // Calculate the absolute cursor position. const selection = this.settingLabels[cursor].text; - const key = reverseValueToKeySetting(selection); + const key = toPascalSnakeCase(selection); const settingName = SettingKeyboard[key]; const activeConfig = this.getActiveConfig(); const success = deleteBind(this.getActiveConfig(), settingName); diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index 82e19d857c5..e8f9b5e1c38 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -69,10 +69,10 @@ import { padInt, randIntRange, rgbHexToRgba, - toReadableString, } from "#utils/common"; import type { StarterPreferences } from "#utils/data"; import { loadStarterPreferences, saveStarterPreferences } from "#utils/data"; +import { toTitleCase } from "#utils/strings"; import { argbFromRgba } from "@material/material-color-utilities"; import i18next from "i18next"; import type { GameObjects } from "phaser"; @@ -3527,7 +3527,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { this.pokemonLuckLabelText.setVisible(this.pokemonLuckText.visible); //Growth translate - let growthReadable = toReadableString(GrowthRate[species.growthRate]); + let growthReadable = toTitleCase(GrowthRate[species.growthRate]); const growthAux = growthReadable.replace(" ", "_"); if (i18next.exists("growth:" + growthAux)) { growthReadable = i18next.t(("growth:" + growthAux) as any); diff --git a/src/ui/summary-ui-handler.ts b/src/ui/summary-ui-handler.ts index c5c714f0b34..b51bdfdb157 100644 --- a/src/ui/summary-ui-handler.ts +++ b/src/ui/summary-ui-handler.ts @@ -35,9 +35,9 @@ import { isNullOrUndefined, padInt, rgbHexToRgba, - toReadableString, } from "#utils/common"; import { getEnumValues } from "#utils/enums"; +import { toTitleCase } from "#utils/strings"; import { argbFromRgba } from "@material/material-color-utilities"; import i18next from "i18next"; @@ -962,8 +962,8 @@ export class SummaryUiHandler extends UiHandler { this.passiveContainer?.descriptionText?.setVisible(false); const closeFragment = getBBCodeFrag("", TextStyle.WINDOW_ALT); - const rawNature = toReadableString(Nature[this.pokemon?.getNature()!]); // TODO: is this bang correct? - const nature = `${getBBCodeFrag(toReadableString(getNatureName(this.pokemon?.getNature()!)), TextStyle.SUMMARY_RED)}${closeFragment}`; // TODO: is this bang correct? + const rawNature = toTitleCase(Nature[this.pokemon?.getNature()!]); // TODO: is this bang correct? + const nature = `${getBBCodeFrag(toTitleCase(getNatureName(this.pokemon?.getNature()!)), TextStyle.SUMMARY_RED)}${closeFragment}`; // TODO: is this bang correct? const memoString = i18next.t("pokemonSummary:memoString", { metFragment: i18next.t( diff --git a/src/utils/common.ts b/src/utils/common.ts index e9ba3acb5e5..66a74ed2c33 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -1,6 +1,5 @@ import { pokerogueApi } from "#api/pokerogue-api"; import { MoneyFormat } from "#enums/money-format"; -import { MoveId } from "#enums/move-id"; import type { Variant } from "#sprites/variant"; import i18next from "i18next"; @@ -10,19 +9,6 @@ export const MissingTextureKey = "__MISSING"; // TODO: Draft tests for these utility functions // TODO: Break up this file -/** - * Convert a `snake_case` string in any capitalization (such as one from an enum reverse mapping) - * into a readable `Title Case` version. - * @param str - The snake case string to be converted. - * @returns The result of converting `str` into title case. - */ -export function toReadableString(str: string): string { - return str - .replace(/_/g, " ") - .split(" ") - .map(s => capitalizeFirstLetter(s.toLowerCase())) - .join(" "); -} export function randomString(length: number, seeded = false) { const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; @@ -278,7 +264,7 @@ export function formatMoney(format: MoneyFormat, amount: number) { } export function formatStat(stat: number, forHp = false): string { - return formatLargeNumber(stat, forHp ? 100000 : 1000000); + return formatLargeNumber(stat, forHp ? 100_000 : 1_000_000); } export function executeIf(condition: boolean, promiseFunc: () => Promise): Promise { @@ -359,31 +345,6 @@ export function fixedInt(value: number): number { return new FixedInt(value) as unknown as number; } -/** - * Formats a string to title case - * @param unformattedText Text to be formatted - * @returns the formatted string - */ -export function formatText(unformattedText: string): string { - const text = unformattedText.split("_"); - for (let i = 0; i < text.length; i++) { - text[i] = text[i].charAt(0).toUpperCase() + text[i].substring(1).toLowerCase(); - } - - return text.join(" "); -} - -export function toCamelCaseString(unformattedText: string): string { - if (!unformattedText) { - return ""; - } - return unformattedText - .split(/[_ ]/) - .filter(f => f) - .map((f, i) => (i ? `${f[0].toUpperCase()}${f.slice(1).toLowerCase()}` : f.toLowerCase())) - .join(""); -} - export function rgbToHsv(r: number, g: number, b: number) { const v = Math.max(r, g, b); const c = v - Math.min(r, g, b); @@ -510,41 +471,6 @@ export function truncateString(str: string, maxLength = 10) { return str; } -/** - * Convert a space-separated string into a capitalized and underscored string. - * @param input - The string to be converted. - * @returns The converted string with words capitalized and separated by underscores. - */ -export function reverseValueToKeySetting(input: string) { - // Split the input string into an array of words - const words = input.split(" "); - // Capitalize the first letter of each word and convert the rest to lowercase - const capitalizedWords = words.map((word: string) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()); - // Join the capitalized words with underscores and return the result - return capitalizedWords.join("_"); -} - -/** - * Capitalize a string. - * @param str - The string to be capitalized. - * @param sep - The separator between the words of the string. - * @param lowerFirstChar - Whether the first character of the string should be lowercase or not. - * @param returnWithSpaces - Whether the returned string should have spaces between the words or not. - * @returns The capitalized string. - */ -export function capitalizeString(str: string, sep: string, lowerFirstChar = true, returnWithSpaces = false) { - if (str) { - const splitedStr = str.toLowerCase().split(sep); - - for (let i = +lowerFirstChar; i < splitedStr?.length; i++) { - splitedStr[i] = splitedStr[i].charAt(0).toUpperCase() + splitedStr[i].substring(1); - } - - return returnWithSpaces ? splitedStr.join(" ") : splitedStr.join(""); - } - return null; -} - /** * Report whether a given value is nullish (`null`/`undefined`). * @param val - The value whose nullishness is being checked @@ -554,15 +480,6 @@ export function isNullOrUndefined(val: any): val is null | undefined { return val === null || val === undefined; } -/** - * Capitalize the first letter of a string. - * @param str - The string whose first letter is being capitalized - * @return The original string with its first letter capitalized - */ -export function capitalizeFirstLetter(str: string) { - return str.charAt(0).toUpperCase() + str.slice(1); -} - /** * This function is used in the context of a Pokémon battle game to calculate the actual integer damage value from a float result. * Many damage calculation formulas involve various parameters and result in float values. @@ -597,26 +514,6 @@ export function isBetween(num: number, min: number, max: number): boolean { return min <= num && num <= max; } -/** - * Helper method to return the animation filename for a given move - * - * @param move the move for which the animation filename is needed - */ -export function animationFileName(move: MoveId): string { - return MoveId[move].toLowerCase().replace(/_/g, "-"); -} - -/** - * Transforms a camelCase string into a kebab-case string - * @param str The camelCase string - * @returns A kebab-case string - * - * @source {@link https://stackoverflow.com/a/67243723/} - */ -export function camelCaseToKebabCase(str: string): string { - return str.replace(/[A-Z]+(?![a-z])|[A-Z]/g, (s, o) => (o ? "-" : "") + s.toLowerCase()); -} - /** Get the localized shiny descriptor for the provided variant * @param variant - The variant to get the shiny descriptor for * @returns The localized shiny descriptor diff --git a/src/utils/strings.ts b/src/utils/strings.ts new file mode 100644 index 00000000000..dfa3d7c887f --- /dev/null +++ b/src/utils/strings.ts @@ -0,0 +1,179 @@ +// TODO: Standardize file and path casing to remove the need for all these different casing methods + +// #region Split string code + +// Regexps involved with splitting words in various case formats. +// Sourced from https://www.npmjs.com/package/change-case (with slight tweaking here and there) + +/** Regex to split at word boundaries.*/ +const SPLIT_LOWER_UPPER_RE = /([\p{Ll}\d])(\p{Lu})/gu; +/** Regex to split around single-letter uppercase words.*/ +const SPLIT_UPPER_UPPER_RE = /(\p{Lu})([\p{Lu}][\p{Ll}])/gu; +/** Regexp involved with stripping non-word delimiters from the result. */ +const DELIM_STRIP_REGEXP = /[-_ ]+/giu; +// The replacement value for splits. +const SPLIT_REPLACE_VALUE = "$1\0$2"; + +/** + * Split any cased string into an array of its constituent words. + * @param string - The string to be split + * @returns The new string, delimited at each instance of one or more spaces, underscores, hyphens + * or lower-to-upper boundaries. + * @remarks + * **DO NOT USE THIS FUNCTION!** + * Exported only to allow for testing. + * @todo Consider tests into [in-source testing](https://vitest.dev/guide/in-source.html) and converting this to unexported + */ +export function splitWords(value: string): string[] { + let result = value.trim(); + result = result.replace(SPLIT_LOWER_UPPER_RE, SPLIT_REPLACE_VALUE).replace(SPLIT_UPPER_UPPER_RE, SPLIT_REPLACE_VALUE); + result = result.replace(DELIM_STRIP_REGEXP, "\0"); + + // Trim the delimiter from around the output string + return trimFromStartAndEnd(result, "\0").split(/\0/g); +} + +/** + * Helper function to remove one or more sequences of characters from either end of a string. + * @param str - The string to replace + * @param charToTrim - The string to remove + * @returns The result of removing all instances of {@linkcode charsToTrim} from either end of {@linkcode str}. + */ +function trimFromStartAndEnd(str: string, charToTrim: string): string { + let start = 0; + let end = str.length; + const blockLength = charToTrim.length; + + while (str.startsWith(charToTrim, start)) { + start += blockLength; + } + if (start - end === blockLength) { + // Occurs if the ENTIRE string is made up of charToTrim (at which point we return nothing) + return ""; + } + while (str.endsWith(charToTrim, end)) { + end -= blockLength; + } + return str.slice(start, end); +} + +// #endregion Split String code + +/** + * Capitalize the first letter of a string. + * @param str - The string whose first letter is to be capitalized + * @return The original string with its first letter capitalized. + * @example + * ```ts + * console.log(capitalizeFirstLetter("consectetur adipiscing elit")); // returns "Consectetur adipiscing elit" + * ``` + */ +export function capitalizeFirstLetter(str: string) { + return str.charAt(0).toUpperCase() + str.slice(1); +} + +/** + * Helper method to convert a string into `Title Case` (such as one used for console logs). + * @param str - The string being converted + * @returns The result of converting `str` into title case. + * @example + * ```ts + * console.log(toTitleCase("lorem ipsum dolor sit amet")); // returns "Lorem Ipsum Dolor Sit Amet" + * ``` + */ +export function toTitleCase(str: string): string { + return splitWords(str) + .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(" "); +} + +/** + * Helper method to convert a string into `camelCase` (such as one used for i18n keys). + * @param str - The string being converted + * @returns The result of converting `str` into camel case. + * @example + * ```ts + * console.log(toCamelCase("BIG_ANGRY_TRAINER")); // returns "bigAngryTrainer" + * ``` + */ +export function toCamelCase(str: string) { + return splitWords(str) + .map((word, index) => (index === 0 ? word.toLowerCase() : capitalizeFirstLetter(word))) + .join(""); +} + +/** + * Helper method to convert a string into `PascalCase`. + * @param str - The string being converted + * @returns The result of converting `str` into pascal case. + * @example + * ```ts + * console.log(toPascalCase("hi how was your day")); // returns "HiHowWasYourDay" + * ``` + * @remarks + */ +export function toPascalCase(str: string) { + return splitWords(str) + .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(""); +} + +/** + * Helper method to convert a string into `kebab-case` (such as one used for filenames). + * @param str - The string being converted + * @returns The result of converting `str` into kebab case. + * @example + * ```ts + * console.log(toKebabCase("not_kebab-caSe String")); // returns "not-kebab-case-string" + * ``` + */ +export function toKebabCase(str: string): string { + return splitWords(str) + .map(word => word.toLowerCase()) + .join("-"); +} + +/** + * Helper method to convert a string into `snake_case` (such as one used for filenames). + * @param str - The string being converted + * @returns The result of converting `str` into snake case. + * @example + * ```ts + * console.log(toSnakeCase("not-in snake_CaSe")); // returns "not_in_snake_case" + * ``` + */ +export function toSnakeCase(str: string) { + return splitWords(str) + .map(word => word.toLowerCase()) + .join("_"); +} + +/** + * Helper method to convert a string into `UPPER_SNAKE_CASE`. + * @param str - The string being converted + * @returns The result of converting `str` into upper snake case. + * @example + * ```ts + * console.log(toUpperSnakeCase("apples bananas_oranGes-PearS")); // returns "APPLES_BANANAS_ORANGES_PEARS" + * ``` + */ +export function toUpperSnakeCase(str: string) { + return splitWords(str) + .map(word => word.toUpperCase()) + .join("_"); +} + +/** + * Helper method to convert a string into `Pascal_Snake_Case`. + * @param str - The string being converted + * @returns The result of converting `str` into pascal snake case. + * @example + * ```ts + * console.log(toPascalSnakeCase("apples-bananas_oranGes Pears")); // returns "Apples_Bananas_Oranges_Pears" + * ``` + */ +export function toPascalSnakeCase(str: string) { + return splitWords(str) + .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join("_"); +} diff --git a/test/setting-menu/helpers/in-game-manip.ts b/test/setting-menu/helpers/in-game-manip.ts index acc119b2cc2..2f4350bab5c 100644 --- a/test/setting-menu/helpers/in-game-manip.ts +++ b/test/setting-menu/helpers/in-game-manip.ts @@ -1,5 +1,6 @@ import { getIconForLatestInput, getSettingNameWithKeycode } from "#inputs/config-handler"; import { SettingKeyboard } from "#system/settings-keyboard"; +import { toPascalSnakeCase } from "#utils/strings"; import { expect } from "vitest"; export class InGameManip { @@ -56,22 +57,11 @@ export class InGameManip { return this; } - normalizeSettingNameString(input) { - // Convert the input string to lower case - const lowerCasedInput = input.toLowerCase(); - - // Replace underscores with spaces, capitalize the first letter of each word, and join them back with underscores - const words = lowerCasedInput.split("_").map(word => word.charAt(0).toUpperCase() + word.slice(1)); - const result = words.join("_"); - - return result; - } - weShouldTriggerTheButton(settingName) { if (!settingName.includes("Button_")) { settingName = "Button_" + settingName; } - this.settingName = SettingKeyboard[this.normalizeSettingNameString(settingName)]; + this.settingName = SettingKeyboard[toPascalSnakeCase(settingName)]; expect(getSettingNameWithKeycode(this.config, this.keycode)).toEqual(this.settingName); return this; } diff --git a/test/setting-menu/helpers/menu-manip.ts b/test/setting-menu/helpers/menu-manip.ts index 29e096608f1..276fef2f973 100644 --- a/test/setting-menu/helpers/menu-manip.ts +++ b/test/setting-menu/helpers/menu-manip.ts @@ -29,6 +29,7 @@ export class MenuManip { this.specialCaseIcon = null; } + // TODO: Review this convertNameToButtonString(input) { // Check if the input starts with "Alt_Button" if (input.startsWith("Alt_Button")) { diff --git a/test/test-utils/helpers/move-helper.ts b/test/test-utils/helpers/move-helper.ts index 4fa14b573ab..15605edf31d 100644 --- a/test/test-utils/helpers/move-helper.ts +++ b/test/test-utils/helpers/move-helper.ts @@ -12,7 +12,8 @@ import type { CommandPhase } from "#phases/command-phase"; import type { EnemyCommandPhase } from "#phases/enemy-command-phase"; import { MoveEffectPhase } from "#phases/move-effect-phase"; import { GameManagerHelper } from "#test/test-utils/helpers/game-manager-helper"; -import { coerceArray, toReadableString } from "#utils/common"; +import { coerceArray } from "#utils/common"; +import { toTitleCase } from "#utils/strings"; import type { MockInstance } from "vitest"; import { expect, vi } from "vitest"; @@ -66,12 +67,12 @@ export class MoveHelper extends GameManagerHelper { const movePosition = this.getMovePosition(pkmIndex, move); if (movePosition === -1) { expect.fail( - `MoveHelper.select called with move '${toReadableString(MoveId[move])}' not in moveset!` + - `\nBattler Index: ${toReadableString(BattlerIndex[pkmIndex])}` + + `MoveHelper.select called with move '${toTitleCase(MoveId[move])}' not in moveset!` + + `\nBattler Index: ${toTitleCase(BattlerIndex[pkmIndex])}` + `\nMoveset: [${this.game.scene .getPlayerParty() [pkmIndex].getMoveset() - .map(pm => toReadableString(MoveId[pm.moveId])) + .map(pm => toTitleCase(MoveId[pm.moveId])) .join(", ")}]`, ); } @@ -110,12 +111,12 @@ export class MoveHelper extends GameManagerHelper { const movePosition = this.getMovePosition(pkmIndex, move); if (movePosition === -1) { expect.fail( - `MoveHelper.selectWithTera called with move '${toReadableString(MoveId[move])}' not in moveset!` + - `\nBattler Index: ${toReadableString(BattlerIndex[pkmIndex])}` + + `MoveHelper.selectWithTera called with move '${toTitleCase(MoveId[move])}' not in moveset!` + + `\nBattler Index: ${toTitleCase(BattlerIndex[pkmIndex])}` + `\nMoveset: [${this.game.scene .getPlayerParty() [pkmIndex].getMoveset() - .map(pm => toReadableString(MoveId[pm.moveId])) + .map(pm => toTitleCase(MoveId[pm.moveId])) .join(", ")}]`, ); } diff --git a/test/utils/strings.test.ts b/test/utils/strings.test.ts new file mode 100644 index 00000000000..3d6eb235ba8 --- /dev/null +++ b/test/utils/strings.test.ts @@ -0,0 +1,47 @@ +import { splitWords } from "#utils/strings"; +import { describe, expect, it } from "vitest"; + +interface testCase { + input: string; + words: string[]; +} + +const testCases: testCase[] = [ + { + input: "Lorem ipsum dolor sit amet", + words: ["Lorem", "ipsum", "dolor", "sit", "amet"], + }, + { + input: "consectetur-adipiscing-elit", + words: ["consectetur", "adipiscing", "elit"], + }, + { + input: "sed_do_eiusmod_tempor_incididunt_ut_labore", + words: ["sed", "do", "eiusmod", "tempor", "incididunt", "ut", "labore"], + }, + { + input: "Et Dolore Magna Aliqua", + words: ["Et", "Dolore", "Magna", "Aliqua"], + }, + { + input: "BIG_ANGRY_TRAINER", + words: ["BIG", "ANGRY", "TRAINER"], + }, + { + input: "ApplesBananasOrangesAndAPear", + words: ["Apples", "Bananas", "Oranges", "And", "A", "Pear"], + }, + { + input: "mysteryEncounters/anOfferYouCantRefuse", + words: ["mystery", "Encounters/an", "Offer", "You", "Cant", "Refuse"], + }, +]; + +describe("Utils - Casing -", () => { + describe("splitWords", () => { + it.each(testCases)("should split a string into its constituent words - $input", ({ input, words }) => { + const ret = splitWords(input); + expect(ret).toEqual(words); + }); + }); +}); From 23cd816e32fb369900dcfb1e9b99c23852e4769f Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Sun, 27 Jul 2025 19:01:44 -0600 Subject: [PATCH 28/59] [Refactor] Prevent serialization of full species in pokemon summon data https://github.com/pagefaultgames/pokerogue/pull/6145 * Prevent serialization of entire species form in pokemon summon data * Apply suggestions from code review Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com> Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Apply Kev's suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --------- Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com> Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/@types/type-helpers.ts | 11 +++ src/data/pokemon/pokemon-data.ts | 132 ++++++++++++++++++++++++++++++- 2 files changed, 141 insertions(+), 2 deletions(-) diff --git a/src/@types/type-helpers.ts b/src/@types/type-helpers.ts index 3a5c88e3f15..b3e5b1cfc07 100644 --- a/src/@types/type-helpers.ts +++ b/src/@types/type-helpers.ts @@ -75,3 +75,14 @@ export type NonFunctionPropertiesRecursive = { }; export type AbstractConstructor = abstract new (...args: any[]) => T; + +/** + * Type helper that iterates through the fields of the type and coerces any `null` properties to `undefined` (including in union types). + * + * @remarks + * This is primarily useful when an object with nullable properties wants to be serialized and have its `null` + * properties coerced to `undefined`. + */ +export type CoerceNullPropertiesToUndefined = { + [K in keyof T]: null extends T[K] ? Exclude | undefined : T[K]; +}; diff --git a/src/data/pokemon/pokemon-data.ts b/src/data/pokemon/pokemon-data.ts index 6ae86bed5e7..a2a0138a17a 100644 --- a/src/data/pokemon/pokemon-data.ts +++ b/src/data/pokemon/pokemon-data.ts @@ -1,18 +1,29 @@ import { type BattlerTag, loadBattlerTag } from "#data/battler-tags"; +import { allSpecies } from "#data/data-lists"; import type { Gender } from "#data/gender"; import { PokemonMove } from "#data/moves/pokemon-move"; -import type { PokemonSpeciesForm } from "#data/pokemon-species"; +import { getPokemonSpeciesForm, type PokemonSpeciesForm } from "#data/pokemon-species"; import type { TypeDamageMultiplier } from "#data/type"; import type { AbilityId } from "#enums/ability-id"; import type { BerryType } from "#enums/berry-type"; import type { MoveId } from "#enums/move-id"; import type { Nature } from "#enums/nature"; import type { PokemonType } from "#enums/pokemon-type"; +import type { SpeciesId } from "#enums/species-id"; import type { AttackMoveResult } from "#types/attack-move-result"; import type { IllusionData } from "#types/illusion-data"; import type { TurnMove } from "#types/turn-move"; +import type { CoerceNullPropertiesToUndefined } from "#types/type-helpers"; import { isNullOrUndefined } from "#utils/common"; +/** + * The type that {@linkcode PokemonSpeciesForm} is converted to when an object containing it serializes it. + */ +type SerializedSpeciesForm = { + id: SpeciesId; + formIdx: number; +}; + /** * Permanent data that can customize a Pokemon in non-standard ways from its Species. * Includes abilities, nature, changed types, etc. @@ -41,9 +52,59 @@ export class CustomPokemonData { } } +/** + * Deserialize a pokemon species form from an object containing `id` and `formIdx` properties. + * @param value - The value to deserialize + * @returns The `PokemonSpeciesForm` or `null` if the fields could not be properly discerned + */ +function deserializePokemonSpeciesForm(value: SerializedSpeciesForm | PokemonSpeciesForm): PokemonSpeciesForm | null { + // @ts-expect-error: We may be deserializing a PokemonSpeciesForm, but we catch later on + let { id, formIdx } = value; + + if (isNullOrUndefined(id) || isNullOrUndefined(formIdx)) { + // @ts-expect-error: Typescript doesn't know that in block, `value` must be a PokemonSpeciesForm + id = value.speciesId; + // @ts-expect-error: Same as above (plus we are accessing a protected property) + formIdx = value._formIndex; + } + // If for some reason either of these fields are null/undefined, we cannot reconstruct the species form + if (isNullOrUndefined(id) || isNullOrUndefined(formIdx)) { + return null; + } + return getPokemonSpeciesForm(id, formIdx); +} + +interface SerializedIllusionData extends Omit { + /** The id of the illusioned fusion species, or `undefined` if not a fusion */ + fusionSpecies?: SpeciesId; +} + +interface SerializedPokemonSummonData { + statStages: number[]; + moveQueue: TurnMove[]; + tags: BattlerTag[]; + abilitySuppressed: boolean; + speciesForm?: SerializedSpeciesForm; + fusionSpeciesForm?: SerializedSpeciesForm; + ability?: AbilityId; + passiveAbility?: AbilityId; + gender?: Gender; + fusionGender?: Gender; + stats: number[]; + moveset?: PokemonMove[]; + types: PokemonType[]; + addedType?: PokemonType; + illusion?: SerializedIllusionData; + illusionBroken: boolean; + berriesEatenLast: BerryType[]; + moveHistory: TurnMove[]; +} + /** * Persistent in-battle data for a {@linkcode Pokemon}. * Resets on switch or new battle. + * + * @sealed */ export class PokemonSummonData { /** [Atk, Def, SpAtk, SpDef, Spd, Acc, Eva] */ @@ -86,7 +147,7 @@ export class PokemonSummonData { */ public moveHistory: TurnMove[] = []; - constructor(source?: PokemonSummonData | Partial) { + constructor(source?: PokemonSummonData | SerializedPokemonSummonData) { if (isNullOrUndefined(source)) { return; } @@ -97,6 +158,30 @@ export class PokemonSummonData { continue; } + if (key === "speciesForm" || key === "fusionSpeciesForm") { + this[key] = deserializePokemonSpeciesForm(value); + } + + if (key === "illusion" && typeof value === "object") { + // Make a copy so as not to mutate provided value + const illusionData = { + ...value, + }; + if (!isNullOrUndefined(illusionData.fusionSpecies)) { + switch (typeof illusionData.fusionSpecies) { + case "object": + illusionData.fusionSpecies = allSpecies[illusionData.fusionSpecies.speciesId]; + break; + case "number": + illusionData.fusionSpecies = allSpecies[illusionData.fusionSpecies]; + break; + default: + illusionData.fusionSpecies = undefined; + } + } + this[key] = illusionData as IllusionData; + } + if (key === "moveset") { this.moveset = value?.map((m: any) => PokemonMove.loadMove(m)); continue; @@ -110,6 +195,49 @@ export class PokemonSummonData { this[key] = value; } } + + /** + * Serialize this PokemonSummonData to JSON, converting {@linkcode PokemonSpeciesForm} and {@linkcode IllusionData.fusionSpecies} + * into simpler types instead of serializing all of their fields. + * + * @remarks + * - `IllusionData.fusionSpecies` is serialized as just the species ID + * - `PokemonSpeciesForm` and `PokemonSpeciesForm.fusionSpeciesForm` are converted into {@linkcode SerializedSpeciesForm} objects + */ + public toJSON(): SerializedPokemonSummonData { + // Pokemon species forms are never saved, only the species ID. + const illusion = this.illusion; + const speciesForm = this.speciesForm; + const fusionSpeciesForm = this.fusionSpeciesForm; + const illusionSpeciesForm = illusion?.fusionSpecies; + const t = { + // the "as omit" is required to avoid TS resolving the overwritten properties to "never" + // We coerce null to undefined in the type, as the for loop below replaces `null` with `undefined` + ...(this as Omit< + CoerceNullPropertiesToUndefined, + "speciesForm" | "fusionSpeciesForm" | "illusion" + >), + speciesForm: isNullOrUndefined(speciesForm) + ? undefined + : { id: speciesForm.speciesId, formIdx: speciesForm.formIndex }, + fusionSpeciesForm: isNullOrUndefined(fusionSpeciesForm) + ? undefined + : { id: fusionSpeciesForm.speciesId, formIdx: fusionSpeciesForm.formIndex }, + illusion: isNullOrUndefined(illusion) + ? undefined + : { + ...(this.illusion as Omit), + fusionSpecies: illusionSpeciesForm?.speciesId, + }, + }; + // Replace `null` with `undefined`, as `undefined` never gets serialized + for (const [key, value] of Object.entries(t)) { + if (value === null) { + t[key] = undefined; + } + } + return t; + } } // TODO: Merge this inside `summmonData` but exclude from save if/when a save data serializer is added From 7842bf435fe72892a00b126d444b73e537eb5326 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sun, 27 Jul 2025 18:03:34 -0700 Subject: [PATCH 29/59] [Docs] Add `@module` modifier tag to `tsdoc.json` --- tsdoc.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tsdoc.json b/tsdoc.json index b4cbc9a62a5..689f7a96c5c 100644 --- a/tsdoc.json +++ b/tsdoc.json @@ -9,6 +9,10 @@ { "tagName": "@linkcode", "syntaxKind": "inline" + }, + { + "tagName": "@module", + "syntaxKind": "modifier" } ] } From d75836d720db26faf9dd5e7c1a8d1c32d23b8430 Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Sun, 27 Jul 2025 23:17:46 -0400 Subject: [PATCH 30/59] [Bug] Fix camel case bug in `strings.ts` (#6161) --- src/utils/strings.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/utils/strings.ts b/src/utils/strings.ts index dfa3d7c887f..bf5e5c6473f 100644 --- a/src/utils/strings.ts +++ b/src/utils/strings.ts @@ -98,7 +98,9 @@ export function toTitleCase(str: string): string { */ export function toCamelCase(str: string) { return splitWords(str) - .map((word, index) => (index === 0 ? word.toLowerCase() : capitalizeFirstLetter(word))) + .map((word, index) => + index === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(), + ) .join(""); } From 0ceafe9cfe439fba73830106274dd4df9faa6800 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sun, 27 Jul 2025 21:20:02 -0700 Subject: [PATCH 31/59] [Dev] Change `target` to `ES2023` in `tsconfig.json` (#6160) --- src/field/pokemon.ts | 5 ++--- src/field/trainer.ts | 1 - src/modifier/modifier.ts | 20 ++++++++++---------- tsconfig.json | 8 ++++---- 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 703e890e555..f42b05bd8de 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -213,7 +213,6 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * TODO: Stop treating this like a unique ID and stop treating 0 as no pokemon */ public id: number; - public name: string; public nickname: string; public species: PokemonSpecies; public formIndex: number; @@ -5665,7 +5664,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } export class PlayerPokemon extends Pokemon { - protected battleInfo: PlayerBattleInfo; + protected declare battleInfo: PlayerBattleInfo; public compatibleTms: MoveId[]; constructor( @@ -6194,7 +6193,7 @@ export class PlayerPokemon extends Pokemon { } export class EnemyPokemon extends Pokemon { - protected battleInfo: EnemyBattleInfo; + protected declare battleInfo: EnemyBattleInfo; public trainerSlot: TrainerSlot; public aiType: AiType; public bossSegments: number; diff --git a/src/field/trainer.ts b/src/field/trainer.ts index a8af298e1cf..584c9310932 100644 --- a/src/field/trainer.ts +++ b/src/field/trainer.ts @@ -30,7 +30,6 @@ export class Trainer extends Phaser.GameObjects.Container { public config: TrainerConfig; public variant: TrainerVariant; public partyTemplateIndex: number; - public name: string; public partnerName: string; public nameKey: string; public partnerNameKey: string | undefined; diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index f6a9ed48847..b31bee7fc69 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -462,7 +462,7 @@ export abstract class LapsingPersistentModifier extends PersistentModifier { * @see {@linkcode apply} */ export class DoubleBattleChanceBoosterModifier extends LapsingPersistentModifier { - public override type: DoubleBattleChanceBoosterModifierType; + public declare type: DoubleBattleChanceBoosterModifierType; match(modifier: Modifier): boolean { return modifier instanceof DoubleBattleChanceBoosterModifier && modifier.getMaxBattles() === this.getMaxBattles(); @@ -936,7 +936,7 @@ export class EvoTrackerModifier extends PokemonHeldItemModifier { * Currently used by Shuckle Juice item */ export class PokemonBaseStatTotalModifier extends PokemonHeldItemModifier { - public override type: PokemonBaseStatTotalModifierType; + public declare type: PokemonBaseStatTotalModifierType; public isTransferable = false; public statModifier: 10 | -15; @@ -2074,7 +2074,7 @@ export abstract class ConsumablePokemonModifier extends ConsumableModifier { } export class TerrastalizeModifier extends ConsumablePokemonModifier { - public override type: TerastallizeModifierType; + public declare type: TerastallizeModifierType; public teraType: PokemonType; constructor(type: TerastallizeModifierType, pokemonId: number, teraType: PokemonType) { @@ -2318,7 +2318,7 @@ export class PokemonLevelIncrementModifier extends ConsumablePokemonModifier { } export class TmModifier extends ConsumablePokemonModifier { - public override type: TmModifierType; + public declare type: TmModifierType; /** * Applies {@linkcode TmModifier} @@ -2365,7 +2365,7 @@ export class RememberMoveModifier extends ConsumablePokemonModifier { } export class EvolutionItemModifier extends ConsumablePokemonModifier { - public override type: EvolutionItemModifierType; + public declare type: EvolutionItemModifierType; /** * Applies {@linkcode EvolutionItemModifier} * @param playerPokemon The {@linkcode PlayerPokemon} that should evolve via item @@ -2530,7 +2530,7 @@ export class ExpBoosterModifier extends PersistentModifier { } export class PokemonExpBoosterModifier extends PokemonHeldItemModifier { - public override type: PokemonExpBoosterModifierType; + public declare type: PokemonExpBoosterModifierType; private boostMultiplier: number; @@ -2627,7 +2627,7 @@ export class ExpBalanceModifier extends PersistentModifier { } export class PokemonFriendshipBoosterModifier extends PokemonHeldItemModifier { - public override type: PokemonFriendshipBoosterModifierType; + public declare type: PokemonFriendshipBoosterModifierType; matchType(modifier: Modifier): boolean { return modifier instanceof PokemonFriendshipBoosterModifier; @@ -2684,7 +2684,7 @@ export class PokemonNatureWeightModifier extends PokemonHeldItemModifier { } export class PokemonMoveAccuracyBoosterModifier extends PokemonHeldItemModifier { - public override type: PokemonMoveAccuracyBoosterModifierType; + public declare type: PokemonMoveAccuracyBoosterModifierType; private accuracyAmount: number; constructor(type: PokemonMoveAccuracyBoosterModifierType, pokemonId: number, accuracy: number, stackCount?: number) { @@ -2736,7 +2736,7 @@ export class PokemonMoveAccuracyBoosterModifier extends PokemonHeldItemModifier } export class PokemonMultiHitModifier extends PokemonHeldItemModifier { - public override type: PokemonMultiHitModifierType; + public declare type: PokemonMultiHitModifierType; matchType(modifier: Modifier): boolean { return modifier instanceof PokemonMultiHitModifier; @@ -2817,7 +2817,7 @@ export class PokemonMultiHitModifier extends PokemonHeldItemModifier { } export class PokemonFormChangeItemModifier extends PokemonHeldItemModifier { - public override type: FormChangeItemModifierType; + public declare type: FormChangeItemModifierType; public formChangeItem: FormChangeItem; public active: boolean; public isTransferable = false; diff --git a/tsconfig.json b/tsconfig.json index 1e067dcff03..9aa06829789 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,11 @@ { "compilerOptions": { - "target": "ES2020", - "module": "ES2020", + "target": "ES2023", + "module": "ES2022", // Modifying this option requires all values to be set manually because the defaults get overridden - // Values other than "ES2024.Promise" taken from https://github.com/microsoft/TypeScript/blob/main/src/lib/es2020.full.d.ts + // Values other than "ES2024.Promise" taken from https://github.com/microsoft/TypeScript/blob/main/src/lib/es2023.full.d.ts "lib": [ - "ES2020", + "ES2023", "ES2024.Promise", "DOM", "DOM.AsyncIterable", From aab9a698fc287404b86984b95554c3261f2e6cd6 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Sun, 25 May 2025 17:26:37 -0500 Subject: [PATCH 32/59] breakup fight and ball commands into their own methods --- src/phases/command-phase.ts | 418 +++++++++++++++++++----------------- 1 file changed, 222 insertions(+), 196 deletions(-) diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index 14674037fbe..92c01aa4b85 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -148,206 +148,232 @@ export class CommandPhase extends FieldPhase { } /** - * TODO: Remove `args` and clean this thing up - * Code will need to be copied over from pkty except replacing the `virtual` and `ignorePP` args with a corresponding `MoveUseMode`. + * + * @param user - The pokemon using the move + * @param cursor - */ - handleCommand(command: Command, cursor: number, ...args: any[]): boolean { + private queueFightErrorMessage(user: PlayerPokemon, cursor: number) { + const move = user.getMoveset()[cursor]; + globalScene.ui.setMode(UiMode.MESSAGE); + + // Decides between a Disabled, Not Implemented, or No PP translation message + const errorMessage = user.isMoveRestricted(move.moveId, user) + ? user.getRestrictingTag(move.moveId, user)!.selectionDeniedText(user, move.moveId) + : move.getName().endsWith(" (N)") + ? "battle:moveNotImplemented" + : "battle:moveNoPP"; + const moveName = move.getName().replace(" (N)", ""); // Trims off the indicator + + globalScene.ui.showText( + i18next.t(errorMessage, { moveName: moveName }), + null, + () => { + globalScene.ui.clearText(); + globalScene.ui.setMode(UiMode.FIGHT, this.fieldIndex); + }, + null, + true, + ); + } + + /** Helper method for {@linkcode handleFightCommand} that returns the moveID for the phase + * based on the move passed in or the cursor. + * + * Does not check if the move is usable or not, that should be handled by the caller. + */ + private comptueMoveId(playerPokemon: PlayerPokemon, cursor: number, move?: TurnMove): MoveId { + return move?.move ?? (cursor > -1 ? playerPokemon.getMoveset()[cursor]?.moveId : MoveId.NONE); + } + + /** + * Handle fight logic + * @param command - The command to handle (FIGHT or TERA) + * @param cursor - The index that the cursor is placed on, or -1 if no move can be selected. + * @param args - Any additional arguments to pass to the command + */ + private handleFightCommand( + command: Command.FIGHT | Command.TERA, + cursor: number, + useMode: MoveUseMode = MoveUseMode.NORMAL, + move?: TurnMove, + ): boolean { + const playerPokemon = globalScene.getPlayerField()[this.fieldIndex]; + const ignorePP = isIgnorePP(useMode); + + /** Whether or not to display an error message instead of attempting to initiate the command selection process */ + let canUse = cursor !== -1 || !playerPokemon.trySelectMove(cursor, ignorePP); + + const useStruggle = canUse + ? false + : cursor > -1 && !playerPokemon.getMoveset().some(m => m.isUsable(playerPokemon)); + + canUse = canUse || useStruggle; + + if (!canUse) { + this.queueFightErrorMessage(playerPokemon, cursor); + return false; + } + + const moveId = useStruggle ? MoveId.STRUGGLE : this.comptueMoveId(playerPokemon, cursor, move); + + const turnCommand: TurnCommand = { + command: Command.FIGHT, + cursor: cursor, + move: { move: moveId, targets: [], useMode }, + args: [useMode, move], + }; + const preTurnCommand: TurnCommand = { + command: command, + targets: [this.fieldIndex], + skip: command === Command.FIGHT, + }; + + const moveTargets: MoveTargetSet = + move === undefined + ? getMoveTargets(playerPokemon, moveId) + : { + targets: move.targets, + multiple: move.targets.length > 1, + }; + + if (!moveId) { + turnCommand.targets = [this.fieldIndex]; + } + + console.log(moveTargets, getPokemonNameWithAffix(playerPokemon)); + + if (moveTargets.multiple) { + globalScene.phaseManager.unshiftNew("SelectTargetPhase", this.fieldIndex); + } + + if (turnCommand.move && (moveTargets.targets.length <= 1 || moveTargets.multiple)) { + turnCommand.move.targets = moveTargets.targets; + } else if ( + turnCommand.move && + playerPokemon.getTag(BattlerTagType.CHARGING) && + playerPokemon.getMoveQueue().length >= 1 + ) { + turnCommand.move.targets = playerPokemon.getMoveQueue()[0].targets; + } else { + globalScene.phaseManager.unshiftNew("SelectTargetPhase", this.fieldIndex); + } + + globalScene.currentBattle.preTurnCommands[this.fieldIndex] = preTurnCommand; + globalScene.currentBattle.turnCommands[this.fieldIndex] = turnCommand; + + return true; + } + + private queueShowText(key: string) { + globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); + globalScene.ui.setMode(UiMode.MESSAGE); + + globalScene.ui.showText( + i18next.t(key), + null, + () => { + globalScene.ui.showText("", 0); + globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); + }, + null, + true, + ); + } + + /** + * Helper method for {@linkcode handleBallCommand} that checks if the pokeball can be thrown. + * + * The pokeball may not be thrown if: + * - It is a trainer battle + * - The biome is {@linkcode Biome.END} and it is not classic mode + * - The biome is {@linkcode Biome.END} and the fresh start challenge is active + * - The biome is {@linkcode Biome.END} and the player has not either caught the target before or caught all but one starter + * - The player is in a mystery encounter that disallows catching the pokemon + * @returns Whether a pokeball can be thrown + * + */ + private checkCanUseBall(): boolean { + if ( + globalScene.arena.biomeType === BiomeId.END && + (!globalScene.gameMode.isClassic || + globalScene.gameMode.isFreshStartChallenge() || + (globalScene + .getEnemyField() + .some(p => p.isActive() && !globalScene.gameData.dexData[p.species.speciesId].caughtAttr) && + globalScene.gameData.getStarterCount(d => !!d.caughtAttr) < Object.keys(speciesStarterCosts).length - 1)) + ) { + this.queueShowText("battle:noPokeballForce"); + } else if (globalScene.currentBattle.battleType === BattleType.TRAINER) { + this.queueShowText("battle:noPokeballTrainer"); + } else if ( + globalScene.currentBattle.isBattleMysteryEncounter() && + !globalScene.currentBattle.mysteryEncounter!.catchAllowed + ) { + this.queueShowText("battle:noPokeballMysteryEncounter"); + } else { + return true; + } + + return false; + } + + /** + * Helper method for {@linkcode handleCommand} that handles the logic when the selected command is to use a pokeball. + * + * @param cursor - The index of the pokeball to use + * @returns Whether the command was successfully initiated + */ + handleBallCommand(cursor: number): boolean { + const targets = globalScene + .getEnemyField() + .filter(p => p.isActive(true)) + .map(p => p.getBattlerIndex()); + if (targets.length > 1) { + this.queueShowText("battle:noPokeballMulti"); + return false; + } + + if (!this.checkCanUseBall()) { + return false; + } + + if (cursor < 5) { + const targetPokemon = globalScene.getEnemyPokemon(); + if ( + targetPokemon?.isBoss() && + targetPokemon?.bossSegmentIndex >= 1 && + // TODO: Decouple this hardcoded exception for wonder guard and just check the target... + !targetPokemon?.hasAbility(AbilityId.WONDER_GUARD, false, true) && + cursor < PokeballType.MASTER_BALL + ) { + this.queueShowText("battle:noPokeballStrong"); + return false; + } + + globalScene.currentBattle.turnCommands[this.fieldIndex] = { + command: Command.BALL, + cursor: cursor, + }; + globalScene.currentBattle.turnCommands[this.fieldIndex]!.targets = targets; + if (this.fieldIndex) { + globalScene.currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true; + } + return true; + } + + return false; + } + + handleCommand(command: Command, cursor: number, useMode: MoveUseMode = MoveUseMode.NORMAL, move?: TurnMove): boolean { const playerPokemon = globalScene.getPlayerField()[this.fieldIndex]; let success = false; switch (command) { - // TODO: We don't need 2 args for this - moveUseMode is carried over from queuedMove case Command.TERA: - case Command.FIGHT: { - let useStruggle = false; - const turnMove: TurnMove | undefined = args.length === 2 ? (args[1] as TurnMove) : undefined; - if ( - cursor === -1 || - playerPokemon.trySelectMove(cursor, isIgnorePP(args[0] as MoveUseMode)) || - (useStruggle = cursor > -1 && !playerPokemon.getMoveset().filter(m => m.isUsable(playerPokemon)).length) - ) { - let moveId: MoveId; - if (useStruggle) { - moveId = MoveId.STRUGGLE; - } else if (turnMove !== undefined) { - moveId = turnMove.move; - } else if (cursor > -1) { - moveId = playerPokemon.getMoveset()[cursor].moveId; - } else { - moveId = MoveId.NONE; - } - - const turnCommand: TurnCommand = { - command: Command.FIGHT, - cursor: cursor, - move: { move: moveId, targets: [], useMode: args[0] }, - args: args, - }; - const preTurnCommand: TurnCommand = { - command: command, - targets: [this.fieldIndex], - skip: command === Command.FIGHT, - }; - const moveTargets: MoveTargetSet = - turnMove === undefined - ? getMoveTargets(playerPokemon, moveId) - : { - targets: turnMove.targets, - multiple: turnMove.targets.length > 1, - }; - if (!moveId) { - turnCommand.targets = [this.fieldIndex]; - } - console.log(moveTargets, getPokemonNameWithAffix(playerPokemon)); - if (moveTargets.targets.length > 1 && moveTargets.multiple) { - globalScene.phaseManager.unshiftNew("SelectTargetPhase", this.fieldIndex); - } - if (turnCommand.move && (moveTargets.targets.length <= 1 || moveTargets.multiple)) { - turnCommand.move.targets = moveTargets.targets; - } else if ( - turnCommand.move && - playerPokemon.getTag(BattlerTagType.CHARGING) && - playerPokemon.getMoveQueue().length >= 1 - ) { - turnCommand.move.targets = playerPokemon.getMoveQueue()[0].targets; - } else { - globalScene.phaseManager.unshiftNew("SelectTargetPhase", this.fieldIndex); - } - globalScene.currentBattle.preTurnCommands[this.fieldIndex] = preTurnCommand; - globalScene.currentBattle.turnCommands[this.fieldIndex] = turnCommand; - success = true; - } else if (cursor < playerPokemon.getMoveset().length) { - const move = playerPokemon.getMoveset()[cursor]; - globalScene.ui.setMode(UiMode.MESSAGE); - - // Decides between a Disabled, Not Implemented, or No PP translation message - const errorMessage = playerPokemon.isMoveRestricted(move.moveId, playerPokemon) - ? playerPokemon - .getRestrictingTag(move.moveId, playerPokemon)! - .selectionDeniedText(playerPokemon, move.moveId) - : move.getName().endsWith(" (N)") - ? "battle:moveNotImplemented" - : "battle:moveNoPP"; - const moveName = move.getName().replace(" (N)", ""); // Trims off the indicator - - globalScene.ui.showText( - i18next.t(errorMessage, { moveName: moveName }), - null, - () => { - globalScene.ui.clearText(); - globalScene.ui.setMode(UiMode.FIGHT, this.fieldIndex); - }, - null, - true, - ); - } - break; - } - case Command.BALL: { - const notInDex = - globalScene - .getEnemyField() - .filter(p => p.isActive(true)) - .some(p => !globalScene.gameData.dexData[p.species.speciesId].caughtAttr) && - globalScene.gameData.getStarterCount(d => !!d.caughtAttr) < Object.keys(speciesStarterCosts).length - 1; - if ( - globalScene.arena.biomeType === BiomeId.END && - (!globalScene.gameMode.isClassic || globalScene.gameMode.isFreshStartChallenge() || notInDex) - ) { - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - globalScene.ui.setMode(UiMode.MESSAGE); - globalScene.ui.showText( - i18next.t("battle:noPokeballForce"), - null, - () => { - globalScene.ui.showText("", 0); - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - }, - null, - true, - ); - } else if (globalScene.currentBattle.battleType === BattleType.TRAINER) { - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - globalScene.ui.setMode(UiMode.MESSAGE); - globalScene.ui.showText( - i18next.t("battle:noPokeballTrainer"), - null, - () => { - globalScene.ui.showText("", 0); - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - }, - null, - true, - ); - } else if ( - globalScene.currentBattle.isBattleMysteryEncounter() && - !globalScene.currentBattle.mysteryEncounter!.catchAllowed - ) { - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - globalScene.ui.setMode(UiMode.MESSAGE); - globalScene.ui.showText( - i18next.t("battle:noPokeballMysteryEncounter"), - null, - () => { - globalScene.ui.showText("", 0); - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - }, - null, - true, - ); - } else { - const targets = globalScene - .getEnemyField() - .filter(p => p.isActive(true)) - .map(p => p.getBattlerIndex()); - if (targets.length > 1) { - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - globalScene.ui.setMode(UiMode.MESSAGE); - globalScene.ui.showText( - i18next.t("battle:noPokeballMulti"), - null, - () => { - globalScene.ui.showText("", 0); - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - }, - null, - true, - ); - } else if (cursor < 5) { - const targetPokemon = globalScene.getEnemyField().find(p => p.isActive(true)); - if ( - targetPokemon?.isBoss() && - targetPokemon?.bossSegmentIndex >= 1 && - !targetPokemon?.hasAbility(AbilityId.WONDER_GUARD, false, true) && - cursor < PokeballType.MASTER_BALL - ) { - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - globalScene.ui.setMode(UiMode.MESSAGE); - globalScene.ui.showText( - i18next.t("battle:noPokeballStrong"), - null, - () => { - globalScene.ui.showText("", 0); - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - }, - null, - true, - ); - } else { - globalScene.currentBattle.turnCommands[this.fieldIndex] = { - command: Command.BALL, - cursor: cursor, - }; - globalScene.currentBattle.turnCommands[this.fieldIndex]!.targets = targets; - if (this.fieldIndex) { - globalScene.currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true; - } - success = true; - } - } - } - break; - } + case Command.FIGHT: + return this.handleFightCommand(command, cursor, useMode, move); + case Command.BALL: + return this.handleBallCommand(cursor); case Command.POKEMON: case Command.RUN: { const isSwitch = command === Command.POKEMON; @@ -388,11 +414,11 @@ export class CommandPhase extends FieldPhase { true, ); } else { - const batonPass = isSwitch && (args[0] as boolean); + const batonPass = isSwitch && useMode; const trappedAbMessages: string[] = []; if (batonPass || !playerPokemon.isTrapped(trappedAbMessages)) { currentBattle.turnCommands[this.fieldIndex] = isSwitch - ? { command: Command.POKEMON, cursor: cursor, args: args } + ? { command: Command.POKEMON, cursor: cursor } : { command: Command.RUN }; success = true; if (!isSwitch && this.fieldIndex) { From 29232074c70fc433788596611bec51612ea7214c Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Mon, 26 May 2025 11:46:53 -0500 Subject: [PATCH 33/59] Breakup run and pokemon commands --- src/phases/command-phase.ts | 274 +++++++++++++++++++++--------------- 1 file changed, 158 insertions(+), 116 deletions(-) diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index 92c01aa4b85..a37721ac912 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -20,15 +20,19 @@ import { UiMode } from "#enums/ui-mode"; import type { PlayerPokemon } from "#field/pokemon"; import type { MoveTargetSet } from "#moves/move"; import { getMoveTargets } from "#moves/move-utils"; -import { FieldPhase } from "#phases/field-phase"; import type { TurnMove } from "#types/turn-move"; -import { isNullOrUndefined } from "#utils/common"; import i18next from "i18next"; +import { FieldPhase } from "./field-phase"; export class CommandPhase extends FieldPhase { public readonly phaseName = "CommandPhase"; protected fieldIndex: number; + /** + * Whether the command phase is handling a switch command + */ + private isSwitch = false; + constructor(fieldIndex: number) { super(); @@ -150,7 +154,7 @@ export class CommandPhase extends FieldPhase { /** * * @param user - The pokemon using the move - * @param cursor - + * @param cursor - The index of the move in the moveset */ private queueFightErrorMessage(user: PlayerPokemon, cursor: number) { const move = user.getMoveset()[cursor]; @@ -264,6 +268,11 @@ export class CommandPhase extends FieldPhase { return true; } + /** + * Set the mode in preparation to show the text, and then show the text. + * Only works for parameterless i18next keys. + * @param key - The i18next key for the text to show + */ private queueShowText(key: string) { globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); globalScene.ui.setMode(UiMode.MESSAGE); @@ -323,7 +332,7 @@ export class CommandPhase extends FieldPhase { * @param cursor - The index of the pokeball to use * @returns Whether the command was successfully initiated */ - handleBallCommand(cursor: number): boolean { + private handleBallCommand(cursor: number): boolean { const targets = globalScene .getEnemyField() .filter(p => p.isActive(true)) @@ -364,125 +373,158 @@ export class CommandPhase extends FieldPhase { return false; } - handleCommand(command: Command, cursor: number, useMode: MoveUseMode = MoveUseMode.NORMAL, move?: TurnMove): boolean { + /** + * Common helper method to handle the logic for effects that prevent the pokemon from leaving the field + * due to trapping abilities or effects. + * + * This method queues the proper messages in the case of trapping abilities or effects + * + * @returns Whether the pokemon is currently trapped + */ + private handleTrap(): boolean { const playerPokemon = globalScene.getPlayerField()[this.fieldIndex]; + const trappedAbMessages: string[] = []; + const isSwitch = this.isSwitch; + if (!playerPokemon.isTrapped(trappedAbMessages)) { + return false; + } + if (trappedAbMessages.length > 0) { + if (isSwitch) { + globalScene.ui.setMode(UiMode.MESSAGE); + } + globalScene.ui.showText( + trappedAbMessages[0], + null, + () => { + globalScene.ui.showText("", 0); + if (isSwitch) { + globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); + } + }, + null, + true, + ); + } else { + const trapTag = playerPokemon.getTag(TrappedTag); + const fairyLockTag = globalScene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, ArenaTagSide.PLAYER); + + if (!isSwitch) { + globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); + globalScene.ui.setMode(UiMode.MESSAGE); + } + if (trapTag) { + this.showNoEscapeText(trapTag, false); + } else if (fairyLockTag) { + this.showNoEscapeText(fairyLockTag, false); + } + } + + return true; + } + + /** + * Common helper method that attempts to have the pokemon leave the field. + * Checks for trapping abilities and effects. + * + * @param cursor - The index of the option that the cursor is on + * @param isBatonSwitch - Whether the switch command is switching via the Baton item + * @returns whether the pokemon is able to leave the field, indicating the command phase should end + */ + private tryLeaveField(cursor?: number, isBatonSwitch = false): boolean { + const currentBattle = globalScene.currentBattle; + + if (isBatonSwitch && !this.handleTrap()) { + currentBattle.turnCommands[this.fieldIndex] = { + command: this.isSwitch ? Command.POKEMON : Command.RUN, + cursor: cursor, + }; + if (!this.isSwitch && this.fieldIndex) { + currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true; + } + return true; + } + + return false; + } + + private handleRunCommand(): boolean { + const { currentBattle, arena } = globalScene; + const mysteryEncounterFleeAllowed = currentBattle.mysteryEncounter?.fleeAllowed ?? true; + if (arena.biomeType === BiomeId.END || !mysteryEncounterFleeAllowed) { + this.queueShowText("battle:noEscapeForce"); + return false; + } + if ( + currentBattle.battleType === BattleType.TRAINER || + currentBattle.mysteryEncounter?.encounterMode === MysteryEncounterMode.TRAINER_BATTLE + ) { + this.queueShowText("battle:noEscapeTrainer"); + return false; + } + + const success = this.tryLeaveField(); + + return success; + } + + /** + * Show a message indicating that the pokemon cannot escape, and then return to the command phase. + */ + private showNoEscapeText(tag: any, isSwitch: boolean): void { + globalScene.ui.showText( + i18next.t("battle:noEscapePokemon", { + pokemonName: + tag.sourceId && globalScene.getPokemonById(tag.sourceId) + ? getPokemonNameWithAffix(globalScene.getPokemonById(tag.sourceId)!) + : "", + moveName: tag.getMoveName(), + escapeVerb: i18next.t(isSwitch ? "battle:escapeVerbSwitch" : "battle:escapeVerbFlee"), + }), + null, + () => { + globalScene.ui.showText("", 0); + if (!isSwitch) { + globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); + } + }, + null, + true, + ); + } + + // Overloads for handleCommand to provide a more specific type signature for the different options + handleCommand(command: Command.FIGHT | Command.TERA, cursor: number, useMode?: MoveUseMode, move?: TurnMove): boolean; + handleCommand(command: Command.BALL, cursor: number): boolean; + handleCommand(command: Command.POKEMON, cursor: number, useBaton: boolean): boolean; + handleCommand(command: Command.RUN, cursor: number): boolean; + handleCommand(command: Command, cursor: number, useMode?: boolean | MoveUseMode, move?: TurnMove): boolean; + + /** + * Process the command phase logic based on the selected command + * + * @param command - The kind of command to handle + * @param cursor - The index of option that the cursor is on, or -1 if no option is selected + * @param useMode - The mode to use for the move, if applicable. For switches, a boolean that specifies whether the switch is a Baton switch. + * @param move - For {@linkcode Command.FIGHT}, the move to use + */ + handleCommand(command: Command, cursor: number, useMode: boolean | MoveUseMode = false, move?: TurnMove): boolean { let success = false; switch (command) { case Command.TERA: case Command.FIGHT: - return this.handleFightCommand(command, cursor, useMode, move); - case Command.BALL: - return this.handleBallCommand(cursor); - case Command.POKEMON: - case Command.RUN: { - const isSwitch = command === Command.POKEMON; - const { currentBattle, arena } = globalScene; - const mysteryEncounterFleeAllowed = currentBattle.mysteryEncounter?.fleeAllowed; - if ( - !isSwitch && - (arena.biomeType === BiomeId.END || - (!isNullOrUndefined(mysteryEncounterFleeAllowed) && !mysteryEncounterFleeAllowed)) - ) { - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - globalScene.ui.setMode(UiMode.MESSAGE); - globalScene.ui.showText( - i18next.t("battle:noEscapeForce"), - null, - () => { - globalScene.ui.showText("", 0); - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - }, - null, - true, - ); - } else if ( - !isSwitch && - (currentBattle.battleType === BattleType.TRAINER || - currentBattle.mysteryEncounter?.encounterMode === MysteryEncounterMode.TRAINER_BATTLE) - ) { - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - globalScene.ui.setMode(UiMode.MESSAGE); - globalScene.ui.showText( - i18next.t("battle:noEscapeTrainer"), - null, - () => { - globalScene.ui.showText("", 0); - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - }, - null, - true, - ); - } else { - const batonPass = isSwitch && useMode; - const trappedAbMessages: string[] = []; - if (batonPass || !playerPokemon.isTrapped(trappedAbMessages)) { - currentBattle.turnCommands[this.fieldIndex] = isSwitch - ? { command: Command.POKEMON, cursor: cursor } - : { command: Command.RUN }; - success = true; - if (!isSwitch && this.fieldIndex) { - currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true; - } - } else if (trappedAbMessages.length > 0) { - if (!isSwitch) { - globalScene.ui.setMode(UiMode.MESSAGE); - } - globalScene.ui.showText( - trappedAbMessages[0], - null, - () => { - globalScene.ui.showText("", 0); - if (!isSwitch) { - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - } - }, - null, - true, - ); - } else { - const trapTag = playerPokemon.getTag(TrappedTag); - const fairyLockTag = globalScene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, ArenaTagSide.PLAYER); - - if (!trapTag && !fairyLockTag) { - i18next.t(`battle:noEscape${isSwitch ? "Switch" : "Flee"}`); - break; - } - if (!isSwitch) { - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - globalScene.ui.setMode(UiMode.MESSAGE); - } - const showNoEscapeText = (tag: any) => { - globalScene.ui.showText( - i18next.t("battle:noEscapePokemon", { - pokemonName: - tag.sourceId && globalScene.getPokemonById(tag.sourceId) - ? getPokemonNameWithAffix(globalScene.getPokemonById(tag.sourceId)!) - : "", - moveName: tag.getMoveName(), - escapeVerb: isSwitch ? i18next.t("battle:escapeVerbSwitch") : i18next.t("battle:escapeVerbFlee"), - }), - null, - () => { - globalScene.ui.showText("", 0); - if (!isSwitch) { - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - } - }, - null, - true, - ); - }; - - if (trapTag) { - showNoEscapeText(trapTag); - } else if (fairyLockTag) { - showNoEscapeText(fairyLockTag); - } - } - } + success = this.handleFightCommand(command, cursor, typeof useMode === "boolean" ? undefined : useMode, move); break; - } + case Command.BALL: + success = this.handleBallCommand(cursor); + break; + case Command.POKEMON: + this.isSwitch = true; + success = this.tryLeaveField(cursor, typeof useMode === "boolean" ? useMode : undefined); + this.isSwitch = false; + break; + case Command.RUN: + success = this.handleRunCommand(); } if (success) { From 1fcaf55047d3f43af13872d8894376f4d02c1fdd Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Mon, 26 May 2025 15:56:14 -0500 Subject: [PATCH 34/59] Breakup commandPhase#start Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com> --- src/phases/command-phase.ts | 205 ++++++++++++++++++++++-------------- 1 file changed, 125 insertions(+), 80 deletions(-) diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index a37721ac912..943eca87e5a 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -1,14 +1,13 @@ import type { TurnCommand } from "#app/battle"; +import { BattlerTagType } from "#app/enums/battler-tag-type"; import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; import { speciesStarterCosts } from "#balance/starters"; -import type { EncoreTag } from "#data/battler-tags"; import { TrappedTag } from "#data/battler-tags"; import { AbilityId } from "#enums/ability-id"; import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagType } from "#enums/arena-tag-type"; import { BattleType } from "#enums/battle-type"; -import { BattlerTagType } from "#enums/battler-tag-type"; import { BiomeId } from "#enums/biome-id"; import { Command } from "#enums/command"; import { FieldPosition } from "#enums/field-position"; @@ -39,11 +38,13 @@ export class CommandPhase extends FieldPhase { this.fieldIndex = fieldIndex; } - start() { - super.start(); - - globalScene.updateGameInfo(); - + /** + * Resets the cursor to the position of {@linkcode Command.FIGHT} if any of the following are true + * - The setting to remember the last action is not enabled + * - This is the first turn of a mystery encounter, trainer battle, or the END biome + * - The cursor is currently on the POKEMON command + */ + private resetCursorIfNeeded() { const commandUiHandler = globalScene.ui.handlers[UiMode.COMMAND]; // If one of these conditions is true, we always reset the cursor to Command.FIGHT @@ -52,33 +53,42 @@ export class CommandPhase extends FieldPhase { globalScene.currentBattle.battleType === BattleType.TRAINER || globalScene.arena.biomeType === BiomeId.END; - if (commandUiHandler) { - if ( - (globalScene.currentBattle.turn === 1 && (!globalScene.commandCursorMemory || cursorResetEvent)) || - commandUiHandler.getCursor() === Command.POKEMON - ) { - commandUiHandler.setCursor(Command.FIGHT); - } else { - commandUiHandler.setCursor(commandUiHandler.getCursor()); - } + if ( + (commandUiHandler && + globalScene.currentBattle.turn === 1 && + (!globalScene.commandCursorMemory || cursorResetEvent)) || + commandUiHandler.getCursor() === Command.POKEMON + ) { + commandUiHandler.setCursor(Command.FIGHT); + } + } + + /** + * Submethod of {@linkcode start} that validates field index logic for nonzero field indices. + * Must only be called if the field index is nonzero. + */ + private handleFieldIndexLogic() { + // If we somehow are attempting to check the right pokemon but there's only one pokemon out + // Switch back to the center pokemon. This can happen rarely in double battles with mid turn switching + // TODO: Prevent this from happening in the first place + if (globalScene.getPlayerField().filter(p => p.isActive()).length === 1) { + this.fieldIndex = FieldPosition.CENTER; + return; } - if (this.fieldIndex) { - // If we somehow are attempting to check the right pokemon but there's only one pokemon out - // Switch back to the center pokemon. This can happen rarely in double battles with mid turn switching - if (globalScene.getPlayerField().filter(p => p.isActive()).length === 1) { - this.fieldIndex = FieldPosition.CENTER; - } else { - const allyCommand = globalScene.currentBattle.turnCommands[this.fieldIndex - 1]; - if (allyCommand?.command === Command.BALL || allyCommand?.command === Command.RUN) { - globalScene.currentBattle.turnCommands[this.fieldIndex] = { - command: allyCommand?.command, - skip: true, - }; - } - } + const allyCommand = globalScene.currentBattle.turnCommands[this.fieldIndex - 1]; + if (allyCommand?.command === Command.BALL || allyCommand?.command === Command.RUN) { + globalScene.currentBattle.turnCommands[this.fieldIndex] = { + command: allyCommand?.command, + skip: true, + }; } + } + /** Submethod of {@linkcode start} that sets the turn command to skip if this pokemon is commanding its ally + * via {@linkcode Abilities.COMMANDER}. + */ + private checkCommander() { // If the Pokemon has applied Commander's effects to its ally, skip this command if ( globalScene.currentBattle?.double && @@ -90,64 +100,99 @@ export class CommandPhase extends FieldPhase { skip: true, }; } + } - // Checks if the Pokemon is under the effects of Encore. If so, Encore can end early if the encored move has no more PP. - const encoreTag = this.getPokemon().getTag(BattlerTagType.ENCORE) as EncoreTag | undefined; - if (encoreTag) { - this.getPokemon().lapseTag(BattlerTagType.ENCORE); + /** + * Clear out all unusable moves in front of the currently acting pokemon's move queue. + * TODO: Refactor move queue handling to ensure that this method is not necessary. + */ + private clearUnusuableMoves() { + const playerPokemon = this.getPokemon(); + const moveQueue = playerPokemon.getMoveQueue(); + if (moveQueue.length === 0) { + return; } + let entriesToDelete = 0; + const moveset = playerPokemon.getMoveset(); + for (const queuedMove of moveQueue) { + const movesetQueuedMove = moveset.find(m => m.moveId === queuedMove.move); + if ( + queuedMove.move !== MoveId.NONE && + !isVirtual(queuedMove.useMode) && + !movesetQueuedMove?.isUsable(playerPokemon, isIgnorePP(queuedMove.useMode)) + ) { + entriesToDelete++; + } else { + break; + } + } + if (entriesToDelete) { + moveQueue.splice(0, entriesToDelete); + } + } + + /** + * Attempt to execute the first usable move in this Pokemon's move queue + * @returns Whether a queued move was successfully set to be executed. + */ + private tryExecuteQueuedMove(): boolean { + this.clearUnusuableMoves(); + const playerPokemon = globalScene.getPlayerField()[this.fieldIndex]; + const moveQueue = playerPokemon.getMoveQueue(); + + if (moveQueue.length === 0) { + return false; + } + + const queuedMove = moveQueue[0]; + if (queuedMove.move === MoveId.NONE) { + this.handleCommand(Command.FIGHT, -1); + return true; + } + const moveIndex = playerPokemon.getMoveset().findIndex(m => m.moveId === queuedMove.move); + if (!isVirtual(queuedMove.useMode) && moveIndex === -1) { + globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); + } else { + this.handleCommand(Command.FIGHT, moveIndex, queuedMove.useMode, queuedMove); + } + + return true; + } + + start() { + super.start(); + + globalScene.updateGameInfo(); + this.resetCursorIfNeeded(); + + if (this.fieldIndex) { + this.handleFieldIndexLogic(); + } + + this.checkCommander(); + + const playerPokemon = this.getPokemon(); + + // Note: It is OK to call this if the target is not under the effect of encore; it will simply do nothing. + playerPokemon.lapseTag(BattlerTagType.ENCORE); + if (globalScene.currentBattle.turnCommands[this.fieldIndex]?.skip) { return this.end(); } - const playerPokemon = globalScene.getPlayerField()[this.fieldIndex]; - - const moveQueue = playerPokemon.getMoveQueue(); - - while ( - moveQueue.length && - moveQueue[0] && - moveQueue[0].move && - !isVirtual(moveQueue[0].useMode) && - (!playerPokemon.getMoveset().find(m => m.moveId === moveQueue[0].move) || - !playerPokemon - .getMoveset() - [playerPokemon.getMoveset().findIndex(m => m.moveId === moveQueue[0].move)].isUsable( - playerPokemon, - isIgnorePP(moveQueue[0].useMode), - )) - ) { - moveQueue.shift(); + if (this.tryExecuteQueuedMove()) { + return; } - // TODO: Refactor this. I did a few simple find/replace matches but this is just ABHORRENTLY structured - if (moveQueue.length > 0) { - const queuedMove = moveQueue[0]; - if (!queuedMove.move) { - this.handleCommand(Command.FIGHT, -1, MoveUseMode.NORMAL); - } else { - const moveIndex = playerPokemon.getMoveset().findIndex(m => m.moveId === queuedMove.move); - if ( - (moveIndex > -1 && - playerPokemon.getMoveset()[moveIndex].isUsable(playerPokemon, isIgnorePP(queuedMove.useMode))) || - isVirtual(queuedMove.useMode) - ) { - this.handleCommand(Command.FIGHT, moveIndex, queuedMove.useMode, queuedMove); - } else { - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - } - } + if ( + globalScene.currentBattle.isBattleMysteryEncounter() && + globalScene.currentBattle.mysteryEncounter?.skipToFightInput + ) { + globalScene.ui.clearText(); + globalScene.ui.setMode(UiMode.FIGHT, this.fieldIndex); } else { - if ( - globalScene.currentBattle.isBattleMysteryEncounter() && - globalScene.currentBattle.mysteryEncounter?.skipToFightInput - ) { - globalScene.ui.clearText(); - globalScene.ui.setMode(UiMode.FIGHT, this.fieldIndex); - } else { - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - } + globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); } } @@ -201,7 +246,7 @@ export class CommandPhase extends FieldPhase { useMode: MoveUseMode = MoveUseMode.NORMAL, move?: TurnMove, ): boolean { - const playerPokemon = globalScene.getPlayerField()[this.fieldIndex]; + const playerPokemon = this.getPokemon(); const ignorePP = isIgnorePP(useMode); /** Whether or not to display an error message instead of attempting to initiate the command selection process */ @@ -382,7 +427,7 @@ export class CommandPhase extends FieldPhase { * @returns Whether the pokemon is currently trapped */ private handleTrap(): boolean { - const playerPokemon = globalScene.getPlayerField()[this.fieldIndex]; + const playerPokemon = this.getPokemon(); const trappedAbMessages: string[] = []; const isSwitch = this.isSwitch; if (!playerPokemon.isTrapped(trappedAbMessages)) { From ea803195edc1a6140532d65d34b676cfaf54aa6e Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Mon, 26 May 2025 16:41:58 -0500 Subject: [PATCH 35/59] Minor touchups --- src/phases/command-phase.ts | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index 943eca87e5a..34ca7bb71e5 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -249,8 +249,7 @@ export class CommandPhase extends FieldPhase { const playerPokemon = this.getPokemon(); const ignorePP = isIgnorePP(useMode); - /** Whether or not to display an error message instead of attempting to initiate the command selection process */ - let canUse = cursor !== -1 || !playerPokemon.trySelectMove(cursor, ignorePP); + let canUse = cursor === -1 || playerPokemon.trySelectMove(cursor, ignorePP); const useStruggle = canUse ? false @@ -291,7 +290,7 @@ export class CommandPhase extends FieldPhase { console.log(moveTargets, getPokemonNameWithAffix(playerPokemon)); - if (moveTargets.multiple) { + if (moveTargets.targets.length > 1 && moveTargets.multiple) { globalScene.phaseManager.unshiftNew("SelectTargetPhase", this.fieldIndex); } @@ -419,7 +418,7 @@ export class CommandPhase extends FieldPhase { } /** - * Common helper method to handle the logic for effects that prevent the pokemon from leaving the field + * Helper method to handle the logic for effects that prevent the pokemon from leaving the field * due to trapping abilities or effects. * * This method queues the proper messages in the case of trapping abilities or effects @@ -478,11 +477,16 @@ export class CommandPhase extends FieldPhase { private tryLeaveField(cursor?: number, isBatonSwitch = false): boolean { const currentBattle = globalScene.currentBattle; - if (isBatonSwitch && !this.handleTrap()) { - currentBattle.turnCommands[this.fieldIndex] = { - command: this.isSwitch ? Command.POKEMON : Command.RUN, - cursor: cursor, - }; + if (isBatonSwitch || !this.handleTrap()) { + currentBattle.turnCommands[this.fieldIndex] = this.isSwitch + ? { + command: Command.POKEMON, + cursor: cursor, + args: [isBatonSwitch], + } + : { + command: Command.RUN, + }; if (!this.isSwitch && this.fieldIndex) { currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true; } @@ -537,7 +541,7 @@ export class CommandPhase extends FieldPhase { ); } - // Overloads for handleCommand to provide a more specific type signature for the different options + // Overloads for handleCommand to provide a more specific signature for the different options handleCommand(command: Command.FIGHT | Command.TERA, cursor: number, useMode?: MoveUseMode, move?: TurnMove): boolean; handleCommand(command: Command.BALL, cursor: number): boolean; handleCommand(command: Command.POKEMON, cursor: number, useBaton: boolean): boolean; From 5616e3d4f33a6423b7beb6d14ef2ab4427825e97 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Mon, 26 May 2025 16:49:00 -0500 Subject: [PATCH 36/59] Add overload for handle command --- src/phases/command-phase.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index 34ca7bb71e5..c9a83096359 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -249,6 +249,7 @@ export class CommandPhase extends FieldPhase { const playerPokemon = this.getPokemon(); const ignorePP = isIgnorePP(useMode); + let canUse = cursor === -1 || playerPokemon.trySelectMove(cursor, ignorePP); let canUse = cursor === -1 || playerPokemon.trySelectMove(cursor, ignorePP); const useStruggle = canUse From c4a697d8cec7b16c1908cc82e5433a7896dcd6c0 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Mon, 26 May 2025 20:40:48 -0500 Subject: [PATCH 37/59] Fix improperly named computeMoveId method --- src/phases/command-phase.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index c9a83096359..024e54f8065 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -230,7 +230,7 @@ export class CommandPhase extends FieldPhase { * * Does not check if the move is usable or not, that should be handled by the caller. */ - private comptueMoveId(playerPokemon: PlayerPokemon, cursor: number, move?: TurnMove): MoveId { + private computeMoveId(playerPokemon: PlayerPokemon, cursor: number, move?: TurnMove): MoveId { return move?.move ?? (cursor > -1 ? playerPokemon.getMoveset()[cursor]?.moveId : MoveId.NONE); } @@ -263,7 +263,7 @@ export class CommandPhase extends FieldPhase { return false; } - const moveId = useStruggle ? MoveId.STRUGGLE : this.comptueMoveId(playerPokemon, cursor, move); + const moveId = useStruggle ? MoveId.STRUGGLE : this.computeMoveId(playerPokemon, cursor, move); const turnCommand: TurnCommand = { command: Command.FIGHT, From 4a87e06090614d713a3966802e12a3248850f059 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Mon, 26 May 2025 23:13:46 -0500 Subject: [PATCH 38/59] Improve `canUse` computation --- src/phases/command-phase.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index 024e54f8065..85b919148e3 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -252,13 +252,12 @@ export class CommandPhase extends FieldPhase { let canUse = cursor === -1 || playerPokemon.trySelectMove(cursor, ignorePP); let canUse = cursor === -1 || playerPokemon.trySelectMove(cursor, ignorePP); + // Ternary here ensures we don't compute struggle conditions unless necessary const useStruggle = canUse ? false : cursor > -1 && !playerPokemon.getMoveset().some(m => m.isUsable(playerPokemon)); - canUse = canUse || useStruggle; - - if (!canUse) { + if (!canUse && !useStruggle) { this.queueFightErrorMessage(playerPokemon, cursor); return false; } From cd2e73cb281457f26060ca3217f24db8ad9848a7 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Mon, 26 May 2025 23:18:04 -0500 Subject: [PATCH 39/59] Explicitly check against Moves.NONE Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com> --- src/phases/command-phase.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index 85b919148e3..a8ea0abe5c2 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -284,7 +284,7 @@ export class CommandPhase extends FieldPhase { multiple: move.targets.length > 1, }; - if (!moveId) { + if (moveId === Moves.NONE) { turnCommand.targets = [this.fieldIndex]; } From c9e7fd04c06351e1b1f2fbdec3cf9ef1bcdb0446 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Fri, 6 Jun 2025 10:41:34 -0500 Subject: [PATCH 40/59] Update with Bertie's comments Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com> --- src/phases/command-phase.ts | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index a8ea0abe5c2..ce83c25d314 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -225,7 +225,8 @@ export class CommandPhase extends FieldPhase { ); } - /** Helper method for {@linkcode handleFightCommand} that returns the moveID for the phase + /** + * Helper method for {@linkcode handleFightCommand} that returns the moveID for the phase * based on the move passed in or the cursor. * * Does not check if the move is usable or not, that should be handled by the caller. @@ -235,10 +236,16 @@ export class CommandPhase extends FieldPhase { } /** - * Handle fight logic + * Process the logic for executing a fight-related command + * + * @remarks + * - Validates whether the move can be used, using struggle if not + * - Constructs the turn command and inserts it into the battle's turn commands + * * @param command - The command to handle (FIGHT or TERA) * @param cursor - The index that the cursor is placed on, or -1 if no move can be selected. - * @param args - Any additional arguments to pass to the command + * @param ignorePP - Whether to ignore PP when checking if the move can be used. + * @param move - The move to force the command to use, if any. */ private handleFightCommand( command: Command.FIGHT | Command.TERA, @@ -249,7 +256,6 @@ export class CommandPhase extends FieldPhase { const playerPokemon = this.getPokemon(); const ignorePP = isIgnorePP(useMode); - let canUse = cursor === -1 || playerPokemon.trySelectMove(cursor, ignorePP); let canUse = cursor === -1 || playerPokemon.trySelectMove(cursor, ignorePP); // Ternary here ensures we don't compute struggle conditions unless necessary @@ -257,7 +263,9 @@ export class CommandPhase extends FieldPhase { ? false : cursor > -1 && !playerPokemon.getMoveset().some(m => m.isUsable(playerPokemon)); - if (!canUse && !useStruggle) { + canUse ||= useStruggle; + + if (!canUse) { this.queueFightErrorMessage(playerPokemon, cursor); return false; } @@ -284,7 +292,7 @@ export class CommandPhase extends FieldPhase { multiple: move.targets.length > 1, }; - if (moveId === Moves.NONE) { + if (moveId === MoveId.NONE) { turnCommand.targets = [this.fieldIndex]; } From 2b09ca9159c5b533c31f3def9fc206fd61c371b2 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Tue, 15 Jul 2025 22:12:19 -0700 Subject: [PATCH 41/59] Fix imports --- src/phases/command-phase.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index ce83c25d314..d44681ab12c 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -1,5 +1,4 @@ import type { TurnCommand } from "#app/battle"; -import { BattlerTagType } from "#app/enums/battler-tag-type"; import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; import { speciesStarterCosts } from "#balance/starters"; @@ -8,6 +7,7 @@ import { AbilityId } from "#enums/ability-id"; import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagType } from "#enums/arena-tag-type"; import { BattleType } from "#enums/battle-type"; +import { BattlerTagType } from "#enums/battler-tag-type"; import { BiomeId } from "#enums/biome-id"; import { Command } from "#enums/command"; import { FieldPosition } from "#enums/field-position"; @@ -19,9 +19,9 @@ import { UiMode } from "#enums/ui-mode"; import type { PlayerPokemon } from "#field/pokemon"; import type { MoveTargetSet } from "#moves/move"; import { getMoveTargets } from "#moves/move-utils"; +import { FieldPhase } from "#phases/field-phase"; import type { TurnMove } from "#types/turn-move"; import i18next from "i18next"; -import { FieldPhase } from "./field-phase"; export class CommandPhase extends FieldPhase { public readonly phaseName = "CommandPhase"; From f6185daaae2f23a838b40ed78f0f03768883072b Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Wed, 23 Jul 2025 21:15:23 -0600 Subject: [PATCH 42/59] Apply kev's suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/phases/command-phase.ts | 115 +++++++++++++++++++++--------------- 1 file changed, 69 insertions(+), 46 deletions(-) diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index d44681ab12c..44b365146d9 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -44,19 +44,21 @@ export class CommandPhase extends FieldPhase { * - This is the first turn of a mystery encounter, trainer battle, or the END biome * - The cursor is currently on the POKEMON command */ - private resetCursorIfNeeded() { + private resetCursorIfNeeded(): void { const commandUiHandler = globalScene.ui.handlers[UiMode.COMMAND]; + const { arena, commandCursorMemory, currentBattle } = globalScene; + const { battleType, turn } = currentBattle; + const { biomeType } = arena; // If one of these conditions is true, we always reset the cursor to Command.FIGHT const cursorResetEvent = - globalScene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER || - globalScene.currentBattle.battleType === BattleType.TRAINER || - globalScene.arena.biomeType === BiomeId.END; + battleType === BattleType.MYSTERY_ENCOUNTER || battleType === BattleType.TRAINER || biomeType === BiomeId.END; + if (!commandUiHandler) { + return; + } if ( - (commandUiHandler && - globalScene.currentBattle.turn === 1 && - (!globalScene.commandCursorMemory || cursorResetEvent)) || + (turn === 1 && (!commandCursorMemory || cursorResetEvent)) || commandUiHandler.getCursor() === Command.POKEMON ) { commandUiHandler.setCursor(Command.FIGHT); @@ -67,7 +69,7 @@ export class CommandPhase extends FieldPhase { * Submethod of {@linkcode start} that validates field index logic for nonzero field indices. * Must only be called if the field index is nonzero. */ - private handleFieldIndexLogic() { + private handleFieldIndexLogic(): void { // If we somehow are attempting to check the right pokemon but there's only one pokemon out // Switch back to the center pokemon. This can happen rarely in double battles with mid turn switching // TODO: Prevent this from happening in the first place @@ -85,10 +87,11 @@ export class CommandPhase extends FieldPhase { } } - /** Submethod of {@linkcode start} that sets the turn command to skip if this pokemon is commanding its ally - * via {@linkcode Abilities.COMMANDER}. + /** + * Submethod of {@linkcode start} that sets the turn command to skip if this pokemon + * is commanding its ally via {@linkcode AbilityId.COMMANDER}. */ - private checkCommander() { + private checkCommander(): void { // If the Pokemon has applied Commander's effects to its ally, skip this command if ( globalScene.currentBattle?.double && @@ -104,9 +107,10 @@ export class CommandPhase extends FieldPhase { /** * Clear out all unusable moves in front of the currently acting pokemon's move queue. + * * TODO: Refactor move queue handling to ensure that this method is not necessary. */ - private clearUnusuableMoves() { + private clearUnusuableMoves(): void { const playerPokemon = this.getPokemon(); const moveQueue = playerPokemon.getMoveQueue(); if (moveQueue.length === 0) { @@ -160,7 +164,7 @@ export class CommandPhase extends FieldPhase { return true; } - start() { + public override start(): void { super.start(); globalScene.updateGameInfo(); @@ -178,7 +182,8 @@ export class CommandPhase extends FieldPhase { playerPokemon.lapseTag(BattlerTagType.ENCORE); if (globalScene.currentBattle.turnCommands[this.fieldIndex]?.skip) { - return this.end(); + this.end(); + return; } if (this.tryExecuteQueuedMove()) { @@ -197,7 +202,8 @@ export class CommandPhase extends FieldPhase { } /** - * + * Submethod of {@linkcode handleFightCommand} responsible for queuing the appropriate + * error message when a move cannot be used. * @param user - The pokemon using the move * @param cursor - The index of the move in the moveset */ @@ -231,7 +237,7 @@ export class CommandPhase extends FieldPhase { * * Does not check if the move is usable or not, that should be handled by the caller. */ - private computeMoveId(playerPokemon: PlayerPokemon, cursor: number, move?: TurnMove): MoveId { + private computeMoveId(playerPokemon: PlayerPokemon, cursor: number, move: TurnMove | undefined): MoveId { return move?.move ?? (cursor > -1 ? playerPokemon.getMoveset()[cursor]?.moveId : MoveId.NONE); } @@ -274,12 +280,12 @@ export class CommandPhase extends FieldPhase { const turnCommand: TurnCommand = { command: Command.FIGHT, - cursor: cursor, + cursor, move: { move: moveId, targets: [], useMode }, args: [useMode, move], }; const preTurnCommand: TurnCommand = { - command: command, + command, targets: [this.fieldIndex], skip: command === Command.FIGHT, }; @@ -296,7 +302,14 @@ export class CommandPhase extends FieldPhase { turnCommand.targets = [this.fieldIndex]; } - console.log(moveTargets, getPokemonNameWithAffix(playerPokemon)); + console.log( + "Move:", + MoveId[moveId], + "Move targets:", + moveTargets, + "\nPlayer Pokemon:", + getPokemonNameWithAffix(playerPokemon), + ); if (moveTargets.targets.length > 1 && moveTargets.multiple) { globalScene.phaseManager.unshiftNew("SelectTargetPhase", this.fieldIndex); @@ -325,7 +338,7 @@ export class CommandPhase extends FieldPhase { * Only works for parameterless i18next keys. * @param key - The i18next key for the text to show */ - private queueShowText(key: string) { + private queueShowText(key: string): void { globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); globalScene.ui.setMode(UiMode.MESSAGE); @@ -344,32 +357,35 @@ export class CommandPhase extends FieldPhase { /** * Helper method for {@linkcode handleBallCommand} that checks if the pokeball can be thrown. * - * The pokeball may not be thrown if: + * The pokeball may not be thrown if any of the following are true: * - It is a trainer battle - * - The biome is {@linkcode Biome.END} and it is not classic mode - * - The biome is {@linkcode Biome.END} and the fresh start challenge is active - * - The biome is {@linkcode Biome.END} and the player has not either caught the target before or caught all but one starter + * - The player is in the {@linkcode BiomeId.END | End} biome and + * - it is not classic mode; or + * - the fresh start challenge is active; or + * - the player has not caught the target before and the player is still missing more than one starter * - The player is in a mystery encounter that disallows catching the pokemon * @returns Whether a pokeball can be thrown - * */ private checkCanUseBall(): boolean { + const { arena, currentBattle, gameData, gameMode } = globalScene; + const { battleType } = currentBattle; + const { biomeType } = arena; + const { isClassic } = gameMode; + const { dexData } = gameData; + + const someUncaughtSpeciesOnField = globalScene + .getEnemyField() + .some(p => p.isActive() && !dexData[p.species.speciesId].caughtAttr); + const missingMultipleStarters = + gameData.getStarterCount(d => !!d.caughtAttr) < Object.keys(speciesStarterCosts).length - 1; if ( - globalScene.arena.biomeType === BiomeId.END && - (!globalScene.gameMode.isClassic || - globalScene.gameMode.isFreshStartChallenge() || - (globalScene - .getEnemyField() - .some(p => p.isActive() && !globalScene.gameData.dexData[p.species.speciesId].caughtAttr) && - globalScene.gameData.getStarterCount(d => !!d.caughtAttr) < Object.keys(speciesStarterCosts).length - 1)) + biomeType === BiomeId.END && + (!isClassic || gameMode.isFreshStartChallenge() || (someUncaughtSpeciesOnField && missingMultipleStarters)) ) { this.queueShowText("battle:noPokeballForce"); - } else if (globalScene.currentBattle.battleType === BattleType.TRAINER) { + } else if (battleType === BattleType.TRAINER) { this.queueShowText("battle:noPokeballTrainer"); - } else if ( - globalScene.currentBattle.isBattleMysteryEncounter() && - !globalScene.currentBattle.mysteryEncounter!.catchAllowed - ) { + } else if (currentBattle.isBattleMysteryEncounter() && !currentBattle.mysteryEncounter!.catchAllowed) { this.queueShowText("battle:noPokeballMysteryEncounter"); } else { return true; @@ -398,7 +414,8 @@ export class CommandPhase extends FieldPhase { return false; } - if (cursor < 5) { + const numBallTypes = 5; + if (cursor < numBallTypes) { const targetPokemon = globalScene.getEnemyPokemon(); if ( targetPokemon?.isBoss() && @@ -489,7 +506,7 @@ export class CommandPhase extends FieldPhase { currentBattle.turnCommands[this.fieldIndex] = this.isSwitch ? { command: Command.POKEMON, - cursor: cursor, + cursor, args: [isBatonSwitch], } : { @@ -550,12 +567,6 @@ export class CommandPhase extends FieldPhase { } // Overloads for handleCommand to provide a more specific signature for the different options - handleCommand(command: Command.FIGHT | Command.TERA, cursor: number, useMode?: MoveUseMode, move?: TurnMove): boolean; - handleCommand(command: Command.BALL, cursor: number): boolean; - handleCommand(command: Command.POKEMON, cursor: number, useBaton: boolean): boolean; - handleCommand(command: Command.RUN, cursor: number): boolean; - handleCommand(command: Command, cursor: number, useMode?: boolean | MoveUseMode, move?: TurnMove): boolean; - /** * Process the command phase logic based on the selected command * @@ -563,8 +574,20 @@ export class CommandPhase extends FieldPhase { * @param cursor - The index of option that the cursor is on, or -1 if no option is selected * @param useMode - The mode to use for the move, if applicable. For switches, a boolean that specifies whether the switch is a Baton switch. * @param move - For {@linkcode Command.FIGHT}, the move to use + * @returns Whether the command was successful */ - handleCommand(command: Command, cursor: number, useMode: boolean | MoveUseMode = false, move?: TurnMove): boolean { + handleCommand(command: Command.FIGHT | Command.TERA, cursor: number, useMode?: MoveUseMode, move?: TurnMove): boolean; + handleCommand(command: Command.BALL, cursor: number): boolean; + handleCommand(command: Command.POKEMON, cursor: number, useBaton: boolean): boolean; + handleCommand(command: Command.RUN, cursor: number): boolean; + handleCommand(command: Command, cursor: number, useMode?: boolean | MoveUseMode, move?: TurnMove): boolean; + + public handleCommand( + command: Command, + cursor: number, + useMode: boolean | MoveUseMode = false, + move?: TurnMove, + ): boolean { let success = false; switch (command) { From ec5353bab58f7fc76d424f6b7e8d9170e04fdbc9 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Thu, 24 Jul 2025 15:14:24 -0600 Subject: [PATCH 43/59] Improve documentation Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com> --- src/phases/command-phase.ts | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index 44b365146d9..016d4ff5d3b 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -107,9 +107,8 @@ export class CommandPhase extends FieldPhase { /** * Clear out all unusable moves in front of the currently acting pokemon's move queue. - * - * TODO: Refactor move queue handling to ensure that this method is not necessary. */ + // TODO: Refactor move queue handling to ensure that this method is not necessary. private clearUnusuableMoves(): void { const playerPokemon = this.getPokemon(); const moveQueue = playerPokemon.getMoveQueue(); @@ -355,8 +354,10 @@ export class CommandPhase extends FieldPhase { } /** - * Helper method for {@linkcode handleBallCommand} that checks if the pokeball can be thrown. + * Helper method for {@linkcode handleBallCommand} that checks if a pokeball can be thrown + * and displays the appropriate error message. * + * @remarks * The pokeball may not be thrown if any of the following are true: * - It is a trainer battle * - The player is in the {@linkcode BiomeId.END | End} biome and @@ -443,10 +444,10 @@ export class CommandPhase extends FieldPhase { } /** - * Helper method to handle the logic for effects that prevent the pokemon from leaving the field + * Submethod of {@linkcode tryLeaveField} to handle the logic for effects that prevent the pokemon from leaving the field * due to trapping abilities or effects. * - * This method queues the proper messages in the case of trapping abilities or effects + * This method queues the proper messages in the case of trapping abilities or effects. * * @returns Whether the pokemon is currently trapped */ @@ -496,8 +497,7 @@ export class CommandPhase extends FieldPhase { * Checks for trapping abilities and effects. * * @param cursor - The index of the option that the cursor is on - * @param isBatonSwitch - Whether the switch command is switching via the Baton item - * @returns whether the pokemon is able to leave the field, indicating the command phase should end + * @returns Whether the pokemon is able to leave the field, indicating the command phase should end */ private tryLeaveField(cursor?: number, isBatonSwitch = false): boolean { const currentBattle = globalScene.currentBattle; @@ -521,6 +521,19 @@ export class CommandPhase extends FieldPhase { return false; } + /** + * Helper method for {@linkcode handleCommand} that handles the logic when the selected command is RUN. + * + * @remarks + * Checks if the player is allowed to flee, and if not, queues the appropriate message. + * + * The player cannot flee if: + * - The player is in the {@linkcode BiomeId.END | End} biome + * - The player is in a trainer battle + * - The player is in a mystery encounter that disallows fleeing + * - The player's pokemon is trapped by an ability or effect + * @returns Whether the pokemon is able to leave the field, indicating the command phase should end + */ private handleRunCommand(): boolean { const { currentBattle, arena } = globalScene; const mysteryEncounterFleeAllowed = currentBattle.mysteryEncounter?.fleeAllowed ?? true; From af6a057bfc7f96179cb9c3723feb73646fad6372 Mon Sep 17 00:00:00 2001 From: AJ Fontaine <36677462+Fontbane@users.noreply.github.com> Date: Mon, 28 Jul 2025 01:46:11 -0400 Subject: [PATCH 44/59] [Balance][Challenge] Added expanded Fresh Start options (#6162) --- src/data/challenge.ts | 35 ++++++++++++++++++++++++++--------- src/system/achv.ts | 2 +- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/data/challenge.ts b/src/data/challenge.ts index db4ec931bc2..aaa82a7d20f 100644 --- a/src/data/challenge.ts +++ b/src/data/challenge.ts @@ -4,6 +4,7 @@ import { defaultStarterSpecies } from "#app/constants"; import { globalScene } from "#app/global-scene"; import { pokemonEvolutions } from "#balance/pokemon-evolutions"; import { speciesStarterCosts } from "#balance/starters"; +import { getEggTierForSpecies } from "#data/egg"; import { pokemonFormChanges } from "#data/pokemon-forms"; import type { PokemonSpecies } from "#data/pokemon-species"; import { getPokemonSpeciesForm } from "#data/pokemon-species"; @@ -11,6 +12,7 @@ import { BattleType } from "#enums/battle-type"; import { ChallengeType } from "#enums/challenge-type"; import { Challenges } from "#enums/challenges"; import { TypeColor, TypeShadow } from "#enums/color"; +import { EggTier } from "#enums/egg-type"; import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves"; import { ModifierTier } from "#enums/modifier-tier"; import type { MoveId } from "#enums/move-id"; @@ -683,11 +685,14 @@ export class SingleTypeChallenge extends Challenge { */ export class FreshStartChallenge extends Challenge { constructor() { - super(Challenges.FRESH_START, 1); + super(Challenges.FRESH_START, 3); } applyStarterChoice(pokemon: PokemonSpecies, valid: BooleanHolder): boolean { - if (!defaultStarterSpecies.includes(pokemon.speciesId)) { + if ( + (this.value === 1 && !defaultStarterSpecies.includes(pokemon.speciesId)) || + (this.value === 2 && getEggTierForSpecies(pokemon) >= EggTier.EPIC) + ) { valid.value = false; return true; } @@ -695,15 +700,12 @@ export class FreshStartChallenge extends Challenge { } applyStarterCost(species: SpeciesId, cost: NumberHolder): boolean { - if (defaultStarterSpecies.includes(species)) { - cost.value = speciesStarterCosts[species]; - return true; - } - return false; + cost.value = speciesStarterCosts[species]; + return true; } applyStarterModify(pokemon: Pokemon): boolean { - pokemon.abilityIndex = 0; // Always base ability, not hidden ability + pokemon.abilityIndex = pokemon.abilityIndex % 2; // Always base ability, if you set it to hidden it wraps to first ability pokemon.passive = false; // Passive isn't unlocked pokemon.nature = Nature.HARDY; // Neutral nature pokemon.moveset = pokemon.species @@ -715,7 +717,22 @@ export class FreshStartChallenge extends Challenge { pokemon.luck = 0; // No luck pokemon.shiny = false; // Not shiny pokemon.variant = 0; // Not shiny - pokemon.formIndex = 0; // Froakie should be base form + if (pokemon.species.speciesId === SpeciesId.ZYGARDE && pokemon.formIndex >= 2) { + pokemon.formIndex -= 2; // Sets 10%-PC to 10%-AB and 50%-PC to 50%-AB + } else if ( + pokemon.formIndex > 0 && + [ + SpeciesId.PIKACHU, + SpeciesId.EEVEE, + SpeciesId.PICHU, + SpeciesId.ROTOM, + SpeciesId.MELOETTA, + SpeciesId.FROAKIE, + SpeciesId.ROCKRUFF, + ].includes(pokemon.species.speciesId) + ) { + pokemon.formIndex = 0; // These mons are set to form 0 because they're meant to be unlocks or mid-run form changes + } pokemon.ivs = [15, 15, 15, 15, 15, 15]; // Default IVs of 15 for all stats (Updated to 15 from 10 in 1.2.0) pokemon.teraType = pokemon.species.type1; // Always primary tera type return true; diff --git a/src/system/achv.ts b/src/system/achv.ts index abe6f264d20..69eade02e35 100644 --- a/src/system/achv.ts +++ b/src/system/achv.ts @@ -890,7 +890,7 @@ export const achvs = { 100, c => c instanceof FreshStartChallenge && - c.value > 0 && + c.value === 1 && !globalScene.gameMode.challenges.some( c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT].includes(c.id) && c.value > 0, ), From 94a77286811271f81ac514fa8b862d073f15df09 Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Mon, 28 Jul 2025 14:43:36 -0400 Subject: [PATCH 45/59] [Dev] Add `workflow-dispatch` trigger to tests github workflow (#6152) Add `workflow-dispatch` trigger to github workflow Co-authored-by: damocleas --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d3dd23eb379..764a35ace60 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,6 +11,7 @@ on: - beta merge_group: types: [checks_requested] + workflow_dispatch: jobs: check-path-change-filter: From 6b269dc3f5ae334ca5a40bea54ae032a94fae6d7 Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Mon, 28 Jul 2025 14:51:43 -0400 Subject: [PATCH 46/59] [Test] Add support for custom boilerplates to `create-test.js` (#6158) * Added support for custom boilerplates to test:create script * Added support for custom boilerplates to create-test.js * Fixed syntax error * Update create-test.js Co-authored-by: Amani H. <109637146+xsn34kzx@users.noreply.github.com> * Fix pluralization error in `create-test.js` --------- Co-authored-by: Amani H. <109637146+xsn34kzx@users.noreply.github.com> --- .../default.ts} | 0 scripts/create-test/create-test.js | 107 +++++++++++------- 2 files changed, 63 insertions(+), 44 deletions(-) rename scripts/create-test/{test-boilerplate.ts => boilerplates/default.ts} (100%) diff --git a/scripts/create-test/test-boilerplate.ts b/scripts/create-test/boilerplates/default.ts similarity index 100% rename from scripts/create-test/test-boilerplate.ts rename to scripts/create-test/boilerplates/default.ts diff --git a/scripts/create-test/create-test.js b/scripts/create-test/create-test.js index c0f27f8891e..765993959d1 100644 --- a/scripts/create-test/create-test.js +++ b/scripts/create-test/create-test.js @@ -17,15 +17,20 @@ const version = "2.0.1"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const projectRoot = path.join(__dirname, "..", ".."); -const boilerplateFilePath = path.join(__dirname, "test-boilerplate.ts"); -const choices = [ - { label: "Move", dir: "moves" }, - { label: "Ability", dir: "abilities" }, - { label: "Item", dir: "items" }, - { label: "Mystery Encounter", dir: "mystery-encounter/encounters" }, - { label: "Utils", dir: "utils" }, - { label: "UI", dir: "ui" }, -]; + +const choices = /** @type {const} */ (["Move", "Ability", "Item", "Reward", "Mystery Encounter", "Utils", "UI"]); +/** @typedef {choices[number]} choiceType */ + +/** @satisfies {{[k in choiceType]: string}} */ +const choicesToDirs = /** @type {const} */ ({ + Move: "moves", + Ability: "abilities", + Item: "items", + Reward: "rewards", + "Mystery Encounter": "mystery-encounter/encounters", + Utils: "utils", + UI: "ui", +}); //#endregion //#region Functions @@ -41,48 +46,47 @@ function getTestFolderPath(...folders) { /** * Prompts the user to select a type via list. - * @returns {Promise<{selectedOption: {label: string, dir: string}}>} the selected type + * @returns {Promise} the selected type */ async function promptTestType() { - const typeAnswer = await inquirer + /** @type {choiceType | "EXIT"} */ + const choice = await inquirer .prompt([ { type: "list", name: "selectedOption", message: "What type of test would you like to create?", - choices: [...choices.map(choice => ({ name: choice.label, value: choice })), { name: "EXIT", value: "N/A" }], + choices: [...choices, "EXIT"], }, ]) - .then(ans => ans.selectedOption); + .then(ta => ta.selectedOption); - if (typeAnswer.name === "EXIT") { + if (choice === "EXIT") { console.log("Exiting..."); return process.exit(0); } - if (!choices.some(choice => choice.dir === typeAnswer.dir)) { - console.error(`Please provide a valid type: (${choices.map(choice => choice.label).join(", ")})!`); - return await promptTestType(); - } - return typeAnswer; + return choice; } /** * Prompts the user to provide a file name. - * @param {string} selectedType - * @returns {Promise<{userInput: string}>} the selected file name + * @param {choiceType} selectedType The chosen string (used to display console logs) + * @returns {Promise} the selected file name */ async function promptFileName(selectedType) { - /** @type {{userInput: string}} */ - const fileNameAnswer = await inquirer.prompt([ - { - type: "input", - name: "userInput", - message: `Please provide the name of the ${selectedType}:`, - }, - ]); + /** @type {string} */ + const fileNameAnswer = await inquirer + .prompt([ + { + type: "input", + name: "userInput", + message: `Please provide the name of the ${selectedType}.`, + }, + ]) + .then(fa => fa.userInput); - if (!fileNameAnswer.userInput || fileNameAnswer.userInput.trim().length === 0) { + if (fileNameAnswer.trim().length === 0) { console.error("Please provide a valid file name!"); return await promptFileName(selectedType); } @@ -90,51 +94,66 @@ async function promptFileName(selectedType) { return fileNameAnswer; } +/** + * Obtain the path to the boilerplate file based on the current option. + * @param {choiceType} choiceType The choice selected + * @returns {string} The path to the boilerplate file + */ +function getBoilerplatePath(choiceType) { + switch (choiceType) { + // case "Reward": + // return path.join(__dirname, "boilerplates/reward.ts"); + default: + return path.join(__dirname, "boilerplates/default.ts"); + } +} + /** * Runs the interactive test:create "CLI" * @returns {Promise} */ async function runInteractive() { - console.group(chalk.grey(`Create Test - v${version}\n`)); + console.group(chalk.grey(`🧪 Create Test - v${version}\n`)); try { - const typeAnswer = await promptTestType(); - const fileNameAnswer = await promptFileName(typeAnswer.selectedOption.label); + const choice = await promptTestType(); + const fileNameAnswer = await promptFileName(choice); - const type = typeAnswer.selectedOption; // Convert fileName from snake_case or camelCase to kebab-case - const fileName = fileNameAnswer.userInput + const fileName = fileNameAnswer .replace(/_+/g, "-") // Convert snake_case (underscore) to kebab-case (dashes) .replace(/([a-z])([A-Z])/g, "$1-$2") // Convert camelCase to kebab-case .replace(/\s+/g, "-") // Replace spaces with dashes .toLowerCase(); // Ensure all lowercase - // Format the description for the test case + // Format the description for the test case in Title Case const formattedName = fileName.replace(/-/g, " ").replace(/\b\w/g, char => char.toUpperCase()); + const description = `${choice} - ${formattedName}`; + // Determine the directory based on the type - const dir = getTestFolderPath(type.dir); - const description = `${type.label} - ${formattedName}`; + const localDir = choicesToDirs[choice]; + const absoluteDir = getTestFolderPath(localDir); // Define the content template - const content = fs.readFileSync(boilerplateFilePath, "utf8").replace("{{description}}", description); + const content = fs.readFileSync(getBoilerplatePath(choice), "utf8").replace("{{description}}", description); // Ensure the directory exists - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); + if (!fs.existsSync(absoluteDir)) { + fs.mkdirSync(absoluteDir, { recursive: true }); } // Create the file with the given name - const filePath = path.join(dir, `${fileName}.test.ts`); + const filePath = path.join(absoluteDir, `${fileName}.test.ts`); if (fs.existsSync(filePath)) { - console.error(chalk.red.bold(`\n✗ File "${fileName}.test.ts" already exists!\n`)); + console.error(chalk.red.bold(`✗ File "${fileName}.test.ts" already exists!\n`)); process.exit(1); } // Write the template content to the file fs.writeFileSync(filePath, content, "utf8"); - console.log(chalk.green.bold(`\n✔ File created at: test/${type.dir}/${fileName}.test.ts\n`)); + 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)); From 50e8387f81e83bb2e30922fd58638e8b1e032af4 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Mon, 28 Jul 2025 20:13:10 -0600 Subject: [PATCH 47/59] [Refactor] Mark nickname in pokemon as optional (#6168) Mark nickname in pokemon as optional --- src/@types/illusion-data.ts | 2 +- src/field/pokemon.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/@types/illusion-data.ts b/src/@types/illusion-data.ts index b152e3fdf59..5bf86d23ac2 100644 --- a/src/@types/illusion-data.ts +++ b/src/@types/illusion-data.ts @@ -11,7 +11,7 @@ export interface IllusionData { /** The name of pokemon featured in the illusion */ name: string; /** The nickname of the pokemon featured in the illusion */ - nickname: string; + nickname?: string; /** Whether the pokemon featured in the illusion is shiny or not */ shiny: boolean; /** The variant of the pokemon featured in the illusion */ diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index f42b05bd8de..6d7b1e29227 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -213,7 +213,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * TODO: Stop treating this like a unique ID and stop treating 0 as no pokemon */ public id: number; - public nickname: string; + /** + * The Pokemon's current nickname, or `undefined` if it currently lacks one. + * If omitted, references to this should refer to the default name for this Pokemon's species. + */ + public nickname?: string; public species: PokemonSpecies; public formIndex: number; public abilityIndex: number; @@ -443,7 +447,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { getNameToRender(useIllusion = true) { const illusion = this.summonData.illusion; const name = useIllusion ? (illusion?.name ?? this.name) : this.name; - const nickname: string = useIllusion ? (illusion?.nickname ?? this.nickname) : this.nickname; + const nickname: string | undefined = useIllusion ? illusion?.nickname : this.nickname; try { if (nickname) { return decodeURIComponent(escape(atob(nickname))); // TODO: Remove `atob` and `escape`... eventually... From 745accf5b2ee4e28ce74fda91972f5bdaa43ba96 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Tue, 29 Jul 2025 08:59:41 -0700 Subject: [PATCH 48/59] [Dev] Update Vite from 6.3.5 to 7.0.6 (#6163) --- package.json | 8 +- pnpm-lock.yaml | 918 ++++++++++++++++++++++--------------------------- 2 files changed, 418 insertions(+), 508 deletions(-) diff --git a/package.json b/package.json index 02d063389b7..71a8b1ae334 100644 --- a/package.json +++ b/package.json @@ -30,19 +30,19 @@ "@biomejs/biome": "2.0.0", "@ls-lint/ls-lint": "2.3.1", "@types/jsdom": "^21.1.7", - "@types/node": "^22.16.3", + "@types/node": "^22.16.5", "@vitest/coverage-istanbul": "^3.2.4", "@vitest/expect": "^3.2.4", "chalk": "^5.4.1", "dependency-cruiser": "^16.10.4", - "inquirer": "^12.7.0", + "inquirer": "^12.8.2", "jsdom": "^26.1.0", "lefthook": "^1.12.2", "msw": "^2.10.4", "phaser3spectorjs": "^0.0.8", - "typedoc": "^0.28.7", + "typedoc": "^0.28.8", "typescript": "^5.8.3", - "vite": "^6.3.5", + "vite": "^7.0.6", "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.2.4", "vitest-canvas-mock": "^0.3.3" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 86a7bb904a1..900be6fd76e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,11 +52,11 @@ importers: specifier: ^21.1.7 version: 21.1.7 '@types/node': - specifier: ^22.16.3 - version: 22.16.3 + specifier: ^22.16.5 + version: 22.16.5 '@vitest/coverage-istanbul': specifier: ^3.2.4 - version: 3.2.4(vitest@3.2.4(@types/node@22.16.3)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.3)(typescript@5.8.3))(yaml@2.8.0)) + version: 3.2.4(vitest@3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(yaml@2.8.0)) '@vitest/expect': specifier: ^3.2.4 version: 3.2.4 @@ -67,8 +67,8 @@ importers: specifier: ^16.10.4 version: 16.10.4 inquirer: - specifier: ^12.7.0 - version: 12.7.0(@types/node@22.16.3) + specifier: ^12.8.2 + version: 12.8.2(@types/node@22.16.5) jsdom: specifier: ^26.1.0 version: 26.1.0 @@ -77,28 +77,28 @@ importers: version: 1.12.2 msw: specifier: ^2.10.4 - version: 2.10.4(@types/node@22.16.3)(typescript@5.8.3) + version: 2.10.4(@types/node@22.16.5)(typescript@5.8.3) phaser3spectorjs: specifier: ^0.0.8 version: 0.0.8 typedoc: - specifier: ^0.28.7 - version: 0.28.7(typescript@5.8.3) + specifier: ^0.28.8 + version: 0.28.8(typescript@5.8.3) typescript: specifier: ^5.8.3 version: 5.8.3 vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@22.16.3)(yaml@2.8.0) + specifier: ^7.0.6 + version: 7.0.6(@types/node@22.16.5)(yaml@2.8.0) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@22.16.3)(yaml@2.8.0)) + version: 5.1.4(typescript@5.8.3)(vite@7.0.6(@types/node@22.16.5)(yaml@2.8.0)) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@22.16.3)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.3)(typescript@5.8.3))(yaml@2.8.0) + version: 3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(yaml@2.8.0) vitest-canvas-mock: specifier: ^0.3.3 - version: 0.3.3(vitest@3.2.4(@types/node@22.16.3)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.3)(typescript@5.8.3))(yaml@2.8.0)) + version: 0.3.3(vitest@3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(yaml@2.8.0)) packages: @@ -155,8 +155,8 @@ packages: resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.27.6': - resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==} + '@babel/helpers@7.28.2': + resolution: {integrity: sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==} engines: {node: '>=6.9.0'} '@babel/parser@7.28.0': @@ -164,8 +164,8 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - '@babel/runtime@7.27.6': - resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} + '@babel/runtime@7.28.2': + resolution: {integrity: sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==} engines: {node: '>=6.9.0'} '@babel/template@7.27.2': @@ -176,8 +176,8 @@ packages: resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==} engines: {node: '>=6.9.0'} - '@babel/types@7.28.1': - resolution: {integrity: sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==} + '@babel/types@7.28.2': + resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} engines: {node: '>=6.9.0'} '@biomejs/biome@2.0.0': @@ -270,167 +270,167 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} - '@esbuild/aix-ppc64@0.25.6': - resolution: {integrity: sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==} + '@esbuild/aix-ppc64@0.25.8': + resolution: {integrity: sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.6': - resolution: {integrity: sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==} + '@esbuild/android-arm64@0.25.8': + resolution: {integrity: sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.6': - resolution: {integrity: sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==} + '@esbuild/android-arm@0.25.8': + resolution: {integrity: sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.6': - resolution: {integrity: sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==} + '@esbuild/android-x64@0.25.8': + resolution: {integrity: sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.6': - resolution: {integrity: sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==} + '@esbuild/darwin-arm64@0.25.8': + resolution: {integrity: sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.6': - resolution: {integrity: sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==} + '@esbuild/darwin-x64@0.25.8': + resolution: {integrity: sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.6': - resolution: {integrity: sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==} + '@esbuild/freebsd-arm64@0.25.8': + resolution: {integrity: sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.6': - resolution: {integrity: sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==} + '@esbuild/freebsd-x64@0.25.8': + resolution: {integrity: sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.6': - resolution: {integrity: sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==} + '@esbuild/linux-arm64@0.25.8': + resolution: {integrity: sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.6': - resolution: {integrity: sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==} + '@esbuild/linux-arm@0.25.8': + resolution: {integrity: sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.6': - resolution: {integrity: sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==} + '@esbuild/linux-ia32@0.25.8': + resolution: {integrity: sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.6': - resolution: {integrity: sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==} + '@esbuild/linux-loong64@0.25.8': + resolution: {integrity: sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.6': - resolution: {integrity: sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==} + '@esbuild/linux-mips64el@0.25.8': + resolution: {integrity: sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.6': - resolution: {integrity: sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==} + '@esbuild/linux-ppc64@0.25.8': + resolution: {integrity: sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.6': - resolution: {integrity: sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==} + '@esbuild/linux-riscv64@0.25.8': + resolution: {integrity: sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.6': - resolution: {integrity: sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==} + '@esbuild/linux-s390x@0.25.8': + resolution: {integrity: sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.6': - resolution: {integrity: sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==} + '@esbuild/linux-x64@0.25.8': + resolution: {integrity: sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.6': - resolution: {integrity: sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==} + '@esbuild/netbsd-arm64@0.25.8': + resolution: {integrity: sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.6': - resolution: {integrity: sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==} + '@esbuild/netbsd-x64@0.25.8': + resolution: {integrity: sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.6': - resolution: {integrity: sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==} + '@esbuild/openbsd-arm64@0.25.8': + resolution: {integrity: sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.6': - resolution: {integrity: sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==} + '@esbuild/openbsd-x64@0.25.8': + resolution: {integrity: sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.25.6': - resolution: {integrity: sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==} + '@esbuild/openharmony-arm64@0.25.8': + resolution: {integrity: sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.25.6': - resolution: {integrity: sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==} + '@esbuild/sunos-x64@0.25.8': + resolution: {integrity: sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.6': - resolution: {integrity: sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==} + '@esbuild/win32-arm64@0.25.8': + resolution: {integrity: sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.6': - resolution: {integrity: sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==} + '@esbuild/win32-ia32@0.25.8': + resolution: {integrity: sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.6': - resolution: {integrity: sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==} + '@esbuild/win32-x64@0.25.8': + resolution: {integrity: sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==} engines: {node: '>=18'} cpu: [x64] os: [win32] - '@gerrit0/mini-shiki@3.7.0': - resolution: {integrity: sha512-7iY9wg4FWXmeoFJpUL2u+tsmh0d0jcEJHAIzVxl3TG4KL493JNnisdLAILZ77zcD+z3J0keEXZ+lFzUgzQzPDg==} + '@gerrit0/mini-shiki@3.8.1': + resolution: {integrity: sha512-HVZW+8pxoOExr5ZMPK15U79jQAZTO/S6i5byQyyZGjtNj+qaYd82cizTncwFzTQgiLo8uUBym6vh+/1tfJklTw==} - '@inquirer/checkbox@4.1.9': - resolution: {integrity: sha512-DBJBkzI5Wx4jFaYm221LHvAhpKYkhVS0k9plqHwaHhofGNxvYB7J3Bz8w+bFJ05zaMb0sZNHo4KdmENQFlNTuQ==} + '@inquirer/checkbox@4.2.0': + resolution: {integrity: sha512-fdSw07FLJEU5vbpOPzXo5c6xmMGDzbZE2+niuDHX5N6mc6V0Ebso/q3xiHra4D73+PMsC8MJmcaZKuAAoaQsSA==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -438,8 +438,8 @@ packages: '@types/node': optional: true - '@inquirer/confirm@5.1.13': - resolution: {integrity: sha512-EkCtvp67ICIVVzjsquUiVSd+V5HRGOGQfsqA4E4vMWhYnB7InUL0pa0TIWt1i+OfP16Gkds8CdIu6yGZwOM1Yw==} + '@inquirer/confirm@5.1.14': + resolution: {integrity: sha512-5yR4IBfe0kXe59r1YCTG8WXkUbl7Z35HK87Sw+WUyGD8wNUx7JvY7laahzeytyE1oLn74bQnL7hstctQxisQ8Q==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -447,8 +447,8 @@ packages: '@types/node': optional: true - '@inquirer/core@10.1.14': - resolution: {integrity: sha512-Ma+ZpOJPewtIYl6HZHZckeX1STvDnHTCB2GVINNUlSEn2Am6LddWwfPkIGY0IUFVjUUrr/93XlBwTK6mfLjf0A==} + '@inquirer/core@10.1.15': + resolution: {integrity: sha512-8xrp836RZvKkpNbVvgWUlxjT4CraKk2q+I3Ksy+seI2zkcE+y6wNs1BVhgcv8VyImFecUhdQrYLdW32pAjwBdA==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -456,8 +456,8 @@ packages: '@types/node': optional: true - '@inquirer/editor@4.2.14': - resolution: {integrity: sha512-yd2qtLl4QIIax9DTMZ1ZN2pFrrj+yL3kgIWxm34SS6uwCr0sIhsNyudUjAo5q3TqI03xx4SEBkUJqZuAInp9uA==} + '@inquirer/editor@4.2.15': + resolution: {integrity: sha512-wst31XT8DnGOSS4nNJDIklGKnf+8shuauVrWzgKegWUe28zfCftcWZ2vktGdzJgcylWSS2SrDnYUb6alZcwnCQ==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -465,8 +465,8 @@ packages: '@types/node': optional: true - '@inquirer/expand@4.0.16': - resolution: {integrity: sha512-oiDqafWzMtofeJyyGkb1CTPaxUkjIcSxePHHQCfif8t3HV9pHcw1Kgdw3/uGpDvaFfeTluwQtWiqzPVjAqS3zA==} + '@inquirer/expand@4.0.17': + resolution: {integrity: sha512-PSqy9VmJx/VbE3CT453yOfNa+PykpKg/0SYP7odez1/NWBGuDXgPhp4AeGYYKjhLn5lUUavVS/JbeYMPdH50Mw==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -474,12 +474,12 @@ packages: '@types/node': optional: true - '@inquirer/figures@1.0.12': - resolution: {integrity: sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ==} + '@inquirer/figures@1.0.13': + resolution: {integrity: sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==} engines: {node: '>=18'} - '@inquirer/input@4.2.0': - resolution: {integrity: sha512-opqpHPB1NjAmDISi3uvZOTrjEEU5CWVu/HBkDby8t93+6UxYX0Z7Ps0Ltjm5sZiEbWenjubwUkivAEYQmy9xHw==} + '@inquirer/input@4.2.1': + resolution: {integrity: sha512-tVC+O1rBl0lJpoUZv4xY+WGWY8V5b0zxU1XDsMsIHYregdh7bN5X5QnIONNBAl0K765FYlAfNHS2Bhn7SSOVow==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -487,8 +487,8 @@ packages: '@types/node': optional: true - '@inquirer/number@3.0.16': - resolution: {integrity: sha512-kMrXAaKGavBEoBYUCgualbwA9jWUx2TjMA46ek+pEKy38+LFpL9QHlTd8PO2kWPUgI/KB+qi02o4y2rwXbzr3Q==} + '@inquirer/number@3.0.17': + resolution: {integrity: sha512-GcvGHkyIgfZgVnnimURdOueMk0CztycfC8NZTiIY9arIAkeOgt6zG57G+7vC59Jns3UX27LMkPKnKWAOF5xEYg==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -496,8 +496,8 @@ packages: '@types/node': optional: true - '@inquirer/password@4.0.16': - resolution: {integrity: sha512-g8BVNBj5Zeb5/Y3cSN+hDUL7CsIFDIuVxb9EPty3lkxBaYpjL5BNRKSYOF9yOLe+JOcKFd+TSVeADQ4iSY7rbg==} + '@inquirer/password@4.0.17': + resolution: {integrity: sha512-DJolTnNeZ00E1+1TW+8614F7rOJJCM4y4BAGQ3Gq6kQIG+OJ4zr3GLjIjVVJCbKsk2jmkmv6v2kQuN/vriHdZA==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -505,8 +505,8 @@ packages: '@types/node': optional: true - '@inquirer/prompts@7.6.0': - resolution: {integrity: sha512-jAhL7tyMxB3Gfwn4HIJ0yuJ5pvcB5maYUcouGcgd/ub79f9MqZ+aVnBtuFf+VC2GTkCBF+R+eo7Vi63w5VZlzw==} + '@inquirer/prompts@7.7.1': + resolution: {integrity: sha512-XDxPrEWeWUBy8scAXzXuFY45r/q49R0g72bUzgQXZ1DY/xEFX+ESDMkTQolcb5jRBzaNJX2W8XQl6krMNDTjaA==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -514,8 +514,8 @@ packages: '@types/node': optional: true - '@inquirer/rawlist@4.1.4': - resolution: {integrity: sha512-5GGvxVpXXMmfZNtvWw4IsHpR7RzqAR624xtkPd1NxxlV5M+pShMqzL4oRddRkg8rVEOK9fKdJp1jjVML2Lr7TQ==} + '@inquirer/rawlist@4.1.5': + resolution: {integrity: sha512-R5qMyGJqtDdi4Ht521iAkNqyB6p2UPuZUbMifakg1sWtu24gc2Z8CJuw8rP081OckNDMgtDCuLe42Q2Kr3BolA==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -523,8 +523,8 @@ packages: '@types/node': optional: true - '@inquirer/search@3.0.16': - resolution: {integrity: sha512-POCmXo+j97kTGU6aeRjsPyuCpQQfKcMXdeTMw708ZMtWrj5aykZvlUxH4Qgz3+Y1L/cAVZsSpA+UgZCu2GMOMg==} + '@inquirer/search@3.0.17': + resolution: {integrity: sha512-CuBU4BAGFqRYors4TNCYzy9X3DpKtgIW4Boi0WNkm4Ei1hvY9acxKdBdyqzqBCEe4YxSdaQQsasJlFlUJNgojw==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -532,8 +532,8 @@ packages: '@types/node': optional: true - '@inquirer/select@4.2.4': - resolution: {integrity: sha512-unTppUcTjmnbl/q+h8XeQDhAqIOmwWYWNyiiP2e3orXrg6tOaa5DHXja9PChCSbChOsktyKgOieRZFnajzxoBg==} + '@inquirer/select@4.3.1': + resolution: {integrity: sha512-Gfl/5sqOF5vS/LIrSndFgOh7jgoe0UXEizDqahFRkq5aJBLegZ6WjuMh/hVEJwlFQjyLq1z9fRtvUMkb7jM1LA==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -541,8 +541,8 @@ packages: '@types/node': optional: true - '@inquirer/type@3.0.7': - resolution: {integrity: sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==} + '@inquirer/type@3.0.8': + resolution: {integrity: sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -580,8 +580,8 @@ packages: '@material/material-color-utilities@0.2.7': resolution: {integrity: sha512-0FCeqG6WvK4/Cc06F/xXMd/pv4FeisI0c1tUpBbfhA2n9Y8eZEv4Karjbmf2ZqQCPUWMrGp8A571tCjizxoTiQ==} - '@mswjs/interceptors@0.39.2': - resolution: {integrity: sha512-RuzCup9Ct91Y7V79xwCb146RaBRHZ7NBbrIUySumd1rpKqHL5OonaqrGIbug5hNwP/fRyxFMA6ISgw4FTtYFYg==} + '@mswjs/interceptors@0.39.4': + resolution: {integrity: sha512-B82DbrGVCIBrNEfRJbqUFB0eNz0wVzqbenEpmbE71XLVU4yKZbDnRBuxz+7udc/uM7LDWDD4sRJ5tISzHf2QkQ==} engines: {node: '>=18'} '@open-draft/deferred-promise@2.2.0': @@ -593,161 +593,121 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} - '@oxlint/darwin-arm64@1.6.0': - resolution: {integrity: sha512-m3wyqBh1TOHjpr/dXeIZY7OoX+MQazb+bMHQdDtwUvefrafUx+5YHRvulYh1sZSQ449nQ3nk3qj5qj535vZRjg==} - cpu: [arm64] - os: [darwin] - - '@oxlint/darwin-x64@1.6.0': - resolution: {integrity: sha512-75fJfF/9xNypr7cnOYoZBhfmG1yP7ex3pUOeYGakmtZRffO9z1i1quLYhjZsmaDXsAIZ3drMhenYHMmFKS3SRg==} - cpu: [x64] - os: [darwin] - - '@oxlint/linux-arm64-gnu@1.6.0': - resolution: {integrity: sha512-YhXGf0FXa72bEt4F7eTVKx5X3zWpbAOPnaA/dZ6/g8tGhw1m9IFjrabVHFjzcx3dQny4MgA59EhyElkDvpUe8A==} - cpu: [arm64] - os: [linux] - - '@oxlint/linux-arm64-musl@1.6.0': - resolution: {integrity: sha512-T3JDhx8mjGjvh5INsPZJrlKHmZsecgDYvtvussKRdkc1Nnn7WC+jH9sh5qlmYvwzvmetlPVNezAoNvmGO9vtMg==} - cpu: [arm64] - os: [linux] - - '@oxlint/linux-x64-gnu@1.6.0': - resolution: {integrity: sha512-Dx7ghtAl8aXBdqofJpi338At6lkeCtTfoinTYQXd9/TEJx+f+zCGNlQO6nJz3ydJBX48FDuOFKkNC+lUlWrd8w==} - cpu: [x64] - os: [linux] - - '@oxlint/linux-x64-musl@1.6.0': - resolution: {integrity: sha512-7KvMGdWmAZtAtg6IjoEJHKxTXdAcrHnUnqfgs0JpXst7trquV2mxBeRZusQXwxpu4HCSomKMvJfsp1qKaqSFDg==} - cpu: [x64] - os: [linux] - - '@oxlint/win32-arm64@1.6.0': - resolution: {integrity: sha512-iSGC9RwX+dl7o5KFr5aH7Gq3nFbkq/3Gda6mxNPMvNkWrgXdIyiINxpyD8hJu566M+QSv1wEAu934BZotFDyoQ==} - cpu: [arm64] - os: [win32] - - '@oxlint/win32-x64@1.6.0': - resolution: {integrity: sha512-jOj3L/gfLc0IwgOTkZMiZ5c673i/hbAmidlaylT0gE6H18hln9HxPgp5GCf4E4y6mwEJlW8QC5hQi221+9otdA==} - cpu: [x64] - os: [win32] - '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@rollup/rollup-android-arm-eabi@4.45.0': - resolution: {integrity: sha512-2o/FgACbji4tW1dzXOqAV15Eu7DdgbKsF2QKcxfG4xbh5iwU7yr5RRP5/U+0asQliSYv5M4o7BevlGIoSL0LXg==} + '@rollup/rollup-android-arm-eabi@4.46.1': + resolution: {integrity: sha512-oENme6QxtLCqjChRUUo3S6X8hjCXnWmJWnedD7VbGML5GUtaOtAyx+fEEXnBXVf0CBZApMQU0Idwi0FmyxzQhw==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.45.0': - resolution: {integrity: sha512-PSZ0SvMOjEAxwZeTx32eI/j5xSYtDCRxGu5k9zvzoY77xUNssZM+WV6HYBLROpY5CkXsbQjvz40fBb7WPwDqtQ==} + '@rollup/rollup-android-arm64@4.46.1': + resolution: {integrity: sha512-OikvNT3qYTl9+4qQ9Bpn6+XHM+ogtFadRLuT2EXiFQMiNkXFLQfNVppi5o28wvYdHL2s3fM0D/MZJ8UkNFZWsw==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.45.0': - resolution: {integrity: sha512-BA4yPIPssPB2aRAWzmqzQ3y2/KotkLyZukVB7j3psK/U3nVJdceo6qr9pLM2xN6iRP/wKfxEbOb1yrlZH6sYZg==} + '@rollup/rollup-darwin-arm64@4.46.1': + resolution: {integrity: sha512-EFYNNGij2WllnzljQDQnlFTXzSJw87cpAs4TVBAWLdkvic5Uh5tISrIL6NRcxoh/b2EFBG/TK8hgRrGx94zD4A==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.45.0': - resolution: {integrity: sha512-Pr2o0lvTwsiG4HCr43Zy9xXrHspyMvsvEw4FwKYqhli4FuLE5FjcZzuQ4cfPe0iUFCvSQG6lACI0xj74FDZKRA==} + '@rollup/rollup-darwin-x64@4.46.1': + resolution: {integrity: sha512-ZaNH06O1KeTug9WI2+GRBE5Ujt9kZw4a1+OIwnBHal92I8PxSsl5KpsrPvthRynkhMck4XPdvY0z26Cym/b7oA==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.45.0': - resolution: {integrity: sha512-lYE8LkE5h4a/+6VnnLiL14zWMPnx6wNbDG23GcYFpRW1V9hYWHAw9lBZ6ZUIrOaoK7NliF1sdwYGiVmziUF4vA==} + '@rollup/rollup-freebsd-arm64@4.46.1': + resolution: {integrity: sha512-n4SLVebZP8uUlJ2r04+g2U/xFeiQlw09Me5UFqny8HGbARl503LNH5CqFTb5U5jNxTouhRjai6qPT0CR5c/Iig==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.45.0': - resolution: {integrity: sha512-PVQWZK9sbzpvqC9Q0GlehNNSVHR+4m7+wET+7FgSnKG3ci5nAMgGmr9mGBXzAuE5SvguCKJ6mHL6vq1JaJ/gvw==} + '@rollup/rollup-freebsd-x64@4.46.1': + resolution: {integrity: sha512-8vu9c02F16heTqpvo3yeiu7Vi1REDEC/yES/dIfq3tSXe6mLndiwvYr3AAvd1tMNUqE9yeGYa5w7PRbI5QUV+w==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.45.0': - resolution: {integrity: sha512-hLrmRl53prCcD+YXTfNvXd776HTxNh8wPAMllusQ+amcQmtgo3V5i/nkhPN6FakW+QVLoUUr2AsbtIRPFU3xIA==} + '@rollup/rollup-linux-arm-gnueabihf@4.46.1': + resolution: {integrity: sha512-K4ncpWl7sQuyp6rWiGUvb6Q18ba8mzM0rjWJ5JgYKlIXAau1db7hZnR0ldJvqKWWJDxqzSLwGUhA4jp+KqgDtQ==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.45.0': - resolution: {integrity: sha512-XBKGSYcrkdiRRjl+8XvrUR3AosXU0NvF7VuqMsm7s5nRy+nt58ZMB19Jdp1RdqewLcaYnpk8zeVs/4MlLZEJxw==} + '@rollup/rollup-linux-arm-musleabihf@4.46.1': + resolution: {integrity: sha512-YykPnXsjUjmXE6j6k2QBBGAn1YsJUix7pYaPLK3RVE0bQL2jfdbfykPxfF8AgBlqtYbfEnYHmLXNa6QETjdOjQ==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.45.0': - resolution: {integrity: sha512-fRvZZPUiBz7NztBE/2QnCS5AtqLVhXmUOPj9IHlfGEXkapgImf4W9+FSkL8cWqoAjozyUzqFmSc4zh2ooaeF6g==} + '@rollup/rollup-linux-arm64-gnu@4.46.1': + resolution: {integrity: sha512-kKvqBGbZ8i9pCGW3a1FH3HNIVg49dXXTsChGFsHGXQaVJPLA4f/O+XmTxfklhccxdF5FefUn2hvkoGJH0ScWOA==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.45.0': - resolution: {integrity: sha512-Btv2WRZOcUGi8XU80XwIvzTg4U6+l6D0V6sZTrZx214nrwxw5nAi8hysaXj/mctyClWgesyuxbeLylCBNauimg==} + '@rollup/rollup-linux-arm64-musl@4.46.1': + resolution: {integrity: sha512-zzX5nTw1N1plmqC9RGC9vZHFuiM7ZP7oSWQGqpbmfjK7p947D518cVK1/MQudsBdcD84t6k70WNczJOct6+hdg==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.45.0': - resolution: {integrity: sha512-Li0emNnwtUZdLwHjQPBxn4VWztcrw/h7mgLyHiEI5Z0MhpeFGlzaiBHpSNVOMB/xucjXTTcO+dhv469Djr16KA==} + '@rollup/rollup-linux-loongarch64-gnu@4.46.1': + resolution: {integrity: sha512-O8CwgSBo6ewPpktFfSDgB6SJN9XDcPSvuwxfejiddbIC/hn9Tg6Ai0f0eYDf3XvB/+PIWzOQL+7+TZoB8p9Yuw==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.45.0': - resolution: {integrity: sha512-sB8+pfkYx2kvpDCfd63d5ScYT0Fz1LO6jIb2zLZvmK9ob2D8DeVqrmBDE0iDK8KlBVmsTNzrjr3G1xV4eUZhSw==} + '@rollup/rollup-linux-ppc64-gnu@4.46.1': + resolution: {integrity: sha512-JnCfFVEKeq6G3h3z8e60kAp8Rd7QVnWCtPm7cxx+5OtP80g/3nmPtfdCXbVl063e3KsRnGSKDHUQMydmzc/wBA==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.45.0': - resolution: {integrity: sha512-5GQ6PFhh7E6jQm70p1aW05G2cap5zMOvO0se5JMecHeAdj5ZhWEHbJ4hiKpfi1nnnEdTauDXxPgXae/mqjow9w==} + '@rollup/rollup-linux-riscv64-gnu@4.46.1': + resolution: {integrity: sha512-dVxuDqS237eQXkbYzQQfdf/njgeNw6LZuVyEdUaWwRpKHhsLI+y4H/NJV8xJGU19vnOJCVwaBFgr936FHOnJsQ==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.45.0': - resolution: {integrity: sha512-N/euLsBd1rekWcuduakTo/dJw6U6sBP3eUq+RXM9RNfPuWTvG2w/WObDkIvJ2KChy6oxZmOSC08Ak2OJA0UiAA==} + '@rollup/rollup-linux-riscv64-musl@4.46.1': + resolution: {integrity: sha512-CvvgNl2hrZrTR9jXK1ye0Go0HQRT6ohQdDfWR47/KFKiLd5oN5T14jRdUVGF4tnsN8y9oSfMOqH6RuHh+ck8+w==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.45.0': - resolution: {integrity: sha512-2l9sA7d7QdikL0xQwNMO3xURBUNEWyHVHfAsHsUdq+E/pgLTUcCE+gih5PCdmyHmfTDeXUWVhqL0WZzg0nua3g==} + '@rollup/rollup-linux-s390x-gnu@4.46.1': + resolution: {integrity: sha512-x7ANt2VOg2565oGHJ6rIuuAon+A8sfe1IeUx25IKqi49OjSr/K3awoNqr9gCwGEJo9OuXlOn+H2p1VJKx1psxA==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.45.0': - resolution: {integrity: sha512-XZdD3fEEQcwG2KrJDdEQu7NrHonPxxaV0/w2HpvINBdcqebz1aL+0vM2WFJq4DeiAVT6F5SUQas65HY5JDqoPw==} + '@rollup/rollup-linux-x64-gnu@4.46.1': + resolution: {integrity: sha512-9OADZYryz/7E8/qt0vnaHQgmia2Y0wrjSSn1V/uL+zw/i7NUhxbX4cHXdEQ7dnJgzYDS81d8+tf6nbIdRFZQoQ==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.45.0': - resolution: {integrity: sha512-7ayfgvtmmWgKWBkCGg5+xTQ0r5V1owVm67zTrsEY1008L5ro7mCyGYORomARt/OquB9KY7LpxVBZes+oSniAAQ==} + '@rollup/rollup-linux-x64-musl@4.46.1': + resolution: {integrity: sha512-NuvSCbXEKY+NGWHyivzbjSVJi68Xfq1VnIvGmsuXs6TCtveeoDRKutI5vf2ntmNnVq64Q4zInet0UDQ+yMB6tA==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.45.0': - resolution: {integrity: sha512-B+IJgcBnE2bm93jEW5kHisqvPITs4ddLOROAcOc/diBgrEiQJJ6Qcjby75rFSmH5eMGrqJryUgJDhrfj942apQ==} + '@rollup/rollup-win32-arm64-msvc@4.46.1': + resolution: {integrity: sha512-mWz+6FSRb82xuUMMV1X3NGiaPFqbLN9aIueHleTZCc46cJvwTlvIh7reQLk4p97dv0nddyewBhwzryBHH7wtPw==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.45.0': - resolution: {integrity: sha512-+CXwwG66g0/FpWOnP/v1HnrGVSOygK/osUbu3wPRy8ECXjoYKjRAyfxYpDQOfghC5qPJYLPH0oN4MCOjwgdMug==} + '@rollup/rollup-win32-ia32-msvc@4.46.1': + resolution: {integrity: sha512-7Thzy9TMXDw9AU4f4vsLNBxh7/VOKuXi73VH3d/kHGr0tZ3x/ewgL9uC7ojUKmH1/zvmZe2tLapYcZllk3SO8Q==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.45.0': - resolution: {integrity: sha512-SRf1cytG7wqcHVLrBc9VtPK4pU5wxiB/lNIkNmW2ApKXIg+RpqwHfsaEK+e7eH4A1BpI6BX/aBWXxZCIrJg3uA==} + '@rollup/rollup-win32-x64-msvc@4.46.1': + resolution: {integrity: sha512-7GVB4luhFmGUNXXJhH2jJwZCFB3pIOixv2E3s17GQHBFUOQaISlt7aGcQgqvCaDSxTZJUzlK/QJ1FN8S94MrzQ==} cpu: [x64] os: [win32] - '@shikijs/engine-oniguruma@3.7.0': - resolution: {integrity: sha512-5BxcD6LjVWsGu4xyaBC5bu8LdNgPCVBnAkWTtOCs/CZxcB22L8rcoWfv7Hh/3WooVjBZmFtyxhgvkQFedPGnFw==} + '@shikijs/engine-oniguruma@3.8.1': + resolution: {integrity: sha512-KGQJZHlNY7c656qPFEQpIoqOuC4LrxjyNndRdzk5WKB/Ie87+NJCF1xo9KkOUxwxylk7rT6nhlZyTGTC4fCe1g==} - '@shikijs/langs@3.7.0': - resolution: {integrity: sha512-1zYtdfXLr9xDKLTGy5kb7O0zDQsxXiIsw1iIBcNOO8Yi5/Y1qDbJ+0VsFoqTlzdmneO8Ij35g7QKF8kcLyznCQ==} + '@shikijs/langs@3.8.1': + resolution: {integrity: sha512-TjOFg2Wp1w07oKnXjs0AUMb4kJvujML+fJ1C5cmEj45lhjbUXtziT1x2bPQb9Db6kmPhkG5NI2tgYW1/DzhUuQ==} - '@shikijs/themes@3.7.0': - resolution: {integrity: sha512-VJx8497iZPy5zLiiCTSIaOChIcKQwR0FebwE9S3rcN0+J/GTWwQ1v/bqhTbpbY3zybPKeO8wdammqkpXc4NVjQ==} + '@shikijs/themes@3.8.1': + resolution: {integrity: sha512-Vu3t3BBLifc0GB0UPg2Pox1naTemrrvyZv2lkiSw3QayVV60me1ujFQwPZGgUTmwXl1yhCPW8Lieesm0CYruLQ==} - '@shikijs/types@3.7.0': - resolution: {integrity: sha512-MGaLeaRlSWpnP0XSAum3kP3a8vtcTsITqoEPYdt3lQG3YCdQH4DnEhodkYcNMcU0uW0RffhoD1O3e0vG5eSBBg==} + '@shikijs/types@3.8.1': + resolution: {integrity: sha512-5C39Q8/8r1I26suLh+5TPk1DTrbY/kn3IdWA5HdizR0FhlhD05zx5nKCqhzSfDHH3p4S0ZefxWd77DLV+8FhGg==} '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} @@ -770,8 +730,8 @@ packages: '@types/jsdom@21.1.7': resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==} - '@types/node@22.16.3': - resolution: {integrity: sha512-sr4Xz74KOUeYadexo1r8imhRtlVXcs+j3XK3TcoiYk7B1t3YRVJgtaD3cwX73NYb71pmVuMLNRhJ9XKdoDB74g==} + '@types/node@22.16.5': + resolution: {integrity: sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==} '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} @@ -1008,8 +968,8 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - electron-to-chromium@1.5.182: - resolution: {integrity: sha512-Lv65Btwv9W4J9pyODI6EWpdnhfvrve/us5h1WspW8B2Fb0366REPtY3hX7ounk1CkV/TBjWCEvCBBbYbmV0qCA==} + electron-to-chromium@1.5.191: + resolution: {integrity: sha512-xcwe9ELcuxYLUFqZZxL19Z6HVKcvNkIwhbHUz7L3us6u12yR+7uY89dSl570f/IqNthx8dAw3tojG7i4Ni4tDA==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1044,8 +1004,8 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} - esbuild@0.25.6: - resolution: {integrity: sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==} + esbuild@0.25.8: + resolution: {integrity: sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==} engines: {node: '>=18'} hasBin: true @@ -1229,8 +1189,8 @@ packages: resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - inquirer@12.7.0: - resolution: {integrity: sha512-KKFRc++IONSyE2UYw9CJ1V0IWx5yQKomwB+pp3cWomWs+v2+ZsG11G2OVfAjFS6WWCppKw+RfKmpqGfSzD5QBQ==} + inquirer@12.8.2: + resolution: {integrity: sha512-oBDL9f4+cDambZVJdfJu2M5JQfvaug9lbo6fKDlFV40i8t3FGA1Db67ov5Hp5DInG4zmXhHWTSnlXBntnJ7GMA==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -1408,8 +1368,8 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - loupe@3.1.4: - resolution: {integrity: sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==} + loupe@3.2.0: + resolution: {integrity: sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==} lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -1501,8 +1461,8 @@ packages: node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} - nwsapi@2.2.20: - resolution: {integrity: sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==} + nwsapi@2.2.21: + resolution: {integrity: sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==} object-keys@1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} @@ -1518,11 +1478,6 @@ packages: outvariant@1.4.3: resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} - oxlint@1.6.0: - resolution: {integrity: sha512-jtaD65PqzIa1udvSxxscTKBxYKuZoFXyKGLiU1Qjo1ulq3uv/fQDtoV1yey1FrQZrQjACGPi1Widsy1TucC7Jg==} - engines: {node: '>=8.*'} - hasBin: true - package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -1568,19 +1523,14 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@4.0.2: - resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} - prettier@3.6.2: - resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} - engines: {node: '>=14'} - hasBin: true - process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -1629,16 +1579,16 @@ packages: engines: {node: '>= 0.4'} hasBin: true - rollup@4.45.0: - resolution: {integrity: sha512-WLjEcJRIo7i3WDDgOIJqVI2d+lAC3EwvOGy+Xfq6hs+GQuAA4Di/H72xmXkOhrIWFg2PFYSKZYfH0f4vfKXN4A==} + rollup@4.46.1: + resolution: {integrity: sha512-33xGNBsDJAkzt0PvninskHlWnTIPgDtTwhg0U38CUoNP/7H6wI2Cz6dUeoNPbjdTdsYTGuiFFASuUOWovH0SyQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true rrweb-cssom@0.8.0: resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} - run-async@4.0.4: - resolution: {integrity: sha512-2cgeRHnV11lSXBEhq7sN7a5UVjTKm9JTb9x8ApIT//16D7QL96AgnNeWSGoB4gIHc0iYw/Ha0Z+waBaCYZVNhg==} + run-async@4.0.5: + resolution: {integrity: sha512-oN9GTgxUNDBumHTTDmQ8dep6VIJbgj9S3dPP+9XylVLIK4xB9XTXtKWROd5pnhdXR9k0EgO1JRcNh0T+Ny2FsA==} engines: {node: '>=0.12.0'} rxjs@7.8.2: @@ -1833,8 +1783,8 @@ packages: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} - typedoc@0.28.7: - resolution: {integrity: sha512-lpz0Oxl6aidFkmS90VQDQjk/Qf2iw0IUvFqirdONBdj7jPSN9mGXhy66BcGNDxx5ZMyKKiBVAREvPEzT6Uxipw==} + typedoc@0.28.8: + resolution: {integrity: sha512-16GfLopc8icHfdvqZDqdGBoS2AieIRP2rpf9mU+MgN+gGLyEQvAO0QgOa6NJ5QNmQi0LFrDY9in4F2fUNKgJKA==} engines: {node: '>= 18', pnpm: '>= 10'} hasBin: true peerDependencies: @@ -1880,19 +1830,19 @@ packages: vite: optional: true - vite@6.3.5: - resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + vite@7.0.6: + resolution: {integrity: sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==} + engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@types/node': ^20.19.0 || >=22.12.0 jiti: '>=1.21.0' - less: '*' + less: ^4.0.0 lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 terser: ^5.16.0 tsx: ^4.8.1 yaml: ^2.4.2 @@ -2082,11 +2032,11 @@ snapshots: '@babel/generator': 7.28.0 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) - '@babel/helpers': 7.27.6 + '@babel/helpers': 7.28.2 '@babel/parser': 7.28.0 '@babel/template': 7.27.2 '@babel/traverse': 7.28.0 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 convert-source-map: 2.0.0 debug: 4.4.1 gensync: 1.0.0-beta.2 @@ -2098,7 +2048,7 @@ snapshots: '@babel/generator@7.28.0': dependencies: '@babel/parser': 7.28.0 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 '@jridgewell/gen-mapping': 0.3.12 '@jridgewell/trace-mapping': 0.3.29 jsesc: 3.1.0 @@ -2116,7 +2066,7 @@ snapshots: '@babel/helper-module-imports@7.27.1': dependencies: '@babel/traverse': 7.28.0 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 transitivePeerDependencies: - supports-color @@ -2135,22 +2085,22 @@ snapshots: '@babel/helper-validator-option@7.27.1': {} - '@babel/helpers@7.27.6': + '@babel/helpers@7.28.2': dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 '@babel/parser@7.28.0': dependencies: - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 - '@babel/runtime@7.27.6': {} + '@babel/runtime@7.28.2': {} '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 '@babel/parser': 7.28.0 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 '@babel/traverse@7.28.0': dependencies: @@ -2159,12 +2109,12 @@ snapshots: '@babel/helper-globals': 7.28.0 '@babel/parser': 7.28.0 '@babel/template': 7.27.2 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 debug: 4.4.1 transitivePeerDependencies: - supports-color - '@babel/types@7.28.1': + '@babel/types@7.28.2': dependencies: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 @@ -2237,113 +2187,113 @@ snapshots: '@csstools/css-tokenizer@3.0.4': {} - '@esbuild/aix-ppc64@0.25.6': + '@esbuild/aix-ppc64@0.25.8': optional: true - '@esbuild/android-arm64@0.25.6': + '@esbuild/android-arm64@0.25.8': optional: true - '@esbuild/android-arm@0.25.6': + '@esbuild/android-arm@0.25.8': optional: true - '@esbuild/android-x64@0.25.6': + '@esbuild/android-x64@0.25.8': optional: true - '@esbuild/darwin-arm64@0.25.6': + '@esbuild/darwin-arm64@0.25.8': optional: true - '@esbuild/darwin-x64@0.25.6': + '@esbuild/darwin-x64@0.25.8': optional: true - '@esbuild/freebsd-arm64@0.25.6': + '@esbuild/freebsd-arm64@0.25.8': optional: true - '@esbuild/freebsd-x64@0.25.6': + '@esbuild/freebsd-x64@0.25.8': optional: true - '@esbuild/linux-arm64@0.25.6': + '@esbuild/linux-arm64@0.25.8': optional: true - '@esbuild/linux-arm@0.25.6': + '@esbuild/linux-arm@0.25.8': optional: true - '@esbuild/linux-ia32@0.25.6': + '@esbuild/linux-ia32@0.25.8': optional: true - '@esbuild/linux-loong64@0.25.6': + '@esbuild/linux-loong64@0.25.8': optional: true - '@esbuild/linux-mips64el@0.25.6': + '@esbuild/linux-mips64el@0.25.8': optional: true - '@esbuild/linux-ppc64@0.25.6': + '@esbuild/linux-ppc64@0.25.8': optional: true - '@esbuild/linux-riscv64@0.25.6': + '@esbuild/linux-riscv64@0.25.8': optional: true - '@esbuild/linux-s390x@0.25.6': + '@esbuild/linux-s390x@0.25.8': optional: true - '@esbuild/linux-x64@0.25.6': + '@esbuild/linux-x64@0.25.8': optional: true - '@esbuild/netbsd-arm64@0.25.6': + '@esbuild/netbsd-arm64@0.25.8': optional: true - '@esbuild/netbsd-x64@0.25.6': + '@esbuild/netbsd-x64@0.25.8': optional: true - '@esbuild/openbsd-arm64@0.25.6': + '@esbuild/openbsd-arm64@0.25.8': optional: true - '@esbuild/openbsd-x64@0.25.6': + '@esbuild/openbsd-x64@0.25.8': optional: true - '@esbuild/openharmony-arm64@0.25.6': + '@esbuild/openharmony-arm64@0.25.8': optional: true - '@esbuild/sunos-x64@0.25.6': + '@esbuild/sunos-x64@0.25.8': optional: true - '@esbuild/win32-arm64@0.25.6': + '@esbuild/win32-arm64@0.25.8': optional: true - '@esbuild/win32-ia32@0.25.6': + '@esbuild/win32-ia32@0.25.8': optional: true - '@esbuild/win32-x64@0.25.6': + '@esbuild/win32-x64@0.25.8': optional: true - '@gerrit0/mini-shiki@3.7.0': + '@gerrit0/mini-shiki@3.8.1': dependencies: - '@shikijs/engine-oniguruma': 3.7.0 - '@shikijs/langs': 3.7.0 - '@shikijs/themes': 3.7.0 - '@shikijs/types': 3.7.0 + '@shikijs/engine-oniguruma': 3.8.1 + '@shikijs/langs': 3.8.1 + '@shikijs/themes': 3.8.1 + '@shikijs/types': 3.8.1 '@shikijs/vscode-textmate': 10.0.2 - '@inquirer/checkbox@4.1.9(@types/node@22.16.3)': + '@inquirer/checkbox@4.2.0(@types/node@22.16.5)': dependencies: - '@inquirer/core': 10.1.14(@types/node@22.16.3) - '@inquirer/figures': 1.0.12 - '@inquirer/type': 3.0.7(@types/node@22.16.3) + '@inquirer/core': 10.1.15(@types/node@22.16.5) + '@inquirer/figures': 1.0.13 + '@inquirer/type': 3.0.8(@types/node@22.16.5) ansi-escapes: 4.3.2 yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.16.3 + '@types/node': 22.16.5 - '@inquirer/confirm@5.1.13(@types/node@22.16.3)': + '@inquirer/confirm@5.1.14(@types/node@22.16.5)': dependencies: - '@inquirer/core': 10.1.14(@types/node@22.16.3) - '@inquirer/type': 3.0.7(@types/node@22.16.3) + '@inquirer/core': 10.1.15(@types/node@22.16.5) + '@inquirer/type': 3.0.8(@types/node@22.16.5) optionalDependencies: - '@types/node': 22.16.3 + '@types/node': 22.16.5 - '@inquirer/core@10.1.14(@types/node@22.16.3)': + '@inquirer/core@10.1.15(@types/node@22.16.5)': dependencies: - '@inquirer/figures': 1.0.12 - '@inquirer/type': 3.0.7(@types/node@22.16.3) + '@inquirer/figures': 1.0.13 + '@inquirer/type': 3.0.8(@types/node@22.16.5) ansi-escapes: 4.3.2 cli-width: 4.1.0 mute-stream: 2.0.0 @@ -2351,93 +2301,93 @@ snapshots: wrap-ansi: 6.2.0 yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.16.3 + '@types/node': 22.16.5 - '@inquirer/editor@4.2.14(@types/node@22.16.3)': + '@inquirer/editor@4.2.15(@types/node@22.16.5)': dependencies: - '@inquirer/core': 10.1.14(@types/node@22.16.3) - '@inquirer/type': 3.0.7(@types/node@22.16.3) + '@inquirer/core': 10.1.15(@types/node@22.16.5) + '@inquirer/type': 3.0.8(@types/node@22.16.5) external-editor: 3.1.0 optionalDependencies: - '@types/node': 22.16.3 + '@types/node': 22.16.5 - '@inquirer/expand@4.0.16(@types/node@22.16.3)': + '@inquirer/expand@4.0.17(@types/node@22.16.5)': dependencies: - '@inquirer/core': 10.1.14(@types/node@22.16.3) - '@inquirer/type': 3.0.7(@types/node@22.16.3) + '@inquirer/core': 10.1.15(@types/node@22.16.5) + '@inquirer/type': 3.0.8(@types/node@22.16.5) yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.16.3 + '@types/node': 22.16.5 - '@inquirer/figures@1.0.12': {} + '@inquirer/figures@1.0.13': {} - '@inquirer/input@4.2.0(@types/node@22.16.3)': + '@inquirer/input@4.2.1(@types/node@22.16.5)': dependencies: - '@inquirer/core': 10.1.14(@types/node@22.16.3) - '@inquirer/type': 3.0.7(@types/node@22.16.3) + '@inquirer/core': 10.1.15(@types/node@22.16.5) + '@inquirer/type': 3.0.8(@types/node@22.16.5) optionalDependencies: - '@types/node': 22.16.3 + '@types/node': 22.16.5 - '@inquirer/number@3.0.16(@types/node@22.16.3)': + '@inquirer/number@3.0.17(@types/node@22.16.5)': dependencies: - '@inquirer/core': 10.1.14(@types/node@22.16.3) - '@inquirer/type': 3.0.7(@types/node@22.16.3) + '@inquirer/core': 10.1.15(@types/node@22.16.5) + '@inquirer/type': 3.0.8(@types/node@22.16.5) optionalDependencies: - '@types/node': 22.16.3 + '@types/node': 22.16.5 - '@inquirer/password@4.0.16(@types/node@22.16.3)': + '@inquirer/password@4.0.17(@types/node@22.16.5)': dependencies: - '@inquirer/core': 10.1.14(@types/node@22.16.3) - '@inquirer/type': 3.0.7(@types/node@22.16.3) + '@inquirer/core': 10.1.15(@types/node@22.16.5) + '@inquirer/type': 3.0.8(@types/node@22.16.5) ansi-escapes: 4.3.2 optionalDependencies: - '@types/node': 22.16.3 + '@types/node': 22.16.5 - '@inquirer/prompts@7.6.0(@types/node@22.16.3)': + '@inquirer/prompts@7.7.1(@types/node@22.16.5)': dependencies: - '@inquirer/checkbox': 4.1.9(@types/node@22.16.3) - '@inquirer/confirm': 5.1.13(@types/node@22.16.3) - '@inquirer/editor': 4.2.14(@types/node@22.16.3) - '@inquirer/expand': 4.0.16(@types/node@22.16.3) - '@inquirer/input': 4.2.0(@types/node@22.16.3) - '@inquirer/number': 3.0.16(@types/node@22.16.3) - '@inquirer/password': 4.0.16(@types/node@22.16.3) - '@inquirer/rawlist': 4.1.4(@types/node@22.16.3) - '@inquirer/search': 3.0.16(@types/node@22.16.3) - '@inquirer/select': 4.2.4(@types/node@22.16.3) + '@inquirer/checkbox': 4.2.0(@types/node@22.16.5) + '@inquirer/confirm': 5.1.14(@types/node@22.16.5) + '@inquirer/editor': 4.2.15(@types/node@22.16.5) + '@inquirer/expand': 4.0.17(@types/node@22.16.5) + '@inquirer/input': 4.2.1(@types/node@22.16.5) + '@inquirer/number': 3.0.17(@types/node@22.16.5) + '@inquirer/password': 4.0.17(@types/node@22.16.5) + '@inquirer/rawlist': 4.1.5(@types/node@22.16.5) + '@inquirer/search': 3.0.17(@types/node@22.16.5) + '@inquirer/select': 4.3.1(@types/node@22.16.5) optionalDependencies: - '@types/node': 22.16.3 + '@types/node': 22.16.5 - '@inquirer/rawlist@4.1.4(@types/node@22.16.3)': + '@inquirer/rawlist@4.1.5(@types/node@22.16.5)': dependencies: - '@inquirer/core': 10.1.14(@types/node@22.16.3) - '@inquirer/type': 3.0.7(@types/node@22.16.3) + '@inquirer/core': 10.1.15(@types/node@22.16.5) + '@inquirer/type': 3.0.8(@types/node@22.16.5) yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.16.3 + '@types/node': 22.16.5 - '@inquirer/search@3.0.16(@types/node@22.16.3)': + '@inquirer/search@3.0.17(@types/node@22.16.5)': dependencies: - '@inquirer/core': 10.1.14(@types/node@22.16.3) - '@inquirer/figures': 1.0.12 - '@inquirer/type': 3.0.7(@types/node@22.16.3) + '@inquirer/core': 10.1.15(@types/node@22.16.5) + '@inquirer/figures': 1.0.13 + '@inquirer/type': 3.0.8(@types/node@22.16.5) yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.16.3 + '@types/node': 22.16.5 - '@inquirer/select@4.2.4(@types/node@22.16.3)': + '@inquirer/select@4.3.1(@types/node@22.16.5)': dependencies: - '@inquirer/core': 10.1.14(@types/node@22.16.3) - '@inquirer/figures': 1.0.12 - '@inquirer/type': 3.0.7(@types/node@22.16.3) + '@inquirer/core': 10.1.15(@types/node@22.16.5) + '@inquirer/figures': 1.0.13 + '@inquirer/type': 3.0.8(@types/node@22.16.5) ansi-escapes: 4.3.2 yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.16.3 + '@types/node': 22.16.5 - '@inquirer/type@3.0.7(@types/node@22.16.3)': + '@inquirer/type@3.0.8(@types/node@22.16.5)': optionalDependencies: - '@types/node': 22.16.3 + '@types/node': 22.16.5 '@isaacs/cliui@8.0.2': dependencies: @@ -2468,7 +2418,7 @@ snapshots: '@material/material-color-utilities@0.2.7': {} - '@mswjs/interceptors@0.39.2': + '@mswjs/interceptors@0.39.4': dependencies: '@open-draft/deferred-promise': 2.2.0 '@open-draft/logger': 0.3.0 @@ -2486,107 +2436,83 @@ snapshots: '@open-draft/until@2.1.0': {} - '@oxlint/darwin-arm64@1.6.0': - optional: true - - '@oxlint/darwin-x64@1.6.0': - optional: true - - '@oxlint/linux-arm64-gnu@1.6.0': - optional: true - - '@oxlint/linux-arm64-musl@1.6.0': - optional: true - - '@oxlint/linux-x64-gnu@1.6.0': - optional: true - - '@oxlint/linux-x64-musl@1.6.0': - optional: true - - '@oxlint/win32-arm64@1.6.0': - optional: true - - '@oxlint/win32-x64@1.6.0': - optional: true - '@pkgjs/parseargs@0.11.0': optional: true - '@rollup/rollup-android-arm-eabi@4.45.0': + '@rollup/rollup-android-arm-eabi@4.46.1': optional: true - '@rollup/rollup-android-arm64@4.45.0': + '@rollup/rollup-android-arm64@4.46.1': optional: true - '@rollup/rollup-darwin-arm64@4.45.0': + '@rollup/rollup-darwin-arm64@4.46.1': optional: true - '@rollup/rollup-darwin-x64@4.45.0': + '@rollup/rollup-darwin-x64@4.46.1': optional: true - '@rollup/rollup-freebsd-arm64@4.45.0': + '@rollup/rollup-freebsd-arm64@4.46.1': optional: true - '@rollup/rollup-freebsd-x64@4.45.0': + '@rollup/rollup-freebsd-x64@4.46.1': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.45.0': + '@rollup/rollup-linux-arm-gnueabihf@4.46.1': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.45.0': + '@rollup/rollup-linux-arm-musleabihf@4.46.1': optional: true - '@rollup/rollup-linux-arm64-gnu@4.45.0': + '@rollup/rollup-linux-arm64-gnu@4.46.1': optional: true - '@rollup/rollup-linux-arm64-musl@4.45.0': + '@rollup/rollup-linux-arm64-musl@4.46.1': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.45.0': + '@rollup/rollup-linux-loongarch64-gnu@4.46.1': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.45.0': + '@rollup/rollup-linux-ppc64-gnu@4.46.1': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.45.0': + '@rollup/rollup-linux-riscv64-gnu@4.46.1': optional: true - '@rollup/rollup-linux-riscv64-musl@4.45.0': + '@rollup/rollup-linux-riscv64-musl@4.46.1': optional: true - '@rollup/rollup-linux-s390x-gnu@4.45.0': + '@rollup/rollup-linux-s390x-gnu@4.46.1': optional: true - '@rollup/rollup-linux-x64-gnu@4.45.0': + '@rollup/rollup-linux-x64-gnu@4.46.1': optional: true - '@rollup/rollup-linux-x64-musl@4.45.0': + '@rollup/rollup-linux-x64-musl@4.46.1': optional: true - '@rollup/rollup-win32-arm64-msvc@4.45.0': + '@rollup/rollup-win32-arm64-msvc@4.46.1': optional: true - '@rollup/rollup-win32-ia32-msvc@4.45.0': + '@rollup/rollup-win32-ia32-msvc@4.46.1': optional: true - '@rollup/rollup-win32-x64-msvc@4.45.0': + '@rollup/rollup-win32-x64-msvc@4.46.1': optional: true - '@shikijs/engine-oniguruma@3.7.0': + '@shikijs/engine-oniguruma@3.8.1': dependencies: - '@shikijs/types': 3.7.0 + '@shikijs/types': 3.8.1 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@3.7.0': + '@shikijs/langs@3.8.1': dependencies: - '@shikijs/types': 3.7.0 + '@shikijs/types': 3.8.1 - '@shikijs/themes@3.7.0': + '@shikijs/themes@3.8.1': dependencies: - '@shikijs/types': 3.7.0 + '@shikijs/types': 3.8.1 - '@shikijs/types@3.7.0': + '@shikijs/types@3.8.1': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 @@ -2609,11 +2535,11 @@ snapshots: '@types/jsdom@21.1.7': dependencies: - '@types/node': 22.16.3 + '@types/node': 22.16.5 '@types/tough-cookie': 4.0.5 parse5: 7.3.0 - '@types/node@22.16.3': + '@types/node@22.16.5': dependencies: undici-types: 6.21.0 @@ -2623,7 +2549,7 @@ snapshots: '@types/unist@3.0.3': {} - '@vitest/coverage-istanbul@3.2.4(vitest@3.2.4(@types/node@22.16.3)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.3)(typescript@5.8.3))(yaml@2.8.0))': + '@vitest/coverage-istanbul@3.2.4(vitest@3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(yaml@2.8.0))': dependencies: '@istanbuljs/schema': 0.1.3 debug: 4.4.1 @@ -2635,7 +2561,7 @@ snapshots: magicast: 0.3.5 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@22.16.3)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.3)(typescript@5.8.3))(yaml@2.8.0) + vitest: 3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(yaml@2.8.0) transitivePeerDependencies: - supports-color @@ -2647,14 +2573,14 @@ snapshots: chai: 5.2.1 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(msw@2.10.4(@types/node@22.16.3)(typescript@5.8.3))(vite@6.3.5(@types/node@22.16.3)(yaml@2.8.0))': + '@vitest/mocker@3.2.4(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(vite@7.0.6(@types/node@22.16.5)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - msw: 2.10.4(@types/node@22.16.3)(typescript@5.8.3) - vite: 6.3.5(@types/node@22.16.3)(yaml@2.8.0) + msw: 2.10.4(@types/node@22.16.5)(typescript@5.8.3) + vite: 7.0.6(@types/node@22.16.5)(yaml@2.8.0) '@vitest/pretty-format@3.2.4': dependencies: @@ -2679,7 +2605,7 @@ snapshots: '@vitest/utils@3.2.4': dependencies: '@vitest/pretty-format': 3.2.4 - loupe: 3.1.4 + loupe: 3.2.0 tinyrainbow: 2.0.0 acorn-jsx-walk@2.0.0: {} @@ -2734,7 +2660,7 @@ snapshots: browserslist@4.25.1: dependencies: caniuse-lite: 1.0.30001727 - electron-to-chromium: 1.5.182 + electron-to-chromium: 1.5.191 node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.25.1) @@ -2764,7 +2690,7 @@ snapshots: assertion-error: 2.0.1 check-error: 2.1.1 deep-eql: 5.0.2 - loupe: 3.1.4 + loupe: 3.2.0 pathval: 2.0.1 chalk@4.1.2: @@ -2863,7 +2789,7 @@ snapshots: json5: 2.2.3 memoize: 10.1.0 picocolors: 1.1.1 - picomatch: 4.0.2 + picomatch: 4.0.3 prompts: 2.4.2 rechoir: 0.8.0 safe-regex: 2.1.1 @@ -2880,7 +2806,7 @@ snapshots: eastasianwidth@0.2.0: {} - electron-to-chromium@1.5.182: {} + electron-to-chromium@1.5.191: {} emoji-regex@8.0.0: {} @@ -2905,34 +2831,34 @@ snapshots: dependencies: es-errors: 1.3.0 - esbuild@0.25.6: + esbuild@0.25.8: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.6 - '@esbuild/android-arm': 0.25.6 - '@esbuild/android-arm64': 0.25.6 - '@esbuild/android-x64': 0.25.6 - '@esbuild/darwin-arm64': 0.25.6 - '@esbuild/darwin-x64': 0.25.6 - '@esbuild/freebsd-arm64': 0.25.6 - '@esbuild/freebsd-x64': 0.25.6 - '@esbuild/linux-arm': 0.25.6 - '@esbuild/linux-arm64': 0.25.6 - '@esbuild/linux-ia32': 0.25.6 - '@esbuild/linux-loong64': 0.25.6 - '@esbuild/linux-mips64el': 0.25.6 - '@esbuild/linux-ppc64': 0.25.6 - '@esbuild/linux-riscv64': 0.25.6 - '@esbuild/linux-s390x': 0.25.6 - '@esbuild/linux-x64': 0.25.6 - '@esbuild/netbsd-arm64': 0.25.6 - '@esbuild/netbsd-x64': 0.25.6 - '@esbuild/openbsd-arm64': 0.25.6 - '@esbuild/openbsd-x64': 0.25.6 - '@esbuild/openharmony-arm64': 0.25.6 - '@esbuild/sunos-x64': 0.25.6 - '@esbuild/win32-arm64': 0.25.6 - '@esbuild/win32-ia32': 0.25.6 - '@esbuild/win32-x64': 0.25.6 + '@esbuild/aix-ppc64': 0.25.8 + '@esbuild/android-arm': 0.25.8 + '@esbuild/android-arm64': 0.25.8 + '@esbuild/android-x64': 0.25.8 + '@esbuild/darwin-arm64': 0.25.8 + '@esbuild/darwin-x64': 0.25.8 + '@esbuild/freebsd-arm64': 0.25.8 + '@esbuild/freebsd-x64': 0.25.8 + '@esbuild/linux-arm': 0.25.8 + '@esbuild/linux-arm64': 0.25.8 + '@esbuild/linux-ia32': 0.25.8 + '@esbuild/linux-loong64': 0.25.8 + '@esbuild/linux-mips64el': 0.25.8 + '@esbuild/linux-ppc64': 0.25.8 + '@esbuild/linux-riscv64': 0.25.8 + '@esbuild/linux-s390x': 0.25.8 + '@esbuild/linux-x64': 0.25.8 + '@esbuild/netbsd-arm64': 0.25.8 + '@esbuild/netbsd-x64': 0.25.8 + '@esbuild/openbsd-arm64': 0.25.8 + '@esbuild/openbsd-x64': 0.25.8 + '@esbuild/openharmony-arm64': 0.25.8 + '@esbuild/sunos-x64': 0.25.8 + '@esbuild/win32-arm64': 0.25.8 + '@esbuild/win32-ia32': 0.25.8 + '@esbuild/win32-x64': 0.25.8 escalade@3.2.0: {} @@ -2958,9 +2884,9 @@ snapshots: fast-uri@3.0.6: {} - fdir@6.4.6(picomatch@4.0.2): + fdir@6.4.6(picomatch@4.0.3): optionalDependencies: - picomatch: 4.0.2 + picomatch: 4.0.3 foreground-child@3.3.1: dependencies: @@ -3063,7 +2989,7 @@ snapshots: i18next-browser-languagedetector@8.2.0: dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.2 i18next-http-backend@2.7.3: dependencies: @@ -3083,11 +3009,11 @@ snapshots: i18next@22.5.1: dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.2 i18next@24.2.3(typescript@5.8.3): dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.2 optionalDependencies: typescript: 5.8.3 @@ -3107,17 +3033,17 @@ snapshots: ini@4.1.1: {} - inquirer@12.7.0(@types/node@22.16.3): + inquirer@12.8.2(@types/node@22.16.5): dependencies: - '@inquirer/core': 10.1.14(@types/node@22.16.3) - '@inquirer/prompts': 7.6.0(@types/node@22.16.3) - '@inquirer/type': 3.0.7(@types/node@22.16.3) + '@inquirer/core': 10.1.15(@types/node@22.16.5) + '@inquirer/prompts': 7.7.1(@types/node@22.16.5) + '@inquirer/type': 3.0.8(@types/node@22.16.5) ansi-escapes: 4.3.2 mute-stream: 2.0.0 - run-async: 4.0.4 + run-async: 4.0.5 rxjs: 7.8.2 optionalDependencies: - '@types/node': 22.16.3 + '@types/node': 22.16.5 interpret@3.1.1: {} @@ -3203,7 +3129,7 @@ snapshots: http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.20 + nwsapi: 2.2.21 parse5: 7.3.0 rrweb-cssom: 0.8.0 saxes: 6.0.0 @@ -3299,7 +3225,7 @@ snapshots: lodash@4.17.21: {} - loupe@3.1.4: {} + loupe@3.2.0: {} lru-cache@10.4.3: {} @@ -3316,7 +3242,7 @@ snapshots: magicast@0.3.5: dependencies: '@babel/parser': 7.28.0 - '@babel/types': 7.28.1 + '@babel/types': 7.28.2 source-map-js: 1.2.1 make-dir@4.0.0: @@ -3356,13 +3282,13 @@ snapshots: ms@2.1.3: {} - msw@2.10.4(@types/node@22.16.3)(typescript@5.8.3): + msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3): dependencies: '@bundled-es-modules/cookie': 2.0.1 '@bundled-es-modules/statuses': 1.0.1 '@bundled-es-modules/tough-cookie': 0.1.6 - '@inquirer/confirm': 5.1.13(@types/node@22.16.3) - '@mswjs/interceptors': 0.39.2 + '@inquirer/confirm': 5.1.14(@types/node@22.16.5) + '@mswjs/interceptors': 0.39.4 '@open-draft/deferred-promise': 2.2.0 '@open-draft/until': 2.1.0 '@types/cookie': 0.6.0 @@ -3393,7 +3319,7 @@ snapshots: node-releases@2.0.19: {} - nwsapi@2.2.20: {} + nwsapi@2.2.21: {} object-keys@1.1.1: {} @@ -3403,17 +3329,6 @@ snapshots: outvariant@1.4.3: {} - oxlint@1.6.0: - optionalDependencies: - '@oxlint/darwin-arm64': 1.6.0 - '@oxlint/darwin-x64': 1.6.0 - '@oxlint/linux-arm64-gnu': 1.6.0 - '@oxlint/linux-arm64-musl': 1.6.0 - '@oxlint/linux-x64-gnu': 1.6.0 - '@oxlint/linux-x64-musl': 1.6.0 - '@oxlint/win32-arm64': 1.6.0 - '@oxlint/win32-x64': 1.6.0 - package-json-from-dist@1.0.1: {} pako@1.0.11: {} @@ -3462,7 +3377,7 @@ snapshots: picocolors@1.1.1: {} - picomatch@4.0.2: {} + picomatch@4.0.3: {} postcss@8.5.6: dependencies: @@ -3470,8 +3385,6 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - prettier@3.6.2: {} - process-nextick-args@2.0.1: {} prompts@2.4.2: @@ -3517,38 +3430,35 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - rollup@4.45.0: + rollup@4.46.1: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.45.0 - '@rollup/rollup-android-arm64': 4.45.0 - '@rollup/rollup-darwin-arm64': 4.45.0 - '@rollup/rollup-darwin-x64': 4.45.0 - '@rollup/rollup-freebsd-arm64': 4.45.0 - '@rollup/rollup-freebsd-x64': 4.45.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.45.0 - '@rollup/rollup-linux-arm-musleabihf': 4.45.0 - '@rollup/rollup-linux-arm64-gnu': 4.45.0 - '@rollup/rollup-linux-arm64-musl': 4.45.0 - '@rollup/rollup-linux-loongarch64-gnu': 4.45.0 - '@rollup/rollup-linux-powerpc64le-gnu': 4.45.0 - '@rollup/rollup-linux-riscv64-gnu': 4.45.0 - '@rollup/rollup-linux-riscv64-musl': 4.45.0 - '@rollup/rollup-linux-s390x-gnu': 4.45.0 - '@rollup/rollup-linux-x64-gnu': 4.45.0 - '@rollup/rollup-linux-x64-musl': 4.45.0 - '@rollup/rollup-win32-arm64-msvc': 4.45.0 - '@rollup/rollup-win32-ia32-msvc': 4.45.0 - '@rollup/rollup-win32-x64-msvc': 4.45.0 + '@rollup/rollup-android-arm-eabi': 4.46.1 + '@rollup/rollup-android-arm64': 4.46.1 + '@rollup/rollup-darwin-arm64': 4.46.1 + '@rollup/rollup-darwin-x64': 4.46.1 + '@rollup/rollup-freebsd-arm64': 4.46.1 + '@rollup/rollup-freebsd-x64': 4.46.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.46.1 + '@rollup/rollup-linux-arm-musleabihf': 4.46.1 + '@rollup/rollup-linux-arm64-gnu': 4.46.1 + '@rollup/rollup-linux-arm64-musl': 4.46.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.46.1 + '@rollup/rollup-linux-ppc64-gnu': 4.46.1 + '@rollup/rollup-linux-riscv64-gnu': 4.46.1 + '@rollup/rollup-linux-riscv64-musl': 4.46.1 + '@rollup/rollup-linux-s390x-gnu': 4.46.1 + '@rollup/rollup-linux-x64-gnu': 4.46.1 + '@rollup/rollup-linux-x64-musl': 4.46.1 + '@rollup/rollup-win32-arm64-msvc': 4.46.1 + '@rollup/rollup-win32-ia32-msvc': 4.46.1 + '@rollup/rollup-win32-x64-msvc': 4.46.1 fsevents: 2.3.3 rrweb-cssom@0.8.0: {} - run-async@4.0.4: - dependencies: - oxlint: 1.6.0 - prettier: 3.6.2 + run-async@4.0.5: {} rxjs@7.8.2: dependencies: @@ -3657,8 +3567,8 @@ snapshots: tinyglobby@0.2.14: dependencies: - fdir: 6.4.6(picomatch@4.0.2) - picomatch: 4.0.2 + fdir: 6.4.6(picomatch@4.0.3) + picomatch: 4.0.3 tinypool@1.1.1: {} @@ -3716,9 +3626,9 @@ snapshots: type-fest@4.41.0: {} - typedoc@0.28.7(typescript@5.8.3): + typedoc@0.28.8(typescript@5.8.3): dependencies: - '@gerrit0/mini-shiki': 3.7.0 + '@gerrit0/mini-shiki': 3.8.1 lunr: 2.3.9 markdown-it: 14.1.0 minimatch: 9.0.5 @@ -3746,13 +3656,13 @@ snapshots: util-deprecate@1.0.2: {} - vite-node@3.2.4(@types/node@22.16.3)(yaml@2.8.0): + vite-node@3.2.4(@types/node@22.16.5)(yaml@2.8.0): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.3.5(@types/node@22.16.3)(yaml@2.8.0) + vite: 7.0.6(@types/node@22.16.5)(yaml@2.8.0) transitivePeerDependencies: - '@types/node' - jiti @@ -3767,40 +3677,40 @@ snapshots: - tsx - yaml - vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@22.16.3)(yaml@2.8.0)): + vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@7.0.6(@types/node@22.16.5)(yaml@2.8.0)): dependencies: debug: 4.4.1 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.8.3) optionalDependencies: - vite: 6.3.5(@types/node@22.16.3)(yaml@2.8.0) + vite: 7.0.6(@types/node@22.16.5)(yaml@2.8.0) transitivePeerDependencies: - supports-color - typescript - vite@6.3.5(@types/node@22.16.3)(yaml@2.8.0): + vite@7.0.6(@types/node@22.16.5)(yaml@2.8.0): dependencies: - esbuild: 0.25.6 - fdir: 6.4.6(picomatch@4.0.2) - picomatch: 4.0.2 + esbuild: 0.25.8 + fdir: 6.4.6(picomatch@4.0.3) + picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.45.0 + rollup: 4.46.1 tinyglobby: 0.2.14 optionalDependencies: - '@types/node': 22.16.3 + '@types/node': 22.16.5 fsevents: 2.3.3 yaml: 2.8.0 - vitest-canvas-mock@0.3.3(vitest@3.2.4(@types/node@22.16.3)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.3)(typescript@5.8.3))(yaml@2.8.0)): + vitest-canvas-mock@0.3.3(vitest@3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(yaml@2.8.0)): dependencies: jest-canvas-mock: 2.5.2 - vitest: 3.2.4(@types/node@22.16.3)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.3)(typescript@5.8.3))(yaml@2.8.0) + vitest: 3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(yaml@2.8.0) - vitest@3.2.4(@types/node@22.16.3)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.3)(typescript@5.8.3))(yaml@2.8.0): + vitest@3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(yaml@2.8.0): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(msw@2.10.4(@types/node@22.16.3)(typescript@5.8.3))(vite@6.3.5(@types/node@22.16.3)(yaml@2.8.0)) + '@vitest/mocker': 3.2.4(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(vite@7.0.6(@types/node@22.16.5)(yaml@2.8.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -3811,18 +3721,18 @@ snapshots: expect-type: 1.2.2 magic-string: 0.30.17 pathe: 2.0.3 - picomatch: 4.0.2 + picomatch: 4.0.3 std-env: 3.9.0 tinybench: 2.9.0 tinyexec: 0.3.2 tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.3.5(@types/node@22.16.3)(yaml@2.8.0) - vite-node: 3.2.4(@types/node@22.16.3)(yaml@2.8.0) + vite: 7.0.6(@types/node@22.16.5)(yaml@2.8.0) + vite-node: 3.2.4(@types/node@22.16.5)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 22.16.3 + '@types/node': 22.16.5 jsdom: 26.1.0 transitivePeerDependencies: - jiti From 8069fc3a6fb730bd246ec77967083581f9c435ae Mon Sep 17 00:00:00 2001 From: AJ Fontaine <36677462+Fontbane@users.noreply.github.com> Date: Tue, 29 Jul 2025 16:15:17 -0400 Subject: [PATCH 49/59] [ME] [Bug] Disable trades in GTS ME with only 1 valid party member https://github.com/pagefaultgames/pokerogue/pull/6167 --- .../encounters/global-trade-system-encounter.ts | 7 +++++-- .../encounters/global-trade-system-encounter.test.ts | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts index b914927123a..347092fe0b4 100644 --- a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts +++ b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts @@ -39,6 +39,7 @@ import { addPokemonDataToDexAndValidateAchievements } from "#mystery-encounters/ import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option"; +import { PartySizeRequirement } from "#mystery-encounters/mystery-encounter-requirements"; import { PokemonData } from "#system/pokemon-data"; import { MusicPreference } from "#system/settings"; import type { OptionSelectItem } from "#ui/abstact-option-select-ui-handler"; @@ -151,7 +152,8 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil return true; }) .withOption( - MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) + .withSceneRequirement(new PartySizeRequirement([2, 6], true)) // Requires 2 valid party members .withHasDexProgress(true) .withDialogue({ buttonLabel: `${namespace}:option.1.label`, @@ -257,7 +259,8 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil .build(), ) .withOption( - MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) + .withSceneRequirement(new PartySizeRequirement([2, 6], true)) // Requires 2 valid party members .withHasDexProgress(true) .withDialogue({ buttonLabel: `${namespace}:option.2.label`, diff --git a/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts b/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts index 2ac55dabe1c..867a33f6ab6 100644 --- a/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts +++ b/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts @@ -93,7 +93,7 @@ describe("Global Trade System - Mystery Encounter", () => { describe("Option 1 - Check Trade Offers", () => { it("should have the correct properties", () => { const option = GlobalTradeSystemEncounter.options[0]; - expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT); expect(option.dialogue).toBeDefined(); expect(option.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option.1.label`, @@ -154,7 +154,7 @@ describe("Global Trade System - Mystery Encounter", () => { describe("Option 2 - Wonder Trade", () => { it("should have the correct properties", () => { const option = GlobalTradeSystemEncounter.options[1]; - expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT); expect(option.dialogue).toBeDefined(); expect(option.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option.2.label`, From 90d09ea90391466df65ec3837ccc4dfc7e2738f0 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Tue, 29 Jul 2025 15:47:24 -0600 Subject: [PATCH 50/59] [Refactor] Minor refactor of battler tags (#6129) * Minor refactor battler tags * Improve documentation * Update type when loading in pokemon-data constructor for battler tags * Fix issues in tsdoc comments with Wlowscha's suggestions Co-authored-by: Wlowscha <54003515+Wlowscha@users.noreply.github.com> * Apply bertie's suggestions from code review Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com> * Remove unnecessary as const from tagType * Remove missed `as const` * Apply kev's suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update src/data/battler-tags.ts Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com> * Update src/data/battler-tags.ts Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com> --------- Co-authored-by: Wlowscha <54003515+Wlowscha@users.noreply.github.com> Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com> Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/@types/battler-tags.ts | 119 +++++ src/data/battler-tags.ts | 881 ++++++++++++++++++------------- src/data/moves/move.ts | 2 +- src/data/pokemon/pokemon-data.ts | 11 +- src/enums/battler-tag-type.ts | 1 - 5 files changed, 643 insertions(+), 371 deletions(-) create mode 100644 src/@types/battler-tags.ts diff --git a/src/@types/battler-tags.ts b/src/@types/battler-tags.ts new file mode 100644 index 00000000000..d1ff93e0400 --- /dev/null +++ b/src/@types/battler-tags.ts @@ -0,0 +1,119 @@ +// biome-ignore-start lint/correctness/noUnusedImports: Used in a TSDoc comment +import type { AbilityBattlerTag, BattlerTagTypeMap, SerializableBattlerTag, TypeBoostTag } from "#data/battler-tags"; +import type { AbilityId } from "#enums/ability-id"; +// biome-ignore-end lint/correctness/noUnusedImports: end +import type { BattlerTagType } from "#enums/battler-tag-type"; + +/** + * Subset of {@linkcode BattlerTagType}s that restrict the use of moves. + */ +export type MoveRestrictionBattlerTagType = + | BattlerTagType.THROAT_CHOPPED + | BattlerTagType.TORMENT + | BattlerTagType.TAUNT + | BattlerTagType.IMPRISON + | BattlerTagType.HEAL_BLOCK + | BattlerTagType.ENCORE + | BattlerTagType.DISABLED + | BattlerTagType.GORILLA_TACTICS; + +/** + * Subset of {@linkcode BattlerTagType}s that block damage from moves. + */ +export type FormBlockDamageBattlerTagType = BattlerTagType.ICE_FACE | BattlerTagType.DISGUISE; + +/** + * Subset of {@linkcode BattlerTagType}s that are related to trapping effects. + */ +export type TrappingBattlerTagType = + | BattlerTagType.BIND + | BattlerTagType.WRAP + | BattlerTagType.FIRE_SPIN + | BattlerTagType.WHIRLPOOL + | BattlerTagType.CLAMP + | BattlerTagType.SAND_TOMB + | BattlerTagType.MAGMA_STORM + | BattlerTagType.SNAP_TRAP + | BattlerTagType.THUNDER_CAGE + | BattlerTagType.INFESTATION + | BattlerTagType.INGRAIN + | BattlerTagType.OCTOLOCK + | BattlerTagType.NO_RETREAT; + +/** + * Subset of {@linkcode BattlerTagType}s that are related to protection effects. + */ +export type ProtectionBattlerTagType = BattlerTagType.PROTECTED | BattlerTagType.SPIKY_SHIELD | DamageProtectedTagType; +/** + * Subset of {@linkcode BattlerTagType}s related to protection effects that block damage but not status moves. + */ +export type DamageProtectedTagType = ContactSetStatusProtectedTagType | ContactStatStageChangeProtectedTagType; + +/** + * Subset of {@linkcode BattlerTagType}s related to protection effects that set a status effect on the attacker. + */ +export type ContactSetStatusProtectedTagType = BattlerTagType.BANEFUL_BUNKER | BattlerTagType.BURNING_BULWARK; + +/** + * Subset of {@linkcode BattlerTagType}s related to protection effects that change stat stages of the attacker. + */ +export type ContactStatStageChangeProtectedTagType = + | BattlerTagType.KINGS_SHIELD + | BattlerTagType.SILK_TRAP + | BattlerTagType.OBSTRUCT; + +/** Subset of {@linkcode BattlerTagType}s that provide the Endure effect */ +export type EndureTagType = BattlerTagType.ENDURE_TOKEN | BattlerTagType.ENDURING; + +/** + * Subset of {@linkcode BattlerTagType}s that are related to semi-invulnerable states. + */ +export type SemiInvulnerableTagType = + | BattlerTagType.FLYING + | BattlerTagType.UNDERGROUND + | BattlerTagType.UNDERWATER + | BattlerTagType.HIDDEN; + +/** + * Subset of {@linkcode BattlerTagType}s corresponding to {@linkcode AbilityBattlerTag}s + * + * @remarks + * ⚠️ {@linkcode AbilityId.FLASH_FIRE | Flash Fire}'s {@linkcode BattlerTagType.FIRE_BOOST} is not included as it + * subclasses {@linkcode TypeBoostTag} and not `AbilityBattlerTag`. + */ +export type AbilityBattlerTagType = + | BattlerTagType.PROTOSYNTHESIS + | BattlerTagType.QUARK_DRIVE + | BattlerTagType.UNBURDEN + | BattlerTagType.SLOW_START + | BattlerTagType.TRUANT; + +/** + * Subset of {@linkcode BattlerTagType}s related to abilities that boost the highest stat. + */ +export type HighestStatBoostTagType = + | BattlerTagType.QUARK_DRIVE // formatting + | BattlerTagType.PROTOSYNTHESIS; +/** + * Subset of {@linkcode BattlerTagType}s that are able to persist between turns and should therefore be serialized + */ +export type SerializableBattlerTagType = keyof { + [K in keyof BattlerTagTypeMap as BattlerTagTypeMap[K] extends SerializableBattlerTag + ? K + : never]: BattlerTagTypeMap[K]; +}; + +/** + * Subset of {@linkcode BattlerTagType}s that are not able to persist across waves and should therefore not be serialized + */ +export type NonSerializableBattlerTagType = Exclude; + +/** + * Dummy, typescript-only declaration to ensure that + * {@linkcode BattlerTagTypeMap} has an entry for all `BattlerTagType`s. + * + * If a battler tag is missing from the map, Typescript will throw an error on this statement. + * + * ⚠️ Does not actually exist at runtime, so it must not be used! + */ +declare const EnsureAllBattlerTagTypesAreMapped: BattlerTagTypeMap[BattlerTagType] & never; diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index c8ddfe32f0b..79c14f860b6 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -8,6 +8,7 @@ import { SpeciesFormChangeAbilityTrigger } from "#data/form-change-triggers"; import { getStatusEffectHealText } from "#data/status-effect"; import { TerrainType } from "#data/terrain"; import { AbilityId } from "#enums/ability-id"; +import type { BattlerIndex } from "#enums/battler-index"; import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type"; import { BattlerTagType } from "#enums/battler-tag-type"; import { HitResult } from "#enums/hit-result"; @@ -31,41 +32,107 @@ import type { MoveEffectPhase } from "#phases/move-effect-phase"; import type { MovePhase } from "#phases/move-phase"; import type { StatStageChangeCallback } from "#phases/stat-stage-change-phase"; import i18next from "#plugins/i18n"; +import type { + AbilityBattlerTagType, + ContactSetStatusProtectedTagType, + ContactStatStageChangeProtectedTagType, + DamageProtectedTagType, + EndureTagType, + HighestStatBoostTagType, + MoveRestrictionBattlerTagType, + ProtectionBattlerTagType, + SemiInvulnerableTagType, + TrappingBattlerTagType, +} from "#types/battler-tags"; +import type { Mutable, NonFunctionProperties } from "#types/type-helpers"; import { BooleanHolder, coerceArray, getFrameMs, isNullOrUndefined, NumberHolder, toDmgValue } from "#utils/common"; +/* +@module +BattlerTags are used to represent semi-persistent effects that can be attached to a Pokemon. +Note that before serialization, a new tag object is created, and then `loadTag` is called on the +tag with the object that was serialized. + +This means it is straightforward to avoid serializing fields. +Fields that are not set in the constructor and not set in `loadTag` will thus not be serialized. + +Any battler tag that can persist across sessions must extend SerializableBattlerTag in its class definition signature. +Only tags that persist across waves (meaning their effect can last >1 turn) should be considered +serializable. + +Serializable battler tags have strict requirements for their fields. +Properties that are not necessary to reconstruct the tag must not be serialized. This can be avoided +by using a private property. If access to the property is needed outside of the class, then +a getter (and potentially, a setter) should be used instead. + +If a property that is intended to be private must be serialized, then it should instead +be declared as a public readonly propety. Then, in the `loadTag` method (or any method inside the class that needs to adjust the property) +use `(this as Mutable).propertyName = value;` +These rules ensure that Typescript is aware of the shape of the serialized version of the class. +*/ + +/** Interface containing the serializable fields of BattlerTag */ +interface BaseBattlerTag { + /** The tag's remaining duration */ + turnCount: number; + /** The {@linkcode MoveId} that created this tag, or `undefined` if not set by a move */ + sourceMove?: MoveId; + /** The {@linkcode Pokemon.id | PID} of the Pokemon that added this tag, or `undefined` if not set by a pokemon */ + sourceId?: number; +} + /** * A {@linkcode BattlerTag} represents a semi-persistent effect that can be attached to a {@linkcode Pokemon}. * Tags can trigger various effects throughout a turn, and are cleared on switching out * or through their respective {@linkcode BattlerTag.lapse | lapse} methods. */ -export class BattlerTag { - public tagType: BattlerTagType; - public lapseTypes: BattlerTagLapseType[]; +export class BattlerTag implements BaseBattlerTag { + public readonly tagType: BattlerTagType; + public turnCount: number; - public sourceMove: MoveId; + public sourceMove?: MoveId; public sourceId?: number; - public isBatonPassable: boolean; + + //#region non-serializable fields + // Fields that should never be serialized, as they must not change after instantiation + #isBatonPassable = false; + public get isBatonPassable(): boolean { + return this.#isBatonPassable; + } + + #lapseTypes: readonly [BattlerTagLapseType, ...BattlerTagLapseType[]]; + public get lapseTypes(): readonly BattlerTagLapseType[] { + return this.#lapseTypes; + } + //#endregion non-serializable fields constructor( tagType: BattlerTagType, - lapseType: BattlerTagLapseType | BattlerTagLapseType[], + lapseType: BattlerTagLapseType | [BattlerTagLapseType, ...BattlerTagLapseType[]], turnCount: number, sourceMove?: MoveId, sourceId?: number, isBatonPassable = false, ) { this.tagType = tagType; - this.lapseTypes = coerceArray(lapseType); + this.#lapseTypes = coerceArray(lapseType); this.turnCount = turnCount; - this.sourceMove = sourceMove!; // TODO: is this bang correct? + // We intentionally don't want to set source move to `MoveId.NONE` here, so a raw boolean comparison is OK. + if (sourceMove) { + this.sourceMove = sourceMove; + } this.sourceId = sourceId; - this.isBatonPassable = isBatonPassable; + this.#isBatonPassable = isBatonPassable; } canAdd(_pokemon: Pokemon): boolean { return true; } + /** + * Apply effects that occur when the tag is added to a {@linkcode Pokemon} + * @param _pokemon - The {@linkcode Pokemon} the tag was added to + */ onAdd(_pokemon: Pokemon): void {} onRemove(_pokemon: Pokemon): void {} @@ -99,9 +166,9 @@ export class BattlerTag { /** * Load the data for a given {@linkcode BattlerTag} or JSON representation thereof. * Should be inherited from by any battler tag with custom attributes. - * @param source The battler tag to load + * @param source - An object containing the fields needed to reconstruct this tag. */ - loadTag(source: BattlerTag | any): void { + loadTag(source: BaseBattlerTag): void { this.turnCount = source.turnCount; this.sourceMove = source.sourceMove; this.sourceId = source.sourceId; @@ -116,12 +183,9 @@ export class BattlerTag { } } -export interface WeatherBattlerTag { - weatherTypes: WeatherType[]; -} - -export interface TerrainBattlerTag { - terrainTypes: TerrainType[]; +export abstract class SerializableBattlerTag extends BattlerTag { + /** Nonexistent, dummy field to allow typescript to distinguish this class from `BattlerTag` */ + private declare __SerializableBattlerTag: never; } /** @@ -132,8 +196,8 @@ export interface TerrainBattlerTag { * match a condition. A restricted move gets cancelled before it is used. * Players and enemies should not be allowed to select restricted moves. */ -export abstract class MoveRestrictionBattlerTag extends BattlerTag { - /** @override */ +export abstract class MoveRestrictionBattlerTag extends SerializableBattlerTag { + public declare readonly tagType: MoveRestrictionBattlerTagType; override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { if (lapseType === BattlerTagLapseType.PRE_MOVE) { // Cancel the affected pokemon's selected move @@ -154,32 +218,32 @@ export abstract class MoveRestrictionBattlerTag extends BattlerTag { } /** - * Gets whether this tag is restricting a move. + * Determine whether a move's usage is restricted by this tag * - * @param move - {@linkcode MoveId} ID to check restriction for. + * @param move - The {@linkcode MoveId} being checked * @param user - The {@linkcode Pokemon} involved * @returns `true` if the move is restricted by this tag, otherwise `false`. */ public abstract isMoveRestricted(move: MoveId, user?: Pokemon): boolean; /** - * Checks if this tag is restricting a move based on a user's decisions during the target selection phase + * Check if this tag is restricting a move based on a user's decisions during the target selection phase * - * @param {MoveId} _move {@linkcode MoveId} move ID to check restriction for - * @param {Pokemon} _user {@linkcode Pokemon} the user of the above move - * @param {Pokemon} _target {@linkcode Pokemon} the target of the above move - * @returns {boolean} `false` unless overridden by the child tag + * @param _move - {@linkcode MoveId} to check restriction for + * @param _user - The user of the move + * @param _target - The pokemon targeted by the move + * @returns Whether the move is restricted by this tag */ isMoveTargetRestricted(_move: MoveId, _user: Pokemon, _target: Pokemon): boolean { return false; } /** - * Gets the text to display when the player attempts to select a move that is restricted by this tag. + * Get the text to display when the player attempts to select a move that is restricted by this tag. * - * @param {Pokemon} pokemon {@linkcode Pokemon} for which the player is attempting to select the restricted move - * @param {MoveId} move {@linkcode MoveId} ID of the move that is having its selection denied - * @returns {string} text to display when the player attempts to select the restricted move + * @param pokemon - The pokemon for which the player is attempting to select the restricted move + * @param move - The {@linkcode MoveId | ID} of the Move that is having its selection denied + * @returns The text to display when the player attempts to select the restricted move */ abstract selectionDeniedText(pokemon: Pokemon, move: MoveId): string; @@ -188,9 +252,9 @@ export abstract class MoveRestrictionBattlerTag extends BattlerTag { * Because restriction effects also prevent selection of the move, this situation can only arise if a * pokemon first selects a move, then gets outsped by a pokemon using a move that restricts the selected move. * - * @param {Pokemon} _pokemon {@linkcode Pokemon} attempting to use the restricted move - * @param {MoveId} _move {@linkcode MoveId} ID of the move being interrupted - * @returns {string} text to display when the move is interrupted + * @param _pokemon - The pokemon attempting to use the restricted move + * @param _move - The {@linkcode MoveId | ID} of the move being interrupted + * @returns The text to display when the move is interrupted */ interruptedText(_pokemon: Pokemon, _move: MoveId): string { return ""; @@ -200,9 +264,10 @@ export abstract class MoveRestrictionBattlerTag extends BattlerTag { /** * Tag representing the "Throat Chop" effect. Pokemon with this tag cannot use sound-based moves. * @see {@link https://bulbapedia.bulbagarden.net/wiki/Throat_Chop_(move) | Throat Chop} - * @extends MoveRestrictionBattlerTag + * @sealed */ export class ThroatChoppedTag extends MoveRestrictionBattlerTag { + public override readonly tagType = BattlerTagType.THROAT_CHOPPED; constructor() { super( BattlerTagType.THROAT_CHOPPED, @@ -213,10 +278,9 @@ export class ThroatChoppedTag extends MoveRestrictionBattlerTag { } /** - * Checks if a {@linkcode MoveId | move} is restricted by Throat Chop. - * @override - * @param {MoveId} move the {@linkcode MoveId | move} to check for sound-based restriction - * @returns true if the move is sound-based + * Check if a move is restricted by Throat Chop. + * @param move - The {@linkcode MoveId | ID } of the move to check for sound-based restriction + * @returns Whether the move is sound based */ override isMoveRestricted(move: MoveId): boolean { return allMoves[move].hasFlag(MoveFlags.SOUND_BASED); @@ -224,10 +288,9 @@ export class ThroatChoppedTag extends MoveRestrictionBattlerTag { /** * Shows a message when the player attempts to select a move that is restricted by Throat Chop. - * @override - * @param {Pokemon} _pokemon the {@linkcode Pokemon} that is attempting to select the restricted move - * @param {MoveId} move the {@linkcode MoveId | move} that is being restricted - * @returns the message to display when the player attempts to select the restricted move + * @param _pokemon - The {@linkcode Pokemon} that is attempting to select the restricted move + * @param move - The {@linkcode MoveId | move} that is being restricted + * @returns The message to display when the player attempts to select the restricted move */ override selectionDeniedText(_pokemon: Pokemon, move: MoveId): string { return i18next.t("battle:moveCannotBeSelected", { @@ -237,10 +300,9 @@ export class ThroatChoppedTag extends MoveRestrictionBattlerTag { /** * Shows a message when a move is interrupted by Throat Chop. - * @override - * @param {Pokemon} pokemon the interrupted {@linkcode Pokemon} - * @param {MoveId} _move the {@linkcode MoveId | move} that was interrupted - * @returns the message to display when the move is interrupted + * @param pokemon - The interrupted {@linkcode Pokemon} + * @param _move - The {@linkcode MoveId | ID } of the move that was interrupted + * @returns The message to display when the move is interrupted */ override interruptedText(pokemon: Pokemon, _move: MoveId): string { return i18next.t("battle:throatChopInterruptedMove", { @@ -252,10 +314,13 @@ export class ThroatChoppedTag extends MoveRestrictionBattlerTag { /** * Tag representing the "disabling" effect performed by {@linkcode MoveId.DISABLE} and {@linkcode AbilityId.CURSED_BODY}. * When the tag is added, the last-used move of the tag holder is set as the disabled move. + * + * @sealed */ export class DisabledTag extends MoveRestrictionBattlerTag { + public override readonly tagType = BattlerTagType.DISABLED; /** The move being disabled. Gets set when {@linkcode onAdd} is called for this tag. */ - private moveId: MoveId = MoveId.NONE; + public readonly moveId: MoveId = MoveId.NONE; constructor(sourceId: number) { super( @@ -267,14 +332,11 @@ export class DisabledTag extends MoveRestrictionBattlerTag { ); } - /** @override */ override isMoveRestricted(move: MoveId): boolean { return move === this.moveId; } /** - * @override - * * Attempt to disable the target's last move by setting this tag's {@linkcode moveId} * and showing a message. */ @@ -287,7 +349,7 @@ export class DisabledTag extends MoveRestrictionBattlerTag { } super.onAdd(pokemon); - this.moveId = move.move; + (this as Mutable).moveId = move.move; globalScene.phaseManager.queueMessage( i18next.t("battlerTags:disabledOnAdd", { @@ -297,7 +359,6 @@ export class DisabledTag extends MoveRestrictionBattlerTag { ); } - /** @override */ override onRemove(pokemon: Pokemon): void { super.onRemove(pokemon); @@ -309,16 +370,14 @@ export class DisabledTag extends MoveRestrictionBattlerTag { ); } - /** @override */ override selectionDeniedText(_pokemon: Pokemon, move: MoveId): string { return i18next.t("battle:moveDisabled", { moveName: allMoves[move].name }); } /** - * @override - * @param {Pokemon} pokemon {@linkcode Pokemon} attempting to use the restricted move - * @param {MoveId} move {@linkcode MoveId} ID of the move being interrupted - * @returns {string} text to display when the move is interrupted + * @param pokemon - {@linkcode Pokemon} attempting to use the restricted move + * @param move - {@linkcode MoveId | ID} of the move being interrupted + * @returns The text to display when the move is interrupted */ override interruptedText(pokemon: Pokemon, move: MoveId): string { return i18next.t("battle:disableInterruptedMove", { @@ -327,18 +386,21 @@ export class DisabledTag extends MoveRestrictionBattlerTag { }); } - /** @override */ - override loadTag(source: BattlerTag | any): void { + override loadTag(source: NonFunctionProperties): void { super.loadTag(source); - this.moveId = source.moveId; + (this as Mutable).moveId = source.moveId; } } /** * Tag used by Gorilla Tactics to restrict the user to using only one move. + * + * @sealed */ export class GorillaTacticsTag extends MoveRestrictionBattlerTag { - private moveId = MoveId.NONE; + public override readonly tagType = BattlerTagType.GORILLA_TACTICS; + /** ID of the move that the user is locked into using*/ + public readonly moveId: MoveId = MoveId.NONE; constructor() { super(BattlerTagType.GORILLA_TACTICS, BattlerTagLapseType.CUSTOM, 0); @@ -367,18 +429,17 @@ export class GorillaTacticsTag extends MoveRestrictionBattlerTag { super.onAdd(pokemon); // Bang is justified as tag is not added if prior move doesn't exist - this.moveId = pokemon.getLastNonVirtualMove()!.move; + (this as Mutable).moveId = pokemon.getLastNonVirtualMove()!.move; pokemon.setStat(Stat.ATK, pokemon.getStat(Stat.ATK, false) * 1.5, false); } /** * Loads the Gorilla Tactics Battler Tag along with its unique class variable moveId - * @override - * @param source Gorilla Tactics' {@linkcode BattlerTag} information + * @param source - Object containing the fields needed to reconstruct this tag. */ - public override loadTag(source: BattlerTag | any): void { + override loadTag(source: NonFunctionProperties): void { super.loadTag(source); - this.moveId = source.moveId; + (this as Mutable).moveId = source.moveId; } /** @@ -397,7 +458,8 @@ export class GorillaTacticsTag extends MoveRestrictionBattlerTag { /** * BattlerTag that represents the "recharge" effects of moves like Hyper Beam. */ -export class RechargingTag extends BattlerTag { +export class RechargingTag extends SerializableBattlerTag { + public override readonly tagType = BattlerTagType.RECHARGING; constructor(sourceMove: MoveId) { super(BattlerTagType.RECHARGING, [BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END], 2, sourceMove); } @@ -430,6 +492,8 @@ export class RechargingTag extends BattlerTag { * @see {@link https://bulbapedia.bulbagarden.net/wiki/Beak_Blast_(move) | Beak Blast} */ export class BeakBlastChargingTag extends BattlerTag { + public override readonly tagType = BattlerTagType.BEAK_BLAST_CHARGING; + public declare readonly sourceMove: MoveId.BEAK_BLAST; constructor() { super( BattlerTagType.BEAK_BLAST_CHARGING, @@ -454,8 +518,8 @@ export class BeakBlastChargingTag extends BattlerTag { /** * Inflicts `BURN` status on attackers that make contact, and causes this tag * to be removed after the source makes a move (or the turn ends, whichever comes first) - * @param pokemon {@linkcode Pokemon} the owner of this tag - * @param lapseType {@linkcode BattlerTagLapseType} the type of functionality invoked in battle + * @param pokemon - The owner of this tag + * @param lapseType - The type of functionality invoked in battle * @returns `true` if invoked with the `AFTER_HIT` lapse type */ lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { @@ -498,8 +562,8 @@ export class ShellTrapTag extends BattlerTag { /** * "Activates" the shell trap, causing the tag owner to move next. - * @param pokemon {@linkcode Pokemon} the owner of this tag - * @param lapseType {@linkcode BattlerTagLapseType} the type of functionality invoked in battle + * @param pokemon - The owner of this tag + * @param lapseType - The type of functionality invoked in battle * @returns `true` if invoked with the `AFTER_HIT` lapse type */ lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { @@ -529,7 +593,8 @@ export class ShellTrapTag extends BattlerTag { } } -export class TrappedTag extends BattlerTag { +export class TrappedTag extends SerializableBattlerTag { + public declare readonly tagType: TrappingBattlerTagType; constructor( tagType: BattlerTagType, lapseType: BattlerTagLapseType, @@ -546,13 +611,13 @@ export class TrappedTag extends BattlerTag { console.warn(`Failed to get source Pokemon for TrappedTag canAdd; id: ${this.sourceId}`); return false; } - - const move = allMoves[this.sourceMove]; + if (this.sourceMove && allMoves[this.sourceMove]?.hitsSubstitute(source, pokemon)) { + return false; + } const isGhost = pokemon.isOfType(PokemonType.GHOST); const isTrapped = pokemon.getTag(TrappedTag); - const hasSubstitute = move.hitsSubstitute(source, pokemon); - return !isTrapped && !isGhost && !hasSubstitute; + return !isTrapped && !isGhost; } onAdd(pokemon: Pokemon): void { @@ -591,9 +656,9 @@ export class TrappedTag extends BattlerTag { * BattlerTag implementing No Retreat's trapping effect. * This is treated separately from other trapping effects to prevent * Ghost-type Pokemon from being able to reuse the move. - * @extends TrappedTag */ class NoRetreatTag extends TrappedTag { + public override readonly tagType = BattlerTagType.NO_RETREAT; constructor(sourceId: number) { super(BattlerTagType.NO_RETREAT, BattlerTagLapseType.CUSTOM, 0, MoveId.NO_RETREAT, sourceId); } @@ -608,6 +673,7 @@ class NoRetreatTag extends TrappedTag { * BattlerTag that represents the {@link https://bulbapedia.bulbagarden.net/wiki/Flinch Flinch} status condition */ export class FlinchedTag extends BattlerTag { + public override readonly tagType = BattlerTagType.FLINCHED; constructor(sourceMove: MoveId) { super(BattlerTagType.FLINCHED, [BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END], 1, sourceMove); } @@ -639,6 +705,7 @@ export class FlinchedTag extends BattlerTag { } export class InterruptedTag extends BattlerTag { + public override readonly tagType = BattlerTagType.INTERRUPTED; constructor(sourceMove: MoveId) { super(BattlerTagType.INTERRUPTED, BattlerTagLapseType.PRE_MOVE, 0, sourceMove); } @@ -668,7 +735,8 @@ export class InterruptedTag extends BattlerTag { /** * BattlerTag that represents the {@link https://bulbapedia.bulbagarden.net/wiki/Confusion_(status_condition) Confusion} status condition */ -export class ConfusedTag extends BattlerTag { +export class ConfusedTag extends SerializableBattlerTag { + public override readonly tagType = BattlerTagType.CONFUSED; constructor(turnCount: number, sourceMove: MoveId) { super(BattlerTagType.CONFUSED, BattlerTagLapseType.MOVE, turnCount, sourceMove, undefined, true); } @@ -752,10 +820,10 @@ export class ConfusedTag extends BattlerTag { /** * Tag applied to the {@linkcode Move.DESTINY_BOND} user. - * @extends BattlerTag * @see {@linkcode apply} */ -export class DestinyBondTag extends BattlerTag { +export class DestinyBondTag extends SerializableBattlerTag { + public readonly tagType = BattlerTagType.DESTINY_BOND; constructor(sourceMove: MoveId, sourceId: number) { super(BattlerTagType.DESTINY_BOND, BattlerTagLapseType.PRE_MOVE, 1, sourceMove, sourceId, true); } @@ -765,9 +833,9 @@ export class DestinyBondTag extends BattlerTag { * or after receiving fatal damage. When the damage is fatal, * the attacking Pokemon is taken down as well, unless it's a boss. * - * @param {Pokemon} pokemon Pokemon that is attacking the Destiny Bond user. - * @param {BattlerTagLapseType} lapseType CUSTOM or PRE_MOVE - * @returns false if the tag source fainted or one turn has passed since the application + * @param pokemon - The Pokemon that is attacking the Destiny Bond user. + * @param lapseType - CUSTOM or PRE_MOVE + * @returns `false` if the tag source fainted or one turn has passed since the application */ lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { if (lapseType !== BattlerTagLapseType.CUSTOM) { @@ -811,7 +879,9 @@ export class DestinyBondTag extends BattlerTag { } } -export class InfatuatedTag extends BattlerTag { +// Technically serializable as in a double battle, a pokemon could be infatuated by its ally +export class InfatuatedTag extends SerializableBattlerTag { + public override readonly tagType = BattlerTagType.INFATUATED; constructor(sourceMove: number, sourceId: number) { super(BattlerTagType.INFATUATED, BattlerTagLapseType.MOVE, 1, sourceMove, sourceId); } @@ -901,8 +971,9 @@ export class InfatuatedTag extends BattlerTag { } } -export class SeedTag extends BattlerTag { - private sourceIndex: number; +export class SeedTag extends SerializableBattlerTag { + public override readonly tagType = BattlerTagType.SEEDED; + public readonly sourceIndex: BattlerIndex; constructor(sourceId: number) { super(BattlerTagType.SEEDED, BattlerTagLapseType.TURN_END, 1, MoveId.LEECH_SEED, sourceId, true); @@ -910,11 +981,11 @@ export class SeedTag extends BattlerTag { /** * When given a battler tag or json representing one, load the data for it. - * @param {BattlerTag | any} source A battler tag + * @param source - An object containing the fields needed to reconstruct this tag. */ - loadTag(source: BattlerTag | any): void { + override loadTag(source: NonFunctionProperties): void { super.loadTag(source); - this.sourceIndex = source.sourceIndex; + (this as Mutable).sourceIndex = source.sourceIndex; } canAdd(pokemon: Pokemon): boolean { @@ -935,7 +1006,7 @@ export class SeedTag extends BattlerTag { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), }), ); - this.sourceIndex = source.getBattlerIndex(); + (this as Mutable).sourceIndex = source.getBattlerIndex(); } lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { @@ -990,9 +1061,10 @@ export class SeedTag extends BattlerTag { /** * BattlerTag representing the effects of {@link https://bulbapedia.bulbagarden.net/wiki/Powder_(move) | Powder}. * When the afflicted Pokemon uses a Fire-type move, the move is cancelled, and the - * Pokemon takes damage equal to 1/4 of it's maximum HP (rounded down). + * Pokemon takes damage equal to 1/4 of its maximum HP (rounded down). */ export class PowderTag extends BattlerTag { + public override readonly tagType = BattlerTagType.POWDER; constructor() { super(BattlerTagType.POWDER, [BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END], 1); } @@ -1051,7 +1123,7 @@ export class PowderTag extends BattlerTag { } } -export class NightmareTag extends BattlerTag { +export class NightmareTag extends SerializableBattlerTag { constructor() { super(BattlerTagType.NIGHTMARE, BattlerTagLapseType.TURN_END, 1, MoveId.NIGHTMARE); } @@ -1104,7 +1176,7 @@ export class NightmareTag extends BattlerTag { } } -export class FrenzyTag extends BattlerTag { +export class FrenzyTag extends SerializableBattlerTag { constructor(turnCount: number, sourceMove: MoveId, sourceId: number) { super(BattlerTagType.FRENZY, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId); } @@ -1124,6 +1196,8 @@ export class FrenzyTag extends BattlerTag { * Encore forces the target Pokemon to use its most-recent move for 3 turns. */ export class EncoreTag extends MoveRestrictionBattlerTag { + public override readonly tagType = BattlerTagType.ENCORE; + /** The ID of the move the user is locked into using */ public moveId: MoveId; constructor(sourceId: number) { @@ -1136,12 +1210,12 @@ export class EncoreTag extends MoveRestrictionBattlerTag { ); } - loadTag(source: BattlerTag | any): void { + override loadTag(source: NonFunctionProperties): void { super.loadTag(source); - this.moveId = source.moveId as MoveId; + this.moveId = source.moveId; } - canAdd(pokemon: Pokemon): boolean { + override canAdd(pokemon: Pokemon): boolean { const lastMove = pokemon.getLastNonVirtualMove(); if (!lastMove) { return false; @@ -1156,10 +1230,7 @@ export class EncoreTag extends MoveRestrictionBattlerTag { return true; } - onAdd(pokemon: Pokemon): void { - // TODO: shouldn't this be `onAdd`? - super.onRemove(pokemon); - + override onAdd(pokemon: Pokemon): void { globalScene.phaseManager.queueMessage( i18next.t("battlerTags:encoreOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), @@ -1199,7 +1270,7 @@ export class EncoreTag extends MoveRestrictionBattlerTag { /** * Checks if the move matches the moveId stored within the tag and returns a boolean value - * @param move {@linkcode MoveId} the move selected + * @param move - The ID of the move selected * @param user N/A * @returns `true` if the move does not match with the moveId stored and as a result, restricted */ @@ -1223,6 +1294,7 @@ export class EncoreTag extends MoveRestrictionBattlerTag { } export class HelpingHandTag extends BattlerTag { + public override readonly tagType = BattlerTagType.HELPING_HAND; constructor(sourceId: number) { super(BattlerTagType.HELPING_HAND, BattlerTagLapseType.TURN_END, 1, MoveId.HELPING_HAND, sourceId); } @@ -1245,16 +1317,16 @@ export class HelpingHandTag extends BattlerTag { /** * Applies the Ingrain tag to a pokemon - * @extends TrappedTag */ export class IngrainTag extends TrappedTag { + public override readonly tagType = BattlerTagType.INGRAIN; constructor(sourceId: number) { super(BattlerTagType.INGRAIN, BattlerTagLapseType.TURN_END, 1, MoveId.INGRAIN, sourceId); } /** * Check if the Ingrain tag can be added to the pokemon - * @param pokemon {@linkcode Pokemon} The pokemon to check if the tag can be added to + * @param pokemon - The pokemon to check if the tag can be added to * @returns boolean True if the tag can be added, false otherwise */ canAdd(pokemon: Pokemon): boolean { @@ -1295,6 +1367,7 @@ export class IngrainTag extends TrappedTag { * end of each turn. */ export class OctolockTag extends TrappedTag { + public override readonly tagType = BattlerTagType.OCTOLOCK; constructor(sourceId: number) { super(BattlerTagType.OCTOLOCK, BattlerTagLapseType.TURN_END, 1, MoveId.OCTOLOCK, sourceId); } @@ -1317,7 +1390,8 @@ export class OctolockTag extends TrappedTag { } } -export class AquaRingTag extends BattlerTag { +export class AquaRingTag extends SerializableBattlerTag { + public override readonly tagType = BattlerTagType.AQUA_RING; constructor() { super(BattlerTagType.AQUA_RING, BattlerTagLapseType.TURN_END, 1, MoveId.AQUA_RING, undefined, true); } @@ -1353,7 +1427,8 @@ export class AquaRingTag extends BattlerTag { } /** Tag used to allow moves that interact with {@link MoveId.MINIMIZE} to function */ -export class MinimizeTag extends BattlerTag { +export class MinimizeTag extends SerializableBattlerTag { + public override readonly tagType = BattlerTagType.MINIMIZED; constructor() { super(BattlerTagType.MINIMIZED, BattlerTagLapseType.TURN_END, 1, MoveId.MINIMIZE); } @@ -1371,7 +1446,8 @@ export class MinimizeTag extends BattlerTag { } } -export class DrowsyTag extends BattlerTag { +export class DrowsyTag extends SerializableBattlerTag { + public override readonly tagType = BattlerTagType.DROWSY; constructor() { super(BattlerTagType.DROWSY, BattlerTagLapseType.TURN_END, 2, MoveId.YAWN); } @@ -1405,7 +1481,9 @@ export class DrowsyTag extends BattlerTag { } export abstract class DamagingTrapTag extends TrappedTag { - private commonAnim: CommonAnim; + public declare readonly tagType: TrappingBattlerTagType; + /** The animation to play during the damage sequence */ + #commonAnim: CommonAnim; constructor( tagType: BattlerTagType, @@ -1416,16 +1494,7 @@ export abstract class DamagingTrapTag extends TrappedTag { ) { super(tagType, BattlerTagLapseType.TURN_END, turnCount, sourceMove, sourceId); - this.commonAnim = commonAnim; - } - - /** - * When given a battler tag or json representing one, load the data for it. - * @param {BattlerTag | any} source A battler tag - */ - loadTag(source: BattlerTag | any): void { - super.loadTag(source); - this.commonAnim = source.commonAnim as CommonAnim; + this.#commonAnim = commonAnim; } canAdd(pokemon: Pokemon): boolean { @@ -1443,7 +1512,7 @@ export abstract class DamagingTrapTag extends TrappedTag { moveName: this.getMoveName(), }), ); - phaseManager.unshiftNew("CommonAnimPhase", pokemon.getBattlerIndex(), undefined, this.commonAnim); + phaseManager.unshiftNew("CommonAnimPhase", pokemon.getBattlerIndex(), undefined, this.#commonAnim); const cancelled = new BooleanHolder(false); applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled }); @@ -1459,6 +1528,7 @@ export abstract class DamagingTrapTag extends TrappedTag { // TODO: Condense all these tags into 1 singular tag with a modified message func export class BindTag extends DamagingTrapTag { + public override readonly tagType = BattlerTagType.BIND; constructor(turnCount: number, sourceId: number) { super(BattlerTagType.BIND, CommonAnim.BIND, turnCount, MoveId.BIND, sourceId); } @@ -1479,6 +1549,7 @@ export class BindTag extends DamagingTrapTag { } export class WrapTag extends DamagingTrapTag { + public override readonly tagType = BattlerTagType.WRAP; constructor(turnCount: number, sourceId: number) { super(BattlerTagType.WRAP, CommonAnim.WRAP, turnCount, MoveId.WRAP, sourceId); } @@ -1507,18 +1578,21 @@ export abstract class VortexTrapTag extends DamagingTrapTag { } export class FireSpinTag extends VortexTrapTag { + public override readonly tagType = BattlerTagType.FIRE_SPIN; constructor(turnCount: number, sourceId: number) { super(BattlerTagType.FIRE_SPIN, CommonAnim.FIRE_SPIN, turnCount, MoveId.FIRE_SPIN, sourceId); } } export class WhirlpoolTag extends VortexTrapTag { + public override readonly tagType = BattlerTagType.WHIRLPOOL; constructor(turnCount: number, sourceId: number) { super(BattlerTagType.WHIRLPOOL, CommonAnim.WHIRLPOOL, turnCount, MoveId.WHIRLPOOL, sourceId); } } export class ClampTag extends DamagingTrapTag { + public override readonly tagType = BattlerTagType.CLAMP; constructor(turnCount: number, sourceId: number) { super(BattlerTagType.CLAMP, CommonAnim.CLAMP, turnCount, MoveId.CLAMP, sourceId); } @@ -1538,6 +1612,7 @@ export class ClampTag extends DamagingTrapTag { } export class SandTombTag extends DamagingTrapTag { + public override readonly tagType = BattlerTagType.SAND_TOMB; constructor(turnCount: number, sourceId: number) { super(BattlerTagType.SAND_TOMB, CommonAnim.SAND_TOMB, turnCount, MoveId.SAND_TOMB, sourceId); } @@ -1551,6 +1626,7 @@ export class SandTombTag extends DamagingTrapTag { } export class MagmaStormTag extends DamagingTrapTag { + public override readonly tagType = BattlerTagType.MAGMA_STORM; constructor(turnCount: number, sourceId: number) { super(BattlerTagType.MAGMA_STORM, CommonAnim.MAGMA_STORM, turnCount, MoveId.MAGMA_STORM, sourceId); } @@ -1563,6 +1639,7 @@ export class MagmaStormTag extends DamagingTrapTag { } export class SnapTrapTag extends DamagingTrapTag { + public override readonly tagType = BattlerTagType.SNAP_TRAP; constructor(turnCount: number, sourceId: number) { super(BattlerTagType.SNAP_TRAP, CommonAnim.SNAP_TRAP, turnCount, MoveId.SNAP_TRAP, sourceId); } @@ -1575,6 +1652,7 @@ export class SnapTrapTag extends DamagingTrapTag { } export class ThunderCageTag extends DamagingTrapTag { + public override readonly tagType = BattlerTagType.THUNDER_CAGE; constructor(turnCount: number, sourceId: number) { super(BattlerTagType.THUNDER_CAGE, CommonAnim.THUNDER_CAGE, turnCount, MoveId.THUNDER_CAGE, sourceId); } @@ -1594,6 +1672,7 @@ export class ThunderCageTag extends DamagingTrapTag { } export class InfestationTag extends DamagingTrapTag { + public override readonly tagType = BattlerTagType.INFESTATION; constructor(turnCount: number, sourceId: number) { super(BattlerTagType.INFESTATION, CommonAnim.INFESTATION, turnCount, MoveId.INFESTATION, sourceId); } @@ -1613,7 +1692,8 @@ export class InfestationTag extends DamagingTrapTag { } export class ProtectedTag extends BattlerTag { - constructor(sourceMove: MoveId, tagType: BattlerTagType = BattlerTagType.PROTECTED) { + public declare readonly tagType: ProtectionBattlerTagType; + constructor(sourceMove: MoveId, tagType: ProtectionBattlerTagType = BattlerTagType.PROTECTED) { super(tagType, BattlerTagLapseType.TURN_END, 0, sourceMove); } @@ -1649,14 +1729,13 @@ export class ProtectedTag extends BattlerTag { } /** Class for `BattlerTag`s that apply some effect when hit by a contact move */ -export class ContactProtectedTag extends ProtectedTag { +export abstract class ContactProtectedTag extends ProtectedTag { /** * Function to call when a contact move hits the pokemon with this tag. * @param _attacker - The pokemon using the contact move * @param _user - The pokemon that is being attacked and has the tag - * @param _move - The move used by the attacker */ - onContact(_attacker: Pokemon, _user: Pokemon) {} + abstract onContact(_attacker: Pokemon, _user: Pokemon): void; /** * Lapse the tag and apply `onContact` if the move makes contact and @@ -1686,22 +1765,16 @@ export class ContactProtectedTag extends ProtectedTag { /** * `BattlerTag` class for moves that block damaging moves damage the enemy if the enemy's move makes contact * Used by {@linkcode MoveId.SPIKY_SHIELD} + * + * @sealed */ export class ContactDamageProtectedTag extends ContactProtectedTag { - private damageRatio: number; + public override readonly tagType = BattlerTagType.SPIKY_SHIELD; + #damageRatio: number; constructor(sourceMove: MoveId, damageRatio: number) { super(sourceMove, BattlerTagType.SPIKY_SHIELD); - this.damageRatio = damageRatio; - } - - /** - * When given a battler tag or json representing one, load the data for it. - * @param {BattlerTag | any} source A battler tag - */ - loadTag(source: BattlerTag | any): void { - super.loadTag(source); - this.damageRatio = source.damageRatio; + this.#damageRatio = damageRatio; } /** @@ -1713,7 +1786,7 @@ export class ContactDamageProtectedTag extends ContactProtectedTag { const cancelled = new BooleanHolder(false); applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon: user, cancelled }); if (!cancelled.value) { - attacker.damageAndUpdate(toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio)), { + attacker.damageAndUpdate(toDmgValue(attacker.getMaxHp() * (1 / this.#damageRatio)), { result: HitResult.INDIRECT, }); } @@ -1721,20 +1794,22 @@ export class ContactDamageProtectedTag extends ContactProtectedTag { } /** Base class for `BattlerTag`s that block damaging moves but not status moves */ -export class DamageProtectedTag extends ContactProtectedTag {} +export abstract class DamageProtectedTag extends ContactProtectedTag { + public declare readonly tagType: DamageProtectedTagType; +} export class ContactSetStatusProtectedTag extends DamageProtectedTag { + public declare readonly tagType: ContactSetStatusProtectedTagType; + /** The status effect applied to attackers */ + #statusEffect: StatusEffect; /** - * @param sourceMove The move that caused the tag to be applied - * @param tagType The type of the tag - * @param statusEffect The status effect to apply to the attacker + * @param sourceMove - The move that caused the tag to be applied + * @param tagType - The type of the tag + * @param statusEffect - The status effect applied to attackers */ - constructor( - sourceMove: MoveId, - tagType: BattlerTagType, - private statusEffect: StatusEffect, - ) { + constructor(sourceMove: MoveId, tagType: ContactSetStatusProtectedTagType, statusEffect: StatusEffect) { super(sourceMove, tagType); + this.#statusEffect = statusEffect; } /** @@ -1743,7 +1818,7 @@ export class ContactSetStatusProtectedTag extends DamageProtectedTag { * @param user - The pokemon that is being attacked and has the tag */ override onContact(attacker: Pokemon, user: Pokemon): void { - attacker.trySetStatus(this.statusEffect, true, user); + attacker.trySetStatus(this.#statusEffect, true, user); } } @@ -1752,24 +1827,15 @@ export class ContactSetStatusProtectedTag extends DamageProtectedTag { * Used by {@linkcode MoveId.KINGS_SHIELD}, {@linkcode MoveId.OBSTRUCT}, {@linkcode MoveId.SILK_TRAP} */ export class ContactStatStageChangeProtectedTag extends DamageProtectedTag { - private stat: BattleStat; - private levels: number; + public declare readonly tagType: ContactStatStageChangeProtectedTagType; + #stat: BattleStat; + #levels: number; - constructor(sourceMove: MoveId, tagType: BattlerTagType, stat: BattleStat, levels: number) { + constructor(sourceMove: MoveId, tagType: ContactStatStageChangeProtectedTagType, stat: BattleStat, levels: number) { super(sourceMove, tagType); - this.stat = stat; - this.levels = levels; - } - - /** - * When given a battler tag or json representing one, load the data for it. - * @param {BattlerTag | any} source A battler tag - */ - override loadTag(source: BattlerTag | any): void { - super.loadTag(source); - this.stat = source.stat; - this.levels = source.levels; + this.#stat = stat; + this.#levels = levels; } /** @@ -1782,19 +1848,19 @@ export class ContactStatStageChangeProtectedTag extends DamageProtectedTag { "StatStageChangePhase", attacker.getBattlerIndex(), false, - [this.stat], - this.levels, + [this.#stat], + this.#levels, ); } } /** * `BattlerTag` class for effects that cause the affected Pokemon to survive lethal attacks at 1 HP. - * Used for {@link https://bulbapedia.bulbagarden.net/wiki/Endure_(move) | Endure} and - * Endure Tokens. + * Used for {@link https://bulbapedia.bulbagarden.net/wiki/Endure_(move) | Endure} and endure tokens. */ export class EnduringTag extends BattlerTag { - constructor(tagType: BattlerTagType, lapseType: BattlerTagLapseType, sourceMove: MoveId) { + public declare readonly tagType: EndureTagType; + constructor(tagType: EndureTagType, lapseType: BattlerTagLapseType, sourceMove: MoveId) { super(tagType, lapseType, 0, sourceMove); } @@ -1823,6 +1889,7 @@ export class EnduringTag extends BattlerTag { } export class SturdyTag extends BattlerTag { + public override readonly tagType = BattlerTagType.STURDY; constructor(sourceMove: MoveId) { super(BattlerTagType.STURDY, BattlerTagLapseType.TURN_END, 0, sourceMove); } @@ -1841,7 +1908,8 @@ export class SturdyTag extends BattlerTag { } } -export class PerishSongTag extends BattlerTag { +export class PerishSongTag extends SerializableBattlerTag { + public override readonly tagType = BattlerTagType.PERISH_SONG; constructor(turnCount: number) { super(BattlerTagType.PERISH_SONG, BattlerTagLapseType.TURN_END, turnCount, MoveId.PERISH_SONG, undefined, true); } @@ -1873,6 +1941,7 @@ export class PerishSongTag extends BattlerTag { * @see {@link https://bulbapedia.bulbagarden.net/wiki/Center_of_attention | Center of Attention} */ export class CenterOfAttentionTag extends BattlerTag { + public override readonly tagType = BattlerTagType.CENTER_OF_ATTENTION; public powder: boolean; constructor(sourceMove: MoveId) { @@ -1899,30 +1968,26 @@ export class CenterOfAttentionTag extends BattlerTag { } } -export class AbilityBattlerTag extends BattlerTag { - public ability: AbilityId; - - constructor(tagType: BattlerTagType, ability: AbilityId, lapseType: BattlerTagLapseType, turnCount: number) { - super(tagType, lapseType, turnCount); - - this.ability = ability; +export class AbilityBattlerTag extends SerializableBattlerTag { + public declare readonly tagType: AbilityBattlerTagType; + #ability: AbilityId; + /** The ability that the tag corresponds to */ + public get ability(): AbilityId { + return this.#ability; } - /** - * When given a battler tag or json representing one, load the data for it. - * @param {BattlerTag | any} source A battler tag - */ - loadTag(source: BattlerTag | any): void { - super.loadTag(source); - this.ability = source.ability as AbilityId; + constructor(tagType: AbilityBattlerTagType, ability: AbilityId, lapseType: BattlerTagLapseType, turnCount: number) { + super(tagType, lapseType, turnCount); + + this.#ability = ability; } } /** * Tag used by Unburden to double speed - * @extends AbilityBattlerTag */ export class UnburdenTag extends AbilityBattlerTag { + public override readonly tagType = BattlerTagType.UNBURDEN; constructor() { super(BattlerTagType.UNBURDEN, AbilityId.UNBURDEN, BattlerTagLapseType.CUSTOM, 1); } @@ -1935,6 +2000,7 @@ export class UnburdenTag extends AbilityBattlerTag { } export class TruantTag extends AbilityBattlerTag { + public override readonly tagType = BattlerTagType.TRUANT; constructor() { super(BattlerTagType.TRUANT, AbilityId.TRUANT, BattlerTagLapseType.MOVE, 1); } @@ -1969,6 +2035,7 @@ export class TruantTag extends AbilityBattlerTag { } export class SlowStartTag extends AbilityBattlerTag { + public override readonly tagType = BattlerTagType.SLOW_START; constructor() { super(BattlerTagType.SLOW_START, AbilityId.SLOW_START, BattlerTagLapseType.TURN_END, 5); } @@ -2006,18 +2073,19 @@ export class SlowStartTag extends AbilityBattlerTag { } export class HighestStatBoostTag extends AbilityBattlerTag { + public declare readonly tagType: HighestStatBoostTagType; public stat: Stat; public multiplier: number; - constructor(tagType: BattlerTagType, ability: AbilityId) { + constructor(tagType: HighestStatBoostTagType, ability: AbilityId) { super(tagType, ability, BattlerTagLapseType.CUSTOM, 1); } /** * When given a battler tag or json representing one, load the data for it. - * @param {BattlerTag | any} source A battler tag + * @param source - An object containing the fields needed to reconstruct this tag. */ - loadTag(source: BattlerTag | any): void { + loadTag(source: NonFunctionProperties): void { super.loadTag(source); this.stat = source.stat as Stat; this.multiplier = source.multiplier; @@ -2065,43 +2133,32 @@ export class HighestStatBoostTag extends AbilityBattlerTag { } } -export class WeatherHighestStatBoostTag extends HighestStatBoostTag implements WeatherBattlerTag { - public weatherTypes: WeatherType[]; - - constructor(tagType: BattlerTagType, ability: AbilityId, ...weatherTypes: WeatherType[]) { - super(tagType, ability); - this.weatherTypes = weatherTypes; +export class WeatherHighestStatBoostTag extends HighestStatBoostTag { + #weatherTypes: WeatherType[]; + public get weatherTypes(): WeatherType[] { + return this.#weatherTypes; } - /** - * When given a battler tag or json representing one, load the data for it. - * @param {BattlerTag | any} source A battler tag - */ - loadTag(source: BattlerTag | any): void { - super.loadTag(source); - this.weatherTypes = source.weatherTypes.map(w => w as WeatherType); + constructor(tagType: HighestStatBoostTagType, ability: AbilityId, ...weatherTypes: WeatherType[]) { + super(tagType, ability); + this.#weatherTypes = weatherTypes; } } -export class TerrainHighestStatBoostTag extends HighestStatBoostTag implements TerrainBattlerTag { - public terrainTypes: TerrainType[]; - - constructor(tagType: BattlerTagType, ability: AbilityId, ...terrainTypes: TerrainType[]) { - super(tagType, ability); - this.terrainTypes = terrainTypes; +export class TerrainHighestStatBoostTag extends HighestStatBoostTag { + #terrainTypes: TerrainType[]; + public get terrainTypes(): TerrainType[] { + return this.#terrainTypes; } - /** - * When given a battler tag or json representing one, load the data for it. - * @param {BattlerTag | any} source A battler tag - */ - loadTag(source: BattlerTag | any): void { - super.loadTag(source); - this.terrainTypes = source.terrainTypes.map(w => w as TerrainType); + constructor(tagType: HighestStatBoostTagType, ability: AbilityId, ...terrainTypes: TerrainType[]) { + super(tagType, ability); + this.#terrainTypes = terrainTypes; } } -export class SemiInvulnerableTag extends BattlerTag { +export class SemiInvulnerableTag extends SerializableBattlerTag { + public declare readonly tagType: SemiInvulnerableTagType; constructor(tagType: BattlerTagType, turnCount: number, sourceMove: MoveId) { super(tagType, BattlerTagLapseType.MOVE_EFFECT, turnCount, sourceMove); } @@ -2121,22 +2178,16 @@ export class SemiInvulnerableTag extends BattlerTag { } } -export class TypeImmuneTag extends BattlerTag { - public immuneType: PokemonType; +export class TypeImmuneTag extends SerializableBattlerTag { + #immuneType: PokemonType; + public get immuneType(): PokemonType { + return this.#immuneType; + } constructor(tagType: BattlerTagType, sourceMove: MoveId, immuneType: PokemonType, length = 1) { super(tagType, BattlerTagLapseType.TURN_END, length, sourceMove, undefined, true); - this.immuneType = immuneType; - } - - /** - * When given a battler tag or json representing one, load the data for it. - * @param {BattlerTag | any} source A battler tag - */ - loadTag(source: BattlerTag | any): void { - super.loadTag(source); - this.immuneType = source.immuneType as PokemonType; + this.#immuneType = immuneType; } } @@ -2174,10 +2225,20 @@ export class FloatingTag extends TypeImmuneTag { } } -export class TypeBoostTag extends BattlerTag { - public boostedType: PokemonType; - public boostValue: number; - public oneUse: boolean; +export class TypeBoostTag extends SerializableBattlerTag { + #boostedType: PokemonType; + #boostValue: number; + #oneUse: boolean; + + public get boostedType(): PokemonType { + return this.#boostedType; + } + public get boostValue(): number { + return this.#boostValue; + } + public get oneUse(): boolean { + return this.#oneUse; + } constructor( tagType: BattlerTagType, @@ -2188,20 +2249,9 @@ export class TypeBoostTag extends BattlerTag { ) { super(tagType, BattlerTagLapseType.TURN_END, 1, sourceMove); - this.boostedType = boostedType; - this.boostValue = boostValue; - this.oneUse = oneUse; - } - - /** - * When given a battler tag or json representing one, load the data for it. - * @param {BattlerTag | any} source A battler tag - */ - loadTag(source: BattlerTag | any): void { - super.loadTag(source); - this.boostedType = source.boostedType as PokemonType; - this.boostValue = source.boostValue; - this.oneUse = source.oneUse; + this.#boostedType = boostedType; + this.#boostValue = boostValue; + this.#oneUse = oneUse; } lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { @@ -2224,7 +2274,7 @@ export class TypeBoostTag extends BattlerTag { } } -export class CritBoostTag extends BattlerTag { +export class CritBoostTag extends SerializableBattlerTag { constructor(tagType: BattlerTagType, sourceMove: MoveId) { super(tagType, BattlerTagLapseType.TURN_END, 1, sourceMove, undefined, true); } @@ -2256,7 +2306,6 @@ export class CritBoostTag extends BattlerTag { /** * Tag for the effects of Dragon Cheer, which boosts the critical hit ratio of the user's allies. - * @extends {CritBoostTag} */ export class DragonCheerTag extends CritBoostTag { /** The types of the user's ally when the tag is added */ @@ -2273,22 +2322,12 @@ export class DragonCheerTag extends CritBoostTag { } } -export class SaltCuredTag extends BattlerTag { - private sourceIndex: number; - +export class SaltCuredTag extends SerializableBattlerTag { + public override readonly tagType = BattlerTagType.SALT_CURED; constructor(sourceId: number) { super(BattlerTagType.SALT_CURED, BattlerTagLapseType.TURN_END, 1, MoveId.SALT_CURE, sourceId); } - /** - * When given a battler tag or json representing one, load the data for it. - * @param {BattlerTag | any} source A battler tag - */ - loadTag(source: BattlerTag | any): void { - super.loadTag(source); - this.sourceIndex = source.sourceIndex; - } - onAdd(pokemon: Pokemon): void { const source = this.getSourcePokemon(); if (!source) { @@ -2302,7 +2341,6 @@ export class SaltCuredTag extends BattlerTag { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), }), ); - this.sourceIndex = source.getBattlerIndex(); } lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { @@ -2338,22 +2376,11 @@ export class SaltCuredTag extends BattlerTag { } } -export class CursedTag extends BattlerTag { - private sourceIndex: number; - +export class CursedTag extends SerializableBattlerTag { constructor(sourceId: number) { super(BattlerTagType.CURSED, BattlerTagLapseType.TURN_END, 1, MoveId.CURSE, sourceId, true); } - /** - * When given a battler tag or json representing one, load the data for it. - * @param {BattlerTag | any} source A battler tag - */ - loadTag(source: BattlerTag | any): void { - super.loadTag(source); - this.sourceIndex = source.sourceIndex; - } - onAdd(pokemon: Pokemon): void { const source = this.getSourcePokemon(); if (!source) { @@ -2362,7 +2389,6 @@ export class CursedTag extends BattlerTag { } super.onAdd(pokemon); - this.sourceIndex = source.getBattlerIndex(); } lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { @@ -2392,10 +2418,11 @@ export class CursedTag extends BattlerTag { return ret; } } + /** * Battler tag for attacks that remove a type post use. */ -export class RemovedTypeTag extends BattlerTag { +export class RemovedTypeTag extends SerializableBattlerTag { constructor(tagType: BattlerTagType, lapseType: BattlerTagLapseType, sourceMove: MoveId) { super(tagType, lapseType, 1, sourceMove); } @@ -2405,7 +2432,7 @@ export class RemovedTypeTag extends BattlerTag { * Battler tag for effects that ground the source, allowing Ground-type moves to hit them. * @description `IGNORE_FLYING`: Persistent grounding effects (i.e. from Smack Down and Thousand Waves) */ -export class GroundedTag extends BattlerTag { +export class GroundedTag extends SerializableBattlerTag { constructor(tagType: BattlerTagType, lapseType: BattlerTagLapseType, sourceMove: MoveId) { super(tagType, lapseType, 1, sourceMove); } @@ -2478,15 +2505,16 @@ export class RoostedTag extends BattlerTag { } /** Common attributes of form change abilities that block damage */ -export class FormBlockDamageTag extends BattlerTag { - constructor(tagType: BattlerTagType) { +export class FormBlockDamageTag extends SerializableBattlerTag { + public declare readonly tagType: BattlerTagType.ICE_FACE | BattlerTagType.DISGUISE; + constructor(tagType: BattlerTagType.ICE_FACE | BattlerTagType.DISGUISE) { super(tagType, BattlerTagLapseType.CUSTOM, 1); } /** * Determines if the tag can be added to the Pokémon. - * @param {Pokemon} pokemon The Pokémon to which the tag might be added. - * @returns {boolean} True if the tag can be added, false otherwise. + * @param pokemon - The Pokémon to which the tag might be added. + * @returns `true` if the tag can be added, `false` otherwise. */ canAdd(pokemon: Pokemon): boolean { return pokemon.formIndex === 0; @@ -2508,7 +2536,7 @@ export class FormBlockDamageTag extends BattlerTag { /** * Removes the tag from the Pokémon. * Triggers a form change when the tag is removed. - * @param {Pokemon} pokemon The Pokémon from which the tag is removed. + * @param pokemon - The Pokémon from which the tag is removed. */ onRemove(pokemon: Pokemon): void { super.onRemove(pokemon); @@ -2516,12 +2544,14 @@ export class FormBlockDamageTag extends BattlerTag { globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeAbilityTrigger); } } + /** Provides the additional weather-based effects of the Ice Face ability */ export class IceFaceBlockDamageTag extends FormBlockDamageTag { + public override readonly tagType = BattlerTagType.ICE_FACE; /** * Determines if the tag can be added to the Pokémon. - * @param {Pokemon} pokemon The Pokémon to which the tag might be added. - * @returns {boolean} True if the tag can be added, false otherwise. + * @param pokemon - The Pokémon to which the tag might be added. + * @returns `true` if the tag can be added, `false` otherwise. */ canAdd(pokemon: Pokemon): boolean { const weatherType = globalScene.arena.weather?.weatherType; @@ -2535,20 +2565,17 @@ export class IceFaceBlockDamageTag extends FormBlockDamageTag { * Battler tag indicating a Tatsugiri with {@link https://bulbapedia.bulbagarden.net/wiki/Commander_(Ability) | Commander} * has entered the tagged Pokemon's mouth. */ -export class CommandedTag extends BattlerTag { - private _tatsugiriFormKey: string; +export class CommandedTag extends SerializableBattlerTag { + public override readonly tagType = BattlerTagType.COMMANDED; + public readonly tatsugiriFormKey: string; constructor(sourceId: number) { super(BattlerTagType.COMMANDED, BattlerTagLapseType.CUSTOM, 0, MoveId.NONE, sourceId); } - public get tatsugiriFormKey(): string { - return this._tatsugiriFormKey; - } - /** Caches the Tatsugiri's form key and sharply boosts the tagged Pokemon's stats */ override onAdd(pokemon: Pokemon): void { - this._tatsugiriFormKey = this.getSourcePokemon()?.getFormKey() ?? "curly"; + (this as Mutable).tatsugiriFormKey = this.getSourcePokemon()?.getFormKey() ?? "curly"; globalScene.phaseManager.unshiftNew( "StatStageChangePhase", pokemon.getBattlerIndex(), @@ -2565,9 +2592,9 @@ export class CommandedTag extends BattlerTag { } } - override loadTag(source: BattlerTag | any): void { + override loadTag(source: NonFunctionProperties): void { super.loadTag(source); - this._tatsugiriFormKey = source._tatsugiriFormKey; + (this as Mutable).tatsugiriFormKey = source.tatsugiriFormKey; } } @@ -2581,7 +2608,8 @@ export class CommandedTag extends BattlerTag { * - Removing stacks decreases DEF and SPDEF, independently, by one stage for each stack that successfully changed * the stat when added. */ -export class StockpilingTag extends BattlerTag { +export class StockpilingTag extends SerializableBattlerTag { + public override readonly tagType = BattlerTagType.STOCKPILING; public stockpiledCount = 0; public statChangeCounts: { [Stat.DEF]: number; [Stat.SPDEF]: number } = { [Stat.DEF]: 0, @@ -2604,7 +2632,7 @@ export class StockpilingTag extends BattlerTag { } }; - loadTag(source: BattlerTag | any): void { + override loadTag(source: NonFunctionProperties): void { super.loadTag(source); this.stockpiledCount = source.stockpiledCount || 0; this.statChangeCounts = { @@ -2687,10 +2715,10 @@ export class StockpilingTag extends BattlerTag { /** * Battler tag for Gulp Missile used by Cramorant. - * @extends BattlerTag */ -export class GulpMissileTag extends BattlerTag { - constructor(tagType: BattlerTagType, sourceMove: MoveId) { +export class GulpMissileTag extends SerializableBattlerTag { + public declare readonly tagType: BattlerTagType.GULP_MISSILE_ARROKUDA | BattlerTagType.GULP_MISSILE_PIKACHU; + constructor(tagType: BattlerTagType.GULP_MISSILE_ARROKUDA | BattlerTagType.GULP_MISSILE_PIKACHU, sourceMove: MoveId) { super(tagType, BattlerTagLapseType.HIT, 0, sourceMove); } @@ -2729,11 +2757,12 @@ export class GulpMissileTag extends BattlerTag { /** * Gulp Missile's initial form changes are triggered by using Surf and Dive. - * @param {Pokemon} pokemon The Pokemon with Gulp Missile ability. + * @param pokemon - The Pokemon with Gulp Missile ability. * @returns Whether the BattlerTag can be added. */ canAdd(pokemon: Pokemon): boolean { - const isSurfOrDive = [MoveId.SURF, MoveId.DIVE].includes(this.sourceMove); + // Bang here is OK as if sourceMove was undefined, this would just evaluate to false + const isSurfOrDive = [MoveId.SURF, MoveId.DIVE].includes(this.sourceMove!); const isNormalForm = pokemon.formIndex === 0 && !pokemon.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA) && @@ -2755,52 +2784,46 @@ export class GulpMissileTag extends BattlerTag { } /** - * Tag that makes the target drop all of it type immunities + * Tag that makes the target drop the immunities granted by a particular type * and all accuracy checks ignore its evasiveness stat. * * Applied by moves: {@linkcode MoveId.ODOR_SLEUTH | Odor Sleuth}, * {@linkcode MoveId.MIRACLE_EYE | Miracle Eye} and {@linkcode MoveId.FORESIGHT | Foresight}. * - * @extends BattlerTag * @see {@linkcode ignoreImmunity} */ -export class ExposedTag extends BattlerTag { - private defenderType: PokemonType; - private allowedTypes: PokemonType[]; +export class ExposedTag extends SerializableBattlerTag { + public declare readonly tagType: BattlerTagType.IGNORE_DARK | BattlerTagType.IGNORE_GHOST; + #defenderType: PokemonType; + #allowedTypes: readonly PokemonType[]; - constructor(tagType: BattlerTagType, sourceMove: MoveId, defenderType: PokemonType, allowedTypes: PokemonType[]) { + constructor( + tagType: BattlerTagType.IGNORE_DARK | BattlerTagType.IGNORE_GHOST, + sourceMove: MoveId, + defenderType: PokemonType, + allowedTypes: PokemonType[], + ) { super(tagType, BattlerTagLapseType.CUSTOM, 1, sourceMove); - this.defenderType = defenderType; - this.allowedTypes = allowedTypes; + this.#defenderType = defenderType; + this.#allowedTypes = allowedTypes; } /** - * When given a battler tag or json representing one, load the data for it. - * @param {BattlerTag | any} source A battler tag - */ - loadTag(source: BattlerTag | any): void { - super.loadTag(source); - this.defenderType = source.defenderType as PokemonType; - this.allowedTypes = source.allowedTypes as PokemonType[]; - } - - /** - * @param types {@linkcode PokemonType} of the defending Pokemon - * @param moveType {@linkcode PokemonType} of the move targetting it + * @param type - The defending type to check against + * @param moveType - The pokemon type of the move being used * @returns `true` if the move should be allowed to target the defender. */ ignoreImmunity(type: PokemonType, moveType: PokemonType): boolean { - return type === this.defenderType && this.allowedTypes.includes(moveType); + return type === this.#defenderType && this.#allowedTypes.includes(moveType); } } /** * Tag that prevents HP recovery from held items and move effects. It also blocks the usage of recovery moves. * Applied by moves: {@linkcode MoveId.HEAL_BLOCK | Heal Block (5 turns)}, {@linkcode MoveId.PSYCHIC_NOISE | Psychic Noise (2 turns)} - * - * @extends MoveRestrictionBattlerTag */ export class HealBlockTag extends MoveRestrictionBattlerTag { + public override readonly tagType = BattlerTagType.HEAL_BLOCK; constructor(turnCount: number, sourceMove: MoveId) { super( BattlerTagType.HEAL_BLOCK, @@ -2818,7 +2841,7 @@ export class HealBlockTag extends MoveRestrictionBattlerTag { /** * Checks if a move is disabled under Heal Block - * @param {MoveId} move {@linkcode MoveId} the move ID + * @param move - {@linkcode MoveId | ID} of the move being used * @returns `true` if the move has a TRIAGE_MOVE flag and is a status move */ override isMoveRestricted(move: MoveId): boolean { @@ -2828,9 +2851,9 @@ export class HealBlockTag extends MoveRestrictionBattlerTag { /** * Checks if a move is disabled under Heal Block because of its choice of target * Implemented b/c of Pollen Puff - * @param {MoveId} move {@linkcode MoveId} the move ID - * @param {Pokemon} user {@linkcode Pokemon} the move user - * @param {Pokemon} target {@linkcode Pokemon} the target of the move + * @param move - {@linkcode MoveId | ID} of the move being used + * @param user - The pokemon using the move + * @param target - The target of the move * @returns `true` if the move cannot be used because the target is an ally */ override isMoveTargetRestricted(move: MoveId, user: Pokemon, target: Pokemon) { @@ -2851,10 +2874,9 @@ export class HealBlockTag extends MoveRestrictionBattlerTag { } /** - * @override - * @param {Pokemon} pokemon {@linkcode Pokemon} attempting to use the restricted move - * @param {MoveId} move {@linkcode MoveId} ID of the move being interrupted - * @returns {string} text to display when the move is interrupted + * @param pokemon - {@linkcode Pokemon} attempting to use the restricted move + * @param move - {@linkcode MoveId | ID} of the move being interrupted + * @returns Text to display when the move is interrupted */ override interruptedText(pokemon: Pokemon, move: MoveId): string { return i18next.t("battle:moveDisabledHealBlock", { @@ -2880,17 +2902,17 @@ export class HealBlockTag extends MoveRestrictionBattlerTag { /** * Tag that doubles the type effectiveness of Fire-type moves. - * @extends BattlerTag */ -export class TarShotTag extends BattlerTag { +export class TarShotTag extends SerializableBattlerTag { + public override readonly tagType = BattlerTagType.TAR_SHOT; constructor() { super(BattlerTagType.TAR_SHOT, BattlerTagLapseType.CUSTOM, 0); } /** * If the Pokemon is terastallized, the tag cannot be added. - * @param {Pokemon} pokemon the {@linkcode Pokemon} to which the tag is added - * @returns whether the tag is applied + * @param pokemon - The pokemon to check + * @returns Whether the tag can be added */ override canAdd(pokemon: Pokemon): boolean { return !pokemon.isTerastallized; @@ -2910,6 +2932,7 @@ export class TarShotTag extends BattlerTag { * While this tag is in effect, the afflicted Pokemon's moves are changed to Electric type. */ export class ElectrifiedTag extends BattlerTag { + public override readonly tagType = BattlerTagType.ELECTRIFIED; constructor() { super(BattlerTagType.ELECTRIFIED, BattlerTagLapseType.TURN_END, 1, MoveId.ELECTRIFY); } @@ -2928,7 +2951,8 @@ export class ElectrifiedTag extends BattlerTag { * Battler Tag that keeps track of how many times the user has Autotomized * Each count of Autotomization reduces the weight by 100kg */ -export class AutotomizedTag extends BattlerTag { +export class AutotomizedTag extends SerializableBattlerTag { + public override readonly tagType = BattlerTagType.AUTOTOMIZED; public autotomizeCount = 0; constructor(sourceMove: MoveId = MoveId.AUTOTOMIZE) { super(BattlerTagType.AUTOTOMIZED, BattlerTagLapseType.CUSTOM, 1, sourceMove); @@ -2954,20 +2978,45 @@ export class AutotomizedTag extends BattlerTag { onOverlap(pokemon: Pokemon): void { this.onAdd(pokemon); } + + loadTag(source: NonFunctionProperties): void { + super.loadTag(source); + this.autotomizeCount = source.autotomizeCount; + } } /** * Tag implementing the {@link https://bulbapedia.bulbagarden.net/wiki/Substitute_(doll)#Effect | Substitute Doll} effect, * for use with the moves Substitute and Shed Tail. Pokemon with this tag deflect most forms of received attack damage * onto the tag. This tag also grants immunity to most Status moves and several move effects. + * + * @sealed */ -export class SubstituteTag extends BattlerTag { +export class SubstituteTag extends SerializableBattlerTag { + public override readonly tagType = BattlerTagType.SUBSTITUTE; /** The substitute's remaining HP. If HP is depleted, the Substitute fades. */ public hp: number; + + //#region non-serializable properties /** A reference to the sprite representing the Substitute doll */ - public sprite: Phaser.GameObjects.Sprite; + #sprite: Phaser.GameObjects.Sprite; + /** A reference to the sprite representing the Substitute doll */ + public get sprite(): Phaser.GameObjects.Sprite { + return this.#sprite; + } + public set sprite(value: Phaser.GameObjects.Sprite) { + this.#sprite = value; + } /** Is the source Pokemon "in focus," i.e. is it fully visible on the field? */ - public sourceInFocus: boolean; + #sourceInFocus: boolean; + /** Is the source Pokemon "in focus," i.e. is it fully visible on the field? */ + public get sourceInFocus(): boolean { + return this.#sourceInFocus; + } + public set sourceInFocus(value: boolean) { + this.#sourceInFocus = value; + } + //#endregion non-serializable properties constructor(sourceMove: MoveId, sourceId: number) { super( @@ -3078,9 +3127,9 @@ export class SubstituteTag extends BattlerTag { /** * When given a battler tag or json representing one, load the data for it. - * @param {BattlerTag | any} source A battler tag + * @param source - An object containing the necessary properties to load the tag */ - loadTag(source: BattlerTag | any): void { + override loadTag(source: NonFunctionProperties): void { super.loadTag(source); this.hp = source.hp; } @@ -3093,6 +3142,7 @@ export class SubstituteTag extends BattlerTag { * Currently used only in MysteryEncounters to provide start of fight stat buffs. */ export class MysteryEncounterPostSummonTag extends BattlerTag { + public override readonly tagType = BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON; constructor() { super(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON, BattlerTagLapseType.CUSTOM, 1); } @@ -3126,15 +3176,11 @@ export class MysteryEncounterPostSummonTag extends BattlerTag { * Torment does not interrupt the move if the move is performed consecutively in the same turn and right after Torment is applied */ export class TormentTag extends MoveRestrictionBattlerTag { + public override readonly tagType = BattlerTagType.TORMENT; constructor(sourceId: number) { super(BattlerTagType.TORMENT, BattlerTagLapseType.AFTER_MOVE, 1, MoveId.TORMENT, sourceId); } - /** - * Adds the battler tag to the target Pokemon and defines the private class variable 'target' - * 'Target' is used to track the Pokemon's current status - * @param {Pokemon} pokemon the Pokemon tormented - */ override onAdd(pokemon: Pokemon) { super.onAdd(pokemon); globalScene.phaseManager.queueMessage( @@ -3147,7 +3193,7 @@ export class TormentTag extends MoveRestrictionBattlerTag { /** * Torment only ends when the affected Pokemon leaves the battle field - * @param {Pokemon} pokemon the Pokemon under the effects of Torment + * @param pokemon - The Pokemon under the effects of Torment * @param _tagType * @returns `true` if still present | `false` if not */ @@ -3156,8 +3202,8 @@ export class TormentTag extends MoveRestrictionBattlerTag { } /** - * This checks if the current move used is identical to the last used move with a {@linkcode MoveResult} of `SUCCESS`/`MISS` - * @param {MoveId} move the move under investigation + * Check if the current move used is identical to the last used move with a {@linkcode MoveResult} of `SUCCESS`/`MISS` + * @param move - The move under investigation * @returns `true` if there is valid consecutive usage | `false` if the moves are different from each other */ public override isMoveRestricted(move: MoveId, user: Pokemon): boolean { @@ -3189,6 +3235,7 @@ export class TormentTag extends MoveRestrictionBattlerTag { * The tag is removed after 4 turns. */ export class TauntTag extends MoveRestrictionBattlerTag { + public override readonly tagType = BattlerTagType.TAUNT; constructor() { super(BattlerTagType.TAUNT, [BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.AFTER_MOVE], 4, MoveId.TAUNT); } @@ -3214,8 +3261,8 @@ export class TauntTag extends MoveRestrictionBattlerTag { } /** - * Checks if a move is a status move and determines its restriction status on that basis - * @param {MoveId} move the move under investigation + * Check if a move is a status move and determines its restriction status on that basis + * @param move - The move under investigation * @returns `true` if the move is a status move */ override isMoveRestricted(move: MoveId): boolean { @@ -3243,6 +3290,7 @@ export class TauntTag extends MoveRestrictionBattlerTag { * The tag is only removed when the source-user is removed from the field. */ export class ImprisonTag extends MoveRestrictionBattlerTag { + public override readonly tagType = BattlerTagType.IMPRISON; constructor(sourceId: number) { super( BattlerTagType.IMPRISON, @@ -3255,8 +3303,7 @@ export class ImprisonTag extends MoveRestrictionBattlerTag { /** * Checks if the source of Imprison is still active - * @override - * @param pokemon The pokemon this tag is attached to + * @param pokemon - The pokemon this tag is attached to * @returns `true` if the source is still active */ public override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { @@ -3273,8 +3320,7 @@ export class ImprisonTag extends MoveRestrictionBattlerTag { /** * Checks if the source of the tag has the parameter move in its moveset and that the source is still active - * @override - * @param {MoveId} move the move under investigation + * @param move - The move under investigation * @returns `false` if either condition is not met */ public override isMoveRestricted(move: MoveId, _user: Pokemon): boolean { @@ -3306,7 +3352,8 @@ export class ImprisonTag extends MoveRestrictionBattlerTag { * For three turns, starting from the turn of hit, at the end of each turn, the target Pokemon's speed will decrease by 1. * The tag can also expire by taking the target Pokemon off the field, or the Pokemon that originally used the move. */ -export class SyrupBombTag extends BattlerTag { +export class SyrupBombTag extends SerializableBattlerTag { + public override readonly tagType = BattlerTagType.SYRUP_BOMB; constructor(sourceId: number) { super(BattlerTagType.SYRUP_BOMB, BattlerTagLapseType.TURN_END, 3, MoveId.SYRUP_BOMB, sourceId); } @@ -3368,7 +3415,8 @@ export class SyrupBombTag extends BattlerTag { * The effects of Telekinesis can be baton passed to a teammate. * @see {@link https://bulbapedia.bulbagarden.net/wiki/Telekinesis_(move) | MoveId.TELEKINESIS} */ -export class TelekinesisTag extends BattlerTag { +export class TelekinesisTag extends SerializableBattlerTag { + public override readonly tagType = BattlerTagType.TELEKINESIS; constructor(sourceMove: MoveId) { super( BattlerTagType.TELEKINESIS, @@ -3391,9 +3439,9 @@ export class TelekinesisTag extends BattlerTag { /** * Tag that swaps the user's base ATK stat with its base DEF stat. - * @extends BattlerTag */ -export class PowerTrickTag extends BattlerTag { +export class PowerTrickTag extends SerializableBattlerTag { + public override readonly tagType = BattlerTagType.POWER_TRICK; constructor(sourceMove: MoveId, sourceId: number) { super(BattlerTagType.POWER_TRICK, BattlerTagLapseType.CUSTOM, 0, sourceMove, sourceId, true); } @@ -3418,7 +3466,7 @@ export class PowerTrickTag extends BattlerTag { /** * Removes the Power Trick tag and reverts any stat changes if the tag is already applied. - * @param {Pokemon} pokemon The {@linkcode Pokemon} that already has the Power Trick tag. + * @param pokemon - The {@linkcode Pokemon} that already has the Power Trick tag. */ onOverlap(pokemon: Pokemon): void { pokemon.removeTag(this.tagType); @@ -3426,7 +3474,7 @@ export class PowerTrickTag extends BattlerTag { /** * Swaps the user's base ATK stat with its base DEF stat. - * @param {Pokemon} pokemon The {@linkcode Pokemon} whose stats will be swapped. + * @param pokemon - The {@linkcode Pokemon} whose stats will be swapped. */ swapStat(pokemon: Pokemon): void { const temp = pokemon.getStat(Stat.ATK, false); @@ -3440,7 +3488,8 @@ export class PowerTrickTag extends BattlerTag { * If this tag is active when the bearer faints from an opponent's move, the tag reduces that move's PP to 0. * Otherwise, it lapses when the bearer makes another move. */ -export class GrudgeTag extends BattlerTag { +export class GrudgeTag extends SerializableBattlerTag { + public override readonly tagType = BattlerTagType.GRUDGE; constructor() { super(BattlerTagType.GRUDGE, [BattlerTagLapseType.CUSTOM, BattlerTagLapseType.PRE_MOVE], 1, MoveId.GRUDGE); } @@ -3458,7 +3507,7 @@ export class GrudgeTag extends BattlerTag { * Activates Grudge's special effect on the attacking Pokemon and lapses the tag. * @param pokemon * @param lapseType - * @param sourcePokemon {@linkcode Pokemon} the source of the move that fainted the tag's bearer + * @param sourcePokemon - The source of the move that fainted the tag's bearer * @returns `false` if Grudge activates its effect or lapses */ override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType, sourcePokemon?: Pokemon): boolean { @@ -3486,6 +3535,7 @@ export class GrudgeTag extends BattlerTag { * Tag used to heal the user of Psycho Shift of its status effect if Psycho Shift succeeds in transferring its status effect to the target Pokemon */ export class PsychoShiftTag extends BattlerTag { + public override readonly tagType = BattlerTagType.PSYCHO_SHIFT; constructor() { super(BattlerTagType.PSYCHO_SHIFT, BattlerTagLapseType.AFTER_MOVE, 1, MoveId.PSYCHO_SHIFT); } @@ -3510,6 +3560,7 @@ export class PsychoShiftTag extends BattlerTag { * Tag associated with the move Magic Coat. */ export class MagicCoatTag extends BattlerTag { + public override readonly tagType = BattlerTagType.MAGIC_COAT; constructor() { super(BattlerTagType.MAGIC_COAT, BattlerTagLapseType.TURN_END, 1, MoveId.MAGIC_COAT); } @@ -3729,19 +3780,18 @@ export function getBattlerTag( return new PsychoShiftTag(); case BattlerTagType.MAGIC_COAT: return new MagicCoatTag(); - case BattlerTagType.NONE: - default: - return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId); } } /** * When given a battler tag or json representing one, creates an actual BattlerTag object with the same data. - * @param {BattlerTag | any} source A battler tag - * @return {BattlerTag} The valid battler tag + * @param source - An object containing the data necessary to reconstruct the BattlerTag. + * @returns The valid battler tag */ -export function loadBattlerTag(source: BattlerTag | any): BattlerTag { - const tag = getBattlerTag(source.tagType, source.turnCount, source.sourceMove, source.sourceId); +export function loadBattlerTag(source: SerializableBattlerTag): BattlerTag { + // TODO: Remove this bang by fixing the signature of `getBattlerTag` + // to allow undefined sourceIds and sourceMoves (with appropriate fallback for tags that require it) + const tag = getBattlerTag(source.tagType, source.turnCount, source.sourceMove!, source.sourceId!); tag.loadTag(source); return tag; } @@ -3749,8 +3799,8 @@ export function loadBattlerTag(source: BattlerTag | any): BattlerTag { /** * Helper function to verify that the current phase is a MoveEffectPhase and provide quick access to commonly used fields * - * @param _pokemon {@linkcode Pokemon} The Pokémon used to access the current phase - * @returns null if current phase is not MoveEffectPhase, otherwise Object containing the {@linkcode MoveEffectPhase}, and its + * @param _pokemon - The Pokémon used to access the current phase (unused) + * @returns `null` if current phase is not MoveEffectPhase, otherwise Object containing the {@linkcode MoveEffectPhase}, and its * corresponding {@linkcode Move} and user {@linkcode Pokemon} */ function getMoveEffectPhaseData(_pokemon: Pokemon): { phase: MoveEffectPhase; attacker: Pokemon; move: Move } | null { @@ -3764,3 +3814,104 @@ function getMoveEffectPhaseData(_pokemon: Pokemon): { phase: MoveEffectPhase; at } return null; } + +/** + * Map from {@linkcode BattlerTagType} to the corresponding {@linkcode BattlerTag} class. + */ +export type BattlerTagTypeMap = { + [BattlerTagType.RECHARGING]: RechargingTag; + [BattlerTagType.SHELL_TRAP]: ShellTrapTag; + [BattlerTagType.FLINCHED]: FlinchedTag; + [BattlerTagType.INTERRUPTED]: InterruptedTag; + [BattlerTagType.CONFUSED]: ConfusedTag; + [BattlerTagType.INFATUATED]: InfatuatedTag; + [BattlerTagType.SEEDED]: SeedTag; + [BattlerTagType.POWDER]: PowderTag; + [BattlerTagType.NIGHTMARE]: NightmareTag; + [BattlerTagType.FRENZY]: FrenzyTag; + [BattlerTagType.CHARGING]: BattlerTag; + [BattlerTagType.ENCORE]: EncoreTag; + [BattlerTagType.HELPING_HAND]: HelpingHandTag; + [BattlerTagType.INGRAIN]: IngrainTag; + [BattlerTagType.AQUA_RING]: AquaRingTag; + [BattlerTagType.DROWSY]: DrowsyTag; + [BattlerTagType.TRAPPED]: TrappedTag; + [BattlerTagType.NO_RETREAT]: NoRetreatTag; + [BattlerTagType.BIND]: BindTag; + [BattlerTagType.WRAP]: WrapTag; + [BattlerTagType.FIRE_SPIN]: FireSpinTag; + [BattlerTagType.WHIRLPOOL]: WhirlpoolTag; + [BattlerTagType.CLAMP]: ClampTag; + [BattlerTagType.SAND_TOMB]: SandTombTag; + [BattlerTagType.MAGMA_STORM]: MagmaStormTag; + [BattlerTagType.SNAP_TRAP]: SnapTrapTag; + [BattlerTagType.THUNDER_CAGE]: ThunderCageTag; + [BattlerTagType.INFESTATION]: InfestationTag; + [BattlerTagType.PROTECTED]: ProtectedTag; + [BattlerTagType.SPIKY_SHIELD]: ContactDamageProtectedTag; + [BattlerTagType.KINGS_SHIELD]: ContactStatStageChangeProtectedTag; + [BattlerTagType.OBSTRUCT]: ContactStatStageChangeProtectedTag; + [BattlerTagType.SILK_TRAP]: ContactStatStageChangeProtectedTag; + [BattlerTagType.BANEFUL_BUNKER]: ContactSetStatusProtectedTag; + [BattlerTagType.BURNING_BULWARK]: ContactSetStatusProtectedTag; + [BattlerTagType.ENDURING]: EnduringTag; + [BattlerTagType.ENDURE_TOKEN]: EnduringTag; + [BattlerTagType.STURDY]: SturdyTag; + [BattlerTagType.PERISH_SONG]: PerishSongTag; + [BattlerTagType.CENTER_OF_ATTENTION]: CenterOfAttentionTag; + [BattlerTagType.TRUANT]: TruantTag; + [BattlerTagType.SLOW_START]: SlowStartTag; + [BattlerTagType.PROTOSYNTHESIS]: WeatherHighestStatBoostTag; + [BattlerTagType.QUARK_DRIVE]: TerrainHighestStatBoostTag; + [BattlerTagType.FLYING]: SemiInvulnerableTag; + [BattlerTagType.UNDERGROUND]: SemiInvulnerableTag; + [BattlerTagType.UNDERWATER]: SemiInvulnerableTag; + [BattlerTagType.HIDDEN]: SemiInvulnerableTag; + [BattlerTagType.FIRE_BOOST]: TypeBoostTag; + [BattlerTagType.CRIT_BOOST]: CritBoostTag; + [BattlerTagType.DRAGON_CHEER]: DragonCheerTag; + [BattlerTagType.ALWAYS_CRIT]: BattlerTag; + [BattlerTagType.IGNORE_ACCURACY]: BattlerTag; + [BattlerTagType.ALWAYS_GET_HIT]: BattlerTag; + [BattlerTagType.RECEIVE_DOUBLE_DAMAGE]: BattlerTag; + [BattlerTagType.BYPASS_SLEEP]: BattlerTag; + [BattlerTagType.IGNORE_FLYING]: GroundedTag; + [BattlerTagType.ROOSTED]: RoostedTag; + [BattlerTagType.BURNED_UP]: RemovedTypeTag; + [BattlerTagType.DOUBLE_SHOCKED]: RemovedTypeTag; + [BattlerTagType.SALT_CURED]: SaltCuredTag; + [BattlerTagType.CURSED]: CursedTag; + [BattlerTagType.CHARGED]: TypeBoostTag; + [BattlerTagType.FLOATING]: FloatingTag; + [BattlerTagType.MINIMIZED]: MinimizeTag; + [BattlerTagType.DESTINY_BOND]: DestinyBondTag; + [BattlerTagType.ICE_FACE]: IceFaceBlockDamageTag; + [BattlerTagType.DISGUISE]: FormBlockDamageTag; + [BattlerTagType.COMMANDED]: CommandedTag; + [BattlerTagType.STOCKPILING]: StockpilingTag; + [BattlerTagType.OCTOLOCK]: OctolockTag; + [BattlerTagType.DISABLED]: DisabledTag; + [BattlerTagType.IGNORE_GHOST]: ExposedTag; + [BattlerTagType.IGNORE_DARK]: ExposedTag; + [BattlerTagType.GULP_MISSILE_ARROKUDA]: GulpMissileTag; + [BattlerTagType.GULP_MISSILE_PIKACHU]: GulpMissileTag; + [BattlerTagType.BEAK_BLAST_CHARGING]: BeakBlastChargingTag; + [BattlerTagType.TAR_SHOT]: TarShotTag; + [BattlerTagType.ELECTRIFIED]: ElectrifiedTag; + [BattlerTagType.THROAT_CHOPPED]: ThroatChoppedTag; + [BattlerTagType.GORILLA_TACTICS]: GorillaTacticsTag; + [BattlerTagType.UNBURDEN]: UnburdenTag; + [BattlerTagType.SUBSTITUTE]: SubstituteTag; + [BattlerTagType.AUTOTOMIZED]: AutotomizedTag; + [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON]: MysteryEncounterPostSummonTag; + [BattlerTagType.HEAL_BLOCK]: HealBlockTag; + [BattlerTagType.TORMENT]: TormentTag; + [BattlerTagType.TAUNT]: TauntTag; + [BattlerTagType.IMPRISON]: ImprisonTag; + [BattlerTagType.SYRUP_BOMB]: SyrupBombTag; + [BattlerTagType.TELEKINESIS]: TelekinesisTag; + [BattlerTagType.POWER_TRICK]: PowerTrickTag; + [BattlerTagType.GRUDGE]: GrudgeTag; + [BattlerTagType.PSYCHO_SHIFT]: PsychoShiftTag; + [BattlerTagType.MAGIC_COAT]: MagicCoatTag; +}; diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 870a32b6e2c..2684d2f6ebe 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -10768,7 +10768,7 @@ export function initMoves() { new SelfStatusMove(MoveId.NO_RETREAT, PokemonType.FIGHTING, -1, 5, -1, 0, 8) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true) .attr(AddBattlerTagAttr, BattlerTagType.NO_RETREAT, true, false) - .condition((user, target, move) => user.getTag(TrappedTag)?.sourceMove !== MoveId.NO_RETREAT), // fails if the user is currently trapped by No Retreat + .condition((user, target, move) => user.getTag(TrappedTag)?.tagType !== BattlerTagType.NO_RETREAT), // fails if the user is currently trapped by No Retreat new StatusMove(MoveId.TAR_SHOT, PokemonType.ROCK, 100, 15, -1, 0, 8) .attr(StatStageChangeAttr, [ Stat.SPD ], -1) .attr(AddBattlerTagAttr, BattlerTagType.TAR_SHOT, false) diff --git a/src/data/pokemon/pokemon-data.ts b/src/data/pokemon/pokemon-data.ts index a2a0138a17a..972d7627bcd 100644 --- a/src/data/pokemon/pokemon-data.ts +++ b/src/data/pokemon/pokemon-data.ts @@ -1,4 +1,5 @@ -import { type BattlerTag, loadBattlerTag } from "#data/battler-tags"; +import type { BattlerTag } from "#data/battler-tags"; +import { loadBattlerTag, SerializableBattlerTag } from "#data/battler-tags"; import { allSpecies } from "#data/data-lists"; import type { Gender } from "#data/gender"; import { PokemonMove } from "#data/moves/pokemon-move"; @@ -187,9 +188,11 @@ export class PokemonSummonData { continue; } - if (key === "tags") { - // load battler tags - this.tags = value.map((t: BattlerTag) => loadBattlerTag(t)); + if (key === "tags" && Array.isArray(value)) { + // load battler tags, discarding any that are not serializable + this.tags = value + .map((t: SerializableBattlerTag) => loadBattlerTag(t)) + .filter((t): t is SerializableBattlerTag => t instanceof SerializableBattlerTag); continue; } this[key] = value; diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index 719b08c5b81..6d9d2dd4a92 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -1,5 +1,4 @@ export enum BattlerTagType { - NONE = "NONE", RECHARGING = "RECHARGING", FLINCHED = "FLINCHED", INTERRUPTED = "INTERRUPTED", From 091af60e7c8597755cb64197444073eabc08b386 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Tue, 29 Jul 2025 15:31:44 -0700 Subject: [PATCH 51/59] [Docs] Adjust `@module` doc comment in `battler-tags.ts` Note that currently Typedoc is not parsing `@module` docs, but this comment adjustment would be required if and when it gets fixed --- src/data/battler-tags.ts | 46 ++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 79c14f860b6..9dfd43eccb3 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -47,29 +47,29 @@ import type { import type { Mutable, NonFunctionProperties } from "#types/type-helpers"; import { BooleanHolder, coerceArray, getFrameMs, isNullOrUndefined, NumberHolder, toDmgValue } from "#utils/common"; -/* -@module -BattlerTags are used to represent semi-persistent effects that can be attached to a Pokemon. -Note that before serialization, a new tag object is created, and then `loadTag` is called on the -tag with the object that was serialized. - -This means it is straightforward to avoid serializing fields. -Fields that are not set in the constructor and not set in `loadTag` will thus not be serialized. - -Any battler tag that can persist across sessions must extend SerializableBattlerTag in its class definition signature. -Only tags that persist across waves (meaning their effect can last >1 turn) should be considered -serializable. - -Serializable battler tags have strict requirements for their fields. -Properties that are not necessary to reconstruct the tag must not be serialized. This can be avoided -by using a private property. If access to the property is needed outside of the class, then -a getter (and potentially, a setter) should be used instead. - -If a property that is intended to be private must be serialized, then it should instead -be declared as a public readonly propety. Then, in the `loadTag` method (or any method inside the class that needs to adjust the property) -use `(this as Mutable).propertyName = value;` -These rules ensure that Typescript is aware of the shape of the serialized version of the class. -*/ +/** + * @module + * BattlerTags are used to represent semi-persistent effects that can be attached to a Pokemon. + * Note that before serialization, a new tag object is created, and then `loadTag` is called on the + * tag with the object that was serialized. + * + * This means it is straightforward to avoid serializing fields. + * Fields that are not set in the constructor and not set in `loadTag` will thus not be serialized. + * + * Any battler tag that can persist across sessions must extend SerializableBattlerTag in its class definition signature. + * Only tags that persist across waves (meaning their effect can last >1 turn) should be considered + * serializable. + * + * Serializable battler tags have strict requirements for their fields. + * Properties that are not necessary to reconstruct the tag must not be serialized. This can be avoided + * by using a private property. If access to the property is needed outside of the class, then + * a getter (and potentially, a setter) should be used instead. + * + * If a property that is intended to be private must be serialized, then it should instead + * be declared as a public readonly propety. Then, in the `loadTag` method (or any method inside the class that needs to adjust the property) + * use `(this as Mutable).propertyName = value;` + * These rules ensure that Typescript is aware of the shape of the serialized version of the class. + */ /** Interface containing the serializable fields of BattlerTag */ interface BaseBattlerTag { From 26e5daea9f8f50591264261fe2857e677c9f59dc Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Tue, 29 Jul 2025 15:50:57 -0700 Subject: [PATCH 52/59] [Balance] End of turn triggers won't occur when you end a biome (#6169) * [Balance] End of turn triggers won't occur when you end a biome * Add tests * Move phase manipulation logic into `PhaseManager` * Rename "biome end" to "interlude" * Rename `TurnEndPhase#endOfBiome` to `upcomingInterlude` --------- Co-authored-by: Wlowscha <54003515+Wlowscha@users.noreply.github.com> --- src/phase-manager.ts | 13 +++++ src/phases/check-interlude-phase.ts | 18 +++++++ src/phases/turn-end-phase.ts | 8 ++- src/phases/turn-start-phase.ts | 9 ++-- test/phases/check-biome-end-phase.test.ts | 62 +++++++++++++++++++++++ 5 files changed, 104 insertions(+), 6 deletions(-) create mode 100644 src/phases/check-interlude-phase.ts create mode 100644 test/phases/check-biome-end-phase.test.ts diff --git a/src/phase-manager.ts b/src/phase-manager.ts index 73ea373904f..c56afe446ed 100644 --- a/src/phase-manager.ts +++ b/src/phase-manager.ts @@ -9,6 +9,7 @@ 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"; +import { CheckInterludePhase } from "#phases/check-interlude-phase"; import { CheckStatusEffectPhase } from "#phases/check-status-effect-phase"; import { CheckSwitchPhase } from "#phases/check-switch-phase"; import { CommandPhase } from "#phases/command-phase"; @@ -121,6 +122,7 @@ const PHASES = Object.freeze({ AttemptRunPhase, BattleEndPhase, BerryPhase, + CheckInterludePhase, CheckStatusEffectPhase, CheckSwitchPhase, CommandPhase, @@ -665,4 +667,15 @@ export class PhaseManager { ): void { this.startDynamicPhase(this.create(phase, ...args)); } + + /** Prevents end of turn effects from triggering when transitioning to a new biome on a X0 wave */ + public onInterlude(): void { + const phasesToRemove = ["WeatherEffectPhase", "BerryPhase", "CheckStatusEffectPhase"]; + this.phaseQueue = this.phaseQueue.filter(p => !phasesToRemove.includes(p.phaseName)); + + const turnEndPhase = this.findPhase(p => p.phaseName === "TurnEndPhase"); + if (turnEndPhase) { + turnEndPhase.upcomingInterlude = true; + } + } } diff --git a/src/phases/check-interlude-phase.ts b/src/phases/check-interlude-phase.ts new file mode 100644 index 00000000000..1589f74f058 --- /dev/null +++ b/src/phases/check-interlude-phase.ts @@ -0,0 +1,18 @@ +import { globalScene } from "#app/global-scene"; +import { Phase } from "#app/phase"; + +export class CheckInterludePhase extends Phase { + public override readonly phaseName = "CheckInterludePhase"; + + public override start(): void { + super.start(); + const { phaseManager } = globalScene; + const { waveIndex } = globalScene.currentBattle; + + if (waveIndex % 10 === 0 && globalScene.getEnemyParty().every(p => p.isFainted())) { + phaseManager.onInterlude(); + } + + this.end(); + } +} diff --git a/src/phases/turn-end-phase.ts b/src/phases/turn-end-phase.ts index ce3b2958c23..463f26e73a2 100644 --- a/src/phases/turn-end-phase.ts +++ b/src/phases/turn-end-phase.ts @@ -18,6 +18,8 @@ import i18next from "i18next"; export class TurnEndPhase extends FieldPhase { public readonly phaseName = "TurnEndPhase"; + public upcomingInterlude = false; + start() { super.start(); @@ -59,9 +61,11 @@ export class TurnEndPhase extends FieldPhase { pokemon.tempSummonData.waveTurnCount++; }; - this.executeForAll(handlePokemon); + if (!this.upcomingInterlude) { + this.executeForAll(handlePokemon); - globalScene.arena.lapseTags(); + globalScene.arena.lapseTags(); + } if (globalScene.arena.weather && !globalScene.arena.weather.lapse()) { globalScene.arena.trySetWeather(WeatherType.NONE); diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts index 0fc126801ec..9fdac65bb10 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -218,6 +218,7 @@ export class TurnStartPhase extends FieldPhase { break; } } + phaseManager.pushNew("CheckInterludePhase"); phaseManager.pushNew("WeatherEffectPhase"); phaseManager.pushNew("BerryPhase"); @@ -227,10 +228,10 @@ export class TurnStartPhase extends FieldPhase { phaseManager.pushNew("TurnEndPhase"); - /** - * this.end() will call shiftPhase(), which dumps everything from PrependQueue (aka everything that is unshifted()) to the front - * of the queue and dequeues to start the next phase - * this is important since stuff like SwitchSummon, AttemptRun, AttemptCapture Phases break the "flow" and should take precedence + /* + * `this.end()` will call `PhaseManager#shiftPhase()`, which dumps everything from `phaseQueuePrepend` + * (aka everything that is queued via `unshift()`) to the front of the queue and dequeues to start the next phase. + * This is important since stuff like `SwitchSummonPhase`, `AttemptRunPhase`, and `AttemptCapturePhase` break the "flow" and should take precedence */ this.end(); } diff --git a/test/phases/check-biome-end-phase.test.ts b/test/phases/check-biome-end-phase.test.ts new file mode 100644 index 00000000000..06957af9011 --- /dev/null +++ b/test/phases/check-biome-end-phase.test.ts @@ -0,0 +1,62 @@ +import { AbilityId } from "#enums/ability-id"; +import { BerryType } from "#enums/berry-type"; +import { MoveId } from "#enums/move-id"; +import { SpeciesId } from "#enums/species-id"; +import { WeatherType } from "#enums/weather-type"; +import { GameManager } from "#test/test-utils/game-manager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Check Biome End Phase", () => { + 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 + .enemySpecies(SpeciesId.MAGIKARP) + .enemyMoveset(MoveId.SPLASH) + .enemyAbility(AbilityId.BALL_FETCH) + .ability(AbilityId.BALL_FETCH) + .startingLevel(100); + }); + + it("should not trigger end of turn effects when defeating the final pokemon of a biome in classic", async () => { + game.override + .startingWave(10) + .weather(WeatherType.SANDSTORM) + .startingHeldItems([{ name: "BERRY", type: BerryType.SITRUS }]); + await game.classicMode.startBattle([SpeciesId.FEEBAS]); + + const player = game.field.getPlayerPokemon(); + + player.hp = 1; + + game.move.use(MoveId.EXTREME_SPEED); + await game.toEndOfTurn(); + + expect(player.hp).toBe(1); + }); + + it("should not prevent end of turn effects when transitioning waves within a biome", async () => { + game.override.weather(WeatherType.SANDSTORM); + await game.classicMode.startBattle([SpeciesId.FEEBAS]); + + const player = game.field.getPlayerPokemon(); + + game.move.use(MoveId.EXTREME_SPEED); + await game.toEndOfTurn(); + + expect(player.hp).toBeLessThan(player.getMaxHp()); + }); +}); From 0a5f9c2df908f32e3140441349b4f9763a8032b6 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Tue, 29 Jul 2025 23:12:51 -0400 Subject: [PATCH 53/59] Fixed roost tsest --- test/moves/roost.test.ts | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/test/moves/roost.test.ts b/test/moves/roost.test.ts index e1507228c94..d3ada49db1a 100644 --- a/test/moves/roost.test.ts +++ b/test/moves/roost.test.ts @@ -44,13 +44,13 @@ describe("Moves - Roost", () => { // Should lose flying type temporarily expect(hawlucha.getTag(BattlerTagType.ROOSTED)).toBeDefined(); - expect(hawlucha.getTypes()).toEqual([PokemonType.FIGHTING]); + expect(hawlucha).toHaveTypes([PokemonType.FIGHTING]); expect(hawlucha.isGrounded()).toBe(true); await game.toEndOfTurn(); // Should have changed back to fighting/flying - expect(hawlucha.getTypes()).toEqual([PokemonType.FIGHTING, PokemonType.FLYING]); + expect(hawlucha).toHaveTypes([PokemonType.FIGHTING, PokemonType.FLYING]); expect(hawlucha.isGrounded()).toBe(false); }); @@ -64,7 +64,7 @@ describe("Moves - Roost", () => { await game.toEndOfTurn(false); // Should remain psychic type - expect(mew.getTypes()).toEqual([PokemonType.PSYCHIC]); + expect(mew).toHaveTypes([PokemonType.PSYCHIC]); expect(mew.isGrounded()).toBe(true); }); @@ -93,7 +93,7 @@ describe("Moves - Roost", () => { await game.toEndOfTurn(false); // Should only be normal type, and NOT flying type - expect(tornadus.getTypes()).toEqual([PokemonType.NORMAL]); + expect(tornadus).toHaveTypes([PokemonType.NORMAL]); expect(tornadus.isGrounded()).toBe(true); }); @@ -110,7 +110,7 @@ describe("Moves - Roost", () => { await game.toNextTurn(); // Should be pure flying type - expect(player.getTypes()).toEqual([PokemonType.FLYING]); + expect(player).toHaveTypes([PokemonType.FLYING]); expect(player.isGrounded()).toBe(false); game.move.use(MoveId.ROOST); @@ -118,13 +118,13 @@ describe("Moves - Roost", () => { // Should be typeless expect(player.getTag(BattlerTagType.ROOSTED)).toBeDefined(); - expect(player.getTypes()).toEqual([PokemonType.UNKNOWN]); + expect(player).toHaveTypes([PokemonType.UNKNOWN]); expect(player.isGrounded()).toBe(true); await game.toEndOfTurn(); // Should go back to being pure flying - expect(player.getTypes()).toEqual([PokemonType.FLYING]); + expect(player).toHaveTypes([PokemonType.FLYING]); expect(player.isGrounded()).toBe(false); }); @@ -136,17 +136,17 @@ describe("Moves - Roost", () => { ditto.hp = 1; expect(ditto.summonData.speciesForm).toBe(SpeciesId.CORVIKNIGHT); - expect(ditto.getTypes()).toEqual([PokemonType.STEEL, PokemonType.FLYING]); + expect(ditto).toHaveTypes([PokemonType.STEEL, PokemonType.FLYING]); game.move.use(MoveId.ROOST); await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.phaseInterceptor.to("MoveEffectPhase"); // Roost - expect(ditto.getTypes()).toEqual([PokemonType.STEEL]); + expect(ditto).toHaveTypes([PokemonType.STEEL]); await game.toEndOfTurn(); - expect(ditto.getTypes()).toEqual([PokemonType.STEEL, PokemonType.FLYING, PokemonType.GHOST]); + expect(ditto).toHaveTypes([PokemonType.STEEL, PokemonType.FLYING, PokemonType.GHOST]); }); it("should not change normally-Flying type Pokemon with 3 types into Normal-types", async () => { @@ -157,22 +157,22 @@ describe("Moves - Roost", () => { tornadus.hp = 1; expect(tornadus.summonData.speciesForm).toBe(SpeciesId.CORVIKNIGHT); - expect(tornadus.getTypes()).toEqual([PokemonType.STEEL, PokemonType.FLYING]); + expect(tornadus).toHaveTypes([PokemonType.STEEL, PokemonType.FLYING]); game.move.use(MoveId.ROOST); await game.move.forceEnemyMove(MoveId.TRICK_OR_TREAT); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.phaseInterceptor.to("MoveEffectPhase"); // Trick or treat - expect(tornadus.getTypes()).toEqual([PokemonType.STEEL, PokemonType.FLYING, PokemonType.GHOST]); + expect(tornadus).toHaveTypes([PokemonType.STEEL, PokemonType.FLYING, PokemonType.GHOST]); await game.phaseInterceptor.to("MoveEffectPhase"); // Roost - expect(tornadus.getTypes()).toEqual([PokemonType.STEEL, PokemonType.GHOST]); + expect(tornadus).toHaveTypes([PokemonType.STEEL, PokemonType.GHOST]); await game.toEndOfTurn(); - expect(tornadus.getTypes()).toEqual([PokemonType.STEEL, PokemonType.FLYING, PokemonType.GHOST]); + expect(tornadus).toHaveTypes([PokemonType.STEEL, PokemonType.FLYING, PokemonType.GHOST]); }); it("should revert to 3 types when affected by Forest's Curse or Trick-or-Treat", async () => { @@ -185,12 +185,12 @@ describe("Moves - Roost", () => { await game.move.forceEnemyMove(MoveId.TRICK_OR_TREAT); await game.toEndOfTurn(false); - expect(moltres.getTypes()).toEqual([PokemonType.FIRE, PokemonType.GHOST]); + expect(moltres).toHaveTypes([PokemonType.FIRE, PokemonType.GHOST]); expect(moltres.isGrounded()).toBe(true); await game.toNextTurn(); - expect(moltres.getTypes()).toEqual([PokemonType.FIRE, PokemonType.FLYING, PokemonType.GHOST]); + expect(moltres).toHaveTypes([PokemonType.FIRE, PokemonType.FLYING, PokemonType.GHOST]); expect(moltres.isGrounded()).toBe(false); }); }); From 1df4be9b9aa8eac208bfeeb6c206622189a9e4f3 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Wed, 30 Jul 2025 12:16:01 -0400 Subject: [PATCH 54/59] Added TODO tests for roost --- test/moves/roost.test.ts | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/test/moves/roost.test.ts b/test/moves/roost.test.ts index d3ada49db1a..977fe221c61 100644 --- a/test/moves/roost.test.ts +++ b/test/moves/roost.test.ts @@ -26,8 +26,8 @@ describe("Moves - Roost", () => { game = new GameManager(phaserGame); game.override .battleStyle("single") - .enemySpecies(SpeciesId.RELICANTH) - .ability(AbilityId.TRIAGE) + .enemySpecies(SpeciesId.SHUCKLE) + .ability(AbilityId.BALL_FETCH) .startingLevel(100) .enemyLevel(100) .enemyMoveset(MoveId.SPLASH); @@ -68,19 +68,19 @@ describe("Moves - Roost", () => { expect(mew.isGrounded()).toBe(true); }); - it("should not override the user's Tera Type", async () => { + it("should not remove the user's Tera Type", async () => { await game.classicMode.startBattle([SpeciesId.PIDGEOT]); const pidgeot = game.field.getPlayerPokemon(); pidgeot.hp = 1; - pidgeot.teraType = PokemonType.FIGHTING; + pidgeot.teraType = PokemonType.FLYING; game.move.use(MoveId.ROOST, BattlerIndex.PLAYER, undefined, true); await game.toEndOfTurn(false); // Should remain flying type - expect(pidgeot.getTypes(true)).toEqual([PokemonType.FLYING]); - expect(pidgeot.isGrounded()).toBe(true); + expect(pidgeot).toHaveTypes([PokemonType.FLYING], { args: [true] }); + expect(pidgeot.isGrounded()).toBe(false); }); it("should convert pure Flying types into normal types", async () => { @@ -128,7 +128,7 @@ describe("Moves - Roost", () => { expect(player.isGrounded()).toBe(false); }); - it("should consider the user's current types when transforming", async () => { + it("should consider the user's current types when transformed", async () => { game.override.ability(AbilityId.IMPOSTER).enemySpecies(SpeciesId.CORVIKNIGHT); await game.classicMode.startBattle([SpeciesId.DITTO]); @@ -139,6 +139,7 @@ describe("Moves - Roost", () => { expect(ditto).toHaveTypes([PokemonType.STEEL, PokemonType.FLYING]); game.move.use(MoveId.ROOST); + await game.move.forceEnemyMove(MoveId.TRICK_OR_TREAT); await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.phaseInterceptor.to("MoveEffectPhase"); // Roost @@ -175,22 +176,26 @@ describe("Moves - Roost", () => { expect(tornadus).toHaveTypes([PokemonType.STEEL, PokemonType.FLYING, PokemonType.GHOST]); }); - it("should revert to 3 types when affected by Forest's Curse or Trick-or-Treat", async () => { - await game.classicMode.startBattle([SpeciesId.MOLTRES]); + it.each<{ name: string; move: MoveId; type: PokemonType }>([ + { name: "Trick-or-Treat", move: MoveId.TRICK_OR_TREAT, type: PokemonType.GHOST }, + { name: "Forest's Curse", move: MoveId.FORESTS_CURSE, type: PokemonType.GRASS }, + ])("should ignore added types from $name when changing Flying to Normal type", async ({ move, type }) => { + await game.classicMode.startBattle([SpeciesId.TORNADUS]); - const moltres = game.field.getPlayerPokemon(); - moltres.hp = 1; + const tornadus = game.field.getPlayerPokemon(); + tornadus.hp = 1; game.move.use(MoveId.ROOST); - await game.move.forceEnemyMove(MoveId.TRICK_OR_TREAT); + await game.move.forceEnemyMove(move); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.toEndOfTurn(false); - expect(moltres).toHaveTypes([PokemonType.FIRE, PokemonType.GHOST]); - expect(moltres.isGrounded()).toBe(true); + expect(tornadus).toHaveTypes([PokemonType.NORMAL, type]); + expect(tornadus.isGrounded()).toBe(true); await game.toNextTurn(); - expect(moltres).toHaveTypes([PokemonType.FIRE, PokemonType.FLYING, PokemonType.GHOST]); - expect(moltres.isGrounded()).toBe(false); + expect(tornadus).toHaveTypes([PokemonType.FIRE, PokemonType.FLYING, PokemonType.GHOST]); + expect(tornadus.isGrounded()).toBe(false); }); }); From 9f26790a718350204581bfbdc0b1b61ddc9219c7 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Tue, 29 Jul 2025 20:02:30 -0700 Subject: [PATCH 55/59] [Test] Add missing single battle override to `CheckInterludePhase` test --- ...k-biome-end-phase.test.ts => check-interlude-phase.test.ts} | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) rename test/phases/{check-biome-end-phase.test.ts => check-interlude-phase.test.ts} (97%) diff --git a/test/phases/check-biome-end-phase.test.ts b/test/phases/check-interlude-phase.test.ts similarity index 97% rename from test/phases/check-biome-end-phase.test.ts rename to test/phases/check-interlude-phase.test.ts index 06957af9011..d5413d1db35 100644 --- a/test/phases/check-biome-end-phase.test.ts +++ b/test/phases/check-interlude-phase.test.ts @@ -28,7 +28,8 @@ describe("Check Biome End Phase", () => { .enemyMoveset(MoveId.SPLASH) .enemyAbility(AbilityId.BALL_FETCH) .ability(AbilityId.BALL_FETCH) - .startingLevel(100); + .startingLevel(100) + .battleStyle("single"); }); it("should not trigger end of turn effects when defeating the final pokemon of a biome in classic", async () => { From feb11b8aec23b8fe847cef3c1ebfbd93222f5c7f Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Wed, 30 Jul 2025 12:47:42 -0400 Subject: [PATCH 56/59] Removed roost type-change tests to move to another PR --- test/moves/roost.test.ts | 71 ---------------------------------------- 1 file changed, 71 deletions(-) diff --git a/test/moves/roost.test.ts b/test/moves/roost.test.ts index 977fe221c61..44a2cccaef9 100644 --- a/test/moves/roost.test.ts +++ b/test/moves/roost.test.ts @@ -127,75 +127,4 @@ describe("Moves - Roost", () => { expect(player).toHaveTypes([PokemonType.FLYING]); expect(player.isGrounded()).toBe(false); }); - - it("should consider the user's current types when transformed", async () => { - game.override.ability(AbilityId.IMPOSTER).enemySpecies(SpeciesId.CORVIKNIGHT); - await game.classicMode.startBattle([SpeciesId.DITTO]); - - const ditto = game.field.getPlayerPokemon(); - ditto.hp = 1; - - expect(ditto.summonData.speciesForm).toBe(SpeciesId.CORVIKNIGHT); - expect(ditto).toHaveTypes([PokemonType.STEEL, PokemonType.FLYING]); - - game.move.use(MoveId.ROOST); - await game.move.forceEnemyMove(MoveId.TRICK_OR_TREAT); - await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); - await game.phaseInterceptor.to("MoveEffectPhase"); // Roost - - expect(ditto).toHaveTypes([PokemonType.STEEL]); - - await game.toEndOfTurn(); - - expect(ditto).toHaveTypes([PokemonType.STEEL, PokemonType.FLYING, PokemonType.GHOST]); - }); - - it("should not change normally-Flying type Pokemon with 3 types into Normal-types", async () => { - game.override.ability(AbilityId.IMPOSTER).enemySpecies(SpeciesId.CORVIKNIGHT); - await game.classicMode.startBattle([SpeciesId.TORNADUS]); - - const tornadus = game.field.getPlayerPokemon(); - tornadus.hp = 1; - - expect(tornadus.summonData.speciesForm).toBe(SpeciesId.CORVIKNIGHT); - expect(tornadus).toHaveTypes([PokemonType.STEEL, PokemonType.FLYING]); - - game.move.use(MoveId.ROOST); - await game.move.forceEnemyMove(MoveId.TRICK_OR_TREAT); - await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); - await game.phaseInterceptor.to("MoveEffectPhase"); // Trick or treat - - expect(tornadus).toHaveTypes([PokemonType.STEEL, PokemonType.FLYING, PokemonType.GHOST]); - - await game.phaseInterceptor.to("MoveEffectPhase"); // Roost - - expect(tornadus).toHaveTypes([PokemonType.STEEL, PokemonType.GHOST]); - - await game.toEndOfTurn(); - - expect(tornadus).toHaveTypes([PokemonType.STEEL, PokemonType.FLYING, PokemonType.GHOST]); - }); - - it.each<{ name: string; move: MoveId; type: PokemonType }>([ - { name: "Trick-or-Treat", move: MoveId.TRICK_OR_TREAT, type: PokemonType.GHOST }, - { name: "Forest's Curse", move: MoveId.FORESTS_CURSE, type: PokemonType.GRASS }, - ])("should ignore added types from $name when changing Flying to Normal type", async ({ move, type }) => { - await game.classicMode.startBattle([SpeciesId.TORNADUS]); - - const tornadus = game.field.getPlayerPokemon(); - tornadus.hp = 1; - - game.move.use(MoveId.ROOST); - await game.move.forceEnemyMove(move); - await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); - await game.toEndOfTurn(false); - - expect(tornadus).toHaveTypes([PokemonType.NORMAL, type]); - expect(tornadus.isGrounded()).toBe(true); - - await game.toNextTurn(); - - expect(tornadus).toHaveTypes([PokemonType.FIRE, PokemonType.FLYING, PokemonType.GHOST]); - expect(tornadus.isGrounded()).toBe(false); - }); }); From 1fd4859d74c9c647984ba0096e0cc6cf17f3bb3c Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Wed, 30 Jul 2025 16:46:09 -0400 Subject: [PATCH 57/59] Fixed syntax error in test file --- test/moves/recovery-moves.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/moves/recovery-moves.test.ts b/test/moves/recovery-moves.test.ts index 39c78b72bf8..70caa0568c6 100644 --- a/test/moves/recovery-moves.test.ts +++ b/test/moves/recovery-moves.test.ts @@ -5,8 +5,8 @@ import { MoveResult } from "#enums/move-result"; import { SpeciesId } from "#enums/species-id"; import { WeatherType } from "#enums/weather-type"; import { GameManager } from "#test/test-utils/game-manager"; -import { toReadableString } from "#utils/common"; import { getEnumValues } from "#utils/enums"; +import { toTitleCase } from "#utils/strings"; import i18next from "i18next"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -100,7 +100,7 @@ describe("Moves - Recovery Moves - ", () => { wt => ![WeatherType.SUNNY, WeatherType.HARSH_SUN, WeatherType.NONE, WeatherType.STRONG_WINDS].includes(wt), ) .map(wt => ({ - name: toReadableString(WeatherType[wt]), + name: toTitleCase(WeatherType[wt]), weather: wt, })); From cee6e8678acdadbaf611fb2b78109d0a44799883 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Fri, 1 Aug 2025 16:09:19 -0400 Subject: [PATCH 58/59] made lunar blessing/jungle healing not fail on full hp for now --- src/data/moves/move.ts | 37 ++++++++++++++++++++++---------- src/phases/pokemon-heal-phase.ts | 2 +- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index d860ab3add2..6d232e7c8f4 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -1945,14 +1945,27 @@ export class AddSubstituteAttr extends MoveEffectAttr { * @see {@linkcode apply} */ export class HealAttr extends MoveEffectAttr { + /** The percentage of {@linkcode Stat.HP} to heal; default `1` */ + protected healRatio = 1 + /** Whether to display a healing animation upon healing the target; default `false` */ + private showAnim = false + + /** + * Whether the move should fail if the target is at full HP. + * @todo Remove post move failure rework + */ + private failOnFullHp = true; + constructor( - /** The percentage of {@linkcode Stat.HP} to heal; default `1` */ - protected healRatio = 1, - /** Whether to display a healing animation upon healing the target; default `false` */ - private showAnim = false, - selfTarget = true + healRatio = 1, + showAnim = false, + selfTarget = true, + failOnFullHp = true ) { super(selfTarget); + this.healRatio = healRatio; + this.showAnim = showAnim + this.failOnFullHp = failOnFullHp } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { @@ -1979,7 +1992,7 @@ export class HealAttr extends MoveEffectAttr { } override getCondition(): MoveConditionFunc { - return (user, target) => !(this.selfTarget ? user : target).isFullHp(); + return (user, target) => !(this.failOnFullHp && (this.selfTarget ? user : target).isFullHp()); } override getFailedText(user: Pokemon, target: Pokemon): string | undefined { @@ -1999,7 +2012,7 @@ export class HealAttr extends MoveEffectAttr { * Used for: * - {@linkcode MoveId.MOONLIGHT} and variants * - {@linkcode MoveId.SHORE_UP} - * - {@linkcode MoveId.JUNGLE_HEALING} + * - {@linkcode MoveId.FLORAL_HEALING} * - {@linkcode MoveId.SWALLOW} */ export class VariableHealAttr extends HealAttr { @@ -11035,10 +11048,11 @@ export function initMoves() { .attr(HealStatusEffectAttr, false, StatusEffect.FREEZE) .attr(StatusEffectAttr, StatusEffect.BURN), new StatusMove(MoveId.JUNGLE_HEALING, PokemonType.GRASS, -1, 10, -1, 0, 8) - .attr(HealAttr, 0.25, true, false) + .attr(HealAttr, 0.25, true, false, false) .attr(HealStatusEffectAttr, false, getNonVolatileStatusEffects()) .target(MoveTarget.USER_AND_ALLIES) - .triageMove(), + .triageMove() + .edgeCase(), // TODO: Review if jungle healing fails if HP cannot be restored and status cannot be cured new AttackMove(MoveId.WICKED_BLOW, PokemonType.DARK, MoveCategory.PHYSICAL, 75, 100, 5, -1, 0, 8) .attr(CritOnlyAttr) .punchingMove(), @@ -11140,10 +11154,11 @@ export function initMoves() { .windMove() .target(MoveTarget.ALL_NEAR_ENEMIES), new StatusMove(MoveId.LUNAR_BLESSING, PokemonType.PSYCHIC, -1, 5, -1, 0, 8) - .attr(HealAttr, 0.25, true, false) + .attr(HealAttr, 0.25, true, false, false) .attr(HealStatusEffectAttr, false, getNonVolatileStatusEffects()) .target(MoveTarget.USER_AND_ALLIES) - .triageMove(), + .triageMove() + .edgeCase(), // TODO: Review if lunar blessing fails if HP cannot be restored and status cannot be cured new SelfStatusMove(MoveId.TAKE_HEART, PokemonType.PSYCHIC, -1, 10, -1, 0, 8) .attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF ], 1, true) .attr(HealStatusEffectAttr, true, [ StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN, StatusEffect.SLEEP ]), diff --git a/src/phases/pokemon-heal-phase.ts b/src/phases/pokemon-heal-phase.ts index fa6a3222466..573df9c2da9 100644 --- a/src/phases/pokemon-heal-phase.ts +++ b/src/phases/pokemon-heal-phase.ts @@ -69,9 +69,9 @@ export class PokemonHealPhase extends CommonAnimPhase { if (healBlock && this.hpHealed > 0) { globalScene.phaseManager.queueMessage(healBlock.onActivation(pokemon)); - this.message = null; return super.end(); } + if (healOrDamage) { const hpRestoreMultiplier = new NumberHolder(1); if (!this.revive) { From 93c7ab79352b52ec4ecbd4690aec7d635604294c Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Fri, 1 Aug 2025 18:00:21 -0400 Subject: [PATCH 59/59] Refactored healing phase + removed `preventFullHp` param --- src/data/moves/move.ts | 1 - src/field/pokemon.ts | 10 +-- src/modifier/modifier.ts | 36 ++++----- src/phases/pokemon-heal-phase.ts | 123 +++++++++++++++---------------- 4 files changed, 79 insertions(+), 91 deletions(-) diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 6d232e7c8f4..f307bea7897 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -2183,7 +2183,6 @@ export class SacrificialFullRestoreAttr extends SacrificialAttr { false, false, true, - false, this.restorePP), true); diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 2c9329a95ef..d7730ff0cd2 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -24,11 +24,11 @@ import { NoCritTag, WeakenMoveScreenTag } from "#data/arena-tag"; import { AutotomizedTag, BattlerTag, + type BattlerTagTypeMap, CritBoostTag, EncoreTag, ExposedTag, GroundedTag, - type GrudgeTag, getBattlerTag, HighestStatBoostTag, MoveRestrictionBattlerTag, @@ -4240,14 +4240,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return false; } - /**@overload */ - getTag(tagType: BattlerTagType.GRUDGE): GrudgeTag | undefined; - /** @overload */ - getTag(tagType: BattlerTagType.SUBSTITUTE): SubstituteTag | undefined; - - /** @overload */ - getTag(tagType: BattlerTagType): BattlerTag | undefined; + getTag(tagType: T): BattlerTagTypeMap[T] | undefined; /** @overload */ getTag(tagType: Constructor): T | undefined; diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index b31bee7fc69..2cae080d339 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -1933,7 +1933,6 @@ export class PokemonInstantReviveModifier extends PokemonHeldItemModifier { * @returns always `true` */ override apply(pokemon: Pokemon): boolean { - // Restore the Pokemon to half HP globalScene.phaseManager.unshiftNew( "PokemonHealPhase", pokemon.getBattlerIndex(), @@ -1948,6 +1947,7 @@ export class PokemonInstantReviveModifier extends PokemonHeldItemModifier { ); // Remove the Pokemon's FAINT status + // TODO: Remove call to `resetStatus` once StatusEffect.FAINT is canned pokemon.resetStatus(true, false, true, false); // Reapply Commander on the Pokemon's side of the field, if applicable @@ -3549,24 +3549,26 @@ export class EnemyTurnHealModifier extends EnemyPersistentModifier { * @returns `true` if the {@linkcode Pokemon} was healed */ override apply(enemyPokemon: Pokemon): boolean { - if (!enemyPokemon.isFullHp()) { - globalScene.phaseManager.unshiftNew( - "PokemonHealPhase", - enemyPokemon.getBattlerIndex(), - Math.max(Math.floor(enemyPokemon.getMaxHp() / (100 / this.healPercent)) * this.stackCount, 1), - i18next.t("modifier:enemyTurnHealApply", { - pokemonNameWithAffix: getPokemonNameWithAffix(enemyPokemon), - }), - true, - false, - false, - false, - true, - ); - return true; + if (enemyPokemon.isFullHp()) { + return false; } - return false; + // Prevent healing to full from healing tokens + const healAmt = Math.min( + enemyPokemon.getMaxHp() - 1, + toDmgValue((enemyPokemon.getMaxHp() * this.stackCount * this.healPercent) / 100), + ); + globalScene.phaseManager.unshiftNew( + "PokemonHealPhase", + enemyPokemon.getBattlerIndex(), + healAmt, + i18next.t("modifier:enemyTurnHealApply", { + pokemonNameWithAffix: getPokemonNameWithAffix(enemyPokemon), + }), + true, + ); + + return true; } getMaxStackCount(): number { diff --git a/src/phases/pokemon-heal-phase.ts b/src/phases/pokemon-heal-phase.ts index 573df9c2da9..e9914cf377b 100644 --- a/src/phases/pokemon-heal-phase.ts +++ b/src/phases/pokemon-heal-phase.ts @@ -1,12 +1,10 @@ import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; -import type { HealBlockTag } from "#data/battler-tags"; import { getStatusEffectHealText } from "#data/status-effect"; import type { BattlerIndex } from "#enums/battler-index"; import { BattlerTagType } from "#enums/battler-tag-type"; import { HitResult } from "#enums/hit-result"; import { CommonAnim } from "#enums/move-anims-common"; -import { StatusEffect } from "#enums/status-effect"; import { HealingBoosterModifier } from "#modifiers/modifier"; import { CommonAnimPhase } from "#phases/common-anim-phase"; import { HealAchv } from "#system/achv"; @@ -15,13 +13,16 @@ import i18next from "i18next"; export class PokemonHealPhase extends CommonAnimPhase { public readonly phaseName = "PokemonHealPhase"; + + /** The base amount of HP to heal. */ private hpHealed: number; + /** The message to display upon healing the target, or `null` to show no message. */ private message: string | null; + /** Whether to show a message and quit out early upon healing a Pokemon already at full hp. */ private showFullHpMessage: boolean; private skipAnim: boolean; private revive: boolean; private healStatus: boolean; - private preventFullHeal: boolean; private fullRestorePP: boolean; constructor( @@ -32,7 +33,6 @@ export class PokemonHealPhase extends CommonAnimPhase { skipAnim = false, revive = false, healStatus = false, - preventFullHeal = false, fullRestorePP = false, ) { super(battlerIndex, undefined, CommonAnim.HEALTH_UP); @@ -43,12 +43,11 @@ export class PokemonHealPhase extends CommonAnimPhase { this.skipAnim = skipAnim; this.revive = revive; this.healStatus = healStatus; - this.preventFullHeal = preventFullHeal; this.fullRestorePP = fullRestorePP; } start() { - if (!this.skipAnim && (this.revive || this.getPokemon().hp) && !this.getPokemon().isFullHp()) { + if (!this.skipAnim && !this.getPokemon().isFullHp()) { super.start(); } else { this.end(); @@ -58,78 +57,72 @@ export class PokemonHealPhase extends CommonAnimPhase { end() { const pokemon = this.getPokemon(); - if (!pokemon.isOnField() || (!this.revive && !pokemon.isActive())) { - return super.end(); + // Prevent healing off-field pokemon + if (!pokemon.isActive(true)) { + super.end(); + return; } - const hasMessage = !!this.message; - const healOrDamage = !pokemon.isFullHp() || this.hpHealed < 0; - const healBlock = pokemon.getTag(BattlerTagType.HEAL_BLOCK) as HealBlockTag; - let lastStatusEffect = StatusEffect.NONE; - + // Check for heal block, ending the phase early if healing was prevented + const healBlock = pokemon.getTag(BattlerTagType.HEAL_BLOCK); if (healBlock && this.hpHealed > 0) { globalScene.phaseManager.queueMessage(healBlock.onActivation(pokemon)); - return super.end(); + super.end(); + return; } - if (healOrDamage) { - const hpRestoreMultiplier = new NumberHolder(1); - if (!this.revive) { - globalScene.applyModifiers(HealingBoosterModifier, this.player, hpRestoreMultiplier); - } - const healAmount = new NumberHolder(Math.floor(this.hpHealed * hpRestoreMultiplier.value)); - if (healAmount.value < 0) { - pokemon.damageAndUpdate(healAmount.value * -1, { result: HitResult.INDIRECT }); - healAmount.value = 0; - } - // Prevent healing to full if specified (in case of healing tokens so Sturdy doesn't cause a softlock) - if (this.preventFullHeal && pokemon.hp + healAmount.value >= pokemon.getMaxHp()) { - healAmount.value = pokemon.getMaxHp() - pokemon.hp - 1; - } - healAmount.value = pokemon.heal(healAmount.value); - if (healAmount.value) { - globalScene.damageNumberHandler.add(pokemon, healAmount.value, HitResult.HEAL); - } - if (pokemon.isPlayer()) { - globalScene.validateAchvs(HealAchv, healAmount); - if (healAmount.value > globalScene.gameData.gameStats.highestHeal) { - globalScene.gameData.gameStats.highestHeal = healAmount.value; - } - } - if (this.healStatus && !this.revive && pokemon.status) { - lastStatusEffect = pokemon.status.effect; - pokemon.resetStatus(); - } - if (this.fullRestorePP) { - for (const move of this.getPokemon().getMoveset()) { - if (move) { - move.ppUsed = 0; - } - } + // If we would heal the user past full HP, don't. + if (this.hpHealed >= 0 && pokemon.isFullHp()) { + if (this.showFullHpMessage) { + globalScene.phaseManager.queueMessage( + i18next.t("battle:hpIsFull", { + pokemonName: getPokemonNameWithAffix(pokemon), + }), + ); } + super.end(); + return; + } + + // Apply the effect of healing charms for non-revival items + const hpRestoreMultiplier = new NumberHolder(1); + globalScene.applyModifiers(HealingBoosterModifier, this.player, hpRestoreMultiplier); + let healAmount = Math.floor(this.hpHealed * hpRestoreMultiplier.value); + + // If Liquid Ooze is active, damage the user for the healing amount, then return. + // TODO: Refactor liquid ooze to not use a heal phase to do damage + if (healAmount < 0) { + pokemon.damageAndUpdate(-healAmount, { result: HitResult.INDIRECT }); pokemon.updateInfo().then(() => super.end()); - } else if (this.healStatus && !this.revive && pokemon.status) { - lastStatusEffect = pokemon.status.effect; + return; + } + + // Heal the pokemon (capping at max HP), then show damage numbers and + // do achievement validation + healAmount = pokemon.heal(healAmount); + globalScene.damageNumberHandler.add(pokemon, healAmount, HitResult.HEAL); + if (pokemon.isPlayer()) { + globalScene.validateAchvs(HealAchv, healAmount); + globalScene.gameData.gameStats.highestHeal = Math.max(globalScene.gameData.gameStats.highestHeal, healAmount); + } + + // Cure status as applicable + // TODO: This should not be the job of the healing phase + if (this.healStatus && pokemon.status) { + globalScene.phaseManager.queueMessage( + getStatusEffectHealText(pokemon.status.effect, getPokemonNameWithAffix(pokemon)), + ); pokemon.resetStatus(); - pokemon.updateInfo().then(() => super.end()); - } else if (this.showFullHpMessage) { - this.message = i18next.t("battle:hpIsFull", { - pokemonName: getPokemonNameWithAffix(pokemon), - }); } + this.showMessage(); + + pokemon.updateInfo().then(() => super.end()); + } + + private showMessage(): void { if (this.message) { globalScene.phaseManager.queueMessage(this.message); } - - if (this.healStatus && lastStatusEffect && !hasMessage) { - globalScene.phaseManager.queueMessage( - getStatusEffectHealText(lastStatusEffect, getPokemonNameWithAffix(pokemon)), - ); - } - - if (!healOrDamage && !lastStatusEffect) { - super.end(); - } } }