From 67d209f6bcbf34899cadeadeb3a9528d118009c1 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Mon, 19 May 2025 14:16:43 -0400 Subject: [PATCH] Hopefully fixed everything I think --- src/data/moves/move.ts | 11 ++- src/phases/switch-phase.ts | 5 +- test/moves/chilly_reception.test.ts | 135 +++++++++++++++------------- 3 files changed, 87 insertions(+), 64 deletions(-) diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index f4cd1a9f00a..dc5c5e94236 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -6225,6 +6225,12 @@ export class RevivalBlessingAttr extends MoveEffectAttr { */ // TODO: Add custom failure text & locales export class ForceSwitchOutAttr extends ForceSwitch(MoveEffectAttr) { + /** + * Create a new {@linkcode ForceSwitchOutAttr}. + * @param selfSwitch - Whether the move should switch out the user (`true`) or target (`false`); default `false`. + * Self-switching moves that target the user should still set this as `true`. + * @param switchType - A {@linkcode SwitchType} dictating the type of switch logic to implement; default {@linkcode SwitchType.SWITCH} + */ constructor( selfSwitch: boolean = false, switchType: NormalSwitchType = SwitchType.SWITCH @@ -10974,8 +10980,9 @@ export function initMoves() { .attr(ForceSwitchOutAttr, true, SwitchType.SHED_TAIL) .condition(failIfLastInPartyCondition), new SelfStatusMove(Moves.CHILLY_RECEPTION, PokemonType.ICE, -1, 10, -1, 0, 9) - .attr(PreMoveMessageAttr, (user, move) => i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(user) })) - .attr(ChillyReceptionAttr, true), + .attr(PreMoveMessageAttr, (user, target, move) => i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(user) })) + .attr(ChillyReceptionAttr, true) + .edgeCase(), // "Chilly Joke" message shouldn't occur if called indirectly new SelfStatusMove(Moves.TIDY_UP, PokemonType.NORMAL, -1, 10, -1, 0, 9) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPD ], 1, true) .attr(RemoveArenaTrapAttr, true) diff --git a/src/phases/switch-phase.ts b/src/phases/switch-phase.ts index ee8a5f6aa44..5998a82f598 100644 --- a/src/phases/switch-phase.ts +++ b/src/phases/switch-phase.ts @@ -37,7 +37,7 @@ export class SwitchPhase extends BattlePhase { super.start(); // Skip modal switch if impossible (no remaining party members that aren't in battle) - if (this.isModal && !globalScene.getPlayerParty().filter(p => p.isAllowedInBattle() && !p.isActive(true)).length) { + if (this.isModal && globalScene.getBackupPartyMemberIndices(true).length === 0) { return super.end(); } @@ -53,9 +53,10 @@ export class SwitchPhase extends BattlePhase { } // Check if there is any space still on field. + // We use > here as the prior pokemon still technically hasn't "left" the field _per se_ (unless they fainted). if ( this.isModal && - globalScene.getPlayerField().filter(p => p.isActive(true)).length >= globalScene.currentBattle.getBattlerCount() + globalScene.getPlayerField().filter(p => p.isActive(true)).length > globalScene.currentBattle.getBattlerCount() ) { return super.end(); } diff --git a/test/moves/chilly_reception.test.ts b/test/moves/chilly_reception.test.ts index 56da5dd400c..f7b66602ea6 100644 --- a/test/moves/chilly_reception.test.ts +++ b/test/moves/chilly_reception.test.ts @@ -1,11 +1,14 @@ +import { RandomMoveAttr } from "#app/data/moves/move"; import { Abilities } from "#app/enums/abilities"; +import { MoveResult } from "#app/field/pokemon"; +import { getPokemonNameWithAffix } from "#app/messages"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; 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,98 +28,110 @@ describe("Moves - Chilly Reception", () => { game = new GameManager(phaserGame); game.override .battleStyle("single") - .moveset([Moves.CHILLY_RECEPTION, Moves.SNOWSCAPE]) - .enemyMoveset(Array(4).fill(Moves.SPLASH)) + .moveset([Moves.CHILLY_RECEPTION, Moves.SNOWSCAPE, Moves.SPLASH, Moves.METRONOME]) + .enemyMoveset(Moves.SPLASH) .enemyAbility(Abilities.BALL_FETCH) .ability(Abilities.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([Species.SLOWKING, Species.MEOWTH]); + + const [slowking, meowth] = game.scene.getPlayerParty(); + + game.move.select(Moves.CHILLY_RECEPTION); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("BerryPhase", false); + + 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([Species.SLOWKING]); game.move.select(Moves.CHILLY_RECEPTION); 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([Species.SLOWKING, Species.MEOWTH]); - // first turn set up snow with snowscape, try chilly reception on second turn + + expect(game.scene.arena.weather?.weatherType).not.toBe(WeatherType.SNOW); + game.move.select(Moves.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(Moves.CHILLY_RECEPTION); game.doSelectPartyPokemon(1); - await game.phaseInterceptor.to("BerryPhase", false); + expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW); - expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.MEOWTH); + expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase"); + expect(game.scene.getPlayerPokemon()?.species.speciesId).toBe(Species.MEOWTH); + expect(game.scene.getPlayerPokemon()?.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); }); - it("happy case - switch out and weather changes", async () => { + it("should fail 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); + + game.move.select(Moves.SNOWSCAPE); + await game.toNextTurn(); + + expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW); + + game.move.select(Moves.CHILLY_RECEPTION); + game.doSelectPartyPokemon(1); + 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()?.species.speciesId).toBe(Species.SLOWKING); + expect(game.scene.getPlayerPokemon()?.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + }); + + // 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 () => { + vi.spyOn(RandomMoveAttr.prototype, "getMoveOverride").mockReturnValue(Moves.CHILLY_RECEPTION); await game.classicMode.startBattle([Species.SLOWKING, Species.MEOWTH]); - game.move.select(Moves.CHILLY_RECEPTION); - game.doSelectPartyPokemon(1); + const [slowking, meowth] = game.scene.getPlayerParty(); + game.move.select(Moves.METRONOME); + game.doSelectPartyPokemon(1); await game.phaseInterceptor.to("BerryPhase", false); + expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW); - expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.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([Moves.CHILLY_RECEPTION, Moves.TACKLE]) - .moveset(Array(4).fill(Moves.SPLASH)); - + // Bugcheck test for enemy AI bug + it("check case - enemy not selecting chilly reception doesn't change weather", async () => { + game.override.enemyMoveset([Moves.CHILLY_RECEPTION, Moves.TACKLE]); await game.classicMode.startBattle([Species.SLOWKING, Species.MEOWTH]); game.move.select(Moves.SPLASH); await game.forceEnemyMove(Moves.TACKLE); 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(Array(4).fill(Moves.CHILLY_RECEPTION)) - .enemySpecies(Species.MAGIKARP) - .moveset([Moves.SPLASH, Moves.THUNDERBOLT]); - - await game.classicMode.startBattle([Species.JOLTEON]); - const RIVAL_MAGIKARP1 = game.scene.getEnemyPokemon()?.id; - - game.move.select(Moves.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(Moves.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(Moves.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(Moves.SPLASH); - await game.phaseInterceptor.to("BerryPhase", false); - expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW); + expect(game.scene.arena.weather?.weatherType).toBeUndefined(); }); });