diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index f998efbcd9c..c7f9c239dff 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -1250,7 +1250,7 @@ export class MoveTypeChangeAbAttr extends PreAttackAbAttr { /** * Determine if the move type change attribute can be applied - * + * * Can be applied if: * - The ability's condition is met, e.g. pixilate only boosts normal moves, * - The move is not forbidden from having its type changed by an ability, e.g. {@linkcode Moves.MULTI_ATTACK} @@ -1266,7 +1266,7 @@ export class MoveTypeChangeAbAttr extends PreAttackAbAttr { */ override canApplyPreAttack(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _defender: Pokemon | null, move: Move, _args: [NumberHolder?, NumberHolder?, ...any]): boolean { return (!this.condition || this.condition(pokemon, _defender, move)) && - !noAbilityTypeOverrideMoves.has(move.id) && + !noAbilityTypeOverrideMoves.has(move.id) && (!pokemon.isTerastallized || (move.id !== Moves.TERA_BLAST && (move.id !== Moves.TERA_STARSTORM || pokemon.getTeraType() !== PokemonType.STELLAR || !pokemon.hasSpecies(Species.TERAPAGOS)))); diff --git a/src/data/mixins/force-switch.ts b/src/data/mixins/force-switch.ts index 479bf6725a2..dcc59694541 100644 --- a/src/data/mixins/force-switch.ts +++ b/src/data/mixins/force-switch.ts @@ -110,10 +110,14 @@ export function ForceSwitch(Base: TBase) { this.tryFleeWildPokemon(switchOutTarget); } + // NB: `prependToPhase` is used here to ensure that the switch happens before the move ends + // and `arena.ignoreAbilities` is reset. + // This ensures ability ignore effects will persist for the duration of the switch (for hazards). + private trySwitchPlayerPokemon(switchOutTarget: PlayerPokemon): void { // If not forced to switch, add a SwitchPhase to allow picking the next switched in Pokemon. if (this.switchType !== SwitchType.FORCE_SWITCH) { - globalScene.appendToPhase( + globalScene.prependToPhase( new SwitchPhase(this.switchType, switchOutTarget.getFieldIndex(), true, true), MoveEndPhase, ); @@ -124,7 +128,7 @@ export function ForceSwitch(Base: TBase) { const reservePartyMembers = globalScene.getBackupPartyMemberIndices(true); const switchInIndex = reservePartyMembers[switchOutTarget.randSeedInt(reservePartyMembers.length)]; - globalScene.appendToPhase( + globalScene.prependToPhase( new SwitchSummonPhase(this.switchType, switchOutTarget.getFieldIndex(), switchInIndex, false, true), MoveEndPhase, ); @@ -144,7 +148,7 @@ export function ForceSwitch(Base: TBase) { this.switchType === SwitchType.FORCE_SWITCH ? reservePartyIndices[switchOutTarget.randSeedInt(reservePartyIndices.length)] : (globalScene.currentBattle.trainer.getNextSummonIndex(switchOutTarget.trainerSlot) ?? 0); - globalScene.appendToPhase( + globalScene.prependToPhase( new SwitchSummonPhase(this.switchType, switchOutTarget.getFieldIndex(), summonIndex, false, false), MoveEndPhase, ); diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 5b10cd00d04..89a92e36a3f 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -1336,7 +1336,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @see {@linkcode SubstituteTag} * @see {@linkcode getFieldPositionOffset} */ - getSubstituteOffset(): [number, number] { + getSubstituteOffset(): [x: number, y: number] { return this.isPlayer() ? [-30, 10] : [30, -10]; } diff --git a/src/phases/faint-phase.ts b/src/phases/faint-phase.ts index 04ba6fefacf..1aa24d59fa0 100644 --- a/src/phases/faint-phase.ts +++ b/src/phases/faint-phase.ts @@ -61,7 +61,8 @@ export class FaintPhase extends PokemonPhase { faintPokemon.getTag(BattlerTagType.GRUDGE)?.lapse(faintPokemon, BattlerTagLapseType.CUSTOM, this.source); } - // Check for reviver seed + faintPokemon.resetSummonData(); + if (!this.preventInstantRevive) { const instantReviveModifier = globalScene.applyModifier( PokemonInstantReviveModifier, @@ -70,7 +71,6 @@ export class FaintPhase extends PokemonPhase { ) as PokemonInstantReviveModifier; if (instantReviveModifier) { - faintPokemon.resetSummonData(); faintPokemon.loseHeldItem(instantReviveModifier); globalScene.updateModifiers(this.player); return this.end(); @@ -179,11 +179,11 @@ export class FaintPhase extends PokemonPhase { } else { globalScene.unshiftPhase(new VictoryPhase(this.battlerIndex)); if ([BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(globalScene.currentBattle.battleType)) { - const reservePartyIndices = globalScene.getBackupPartyMemberIndices( - false, - (pokemon as EnemyPokemon).trainerSlot, - ); - if (reservePartyIndices.length) { + const hasReservePartyMember = !!globalScene + .getEnemyParty() + .filter(p => p.isActive() && !p.isOnField() && p.trainerSlot === (pokemon as EnemyPokemon).trainerSlot) + .length; + if (hasReservePartyMember) { globalScene.pushPhase(new SwitchSummonPhase(SwitchType.SWITCH, this.fieldIndex, -1, false, false)); } } @@ -217,7 +217,6 @@ export class FaintPhase extends PokemonPhase { globalScene.addFaintedEnemyScore(pokemon as EnemyPokemon); globalScene.currentBattle.addPostBattleLoot(pokemon as EnemyPokemon); } - // TODO: Do we need to leave the field here & now as opposed to during `switchSummonPhase`? pokemon.leaveField(); this.end(); }, diff --git a/src/phases/summon-phase.ts b/src/phases/summon-phase.ts index b6d8b8a5051..fef9b356348 100644 --- a/src/phases/summon-phase.ts +++ b/src/phases/summon-phase.ts @@ -274,6 +274,8 @@ export class SummonPhase extends PartyMemberPokemonPhase { globalScene.unshiftPhase(new ShinySparklePhase(pokemon.getBattlerIndex())); } + 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 e918f0b129b..6a21e341eda 100644 --- a/src/phases/switch-summon-phase.ts +++ b/src/phases/switch-summon-phase.ts @@ -165,8 +165,13 @@ export class SwitchSummonPhase extends SummonPhase { party[this.slotIndex] = this.lastPokemon; party[this.fieldIndex] = switchedInPokemon; const showTextAndSummon = () => { - // TODO: Should this remove the info container? - this.lastPokemon.leaveField(![SwitchType.BATON_PASS, SwitchType.SHED_TAIL].includes(this.switchType), false); + // We don't reset temp effects here as we need to transfer them to tne new pokemon + // TODO: When should this remove the info container? + // Force switch moves did it prior + this.lastPokemon.leaveField( + ![SwitchType.BATON_PASS, SwitchType.SHED_TAIL].includes(this.switchType), + this.doReturn, + ); globalScene.ui.showText( this.player ? i18next.t("battle:playerGo", { @@ -184,13 +189,11 @@ export class SwitchSummonPhase extends SummonPhase { * If this switch is passing a Substitute, make the switched Pokemon matches the returned Pokemon's state as it left. * Otherwise, clear any persisting tags on the returned Pokemon. */ - if (this.switchType === SwitchType.BATON_PASS || this.switchType === SwitchType.SHED_TAIL) { - const substitute = this.lastPokemon.getTag(SubstituteTag); - if (substitute) { - switchedInPokemon.x += this.lastPokemon.getSubstituteOffset()[0]; - switchedInPokemon.y += this.lastPokemon.getSubstituteOffset()[1]; - switchedInPokemon.setAlpha(0.5); - } + const substitute = this.lastPokemon.getTag(SubstituteTag); + if ((this.switchType === SwitchType.BATON_PASS || this.switchType === SwitchType.SHED_TAIL) && substitute) { + switchedInPokemon.x += this.lastPokemon.getSubstituteOffset()[0]; + switchedInPokemon.y += this.lastPokemon.getSubstituteOffset()[1]; + switchedInPokemon.setAlpha(0.5); } this.summon(); }; @@ -209,35 +212,35 @@ export class SwitchSummonPhase extends SummonPhase { onEnd(): void { super.onEnd(); - const pokemon = this.getPokemon(); + const activePokemon = this.getPokemon(); // If not switching at start of battle, reset turn counts and temp data on the newly sent in Pokemon // Needed as we increment turn counters in `TurnEndPhase`. if (this.switchType !== SwitchType.INITIAL_SWITCH) { // No need to reset turn/summon data for initial switch // (since both get initialized to an empty object on object creation) - this.lastPokemon.resetTurnData(); - this.lastPokemon.resetSummonData(); - pokemon.tempSummonData.turnCount--; - pokemon.tempSummonData.waveTurnCount--; - pokemon.turnData.switchedInThisTurn = true; + activePokemon.resetTurnData(); + activePokemon.resetSummonData(); + activePokemon.tempSummonData.turnCount--; + activePokemon.tempSummonData.waveTurnCount--; + activePokemon.turnData.switchedInThisTurn = true; } // Baton Pass over any eligible effects or substitutes before resetting the last pokemon's temporary data. if (this.switchType === SwitchType.BATON_PASS) { - pokemon.transferSummon(this.lastPokemon); + activePokemon.transferSummon(this.lastPokemon); this.lastPokemon.resetTurnData(); this.lastPokemon.resetSummonData(); } else if (this.switchType === SwitchType.SHED_TAIL) { const subTag = this.lastPokemon.getTag(SubstituteTag); if (subTag) { - pokemon.summonData.tags.push(subTag); + activePokemon.summonData.tags.push(subTag); } this.lastPokemon.resetTurnData(); this.lastPokemon.resetSummonData(); } - globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true); + globalScene.triggerPokemonFormChange(activePokemon, SpeciesFormChangeActiveTrigger, true); // Reverts to weather-based forms when weather suppressors (Cloud Nine/Air Lock) are switched out globalScene.arena.triggerWeatherBasedFormChanges(); } diff --git a/test/abilities/mold_breaker.test.ts b/test/abilities/mold_breaker.test.ts index ba33909364f..e9653f89970 100644 --- a/test/abilities/mold_breaker.test.ts +++ b/test/abilities/mold_breaker.test.ts @@ -1,6 +1,8 @@ import { BattlerIndex } from "#app/battle"; +import { ArenaTagSide } from "#app/data/arena-tag"; import { globalScene } from "#app/global-scene"; import { Abilities } from "#enums/abilities"; +import { ArenaTagType } from "#enums/arena-tag-type"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import GameManager from "#test/testUtils/gameManager"; @@ -24,29 +26,60 @@ describe("Abilities - Mold Breaker", () => { beforeEach(() => { game = new GameManager(phaserGame); game.override - .moveset([Moves.SPLASH]) + .moveset([Moves.ERUPTION, Moves.EARTHQUAKE, Moves.DRAGON_TAIL]) .ability(Abilities.MOLD_BREAKER) .battleStyle("single") .disableCrits() .enemySpecies(Species.MAGIKARP) - .enemyAbility(Abilities.BALL_FETCH) + .enemyPassiveAbility(Abilities.NO_GUARD) .enemyMoveset(Moves.SPLASH); }); - it("should turn off the ignore abilities arena variable after the user's move", async () => { - game.override - .enemyMoveset(Moves.SPLASH) - .ability(Abilities.MOLD_BREAKER) - .moveset([Moves.ERUPTION]) - .startingLevel(100) - .enemyLevel(2); + it("should ignore ignorable abilities during the move's execution", async () => { + game.override.startingLevel(100).enemyLevel(2).enemyAbility(Abilities.STURDY); await game.classicMode.startBattle([Species.MAGIKARP]); - const enemy = game.scene.getEnemyPokemon()!; - expect(enemy.isFainted()).toBe(false); + game.move.select(Moves.ERUPTION); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(game.scene.getEnemyPokemon()?.isFainted()).toBe(true); + }); + + it("should turn off ignore abilities arena variable after the user's move concludes", async () => { + game.override.startingLevel(100).enemyLevel(2); + await game.classicMode.startBattle([Species.MAGIKARP]); + + expect(globalScene.arena.ignoreAbilities).toBe(false); game.move.select(Moves.SPLASH); await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); - await game.phaseInterceptor.to("MoveEndPhase", true); + + await game.phaseInterceptor.to("MoveEffectPhase"); + expect(globalScene.arena.ignoreAbilities).toBe(true); + + await game.phaseInterceptor.to("MoveEndPhase"); expect(globalScene.arena.ignoreAbilities).toBe(false); }); + + it("should keep Levitate opponents grounded when using force switch moves", async () => { + game.override.enemyAbility(Abilities.LEVITATE).enemySpecies(Species.WEEZING).startingWave(8); // first rival battle; guaranteed 2 mon party + + // Setup toxic spikes and stealth rock + game.scene.arena.addTag(ArenaTagType.TOXIC_SPIKES, -1, Moves.TOXIC_SPIKES, 1, ArenaTagSide.ENEMY); + game.scene.arena.addTag(ArenaTagType.SPIKES, -1, Moves.CEASELESS_EDGE, 1, ArenaTagSide.ENEMY); + await game.classicMode.startBattle([Species.MAGIKARP]); + + const [weezing1, weezing2] = game.scene.getEnemyParty(); + // Weezing's levitate prevented removal of Toxic Spikes, ignored Spikes damage + expect(game.scene.arena.getTagOnSide(ArenaTagType.TOXIC_SPIKES, ArenaTagSide.ENEMY)).toBeDefined(); + expect(weezing1.getHpRatio()).toBe(1); + + game.move.select(Moves.DRAGON_TAIL); + await game.phaseInterceptor.to("TurnEndPhase"); + + // Levitate was ignored during the switch, causing Toxic Spikes to be removed and Spikes to deal damage + expect(weezing1.isOnField()).toBe(false); + expect(weezing2.isOnField()).toBe(true); + expect(weezing2.getHpRatio()).toBeCloseTo(0.75); + expect(game.scene.arena.getTagOnSide(ArenaTagType.TOXIC_SPIKES, ArenaTagSide.ENEMY)).toBeUndefined(); + }); }); diff --git a/test/abilities/wimp_out.test.ts b/test/abilities/wimp_out.test.ts index c886cf8de8b..d1773f3e84e 100644 --- a/test/abilities/wimp_out.test.ts +++ b/test/abilities/wimp_out.test.ts @@ -349,7 +349,7 @@ describe("Abilities - Wimp Out", () => { confirmNoSwitch(); // Turn 2: get back enough HP that substitute doesn't put us under - wimpod.hp = wimpod.getMaxHp() * 0.8; + wimpod.hp = wimpod.getMaxHp() * 0.78; game.move.select(Moves.SUBSTITUTE); game.doSelectPartyPokemon(1); @@ -373,7 +373,7 @@ describe("Abilities - Wimp Out", () => { it("should disregard Shell Bell recovery while still activating it before switching", async () => { game.override .moveset(Moves.DOUBLE_EDGE) - .enemyMoveset([Moves.SPLASH]) + .enemyMoveset(Moves.SPLASH) .startingHeldItems([{ name: "SHELL_BELL", count: 4 }]); // heals 50% of damage dealt, more than recoil takes away await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); @@ -382,10 +382,13 @@ describe("Abilities - Wimp Out", () => { game.move.select(Moves.DOUBLE_EDGE); game.doSelectPartyPokemon(1); - await game.phaseInterceptor.to("TurnEndPhase"); + await game.phaseInterceptor.to("MoveEffectPhase"); - // Wimp out activated before shell bell healing + // Wimp out check activated from recoil before shell bell procced, but did not deny the pokemon its recovery + expect(wimpod.turnData.damageTaken).toBeGreaterThan(0); expect(wimpod.getHpRatio()).toBeGreaterThan(0.5); + + await game.phaseInterceptor.to("TurnEndPhase"); confirmSwitch(); expect(game.phaseInterceptor.log).toContain("PokemonHealPhase"); }); diff --git a/test/items/reviver_seed.test.ts b/test/items/reviver_seed.test.ts index 707b9df278a..c109794d3d2 100644 --- a/test/items/reviver_seed.test.ts +++ b/test/items/reviver_seed.test.ts @@ -106,10 +106,9 @@ describe("Items - Reviver Seed", () => { // Self-damage tests it.each([ - { moveType: "Relative Recoil", move: Moves.DOUBLE_EDGE }, - { moveType: "HP% Recoil", move: Moves.CHLOROBLAST }, + { moveType: "Recoil", move: Moves.DOUBLE_EDGE }, { moveType: "Self-KO", move: Moves.EXPLOSION }, - { moveType: "Ghost-type Curse", move: Moves.CURSE }, + { moveType: "Self-Deduction", move: Moves.CURSE }, { moveType: "Liquid Ooze", move: Moves.GIGA_DRAIN }, ])("should not activate the holder's reviver seed from $moveType", async ({ move }) => { game.override diff --git a/test/moves/u_turn.test.ts b/test/moves/u_turn.test.ts index 2c04c3aa405..4ceb6865be0 100644 --- a/test/moves/u_turn.test.ts +++ b/test/moves/u_turn.test.ts @@ -32,34 +32,25 @@ describe("Moves - U-turn", () => { .disableCrits(); }); - it("should switch the user out upon use", async () => { - await game.classicMode.startBattle([Species.RAICHU, Species.SHUCKLE]); - const [raichu, shuckle] = game.scene.getPlayerParty(); - expect(raichu).toBeDefined(); - expect(shuckle).toBeDefined(); - - expect(game.scene.getPlayerPokemon()!).toBe(raichu); - game.move.select(Moves.U_TURN); - game.doSelectPartyPokemon(1); - await game.phaseInterceptor.to("TurnEndPhase"); - - expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase"); - expect(game.scene.getPlayerPokemon()!).toBe(shuckle); - }); - - it("triggers regenerator passive once upon switch", async () => { + it("triggers regenerator a single time when a regenerator user switches out with u-turn", async () => { + // arrange + const playerHp = 1; game.override.ability(Abilities.REGENERATOR); await game.classicMode.startBattle([Species.RAICHU, Species.SHUCKLE]); - game.scene.getPlayerPokemon()!.hp = 1; + game.scene.getPlayerPokemon()!.hp = playerHp; + // act game.move.select(Moves.U_TURN); game.doSelectPartyPokemon(1); await game.phaseInterceptor.to("TurnEndPhase"); - expect(game.scene.getPlayerParty()[1].hp).toBeGreaterThan(1); + // assert + expect(game.scene.getPlayerParty()[1].hp).toEqual( + Math.floor(game.scene.getPlayerParty()[1].getMaxHp() * 0.33 + playerHp), + ); expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase"); expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.SHUCKLE); - }); + }, 20000); it("triggers rough skin on the u-turn user before a new pokemon is switched in", async () => { // arrange diff --git a/test/phases/switch-phases.test.ts b/test/phases/switch-phases.test.ts deleted file mode 100644 index 5019856daba..00000000000 --- a/test/phases/switch-phases.test.ts +++ /dev/null @@ -1,240 +0,0 @@ -import { PokemonSummonData, PokemonTurnData } from "#app/field/pokemon"; -import { Abilities } from "#enums/abilities"; -import { BattleType } from "#enums/battle-type"; -import { BattlerTagType } from "#enums/battler-tag-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("Manual Switching -", () => { - 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) - .moveset(Moves.SPLASH) - .enemyMoveset(Moves.SPLASH) - .battleType(BattleType.TRAINER) - .enemyAbility(Abilities.BALL_FETCH); - }); - - describe("Player", () => { - 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.doSwitchPokemon(1); - await game.phaseInterceptor.to("TurnEndPhase"); - - 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.doSwitchPokemon(1); - await game.phaseInterceptor.to("TurnEndPhase"); - - expect(piloSummonSpy).toHaveBeenCalledTimes(1); - expect(piloTurnSpy).toHaveBeenCalledTimes(1); - expect(mamoSummonSpy).toHaveBeenCalledTimes(1); - expect(mamoTurnSpy).toHaveBeenCalledTimes(2); // once from switching, once at turn start - }); - - 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.doSwitchPokemon(1); - await game.phaseInterceptor.to("TurnEndPhase"); - - expect(piloWaveSpy).toHaveBeenCalledTimes(0); - expect(piloBattleWaveSpy).toHaveBeenCalledTimes(0); - expect(mamoWaveSpy).toHaveBeenCalledTimes(0); - expect(mamoBattleWaveSpy).toHaveBeenCalledTimes(0); - }); - }); - - describe("Enemy", () => { - 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(Moves.SPLASH); - game.forceEnemyToSwitch(); - await game.phaseInterceptor.to("TurnEndPhase"); - - 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(Moves.SPLASH); - game.forceEnemyToSwitch(); - await game.phaseInterceptor.to("TurnEndPhase"); - - expect(enemy1SummonSpy).toHaveBeenCalledTimes(1); - expect(enemy1TurnSpy).toHaveBeenCalledTimes(1); - expect(enemy2SummonSpy).toHaveBeenCalledTimes(1); - expect(enemy2TurnSpy).toHaveBeenCalledTimes(2); // once from switching, once at turn start - }); - - 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(Moves.SPLASH); - game.forceEnemyToSwitch(); - await game.phaseInterceptor.to("TurnEndPhase"); - - expect(enemy1WaveSpy).toHaveBeenCalledTimes(0); - expect(enemy1BattleWaveSpy).toHaveBeenCalledTimes(0); - expect(enemy2WaveSpy).toHaveBeenCalledTimes(0); - expect(enemy2BattleWaveSpy).toHaveBeenCalledTimes(0); - }); - }); -}); - -describe.each<{ name: string; playerMove?: Moves; playerAbility?: Abilities; enemyMove?: Moves }>([ - { name: "Self Switch Attack Moves", playerMove: Moves.U_TURN }, - { name: "Target Switch Attack Moves", enemyMove: Moves.DRAGON_TAIL }, - { name: "Self Switch Status Moves", playerMove: Moves.TELEPORT }, - { name: "Target Switch Status Moves", enemyMove: Moves.WHIRLWIND }, - { name: "Self Switch Abilities", playerAbility: Abilities.EMERGENCY_EXIT, enemyMove: Moves.BRAVE_BIRD }, - /* { name: "Fainting", playerMove: Moves.EXPLOSION }, */ // TODO: This calls it twice... -])( - "Mid-Battle Switch Outs - $name - ", - ({ playerMove = Moves.SPLASH, playerAbility = Abilities.BALL_FETCH, enemyMove = 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 - .moveset(playerMove) - .ability(playerAbility) - .battleStyle("single") - .disableCrits() - .enemyLevel(100) - .battleType(BattleType.TRAINER) - .passiveAbility(Abilities.STURDY) - .enemySpecies(Species.MAGIKARP) - .enemyMoveset(enemyMove) - .enemyAbility(Abilities.BALL_FETCH) - .enemyPassiveAbility(Abilities.NO_GUARD); - }); - - 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(playerMove); - game.doSelectPartyPokemon(1); - await game.phaseInterceptor.to("TurnEndPhase"); - - 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(); - piloswine.addTag(BattlerTagType.AQUA_RING, 999); // give piloswine a tag to ensure we know if summonData got reset - 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(playerMove); - game.doSelectPartyPokemon(1); - await game.phaseInterceptor.to("TurnEndPhase"); - - expect(piloSummonSpy).toHaveBeenCalledTimes(1); - expect(piloTurnSpy).toHaveBeenCalledTimes(1); - expect(mamoSummonSpy).toHaveBeenCalledTimes(1); - expect(mamoTurnSpy).toHaveBeenCalledTimes(1); - expect(piloswine.summonData).toEqual(new PokemonSummonData()); - expect(piloswine.turnData).toEqual(new PokemonTurnData()); - }); - - 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(playerMove); - game.doSelectPartyPokemon(1); - await game.phaseInterceptor.to("TurnEndPhase"); - - expect(piloWaveSpy).toHaveBeenCalledTimes(0); - expect(piloBattleWaveSpy).toHaveBeenCalledTimes(0); - expect(mamoWaveSpy).toHaveBeenCalledTimes(0); - expect(mamoBattleWaveSpy).toHaveBeenCalledTimes(0); - }); - }, -);