Revert leaveField tests for other PR

This commit is contained in:
Bertie690 2025-05-18 10:59:01 -04:00
parent a42b7ed9f7
commit ebe4878a2b
11 changed files with 104 additions and 310 deletions

View File

@ -110,10 +110,14 @@ export function ForceSwitch<TBase extends SubMoveOrAbAttr>(Base: TBase) {
this.tryFleeWildPokemon(switchOutTarget); 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 { private trySwitchPlayerPokemon(switchOutTarget: PlayerPokemon): void {
// If not forced to switch, add a SwitchPhase to allow picking the next switched in Pokemon. // If not forced to switch, add a SwitchPhase to allow picking the next switched in Pokemon.
if (this.switchType !== SwitchType.FORCE_SWITCH) { if (this.switchType !== SwitchType.FORCE_SWITCH) {
globalScene.appendToPhase( globalScene.prependToPhase(
new SwitchPhase(this.switchType, switchOutTarget.getFieldIndex(), true, true), new SwitchPhase(this.switchType, switchOutTarget.getFieldIndex(), true, true),
MoveEndPhase, MoveEndPhase,
); );
@ -124,7 +128,7 @@ export function ForceSwitch<TBase extends SubMoveOrAbAttr>(Base: TBase) {
const reservePartyMembers = globalScene.getBackupPartyMemberIndices(true); const reservePartyMembers = globalScene.getBackupPartyMemberIndices(true);
const switchInIndex = reservePartyMembers[switchOutTarget.randSeedInt(reservePartyMembers.length)]; const switchInIndex = reservePartyMembers[switchOutTarget.randSeedInt(reservePartyMembers.length)];
globalScene.appendToPhase( globalScene.prependToPhase(
new SwitchSummonPhase(this.switchType, switchOutTarget.getFieldIndex(), switchInIndex, false, true), new SwitchSummonPhase(this.switchType, switchOutTarget.getFieldIndex(), switchInIndex, false, true),
MoveEndPhase, MoveEndPhase,
); );
@ -144,7 +148,7 @@ export function ForceSwitch<TBase extends SubMoveOrAbAttr>(Base: TBase) {
this.switchType === SwitchType.FORCE_SWITCH this.switchType === SwitchType.FORCE_SWITCH
? reservePartyIndices[switchOutTarget.randSeedInt(reservePartyIndices.length)] ? reservePartyIndices[switchOutTarget.randSeedInt(reservePartyIndices.length)]
: (globalScene.currentBattle.trainer.getNextSummonIndex(switchOutTarget.trainerSlot) ?? 0); : (globalScene.currentBattle.trainer.getNextSummonIndex(switchOutTarget.trainerSlot) ?? 0);
globalScene.appendToPhase( globalScene.prependToPhase(
new SwitchSummonPhase(this.switchType, switchOutTarget.getFieldIndex(), summonIndex, false, false), new SwitchSummonPhase(this.switchType, switchOutTarget.getFieldIndex(), summonIndex, false, false),
MoveEndPhase, MoveEndPhase,
); );

View File

@ -1336,7 +1336,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* @see {@linkcode SubstituteTag} * @see {@linkcode SubstituteTag}
* @see {@linkcode getFieldPositionOffset} * @see {@linkcode getFieldPositionOffset}
*/ */
getSubstituteOffset(): [number, number] { getSubstituteOffset(): [x: number, y: number] {
return this.isPlayer() ? [-30, 10] : [30, -10]; return this.isPlayer() ? [-30, 10] : [30, -10];
} }

View File

@ -61,7 +61,8 @@ export class FaintPhase extends PokemonPhase {
faintPokemon.getTag(BattlerTagType.GRUDGE)?.lapse(faintPokemon, BattlerTagLapseType.CUSTOM, this.source); faintPokemon.getTag(BattlerTagType.GRUDGE)?.lapse(faintPokemon, BattlerTagLapseType.CUSTOM, this.source);
} }
// Check for reviver seed faintPokemon.resetSummonData();
if (!this.preventInstantRevive) { if (!this.preventInstantRevive) {
const instantReviveModifier = globalScene.applyModifier( const instantReviveModifier = globalScene.applyModifier(
PokemonInstantReviveModifier, PokemonInstantReviveModifier,
@ -70,7 +71,6 @@ export class FaintPhase extends PokemonPhase {
) as PokemonInstantReviveModifier; ) as PokemonInstantReviveModifier;
if (instantReviveModifier) { if (instantReviveModifier) {
faintPokemon.resetSummonData();
faintPokemon.loseHeldItem(instantReviveModifier); faintPokemon.loseHeldItem(instantReviveModifier);
globalScene.updateModifiers(this.player); globalScene.updateModifiers(this.player);
return this.end(); return this.end();
@ -179,11 +179,11 @@ export class FaintPhase extends PokemonPhase {
} else { } else {
globalScene.unshiftPhase(new VictoryPhase(this.battlerIndex)); globalScene.unshiftPhase(new VictoryPhase(this.battlerIndex));
if ([BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(globalScene.currentBattle.battleType)) { if ([BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(globalScene.currentBattle.battleType)) {
const reservePartyIndices = globalScene.getBackupPartyMemberIndices( const hasReservePartyMember = !!globalScene
false, .getEnemyParty()
(pokemon as EnemyPokemon).trainerSlot, .filter(p => p.isActive() && !p.isOnField() && p.trainerSlot === (pokemon as EnemyPokemon).trainerSlot)
); .length;
if (reservePartyIndices.length) { if (hasReservePartyMember) {
globalScene.pushPhase(new SwitchSummonPhase(SwitchType.SWITCH, this.fieldIndex, -1, false, false)); 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.addFaintedEnemyScore(pokemon as EnemyPokemon);
globalScene.currentBattle.addPostBattleLoot(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(); pokemon.leaveField();
this.end(); this.end();
}, },

View File

@ -274,6 +274,8 @@ export class SummonPhase extends PartyMemberPokemonPhase {
globalScene.unshiftPhase(new ShinySparklePhase(pokemon.getBattlerIndex())); globalScene.unshiftPhase(new ShinySparklePhase(pokemon.getBattlerIndex()));
} }
pokemon.resetTurnData();
if ( if (
!this.loaded || !this.loaded ||
[BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(globalScene.currentBattle.battleType) || [BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(globalScene.currentBattle.battleType) ||

View File

@ -165,8 +165,13 @@ export class SwitchSummonPhase extends SummonPhase {
party[this.slotIndex] = this.lastPokemon; party[this.slotIndex] = this.lastPokemon;
party[this.fieldIndex] = switchedInPokemon; party[this.fieldIndex] = switchedInPokemon;
const showTextAndSummon = () => { const showTextAndSummon = () => {
// TODO: Should this remove the info container? // We don't reset temp effects here as we need to transfer them to tne new pokemon
this.lastPokemon.leaveField(![SwitchType.BATON_PASS, SwitchType.SHED_TAIL].includes(this.switchType), false); // 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( globalScene.ui.showText(
this.player this.player
? i18next.t("battle:playerGo", { ? i18next.t("battle:playerGo", {
@ -184,14 +189,12 @@ 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. * 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. * 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); const substitute = this.lastPokemon.getTag(SubstituteTag);
if (substitute) { if ((this.switchType === SwitchType.BATON_PASS || this.switchType === SwitchType.SHED_TAIL) && substitute) {
switchedInPokemon.x += this.lastPokemon.getSubstituteOffset()[0]; switchedInPokemon.x += this.lastPokemon.getSubstituteOffset()[0];
switchedInPokemon.y += this.lastPokemon.getSubstituteOffset()[1]; switchedInPokemon.y += this.lastPokemon.getSubstituteOffset()[1];
switchedInPokemon.setAlpha(0.5); switchedInPokemon.setAlpha(0.5);
} }
}
this.summon(); this.summon();
}; };
@ -209,35 +212,35 @@ export class SwitchSummonPhase extends SummonPhase {
onEnd(): void { onEnd(): void {
super.onEnd(); 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 // 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`. // Needed as we increment turn counters in `TurnEndPhase`.
if (this.switchType !== SwitchType.INITIAL_SWITCH) { if (this.switchType !== SwitchType.INITIAL_SWITCH) {
// No need to reset turn/summon data for initial switch // No need to reset turn/summon data for initial switch
// (since both get initialized to an empty object on object creation) // (since both get initialized to an empty object on object creation)
this.lastPokemon.resetTurnData(); activePokemon.resetTurnData();
this.lastPokemon.resetSummonData(); activePokemon.resetSummonData();
pokemon.tempSummonData.turnCount--; activePokemon.tempSummonData.turnCount--;
pokemon.tempSummonData.waveTurnCount--; activePokemon.tempSummonData.waveTurnCount--;
pokemon.turnData.switchedInThisTurn = true; activePokemon.turnData.switchedInThisTurn = true;
} }
// Baton Pass over any eligible effects or substitutes before resetting the last pokemon's temporary data. // Baton Pass over any eligible effects or substitutes before resetting the last pokemon's temporary data.
if (this.switchType === SwitchType.BATON_PASS) { if (this.switchType === SwitchType.BATON_PASS) {
pokemon.transferSummon(this.lastPokemon); activePokemon.transferSummon(this.lastPokemon);
this.lastPokemon.resetTurnData(); this.lastPokemon.resetTurnData();
this.lastPokemon.resetSummonData(); this.lastPokemon.resetSummonData();
} else if (this.switchType === SwitchType.SHED_TAIL) { } else if (this.switchType === SwitchType.SHED_TAIL) {
const subTag = this.lastPokemon.getTag(SubstituteTag); const subTag = this.lastPokemon.getTag(SubstituteTag);
if (subTag) { if (subTag) {
pokemon.summonData.tags.push(subTag); activePokemon.summonData.tags.push(subTag);
} }
this.lastPokemon.resetTurnData(); this.lastPokemon.resetTurnData();
this.lastPokemon.resetSummonData(); 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 // Reverts to weather-based forms when weather suppressors (Cloud Nine/Air Lock) are switched out
globalScene.arena.triggerWeatherBasedFormChanges(); globalScene.arena.triggerWeatherBasedFormChanges();
} }

View File

@ -1,6 +1,8 @@
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { ArenaTagSide } from "#app/data/arena-tag";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { ArenaTagType } from "#enums/arena-tag-type";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import GameManager from "#test/testUtils/gameManager"; import GameManager from "#test/testUtils/gameManager";
@ -24,29 +26,60 @@ describe("Abilities - Mold Breaker", () => {
beforeEach(() => { beforeEach(() => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
game.override game.override
.moveset([Moves.SPLASH]) .moveset([Moves.ERUPTION, Moves.EARTHQUAKE, Moves.DRAGON_TAIL])
.ability(Abilities.MOLD_BREAKER) .ability(Abilities.MOLD_BREAKER)
.battleStyle("single") .battleStyle("single")
.disableCrits() .disableCrits()
.enemySpecies(Species.MAGIKARP) .enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH) .enemyPassiveAbility(Abilities.NO_GUARD)
.enemyMoveset(Moves.SPLASH); .enemyMoveset(Moves.SPLASH);
}); });
it("should turn off the ignore abilities arena variable after the user's move", async () => { it("should ignore ignorable abilities during the move's execution", async () => {
game.override game.override.startingLevel(100).enemyLevel(2).enemyAbility(Abilities.STURDY);
.enemyMoveset(Moves.SPLASH)
.ability(Abilities.MOLD_BREAKER)
.moveset([Moves.ERUPTION])
.startingLevel(100)
.enemyLevel(2);
await game.classicMode.startBattle([Species.MAGIKARP]); 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); game.move.select(Moves.SPLASH);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); 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); 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();
});
}); });

View File

@ -349,7 +349,7 @@ describe("Abilities - Wimp Out", () => {
confirmNoSwitch(); confirmNoSwitch();
// Turn 2: get back enough HP that substitute doesn't put us under // 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.move.select(Moves.SUBSTITUTE);
game.doSelectPartyPokemon(1); game.doSelectPartyPokemon(1);
@ -373,7 +373,7 @@ describe("Abilities - Wimp Out", () => {
it("should disregard Shell Bell recovery while still activating it before switching", async () => { it("should disregard Shell Bell recovery while still activating it before switching", async () => {
game.override game.override
.moveset(Moves.DOUBLE_EDGE) .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 .startingHeldItems([{ name: "SHELL_BELL", count: 4 }]); // heals 50% of damage dealt, more than recoil takes away
await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]);
@ -382,10 +382,13 @@ describe("Abilities - Wimp Out", () => {
game.move.select(Moves.DOUBLE_EDGE); game.move.select(Moves.DOUBLE_EDGE);
game.doSelectPartyPokemon(1); 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); expect(wimpod.getHpRatio()).toBeGreaterThan(0.5);
await game.phaseInterceptor.to("TurnEndPhase");
confirmSwitch(); confirmSwitch();
expect(game.phaseInterceptor.log).toContain("PokemonHealPhase"); expect(game.phaseInterceptor.log).toContain("PokemonHealPhase");
}); });

View File

@ -106,10 +106,9 @@ describe("Items - Reviver Seed", () => {
// Self-damage tests // Self-damage tests
it.each([ it.each([
{ moveType: "Relative Recoil", move: Moves.DOUBLE_EDGE }, { moveType: "Recoil", move: Moves.DOUBLE_EDGE },
{ moveType: "HP% Recoil", move: Moves.CHLOROBLAST },
{ moveType: "Self-KO", move: Moves.EXPLOSION }, { 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 }, { moveType: "Liquid Ooze", move: Moves.GIGA_DRAIN },
])("should not activate the holder's reviver seed from $moveType", async ({ move }) => { ])("should not activate the holder's reviver seed from $moveType", async ({ move }) => {
game.override game.override

View File

@ -32,34 +32,25 @@ describe("Moves - U-turn", () => {
.disableCrits(); .disableCrits();
}); });
it("should switch the user out upon use", async () => { it("triggers regenerator a single time when a regenerator user switches out with u-turn", async () => {
await game.classicMode.startBattle([Species.RAICHU, Species.SHUCKLE]); // arrange
const [raichu, shuckle] = game.scene.getPlayerParty(); const playerHp = 1;
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 () => {
game.override.ability(Abilities.REGENERATOR); game.override.ability(Abilities.REGENERATOR);
await game.classicMode.startBattle([Species.RAICHU, Species.SHUCKLE]); 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.move.select(Moves.U_TURN);
game.doSelectPartyPokemon(1); game.doSelectPartyPokemon(1);
await game.phaseInterceptor.to("TurnEndPhase"); 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.phaseInterceptor.log).toContain("SwitchSummonPhase");
expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.SHUCKLE); 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 () => { it("triggers rough skin on the u-turn user before a new pokemon is switched in", async () => {
// arrange // arrange

View File

@ -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);
});
},
);