diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index cf79f95b808..2520faa3ef8 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -124,6 +124,10 @@ import { MultiHitType } from "#enums/MultiHitType"; import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSleepTalkMoves } from "./invalid-moves"; import { SelectBiomePhase } from "#app/phases/select-biome-phase"; +/** + * 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; type UserMoveConditionFunc = (user: Pokemon, move: Move) => boolean; @@ -1355,15 +1359,16 @@ export class BeakBlastHeaderAttr extends AddBattlerTagHeaderAttr { */ export class PreMoveMessageAttr extends MoveAttr { /** The message to display or a function returning one */ - private message: string | undefined | ((user: Pokemon, target: Pokemon, move: Move) => string | undefined); + 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. - * A value of `undefined` or an empty string (`''`) will cause no message to be displayed + * @remarks + * If {@linkcode message} evaluates to an empty string (`''`), no message will be displayed * (though the move will still suceed). */ - constructor(message: string | undefined | ((user: Pokemon, target: Pokemon, move: Move) => string | undefined)) { + constructor(message: string | ((user: Pokemon, target: Pokemon, move: Move) => string)) { super(); this.message = message; } @@ -1372,10 +1377,10 @@ export class PreMoveMessageAttr extends MoveAttr { const message = typeof this.message === "function" ? this.message(user, target, move) : this.message; + if (message) { globalScene.queueMessage(message, 500); } - // always return `true` to ensure moves succeed even without a message return true; } } @@ -11134,7 +11139,7 @@ export function initMoves() { // Don't display text if current move phase is follow up (ie move called indirectly) // TODO: Change in move-use-type PR to use the move phase's current use type (globalScene.getCurrentPhase() as MovePhase)["followUp"] - ? undefined + ? "" : i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(user) })) .attr(ChillyReceptionAttr, true), new SelfStatusMove(Moves.TIDY_UP, PokemonType.NORMAL, -1, 10, -1, 0, 9) diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 5d63fe6efea..cf843b858ac 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -655,6 +655,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 d3a2bfd1cc6..c265215b60c 100644 --- a/test/moves/chilly_reception.test.ts +++ b/test/moves/chilly_reception.test.ts @@ -77,19 +77,26 @@ describe("Moves - Chilly Reception", () => { game.move.select(Moves.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.phaseInterceptor.to("BerryPhase", false); expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW); expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase"); expect(game.scene.getPlayerPokemon()).toBe(meowth); - expect(slowking.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); + expect(slowking.isOnField()).toBe(false); }); - it("should fail if neither weather change nor switch out succeeds", 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([Species.SLOWKING]); expect(game.scene.arena.weather?.weatherType).not.toBe(WeatherType.SNOW); + const slowking = game.scene.getPlayerPokemon()!; + game.move.select(Moves.SNOWSCAPE); await game.toNextTurn(); @@ -101,12 +108,14 @@ describe("Moves - Chilly Reception", () => { expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW); expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase"); - expect(game.scene.getPlayerPokemon()?.species.speciesId).toBe(Species.SLOWKING); - expect(game.scene.getPlayerPokemon()?.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + 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) }), + ); }); - // TODO: Fix this - it's really easy (just check the current MovePhase's `virtual` flag) - it.todo("should not display message if called indirectly", async () => { + it("should not display message if called indirectly", async () => { vi.spyOn(RandomMoveAttr.prototype, "getMoveOverride").mockReturnValue(Moves.CHILLY_RECEPTION); await game.classicMode.startBattle([Species.SLOWKING, Species.MEOWTH]);