From 43aa772603b644fa0b961f1036d5aac667145596 Mon Sep 17 00:00:00 2001 From: Madmadness65 <59298170+Madmadness65@users.noreply.github.com> Date: Thu, 19 Jun 2025 19:04:57 -0500 Subject: [PATCH 1/4] =?UTF-8?q?[UI/UX]=20Add=20Pok=C3=A9mon=20category=20f?= =?UTF-8?q?lavor=20text=20to=20Pok=C3=A9dex=20(#5957)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add Pokémon category flavor text to Pokédex * Append `_category` to locale entry --- src/data/pokemon-species.ts | 7 ++++--- src/plugins/i18n.ts | 1 + src/ui/pokedex-page-ui-handler.ts | 16 ++++++++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index 74e25bd8bf7..39e8fbd18e6 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -764,7 +764,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali readonly subLegendary: boolean; readonly legendary: boolean; readonly mythical: boolean; - readonly species: string; + public category: string; readonly growthRate: GrowthRate; /** The chance (as a decimal) for this Species to be male, or `null` for genderless species */ readonly malePercent: number | null; @@ -778,7 +778,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali subLegendary: boolean, legendary: boolean, mythical: boolean, - species: string, + category: string, type1: PokemonType, type2: PokemonType | null, height: number, @@ -829,7 +829,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali this.subLegendary = subLegendary; this.legendary = legendary; this.mythical = mythical; - this.species = species; + this.category = category; this.growthRate = growthRate; this.malePercent = malePercent; this.genderDiffs = genderDiffs; @@ -968,6 +968,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali localize(): void { this.name = i18next.t(`pokemon:${SpeciesId[this.speciesId].toLowerCase()}`); + this.category = i18next.t(`pokemonCategory:${SpeciesId[this.speciesId].toLowerCase()}_category`); } getWildSpeciesForLevel(level: number, allowEvolving: boolean, isBoss: boolean, gameMode: GameMode): SpeciesId { diff --git a/src/plugins/i18n.ts b/src/plugins/i18n.ts index eab427e7b4a..7d3b30ed5b0 100644 --- a/src/plugins/i18n.ts +++ b/src/plugins/i18n.ts @@ -245,6 +245,7 @@ export async function initI18n(): Promise { "pokeball", "pokedexUiHandler", "pokemon", + "pokemonCategory", "pokemonEvolutions", "pokemonForm", "pokemonInfo", diff --git a/src/ui/pokedex-page-ui-handler.ts b/src/ui/pokedex-page-ui-handler.ts index 50c15336e36..32a88ab36b2 100644 --- a/src/ui/pokedex-page-ui-handler.ts +++ b/src/ui/pokedex-page-ui-handler.ts @@ -174,6 +174,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler { private pokemonCaughtHatchedContainer: Phaser.GameObjects.Container; private pokemonCaughtCountText: Phaser.GameObjects.Text; private pokemonFormText: Phaser.GameObjects.Text; + private pokemonCategoryText: Phaser.GameObjects.Text; private pokemonHatchedIcon: Phaser.GameObjects.Sprite; private pokemonHatchedCountText: Phaser.GameObjects.Text; private pokemonShinyIcons: Phaser.GameObjects.Sprite[]; @@ -409,6 +410,12 @@ export default class PokedexPageUiHandler extends MessageUiHandler { this.pokemonFormText.setOrigin(0, 0); this.starterSelectContainer.add(this.pokemonFormText); + this.pokemonCategoryText = addTextObject(100, 18, "Category", TextStyle.WINDOW_ALT, { + fontSize: "42px", + }); + this.pokemonCategoryText.setOrigin(1, 0); + this.starterSelectContainer.add(this.pokemonCategoryText); + this.pokemonCaughtHatchedContainer = globalScene.add.container(2, 25); this.pokemonCaughtHatchedContainer.setScale(0.5); this.starterSelectContainer.add(this.pokemonCaughtHatchedContainer); @@ -2354,6 +2361,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler { this.pokemonCaughtHatchedContainer.setVisible(true); this.pokemonCandyContainer.setVisible(false); this.pokemonFormText.setVisible(false); + this.pokemonCategoryText.setVisible(false); const defaultDexAttr = globalScene.gameData.getSpeciesDefaultDexAttr(species, true, true); const props = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr); @@ -2382,6 +2390,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler { this.pokemonCaughtHatchedContainer.setVisible(false); this.pokemonCandyContainer.setVisible(false); this.pokemonFormText.setVisible(false); + this.pokemonCategoryText.setVisible(false); this.setSpeciesDetails(species!, { // TODO: is this bang correct? @@ -2534,6 +2543,13 @@ export default class PokedexPageUiHandler extends MessageUiHandler { this.pokemonNameText.setText(species ? "???" : ""); } + // Setting the category + if (isFormCaught) { + this.pokemonCategoryText.setText(species.category); + } else { + this.pokemonCategoryText.setText(""); + } + // Setting tint of the sprite if (isFormCaught) { this.species.loadAssets(female!, formIndex, shiny, variant as Variant, true).then(() => { From 1e306e25b54e1f8b093a3ffd95d384a2423597dd Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Thu, 19 Jun 2025 20:14:05 -0400 Subject: [PATCH 2/4] [Move] Fixed Chilly Reception displaying message when used virtually https://github.com/pagefaultgames/pokerogue/pull/5843 * Fixed Chilly Reception displaying message when used virtually * Fixed lack of message causing Chilly Reception to fail * Fixed tests * Reverted bool change + fixed test * Fixed test --- src/data/moves/move.ts | 33 ++++-- src/phases/move-phase.ts | 3 + test/moves/chilly_reception.test.ts | 149 +++++++++++++++++----------- 3 files changed, 119 insertions(+), 66 deletions(-) diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index f61e8debc9f..f94c59bb463 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -93,6 +93,10 @@ import { ChargingMove, MoveAttrMap, MoveAttrString, MoveKindString, MoveClassMap import { applyMoveAttrs } from "./apply-attrs"; import { frenzyMissFunc, getMoveTargets } from "./move-utils"; +/** + * A function used to conditionally determine execution of a given {@linkcode MoveAttr}. + * Conventionally returns `true` for success and `false` for failure. +*/ type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean; export type UserMoveConditionFunc = (user: Pokemon, move: Move) => boolean; @@ -1390,18 +1394,31 @@ export class BeakBlastHeaderAttr extends AddBattlerTagHeaderAttr { } } +/** + * Attribute to display a message before a move is executed. + */ export class PreMoveMessageAttr extends MoveAttr { - private message: string | ((user: Pokemon, target: Pokemon, move: Move) => string); + /** The message to display or a function returning one */ + private message: string | ((user: Pokemon, target: Pokemon, move: Move) => string | undefined); + /** + * Create a new {@linkcode PreMoveMessageAttr} to display a message before move execution. + * @param message - The message to display before move use, either as a string or a function producing one. + * @remarks + * If {@linkcode message} evaluates to an empty string (`''`), no message will be displayed + * (though the move will still succeed). + */ constructor(message: string | ((user: Pokemon, target: Pokemon, move: Move) => string)) { super(); this.message = message; } - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - const message = typeof this.message === "string" - ? this.message as string - : this.message(user, target, move); + apply(user: Pokemon, target: Pokemon, move: Move, _args: any[]): boolean { + const message = typeof this.message === "function" + ? this.message(user, target, move) + : this.message; + + // TODO: Consider changing if/when MoveAttr `apply` return values become significant if (message) { globalScene.phaseManager.queueMessage(message, 500); return true; @@ -11299,7 +11316,11 @@ export function initMoves() { .attr(ForceSwitchOutAttr, true, SwitchType.SHED_TAIL) .condition(failIfLastInPartyCondition), new SelfStatusMove(MoveId.CHILLY_RECEPTION, PokemonType.ICE, -1, 10, -1, 0, 9) - .attr(PreMoveMessageAttr, (user, move) => i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(user) })) + .attr(PreMoveMessageAttr, (user, _target, _move) => + // Don't display text if current move phase is follow up (ie move called indirectly) + isVirtual((globalScene.phaseManager.getCurrentPhase() as MovePhase).useMode) + ? "" + : i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(user) })) .attr(ChillyReceptionAttr, true), new SelfStatusMove(MoveId.TIDY_UP, PokemonType.NORMAL, -1, 10, -1, 0, 9) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPD ], 1, true) diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 41a1042387b..2e94b085948 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -668,6 +668,9 @@ export class MovePhase extends BattlePhase { }), 500, ); + + // Moves with pre-use messages (Magnitude, Chilly Reception, Fickle Beam, etc.) always display their messages even on failure + // TODO: This assumes single target for message funcs - is this sustainable? applyMoveAttrs("PreMoveMessageAttr", this.pokemon, this.pokemon.getOpponents(false)[0], this.move.getMove()); } diff --git a/test/moves/chilly_reception.test.ts b/test/moves/chilly_reception.test.ts index 14141208161..4d15bfff284 100644 --- a/test/moves/chilly_reception.test.ts +++ b/test/moves/chilly_reception.test.ts @@ -1,11 +1,14 @@ -import { AbilityId } from "#enums/ability-id"; +import { RandomMoveAttr } from "#app/data/moves/move"; +import { MoveResult } from "#enums/move-result"; +import { getPokemonNameWithAffix } from "#app/messages"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; +import { AbilityId } from "#app/enums/ability-id"; import { WeatherType } from "#enums/weather-type"; import GameManager from "#test/testUtils/gameManager"; +import i18next from "i18next"; import Phaser from "phaser"; -//import { TurnInitPhase } from "#app/phases/turn-init-phase"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; describe("Moves - Chilly Reception", () => { let phaserGame: Phaser.Game; @@ -25,95 +28,121 @@ describe("Moves - Chilly Reception", () => { game = new GameManager(phaserGame); game.override .battleStyle("single") - .moveset([MoveId.CHILLY_RECEPTION, MoveId.SNOWSCAPE]) + .moveset([MoveId.CHILLY_RECEPTION, MoveId.SNOWSCAPE, MoveId.SPLASH, MoveId.METRONOME]) .enemyMoveset(MoveId.SPLASH) .enemyAbility(AbilityId.BALL_FETCH) .ability(AbilityId.BALL_FETCH); }); - it("should still change the weather if user can't switch out", async () => { + it("should display message before use, switch the user out and change the weather to snow", async () => { + await game.classicMode.startBattle([SpeciesId.SLOWKING, SpeciesId.MEOWTH]); + + const [slowking, meowth] = game.scene.getPlayerParty(); + + game.move.select(MoveId.CHILLY_RECEPTION); + game.doSelectPartyPokemon(1); + await game.toEndOfTurn(); + + expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW); + expect(game.scene.getPlayerPokemon()).toBe(meowth); + expect(slowking.isOnField()).toBe(false); + expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase"); + expect(game.textInterceptor.logs).toContain( + i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(slowking) }), + ); + }); + + it("should still change weather if user can't switch out", async () => { await game.classicMode.startBattle([SpeciesId.SLOWKING]); game.move.select(MoveId.CHILLY_RECEPTION); + await game.toEndOfTurn(); - await game.phaseInterceptor.to("BerryPhase", false); expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW); + expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase"); + expect(game.scene.getPlayerPokemon()?.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); }); - it("should switch out even if it's snowing", async () => { + it("should still switch out even if weather cannot be changed", async () => { await game.classicMode.startBattle([SpeciesId.SLOWKING, SpeciesId.MEOWTH]); - // first turn set up snow with snowscape, try chilly reception on second turn + + expect(game.scene.arena.weather?.weatherType).not.toBe(WeatherType.SNOW); + + const [slowking, meowth] = game.scene.getPlayerParty(); + game.move.select(MoveId.SNOWSCAPE); - await game.phaseInterceptor.to("BerryPhase", false); + await game.toNextTurn(); + expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW); - await game.phaseInterceptor.to("TurnInitPhase", false); game.move.select(MoveId.CHILLY_RECEPTION); game.doSelectPartyPokemon(1); + // TODO: Uncomment lines once wimp out PR fixes force switches to not reset summon data immediately + // await game.phaseInterceptor.to("SwitchSummonPhase", false); + // expect(slowking.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); + + await game.toEndOfTurn(); - await game.phaseInterceptor.to("BerryPhase", false); expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW); - expect(game.scene.getPlayerField()[0].species.speciesId).toBe(SpeciesId.MEOWTH); + expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase"); + expect(game.scene.getPlayerPokemon()).toBe(meowth); + expect(slowking.isOnField()).toBe(false); }); - it("happy case - switch out and weather changes", async () => { + // Source: https://replay.pokemonshowdown.com/gen9ou-2367532550 + it("should fail (while still displaying message) if neither weather change nor switch out succeeds", async () => { + await game.classicMode.startBattle([SpeciesId.SLOWKING]); + + expect(game.scene.arena.weather?.weatherType).not.toBe(WeatherType.SNOW); + + const slowking = game.scene.getPlayerPokemon()!; + + game.move.select(MoveId.SNOWSCAPE); + await game.toNextTurn(); + + expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW); + + game.move.select(MoveId.CHILLY_RECEPTION); + game.doSelectPartyPokemon(1); + await game.toEndOfTurn(); + + expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW); + expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase"); + expect(game.scene.getPlayerPokemon()).toBe(slowking); + expect(slowking.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + expect(game.textInterceptor.logs).toContain( + i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(slowking) }), + ); + }); + + it("should succeed without message if called indirectly", async () => { + vi.spyOn(RandomMoveAttr.prototype, "getMoveOverride").mockReturnValue(MoveId.CHILLY_RECEPTION); await game.classicMode.startBattle([SpeciesId.SLOWKING, SpeciesId.MEOWTH]); - game.move.select(MoveId.CHILLY_RECEPTION); - game.doSelectPartyPokemon(1); + const [slowking, meowth] = game.scene.getPlayerParty(); + + game.move.select(MoveId.METRONOME); + game.doSelectPartyPokemon(1); + await game.toEndOfTurn(); - await game.phaseInterceptor.to("BerryPhase", false); expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW); - expect(game.scene.getPlayerField()[0].species.speciesId).toBe(SpeciesId.MEOWTH); + expect(game.scene.getPlayerPokemon()).toBe(meowth); + expect(slowking.isOnField()).toBe(false); + expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase"); + expect(game.textInterceptor.logs).not.toContain( + i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(slowking) }), + ); }); - // enemy uses another move and weather doesn't change - it("check case - enemy not selecting chilly reception doesn't change weather ", async () => { - game.override.battleStyle("single").enemyMoveset([MoveId.CHILLY_RECEPTION, MoveId.TACKLE]).moveset(MoveId.SPLASH); - + // Bugcheck test for enemy AI bug + it("check case - enemy not selecting chilly reception doesn't change weather", async () => { + game.override.enemyMoveset([MoveId.CHILLY_RECEPTION, MoveId.TACKLE]); await game.classicMode.startBattle([SpeciesId.SLOWKING, SpeciesId.MEOWTH]); game.move.select(MoveId.SPLASH); await game.move.selectEnemyMove(MoveId.TACKLE); + await game.toEndOfTurn(); - await game.phaseInterceptor.to("BerryPhase", false); - expect(game.scene.arena.weather?.weatherType).toBe(undefined); - }); - - it("enemy trainer - expected behavior ", async () => { - game.override - .battleStyle("single") - .startingWave(8) - .enemyMoveset(MoveId.CHILLY_RECEPTION) - .enemySpecies(SpeciesId.MAGIKARP) - .moveset([MoveId.SPLASH, MoveId.THUNDERBOLT]); - - await game.classicMode.startBattle([SpeciesId.JOLTEON]); - const RIVAL_MAGIKARP1 = game.scene.getEnemyPokemon()?.id; - - game.move.select(MoveId.SPLASH); - await game.phaseInterceptor.to("BerryPhase", false); - expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW); - expect(game.scene.getEnemyPokemon()?.id !== RIVAL_MAGIKARP1); - - await game.phaseInterceptor.to("TurnInitPhase", false); - game.move.select(MoveId.SPLASH); - - // second chilly reception should still switch out - await game.phaseInterceptor.to("BerryPhase", false); - expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW); - await game.phaseInterceptor.to("TurnInitPhase", false); - expect(game.scene.getEnemyPokemon()?.id === RIVAL_MAGIKARP1); - game.move.select(MoveId.THUNDERBOLT); - - // enemy chilly recep move should fail: it's snowing and no option to switch out - // no crashing - await game.phaseInterceptor.to("BerryPhase", false); - expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW); - await game.phaseInterceptor.to("TurnInitPhase", false); - expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW); - game.move.select(MoveId.SPLASH); - await game.phaseInterceptor.to("BerryPhase", false); - expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW); + expect(game.scene.arena.weather?.weatherType).toBeUndefined(); }); }); From 1ff27019640066fd4a40457eb5bafe150cf1dd21 Mon Sep 17 00:00:00 2001 From: lnuvy Date: Fri, 20 Jun 2025 09:45:54 +0900 Subject: [PATCH 3/4] =?UTF-8?q?[Bug]=20Fix=20when=20using=20arrow=20keys?= =?UTF-8?q?=20in=20Pok=C3=A9dex=20after=20catching=20a=20Pok=C3=A9mon=20fr?= =?UTF-8?q?om=20mystery=20event=20(#6000)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix: wrap setOverlayMode args in array to mystery-encounter Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> --- src/data/mystery-encounters/utils/encounter-pokemon-utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index 93abd432ef5..f6ac5b0d38b 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -751,7 +751,7 @@ export async function catchPokemon( UiMode.POKEDEX_PAGE, pokemon.species, pokemon.formIndex, - attributes, + [attributes], null, () => { globalScene.ui.setMode(UiMode.MESSAGE).then(() => { From 4b70fab6085d7088b5f37ef747a9779a59d681ff Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Thu, 19 Jun 2025 20:59:55 -0700 Subject: [PATCH 4/4] [Bug] Remove message for Rock Head activation (#6014) --- src/data/abilities/ability.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 70195d6a152..120d1d413c4 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -306,13 +306,6 @@ export class BlockRecoilDamageAttr extends AbAttr { ): void { cancelled.value = true; } - - getTriggerMessage(pokemon: Pokemon, abilityName: string, ..._args: any[]) { - return i18next.t("abilityTriggers:blockRecoilDamage", { - pokemonName: getPokemonNameWithAffix(pokemon), - abilityName: abilityName, - }); - } } /**