From 55324320854357872f8ac402a4a54ffdc8d9663a Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Sat, 17 May 2025 15:45:59 -0400 Subject: [PATCH] Added teszt file to confirm switching data resets --- src/field/pokemon.ts | 1 + src/phases/faint-phase.ts | 3 +- src/phases/summon-phase.ts | 3 - src/phases/switch-summon-phase.ts | 9 +- test/phases/switch-phases.test.ts | 167 ++++++++++++++++++++++++++++++ 5 files changed, 175 insertions(+), 8 deletions(-) create mode 100644 test/phases/switch-phases.test.ts diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index f301975cf90..d1aa938f3ee 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -7984,6 +7984,7 @@ export class PokemonTurnData { public statStagesDecreased = false; public moveEffectiveness: TypeDamageMultiplier | null = null; public combiningPledge?: Moves; + /** Whether the pokemon was sent into battle during this turn; used for {@linkcode Abilities.STAKEOUT} and {@linkcode Abilities.SPEED_BOOST}. */ public switchedInThisTurn = false; public failedRunAway = false; public joinedRound = false; diff --git a/src/phases/faint-phase.ts b/src/phases/faint-phase.ts index b03b7ec9511..1aa24d59fa0 100644 --- a/src/phases/faint-phase.ts +++ b/src/phases/faint-phase.ts @@ -61,6 +61,8 @@ export class FaintPhase extends PokemonPhase { faintPokemon.getTag(BattlerTagType.GRUDGE)?.lapse(faintPokemon, BattlerTagLapseType.CUSTOM, this.source); } + faintPokemon.resetSummonData(); + if (!this.preventInstantRevive) { const instantReviveModifier = globalScene.applyModifier( PokemonInstantReviveModifier, @@ -69,7 +71,6 @@ export class FaintPhase extends PokemonPhase { ) as PokemonInstantReviveModifier; if (instantReviveModifier) { - faintPokemon.resetSummonData(); faintPokemon.loseHeldItem(instantReviveModifier); globalScene.updateModifiers(this.player); return this.end(); diff --git a/src/phases/summon-phase.ts b/src/phases/summon-phase.ts index 92ad1e20de3..b6d8b8a5051 100644 --- a/src/phases/summon-phase.ts +++ b/src/phases/summon-phase.ts @@ -274,9 +274,6 @@ export class SummonPhase extends PartyMemberPokemonPhase { globalScene.unshiftPhase(new ShinySparklePhase(pokemon.getBattlerIndex())); } - // TODO: This might be a duplicate - check to see if can be removed without breaking things - pokemon.resetTurnData(); - if ( !this.loaded || [BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(globalScene.currentBattle.battleType) || diff --git a/src/phases/switch-summon-phase.ts b/src/phases/switch-summon-phase.ts index 445384d96fa..c3041c5824f 100644 --- a/src/phases/switch-summon-phase.ts +++ b/src/phases/switch-summon-phase.ts @@ -209,9 +209,9 @@ export class SwitchSummonPhase extends SummonPhase { const pokemon = this.getPokemon(); - if (this.switchType === SwitchType.BATON_PASS && pokemon) { + if (this.switchType === SwitchType.BATON_PASS) { pokemon.transferSummon(this.lastPokemon); - } else if (this.switchType === SwitchType.SHED_TAIL && pokemon) { + } else if (this.switchType === SwitchType.SHED_TAIL) { const subTag = this.lastPokemon.getTag(SubstituteTag); if (subTag) { pokemon.summonData.tags.push(subTag); @@ -223,10 +223,11 @@ export class SwitchSummonPhase extends SummonPhase { if (this.switchType !== SwitchType.INITIAL_SWITCH) { pokemon.tempSummonData.turnCount--; pokemon.tempSummonData.waveTurnCount--; - // No need to reset turn/summon data for initial switch since both get initialized to an empty object on object creation + pokemon.turnData.switchedInThisTurn = true; + // No need to reset turn/summon data for initial switch + //(since both get initialized to an empty object on object creation) pokemon.resetTurnData(); pokemon.resetSummonData(); - pokemon.turnData.switchedInThisTurn = true; } globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true); diff --git a/test/phases/switch-phases.test.ts b/test/phases/switch-phases.test.ts new file mode 100644 index 00000000000..4f250acf579 --- /dev/null +++ b/test/phases/switch-phases.test.ts @@ -0,0 +1,167 @@ +import Trainer from "#app/field/trainer"; +import { Abilities } from "#enums/abilities"; +import { BattleType } from "#enums/battle-type"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/testUtils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe.each<{ name: string; selfMove?: Moves; selfAbility?: Abilities; oppMove?: Moves }>([ + { name: "Self Switch Attack Moves", selfMove: Moves.U_TURN }, + { name: "Target Switch Attack Moves", oppMove: Moves.DRAGON_TAIL }, + { name: "Self Switch Status Moves", selfMove: Moves.TELEPORT }, + { name: "Target Switch Status Moves", oppMove: Moves.WHIRLWIND }, + { name: "Self Switch Abilities", selfAbility: Abilities.EMERGENCY_EXIT }, +])( + "Switch Outs - $name - ", + ({ selfMove = Moves.SPLASH, selfAbility = Abilities.BALL_FETCH, oppMove = Moves.SPLASH }) => { + 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") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyPassiveAbility(Abilities.NO_GUARD); + }); + + describe("Player -", () => { + beforeEach(() => { + game.override.moveset(oppMove).ability(selfAbility).enemyMoveset(selfMove).enemyAbility(Abilities.BALL_FETCH); + }); + + it("should only call leaveField once on the switched out pokemon", async () => { + await game.classicMode.startBattle([Species.PILOSWINE, Species.MAMOSWINE]); + + const [piloswine, mamoswine] = game.scene.getPlayerParty(); + const piloLeaveSpy = vi.spyOn(piloswine, "leaveField"); + const mamoLeaveSpy = vi.spyOn(mamoswine, "leaveField"); + + game.move.select(selfMove); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("BerryPhase", false); + + expect(piloLeaveSpy).toHaveBeenCalledTimes(1); + expect(mamoLeaveSpy).toHaveBeenCalledTimes(0); + }); + + it("should only reset summonData/turnData once per switch", async () => { + await game.classicMode.startBattle([Species.PILOSWINE, Species.MAMOSWINE]); + + const [piloswine, mamoswine] = game.scene.getPlayerParty(); + const piloSummonSpy = vi.spyOn(piloswine, "resetSummonData"); + const piloTurnSpy = vi.spyOn(piloswine, "resetTurnData"); + const mamoSummonSpy = vi.spyOn(mamoswine, "resetSummonData"); + const mamoTurnSpy = vi.spyOn(mamoswine, "resetTurnData"); + + game.move.select(selfMove); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("BerryPhase", false); + + expect(piloSummonSpy).toHaveBeenCalledTimes(1); + expect(piloTurnSpy).toHaveBeenCalledTimes(1); + expect(mamoSummonSpy).toHaveBeenCalledTimes(1); + expect(mamoTurnSpy).toHaveBeenCalledTimes(1); + }); + + it("should not reset battleData/waveData upon switching", async () => { + await game.classicMode.startBattle([Species.PILOSWINE, Species.MAMOSWINE]); + + const [piloswine, mamoswine] = game.scene.getPlayerParty(); + const piloWaveSpy = vi.spyOn(piloswine, "resetWaveData"); + const piloBattleWaveSpy = vi.spyOn(piloswine, "resetBattleAndWaveData"); + const mamoWaveSpy = vi.spyOn(mamoswine, "resetWaveData"); + const mamoBattleWaveSpy = vi.spyOn(mamoswine, "resetBattleAndWaveData"); + + game.move.select(selfMove); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(piloWaveSpy).toHaveBeenCalledTimes(0); + expect(piloBattleWaveSpy).toHaveBeenCalledTimes(0); + expect(mamoWaveSpy).toHaveBeenCalledTimes(0); + expect(mamoBattleWaveSpy).toHaveBeenCalledTimes(0); + }); + }); + + describe("Enemy - ", () => { + beforeEach(() => { + game.override + .enemyMoveset(oppMove) + .enemyAbility(selfAbility) + .moveset(selfMove) + .ability(Abilities.BALL_FETCH) + .battleType(BattleType.TRAINER); + + // prevent natural trainer switches + vi.spyOn(Trainer.prototype, "getPartyMemberMatchupScores").mockReturnValue([ + [100, 1], + [100, 1], + ]); + }); + + it("should only call leaveField once on the switched out pokemon", async () => { + await game.classicMode.startBattle([Species.PILOSWINE, Species.MAMOSWINE]); + + const [enemy1, enemy2] = game.scene.getEnemyParty(); + const enemy1LeaveSpy = vi.spyOn(enemy1, "leaveField"); + const enemy2LeaveSpy = vi.spyOn(enemy2, "leaveField"); + + game.move.select(selfMove); + await game.phaseInterceptor.to("BerryPhase", false); + + expect(enemy1LeaveSpy).toHaveBeenCalledTimes(1); + expect(enemy2LeaveSpy).toHaveBeenCalledTimes(0); + }); + + it("should only reset summonData/turnData once per switch", async () => { + await game.classicMode.startBattle([Species.PILOSWINE, Species.MAMOSWINE]); + + const [enemy1, enemy2] = game.scene.getEnemyParty(); + const enemy1SummonSpy = vi.spyOn(enemy1, "resetSummonData"); + const enemy1TurnSpy = vi.spyOn(enemy1, "resetTurnData"); + const enemy2SummonSpy = vi.spyOn(enemy2, "resetSummonData"); + const enemy2TurnSpy = vi.spyOn(enemy2, "resetTurnData"); + + game.move.select(selfMove); + await game.phaseInterceptor.to("BerryPhase", false); + + expect(enemy1SummonSpy).toHaveBeenCalledTimes(1); + expect(enemy1TurnSpy).toHaveBeenCalledTimes(1); + expect(enemy2SummonSpy).toHaveBeenCalledTimes(1); + expect(enemy2TurnSpy).toHaveBeenCalledTimes(1); + }); + + it("should not reset battleData/waveData upon switching", async () => { + await game.classicMode.startBattle([Species.PILOSWINE, Species.MAMOSWINE]); + + const [enemy1, enemy2] = game.scene.getEnemyParty(); + const enemy1WaveSpy = vi.spyOn(enemy1, "resetWaveData"); + const enemy1BattleWaveSpy = vi.spyOn(enemy1, "resetBattleAndWaveData"); + const enemy2WaveSpy = vi.spyOn(enemy2, "resetWaveData"); + const enemy2BattleWaveSpy = vi.spyOn(enemy2, "resetBattleAndWaveData"); + + game.move.select(selfMove); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(enemy1WaveSpy).toHaveBeenCalledTimes(0); + expect(enemy1BattleWaveSpy).toHaveBeenCalledTimes(0); + expect(enemy2WaveSpy).toHaveBeenCalledTimes(0); + expect(enemy2BattleWaveSpy).toHaveBeenCalledTimes(0); + }); + }); + }, +);