Compare commits

..

32 Commits

Author SHA1 Message Date
Sirz Benjie
351ce98efa
Merge a826029a3e into 4b70fab608 2025-06-19 21:02:14 -07:00
damocleas
a826029a3e
Merge branch 'beta' into ab-attr-apply-type-safety 2025-06-20 00:02:09 -04:00
NightKev
4b70fab608
[Bug] Remove message for Rock Head activation (#6014) 2025-06-19 20:59:55 -07:00
lnuvy
1ff2701964
[Bug] Fix when using arrow keys in Pokédex after catching a Pokémon from mystery event (#6000)
fix: wrap setOverlayMode args in array to mystery-encounter

Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com>
2025-06-19 20:45:54 -04:00
Bertie690
1e306e25b5
[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
2025-06-19 17:14:05 -07:00
Madmadness65
43aa772603
[UI/UX] Add Pokémon category flavor text to Pokédex (#5957)
* Add Pokémon category flavor text to Pokédex

* Append `_category` to locale entry
2025-06-20 00:04:57 +00:00
Sirz Benjie
d0f4c4a840
Improve documentation and get rid of ts-expect-error directive 2025-06-19 17:12:08 -06:00
Sirz Benjie
df474da895
Address typing issue in safeguard test 2025-06-19 17:12:07 -06:00
Sirz Benjie
7391446dc2
Fix unnecessary attr type cast in move phase 2025-06-19 17:12:06 -06:00
Sirz Benjie
f2bf6f374e
Fix blockCrit method broken in merge 2025-06-19 17:12:06 -06:00
Sirz Benjie
8ea30f8ba7
Remove empty, unused file 2025-06-19 17:12:05 -06:00
Sirz Benjie
6aef950e76
Update trySetStatus test in pokemon to respect new return value for undefined 2025-06-19 17:12:05 -06:00
Sirz Benjie
1fb7eb6891
Fix regression in flower veil 2025-06-19 17:12:04 -06:00
Sirz Benjie
c6af4acae3
Make cud chew consumption not subclass postTurnAbAttr 2025-06-19 17:12:04 -06:00
Sirz Benjie
bb8e7adfbe
[wip] fix cud chew tests 2025-06-19 17:12:03 -06:00
Sirz Benjie
2c2834734d
Pass source to postApplyDamage in pokemon.ts 2025-06-19 17:12:02 -06:00
Sirz Benjie
44d4088b35
Fix improper simulated check in moody's apply method 2025-06-19 17:12:02 -06:00
Sirz Benjie
d7d59723a4
Remove improper condition in mimicry's ability application 2025-06-19 17:12:01 -06:00
Sirz Benjie
03afffb07f
Fix some tests to mock proper methods 2025-06-19 17:12:01 -06:00
Sirz Benjie
8ff68e894a
update abattr callsite in winstrate challenge encounter 2025-06-19 17:12:00 -06:00
Sirz Benjie
739c3779a9
update ability attribute callsite in shield dust test 2025-06-19 17:12:00 -06:00
Sirz Benjie
bc3970ebf8
Remove latent test functionality 2025-06-19 17:11:59 -06:00
Sirz Benjie
8f34ca1d61
Update abAttr callsites in misc phases 2025-06-19 17:11:58 -06:00
Sirz Benjie
eab7d6d1a0
Update callsites in turn-start-phase 2025-06-19 17:11:58 -06:00
Sirz Benjie
8a96444b8c
Update abattr callsites in MEP 2025-06-19 17:11:57 -06:00
Sirz Benjie
ef9a410920
format with biome and remove cancelled from weather lapse 2025-06-19 17:11:57 -06:00
Sirz Benjie
f432f8cbf6
Update abattr callsites in battler-tags
Also removed stat drop ability application from cancelling ME stat boost effects
2025-06-19 17:11:56 -06:00
Sirz Benjie
1deb74e926
Update abattr callsites in move-phase 2025-06-19 17:11:56 -06:00
Sirz Benjie
8f66dbe69b
Update callsites in moves.ts 2025-06-19 17:11:55 -06:00
Sirz Benjie
b9781bd863
Update callsites in pokemon.ts 2025-06-19 17:11:54 -06:00
Sirz Benjie
55093ec0af
[WIP] update ability signatures 2025-06-19 17:11:54 -06:00
Sirz Benjie
804809bb17
[WIP] Refactor ability attribute apply args 2025-06-19 17:11:53 -06:00
9 changed files with 164 additions and 95 deletions

View File

@ -237,7 +237,15 @@ export interface AbAttrBaseParams {
*/ */
readonly simulated?: boolean; readonly simulated?: boolean;
/** Whether the ability is the passive ability. Default false */ /**
* (For callers of `{@linkcode applyAbAttrs}`): If provided, **only** apply ability attributes of the passive (true) or active (false).
*
* This should almost always be left undefined, as otherwise it will *only* apply attributes of *either* the pokemon's passive (true) or
* non-passive (false) abilities. In almost all cases, you want to apply attributes that are from either.
*
* (For implementations of `AbAttr`): This will *never* be undefined, and will be `true` if the ability being applied
* is the pokemon's passive, and `false` otherwise.
*/
passive?: boolean; passive?: boolean;
} }
@ -285,8 +293,8 @@ export abstract class AbAttr {
*/ */
apply(_params: AbAttrBaseParams): void {} apply(_params: AbAttrBaseParams): void {}
// The NoInfer in the next two signatures enforces that the type of the _params operand // The `Exact` in the next two signatures enforces that the type of the _params operand
// is always compatible with the type of apply. This allows fewer fields, but never a parameter with more. // is always compatible with the type of apply. This allows fewer fields, but never a type with more.
getTriggerMessage(_params: Exact<Parameters<this["apply"]>[0]>, _abilityName: string): string | null { getTriggerMessage(_params: Exact<Parameters<this["apply"]>[0]>, _abilityName: string): string | null {
return null; return null;
} }
@ -314,14 +322,6 @@ export class BlockRecoilDamageAttr extends AbAttr {
override apply({ cancelled }: AbAttrParamsWithCancel): void { override apply({ cancelled }: AbAttrParamsWithCancel): void {
cancelled.value = true; cancelled.value = true;
} }
override getTriggerMessage({ pokemon }: AbAttrParamsWithCancel, abilityName: string) {
// TODO: remove this because this does not exist on cartridge
return i18next.t("abilityTriggers:blockRecoilDamage", {
pokemonName: getPokemonNameWithAffix(pokemon),
abilityName: abilityName,
});
}
} }
export interface DoubleBattleChanceAbAttrParams extends AbAttrBaseParams { export interface DoubleBattleChanceAbAttrParams extends AbAttrBaseParams {

View File

@ -23,15 +23,14 @@ function applySingleAbAttrs<T extends AbAttrString>(
return; return;
} }
// typescript assert
for (const attr of ability.getAttrs(attrType)) { for (const attr of ability.getAttrs(attrType)) {
const condition = attr.getCondition(); const condition = attr.getCondition();
let abShown = false; let abShown = false;
if ( // We require an `as any` cast to suppress an error about the `params` type not being assignable to
(condition && !condition(pokemon)) || // the type of the argument expected by `attr.canApply()`. This is OK, because we know that
// @ts-ignore: typescript can't unify the type of params with the generic type that was passed // `attr` is an instance of the `attrType` class provided to the method, and typescript _will_ check
!attr.canApply(params) // that the `params` object has the correct properties for that class at the callsites.
) { if ((condition && !condition(pokemon)) || !attr.canApply(params as any)) {
continue; continue;
} }
@ -41,17 +40,16 @@ function applySingleAbAttrs<T extends AbAttrString>(
globalScene.phaseManager.queueAbilityDisplay(pokemon, passive, true); globalScene.phaseManager.queueAbilityDisplay(pokemon, passive, true);
abShown = true; abShown = true;
} }
// @ts-expect-error - typescript can't unify the type of params with the generic type that was passed
const message = attr.getTriggerMessage(params, ability.name); const message = attr.getTriggerMessage(params as any, ability.name);
if (message) { if (message) {
if (!simulated) { if (!simulated) {
globalScene.phaseManager.queueMessage(message); globalScene.phaseManager.queueMessage(message);
} }
messages.push(message); messages.push(message);
} }
// The `as any` cast here uses the same reasoning as above.
// @ts-ignore: typescript can't unify the type of params with the generic type that was passed attr.apply(params as any);
attr.apply(params);
if (abShown) { if (abShown) {
globalScene.phaseManager.queueAbilityDisplay(pokemon, passive, false); globalScene.phaseManager.queueAbilityDisplay(pokemon, passive, false);
@ -84,10 +82,10 @@ function applyAbAttrsInternal<T extends CallableAbAttrString>(
params.passive = passive; params.passive = passive;
applySingleAbAttrs(attrType, params, gainedMidTurn, messages); applySingleAbAttrs(attrType, params, gainedMidTurn, messages);
globalScene.phaseManager.clearPhaseQueueSplice(); globalScene.phaseManager.clearPhaseQueueSplice();
// We need to restore passive to its original state in case it was undefined earlier
// this is necessary in case this method is called with an object that is reused.
params.passive = undefined;
} }
// We need to restore passive to its original state in the case that it was undefined on entry
// this is necessary in case this method is called with an object that is reused.
params.passive = undefined;
} }
/** /**

View File

@ -90,6 +90,10 @@ import { applyMoveAttrs } from "./apply-attrs";
import { frenzyMissFunc, getMoveTargets } from "./move-utils"; import { frenzyMissFunc, getMoveTargets } from "./move-utils";
import { AbAttrBaseParams, AbAttrParamsWithCancel, PreAttackModifyPowerAbAttrParams } from "../abilities/ability"; import { AbAttrBaseParams, AbAttrParamsWithCancel, PreAttackModifyPowerAbAttrParams } from "../abilities/ability";
/**
* 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 MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean;
export type UserMoveConditionFunc = (user: Pokemon, move: Move) => boolean; export type UserMoveConditionFunc = (user: Pokemon, move: Move) => boolean;
@ -1396,18 +1400,31 @@ export class BeakBlastHeaderAttr extends AddBattlerTagHeaderAttr {
} }
} }
/**
* Attribute to display a message before a move is executed.
*/
export class PreMoveMessageAttr extends MoveAttr { 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)) { constructor(message: string | ((user: Pokemon, target: Pokemon, move: Move) => string)) {
super(); super();
this.message = message; this.message = message;
} }
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, _args: any[]): boolean {
const message = typeof this.message === "string" const message = typeof this.message === "function"
? this.message as string ? this.message(user, target, move)
: this.message(user, target, move); : this.message;
// TODO: Consider changing if/when MoveAttr `apply` return values become significant
if (message) { if (message) {
globalScene.phaseManager.queueMessage(message, 500); globalScene.phaseManager.queueMessage(message, 500);
return true; return true;
@ -11307,7 +11324,11 @@ export function initMoves() {
.attr(ForceSwitchOutAttr, true, SwitchType.SHED_TAIL) .attr(ForceSwitchOutAttr, true, SwitchType.SHED_TAIL)
.condition(failIfLastInPartyCondition), .condition(failIfLastInPartyCondition),
new SelfStatusMove(MoveId.CHILLY_RECEPTION, PokemonType.ICE, -1, 10, -1, 0, 9) 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), .attr(ChillyReceptionAttr, true),
new SelfStatusMove(MoveId.TIDY_UP, PokemonType.NORMAL, -1, 10, -1, 0, 9) new SelfStatusMove(MoveId.TIDY_UP, PokemonType.NORMAL, -1, 10, -1, 0, 9)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPD ], 1, true) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPD ], 1, true)

View File

@ -751,7 +751,7 @@ export async function catchPokemon(
UiMode.POKEDEX_PAGE, UiMode.POKEDEX_PAGE,
pokemon.species, pokemon.species,
pokemon.formIndex, pokemon.formIndex,
attributes, [attributes],
null, null,
() => { () => {
globalScene.ui.setMode(UiMode.MESSAGE).then(() => { globalScene.ui.setMode(UiMode.MESSAGE).then(() => {

View File

@ -764,7 +764,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
readonly subLegendary: boolean; readonly subLegendary: boolean;
readonly legendary: boolean; readonly legendary: boolean;
readonly mythical: boolean; readonly mythical: boolean;
readonly species: string; public category: string;
readonly growthRate: GrowthRate; readonly growthRate: GrowthRate;
/** The chance (as a decimal) for this Species to be male, or `null` for genderless species */ /** The chance (as a decimal) for this Species to be male, or `null` for genderless species */
readonly malePercent: number | null; readonly malePercent: number | null;
@ -778,7 +778,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
subLegendary: boolean, subLegendary: boolean,
legendary: boolean, legendary: boolean,
mythical: boolean, mythical: boolean,
species: string, category: string,
type1: PokemonType, type1: PokemonType,
type2: PokemonType | null, type2: PokemonType | null,
height: number, height: number,
@ -829,7 +829,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
this.subLegendary = subLegendary; this.subLegendary = subLegendary;
this.legendary = legendary; this.legendary = legendary;
this.mythical = mythical; this.mythical = mythical;
this.species = species; this.category = category;
this.growthRate = growthRate; this.growthRate = growthRate;
this.malePercent = malePercent; this.malePercent = malePercent;
this.genderDiffs = genderDiffs; this.genderDiffs = genderDiffs;
@ -968,6 +968,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
localize(): void { localize(): void {
this.name = i18next.t(`pokemon:${SpeciesId[this.speciesId].toLowerCase()}`); 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 { getWildSpeciesForLevel(level: number, allowEvolving: boolean, isBoss: boolean, gameMode: GameMode): SpeciesId {

View File

@ -679,6 +679,9 @@ export class MovePhase extends BattlePhase {
}), }),
500, 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()); applyMoveAttrs("PreMoveMessageAttr", this.pokemon, this.pokemon.getOpponents(false)[0], this.move.getMove());
} }

View File

@ -245,6 +245,7 @@ export async function initI18n(): Promise<void> {
"pokeball", "pokeball",
"pokedexUiHandler", "pokedexUiHandler",
"pokemon", "pokemon",
"pokemonCategory",
"pokemonEvolutions", "pokemonEvolutions",
"pokemonForm", "pokemonForm",
"pokemonInfo", "pokemonInfo",

View File

@ -174,6 +174,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
private pokemonCaughtHatchedContainer: Phaser.GameObjects.Container; private pokemonCaughtHatchedContainer: Phaser.GameObjects.Container;
private pokemonCaughtCountText: Phaser.GameObjects.Text; private pokemonCaughtCountText: Phaser.GameObjects.Text;
private pokemonFormText: Phaser.GameObjects.Text; private pokemonFormText: Phaser.GameObjects.Text;
private pokemonCategoryText: Phaser.GameObjects.Text;
private pokemonHatchedIcon: Phaser.GameObjects.Sprite; private pokemonHatchedIcon: Phaser.GameObjects.Sprite;
private pokemonHatchedCountText: Phaser.GameObjects.Text; private pokemonHatchedCountText: Phaser.GameObjects.Text;
private pokemonShinyIcons: Phaser.GameObjects.Sprite[]; private pokemonShinyIcons: Phaser.GameObjects.Sprite[];
@ -409,6 +410,12 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
this.pokemonFormText.setOrigin(0, 0); this.pokemonFormText.setOrigin(0, 0);
this.starterSelectContainer.add(this.pokemonFormText); 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 = globalScene.add.container(2, 25);
this.pokemonCaughtHatchedContainer.setScale(0.5); this.pokemonCaughtHatchedContainer.setScale(0.5);
this.starterSelectContainer.add(this.pokemonCaughtHatchedContainer); this.starterSelectContainer.add(this.pokemonCaughtHatchedContainer);
@ -2354,6 +2361,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
this.pokemonCaughtHatchedContainer.setVisible(true); this.pokemonCaughtHatchedContainer.setVisible(true);
this.pokemonCandyContainer.setVisible(false); this.pokemonCandyContainer.setVisible(false);
this.pokemonFormText.setVisible(false); this.pokemonFormText.setVisible(false);
this.pokemonCategoryText.setVisible(false);
const defaultDexAttr = globalScene.gameData.getSpeciesDefaultDexAttr(species, true, true); const defaultDexAttr = globalScene.gameData.getSpeciesDefaultDexAttr(species, true, true);
const props = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr); const props = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr);
@ -2382,6 +2390,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
this.pokemonCaughtHatchedContainer.setVisible(false); this.pokemonCaughtHatchedContainer.setVisible(false);
this.pokemonCandyContainer.setVisible(false); this.pokemonCandyContainer.setVisible(false);
this.pokemonFormText.setVisible(false); this.pokemonFormText.setVisible(false);
this.pokemonCategoryText.setVisible(false);
this.setSpeciesDetails(species!, { this.setSpeciesDetails(species!, {
// TODO: is this bang correct? // TODO: is this bang correct?
@ -2534,6 +2543,13 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
this.pokemonNameText.setText(species ? "???" : ""); this.pokemonNameText.setText(species ? "???" : "");
} }
// Setting the category
if (isFormCaught) {
this.pokemonCategoryText.setText(species.category);
} else {
this.pokemonCategoryText.setText("");
}
// Setting tint of the sprite // Setting tint of the sprite
if (isFormCaught) { if (isFormCaught) {
this.species.loadAssets(female!, formIndex, shiny, variant as Variant, true).then(() => { this.species.loadAssets(female!, formIndex, shiny, variant as Variant, true).then(() => {

View File

@ -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 { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { AbilityId } from "#app/enums/ability-id";
import { WeatherType } from "#enums/weather-type"; import { WeatherType } from "#enums/weather-type";
import GameManager from "#test/testUtils/gameManager"; import GameManager from "#test/testUtils/gameManager";
import i18next from "i18next";
import Phaser from "phaser"; import Phaser from "phaser";
//import { TurnInitPhase } from "#app/phases/turn-init-phase"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Moves - Chilly Reception", () => { describe("Moves - Chilly Reception", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
@ -25,95 +28,121 @@ describe("Moves - Chilly Reception", () => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
game.override game.override
.battleStyle("single") .battleStyle("single")
.moveset([MoveId.CHILLY_RECEPTION, MoveId.SNOWSCAPE]) .moveset([MoveId.CHILLY_RECEPTION, MoveId.SNOWSCAPE, MoveId.SPLASH, MoveId.METRONOME])
.enemyMoveset(MoveId.SPLASH) .enemyMoveset(MoveId.SPLASH)
.enemyAbility(AbilityId.BALL_FETCH) .enemyAbility(AbilityId.BALL_FETCH)
.ability(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]); await game.classicMode.startBattle([SpeciesId.SLOWKING]);
game.move.select(MoveId.CHILLY_RECEPTION); 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.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]); 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); game.move.select(MoveId.SNOWSCAPE);
await game.phaseInterceptor.to("BerryPhase", false); await game.toNextTurn();
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW); expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW);
await game.phaseInterceptor.to("TurnInitPhase", false);
game.move.select(MoveId.CHILLY_RECEPTION); game.move.select(MoveId.CHILLY_RECEPTION);
game.doSelectPartyPokemon(1); 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.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]); await game.classicMode.startBattle([SpeciesId.SLOWKING, SpeciesId.MEOWTH]);
game.move.select(MoveId.CHILLY_RECEPTION); const [slowking, meowth] = game.scene.getPlayerParty();
game.doSelectPartyPokemon(1);
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.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 // Bugcheck test for enemy AI bug
it("check case - enemy not selecting chilly reception doesn't change weather ", async () => { 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); game.override.enemyMoveset([MoveId.CHILLY_RECEPTION, MoveId.TACKLE]);
await game.classicMode.startBattle([SpeciesId.SLOWKING, SpeciesId.MEOWTH]); await game.classicMode.startBattle([SpeciesId.SLOWKING, SpeciesId.MEOWTH]);
game.move.select(MoveId.SPLASH); game.move.select(MoveId.SPLASH);
await game.move.selectEnemyMove(MoveId.TACKLE); await game.move.selectEnemyMove(MoveId.TACKLE);
await game.toEndOfTurn();
await game.phaseInterceptor.to("BerryPhase", false); expect(game.scene.arena.weather?.weatherType).toBeUndefined();
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);
}); });
}); });