Fixed Arena Trap tests and refactored SwitchSummonPhase to be slightly less janky

This commit is contained in:
Bertie690 2025-05-19 13:12:39 -04:00
parent 2b0484a7cb
commit 5f1c98cac6
8 changed files with 157 additions and 88 deletions

View File

@ -129,6 +129,10 @@ export function ForceSwitch<TBase extends SubMoveOrAbAttr>(Base: TBase) {
This ensures ability ignore effects will persist for the duration of the switch (for hazards, etc). This ensures ability ignore effects will persist for the duration of the switch (for hazards, etc).
*/ */
/**
* Method to handle switching out a player Pokemon.
* @param switchOutTarget - The {@linkcode PlayerPokemon} to be switched out.
*/
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) {
@ -149,6 +153,10 @@ export function ForceSwitch<TBase extends SubMoveOrAbAttr>(Base: TBase) {
); );
} }
/**
* Method to handle switching out an opposing trainer's Pokemon.
* @param switchOutTarget - The {@linkcode EnemyPokemon} to be switched out.
*/
private trySwitchTrainerPokemon(switchOutTarget: EnemyPokemon): void { private trySwitchTrainerPokemon(switchOutTarget: EnemyPokemon): void {
// fallback for no trainer // fallback for no trainer
if (!globalScene.currentBattle.trainer) { if (!globalScene.currentBattle.trainer) {
@ -169,8 +177,12 @@ export function ForceSwitch<TBase extends SubMoveOrAbAttr>(Base: TBase) {
); );
} }
/**
* Method to handle fleeing a wild enemy Pokemon, redirecting incoming moves to its ally as applicable.
* @param switchOutTarget - The {@linkcode EnemyPokemon} fleeing the battle.
*/
private tryFleeWildPokemon(switchOutTarget: EnemyPokemon): void { private tryFleeWildPokemon(switchOutTarget: EnemyPokemon): void {
// flee wild pokemon, redirecting moves to an ally in doubles as applicable. switchOutTarget.leaveField(true);
globalScene.queueMessage( globalScene.queueMessage(
i18next.t("moveTriggers:fled", { pokemonName: getPokemonNameWithAffix(switchOutTarget) }), i18next.t("moveTriggers:fled", { pokemonName: getPokemonNameWithAffix(switchOutTarget) }),
null, null,

View File

@ -48,6 +48,7 @@ import {
ConfusionOnStatusEffectAbAttr, ConfusionOnStatusEffectAbAttr,
FieldMoveTypePowerBoostAbAttr, FieldMoveTypePowerBoostAbAttr,
FieldPreventExplosiveMovesAbAttr, FieldPreventExplosiveMovesAbAttr,
ForceSwitchOutImmunityAbAttr,
HealFromBerryUseAbAttr, HealFromBerryUseAbAttr,
IgnoreContactAbAttr, IgnoreContactAbAttr,
IgnoreMoveEffectsAbAttr, IgnoreMoveEffectsAbAttr,
@ -6222,6 +6223,7 @@ export class RevivalBlessingAttr extends MoveEffectAttr {
/** /**
* Attribute to forcibly switch out the user or target of a Move. * Attribute to forcibly switch out the user or target of a Move.
*/ */
// TODO: Add custom failure text & locales
export class ForceSwitchOutAttr extends ForceSwitch(MoveEffectAttr) { export class ForceSwitchOutAttr extends ForceSwitch(MoveEffectAttr) {
constructor( constructor(
selfSwitch: boolean = false, selfSwitch: boolean = false,
@ -6279,6 +6281,14 @@ export class ForceSwitchOutAttr extends ForceSwitch(MoveEffectAttr) {
}; };
} }
getFailedText(_user: Pokemon, target: Pokemon, _move: Move): string | undefined {
const blockedByAbility = new BooleanHolder(false);
applyAbAttrs(ForceSwitchOutImmunityAbAttr, target, blockedByAbility);
if (blockedByAbility.value) {
return i18next.t("moveTriggers:cannotBeSwitchedOut", { pokemonName: getPokemonNameWithAffix(target) });
}
}
getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
const reservePartyMembers = globalScene.getBackupPartyMemberIndices(user.isPlayer() === this.selfSwitch, !user.isPlayer() ? (user as EnemyPokemon).trainerSlot : undefined) const reservePartyMembers = globalScene.getBackupPartyMemberIndices(user.isPlayer() === this.selfSwitch, !user.isPlayer() ? (user as EnemyPokemon).trainerSlot : undefined)
if (reservePartyMembers.length === 0) { if (reservePartyMembers.length === 0) {

View File

@ -342,7 +342,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
public luck: number; public luck: number;
public pauseEvolutions: boolean; public pauseEvolutions: boolean;
public pokerus: boolean; public pokerus: boolean;
// TODO: Document these /** Whether this Pokemon is currently attempting to switch in. */
public switchOutStatus = false; public switchOutStatus = false;
public evoCounter: number; public evoCounter: number;
public teraType: PokemonType; public teraType: PokemonType;

View File

@ -39,16 +39,12 @@ export class CheckSwitchPhase extends BattlePhase {
} }
// ...if there are no other allowed Pokemon in the player's party to switch with // ...if there are no other allowed Pokemon in the player's party to switch with
if ( if (globalScene.getBackupPartyMemberIndices(true).length === 0) {
!globalScene
.getPlayerParty()
.slice(1)
.filter(p => p.isActive()).length
) {
return super.end(); return super.end();
} }
// ...or if any player Pokemon has an effect that prevents the checked Pokemon from switching // ...or if any player Pokemon has an effect that prevents the checked Pokemon from switching
// TODO: Ignore trapping check if baton item is held (since those bypass trapping)
if ( if (
pokemon.getTag(BattlerTagType.FRENZY) || pokemon.getTag(BattlerTagType.FRENZY) ||
pokemon.isTrapped() || pokemon.isTrapped() ||

View File

@ -276,7 +276,7 @@ export class SummonPhase extends PartyMemberPokemonPhase {
globalScene.unshiftPhase(new ShinySparklePhase(pokemon.getBattlerIndex())); globalScene.unshiftPhase(new ShinySparklePhase(pokemon.getBattlerIndex()));
} }
pokemon.resetTurnData(); pokemon.resetTurnData(); // TODO: this can probably be removed...???
if ( if (
!this.loaded || !this.loaded ||

View File

@ -19,16 +19,17 @@ import { SwitchType } from "#enums/switch-type";
export class SwitchSummonPhase extends SummonPhase { export class SwitchSummonPhase extends SummonPhase {
private readonly switchType: SwitchType; private readonly switchType: SwitchType;
private readonly slotIndex: number;
private readonly doReturn: boolean; private readonly doReturn: boolean;
private slotIndex: number;
private lastPokemon: Pokemon; private lastPokemon: Pokemon;
/** /**
* Constructor for creating a new {@linkcode SwitchSummonPhase}, the phase where player and enemy Pokemon are switched out. * Constructor for creating a new {@linkcode SwitchSummonPhase}, the phase where player and enemy Pokemon are switched out
* and replaced by another Pokemon from the same party.
* @param switchType - The type of switch behavior * @param switchType - The type of switch behavior
* @param fieldIndex - Position on the battle field * @param fieldIndex - The position on field of the Pokemon being switched out
* @param slotIndex - The index of pokemon (in party of 6) to switch into * @param slotIndex - The 0-indexed party position of the Pokemon switching in, or `-1` to use the default trainer switch logic.
* @param doReturn - Whether to render "comeback" dialogue * @param doReturn - Whether to render "comeback" dialogue
* @param player - Whether the switch came from the player or enemy; default `true` * @param player - Whether the switch came from the player or enemy; default `true`
*/ */
@ -40,6 +41,9 @@ export class SwitchSummonPhase extends SummonPhase {
this.doReturn = doReturn; this.doReturn = doReturn;
} }
// TODO: This is calling `applyPreSummonAbAttrs` both far too early and on the wrong pokemon;
// `super.start` calls applyPreSummonAbAttrs(PreSummonAbAttr, this.getPokemon()),
// and `this.getPokemon` is the pokemon SWITCHING OUT, NOT IN
start(): void { start(): void {
super.start(); super.start();
} }
@ -47,36 +51,35 @@ export class SwitchSummonPhase extends SummonPhase {
preSummon(): void { preSummon(): void {
const switchOutPokemon = this.getPokemon(); const switchOutPokemon = this.getPokemon();
// if the target is still on-field, remove it and/or hide its info container. // For enemy trainers, pick a pokemon to switch to and/or display the opposing pokeball tray
// Effects are kept to be transferred to the new Pokemon if applicable if (!this.player && globalScene.currentBattle.trainer) {
// TODO: Make moves that switch out pokemon defer to this phase
if (switchOutPokemon.isOnField()) {
switchOutPokemon.leaveField(false, switchOutPokemon.getBattleInfo()?.visible);
}
if (!this.player) {
if (this.slotIndex === -1) { if (this.slotIndex === -1) {
//@ts-ignore this.slotIndex = globalScene.currentBattle.trainer.getNextSummonIndex(this.getTrainerSlotFromFieldIndex());
this.slotIndex = globalScene.currentBattle.trainer?.getNextSummonIndex(
!this.fieldIndex ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER,
); // TODO: what would be the default trainer-slot fallback?
} }
// TODO: Remove this check since `getNextSummonIndex` _should_ always return a number between 0 and party length inclusive
if (this.slotIndex > -1) { if (this.slotIndex > -1) {
this.showEnemyTrainer(!(this.fieldIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER); this.showEnemyTrainer(this.getTrainerSlotFromFieldIndex());
globalScene.pbTrayEnemy.showPbTray(globalScene.getEnemyParty()); globalScene.pbTrayEnemy.showPbTray(globalScene.getEnemyParty());
} }
} }
if ( if (
!this.doReturn || !this.doReturn ||
// TODO: this part of the check need not exist `- `switchAndSummon` returns near immediately if we have no pokemon to switch into
(this.slotIndex !== -1 && (this.slotIndex !== -1 &&
!(this.player ? globalScene.getPlayerParty() : globalScene.getEnemyParty())[this.slotIndex]) !(this.player ? globalScene.getPlayerParty() : globalScene.getEnemyParty())[this.slotIndex])
) { ) {
// If the target is still on-field, remove it and/or hide its info container.
// Effects are kept to be transferred to the new Pokemon later on.
if (switchOutPokemon.isOnField()) {
switchOutPokemon.leaveField(false, switchOutPokemon.getBattleInfo()?.visible);
}
if (this.player) { if (this.player) {
this.switchAndSummon(); this.switchAndSummon();
return; } else {
globalScene.time.delayedCall(750, () => this.switchAndSummon());
} }
globalScene.time.delayedCall(750, () => this.switchAndSummon());
return; return;
} }
@ -84,7 +87,8 @@ export class SwitchSummonPhase extends SummonPhase {
enemyPokemon.removeTagsBySourceId(switchOutPokemon.id), enemyPokemon.removeTagsBySourceId(switchOutPokemon.id),
); );
if (this.switchType === SwitchType.SWITCH || this.switchType === SwitchType.INITIAL_SWITCH) { // If not transferring a substitute, play animation to remove it from the field
if (!this.shouldKeepEffects()) {
const substitute = switchOutPokemon.getTag(SubstituteTag); const substitute = switchOutPokemon.getTag(SubstituteTag);
if (substitute) { if (substitute) {
globalScene.tweens.add({ globalScene.tweens.add({
@ -103,9 +107,7 @@ export class SwitchSummonPhase extends SummonPhase {
pokemonName: getPokemonNameWithAffix(switchOutPokemon), pokemonName: getPokemonNameWithAffix(switchOutPokemon),
}) })
: i18next.t("battle:trainerComeBack", { : i18next.t("battle:trainerComeBack", {
trainerName: globalScene.currentBattle.trainer?.getName( trainerName: globalScene.currentBattle.trainer?.getName(this.getTrainerSlotFromFieldIndex()),
!(this.fieldIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER,
),
pokemonName: switchOutPokemon.getNameToRender(), pokemonName: switchOutPokemon.getNameToRender(),
}), }),
); );
@ -119,19 +121,21 @@ export class SwitchSummonPhase extends SummonPhase {
scale: 0.5, scale: 0.5,
onComplete: () => { onComplete: () => {
globalScene.time.delayedCall(750, () => this.switchAndSummon()); globalScene.time.delayedCall(750, () => this.switchAndSummon());
switchOutPokemon.leaveField(this.switchType === SwitchType.SWITCH, false); switchOutPokemon.leaveField(this.switchType === SwitchType.SWITCH, false); // TODO: do we have to do this right here right now
}, },
}); });
} }
switchAndSummon() { switchAndSummon() {
const party = this.player ? this.getParty() : globalScene.getEnemyParty(); const party = this.player ? this.getParty() : globalScene.getEnemyParty();
const switchedInPokemon: Pokemon | undefined = party[this.slotIndex]; const switchInPokemon: Pokemon | undefined = party[this.slotIndex];
this.lastPokemon = this.getPokemon(); this.lastPokemon = this.getPokemon();
applyPreSummonAbAttrs(PreSummonAbAttr, switchedInPokemon); applyPreSummonAbAttrs(PreSummonAbAttr, switchInPokemon);
applyPreSwitchOutAbAttrs(PreSwitchOutAbAttr, this.lastPokemon); applyPreSwitchOutAbAttrs(PreSwitchOutAbAttr, this.lastPokemon);
if (!switchedInPokemon) { // TODO: Why do we trigger post switch out attributes even if the switch in target doesn't exist?
// (This should almost certainly go somewhere inside `preSummon`)
if (!switchInPokemon) {
this.end(); this.end();
return; return;
} }
@ -139,7 +143,7 @@ export class SwitchSummonPhase extends SummonPhase {
if (this.switchType === SwitchType.BATON_PASS) { if (this.switchType === SwitchType.BATON_PASS) {
// If switching via baton pass, update opposing tags coming from the prior pokemon // If switching via baton pass, update opposing tags coming from the prior pokemon
(this.player ? globalScene.getEnemyField() : globalScene.getPlayerField()).forEach((enemyPokemon: Pokemon) => (this.player ? globalScene.getEnemyField() : globalScene.getPlayerField()).forEach((enemyPokemon: Pokemon) =>
enemyPokemon.transferTagsBySourceId(this.lastPokemon.id, switchedInPokemon.id), enemyPokemon.transferTagsBySourceId(this.lastPokemon.id, switchInPokemon.id),
); );
// If the recipient pokemon lacks a baton, give our baton to it during the swap // If the recipient pokemon lacks a baton, give our baton to it during the swap
@ -147,7 +151,7 @@ export class SwitchSummonPhase extends SummonPhase {
!globalScene.findModifier( !globalScene.findModifier(
m => m =>
m instanceof SwitchEffectTransferModifier && m instanceof SwitchEffectTransferModifier &&
(m as SwitchEffectTransferModifier).pokemonId === switchedInPokemon.id, (m as SwitchEffectTransferModifier).pokemonId === switchInPokemon.id,
) )
) { ) {
const batonPassModifier = globalScene.findModifier( const batonPassModifier = globalScene.findModifier(
@ -159,7 +163,7 @@ export class SwitchSummonPhase extends SummonPhase {
if (batonPassModifier) { if (batonPassModifier) {
globalScene.tryTransferHeldItemModifier( globalScene.tryTransferHeldItemModifier(
batonPassModifier, batonPassModifier,
switchedInPokemon, switchInPokemon,
false, false,
undefined, undefined,
undefined, undefined,
@ -171,12 +175,14 @@ export class SwitchSummonPhase extends SummonPhase {
} }
party[this.slotIndex] = this.lastPokemon; party[this.slotIndex] = this.lastPokemon;
party[this.fieldIndex] = switchedInPokemon; party[this.fieldIndex] = switchInPokemon;
// TODO: Make this text configurable for Dragon Tail & co.
// TODO: Make this a method
const showTextAndSummon = () => { const showTextAndSummon = () => {
globalScene.ui.showText( globalScene.ui.showText(
this.player this.player
? i18next.t("battle:playerGo", { ? i18next.t("battle:playerGo", {
pokemonName: getPokemonNameWithAffix(switchedInPokemon), pokemonName: getPokemonNameWithAffix(switchInPokemon),
}) })
: i18next.t("battle:trainerGo", { : i18next.t("battle:trainerGo", {
trainerName: globalScene.currentBattle.trainer?.getName( trainerName: globalScene.currentBattle.trainer?.getName(
@ -190,15 +196,15 @@ 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) { if (this.shouldKeepEffects()) {
const substitute = this.lastPokemon.getTag(SubstituteTag); const substitute = this.lastPokemon.getTag(SubstituteTag);
if (substitute) { if (substitute) {
switchedInPokemon.x += this.lastPokemon.getSubstituteOffset()[0]; switchInPokemon.x += this.lastPokemon.getSubstituteOffset()[0];
switchedInPokemon.y += this.lastPokemon.getSubstituteOffset()[1]; switchInPokemon.y += this.lastPokemon.getSubstituteOffset()[1];
switchedInPokemon.setAlpha(0.5); switchInPokemon.setAlpha(0.5);
} }
} else { } else {
switchedInPokemon.fieldSetup(); switchInPokemon.fieldSetup();
} }
this.summon(); this.summon();
}; };
@ -253,4 +259,16 @@ export class SwitchSummonPhase extends SummonPhase {
queuePostSummon(): void { queuePostSummon(): void {
globalScene.unshiftPhase(new PostSummonPhase(this.getPokemon().getBattlerIndex())); globalScene.unshiftPhase(new PostSummonPhase(this.getPokemon().getBattlerIndex()));
} }
private shouldKeepEffects(): boolean {
return [SwitchType.BATON_PASS, SwitchType.SHED_TAIL].includes(this.switchType);
}
private getTrainerSlotFromFieldIndex(): TrainerSlot {
return this.player || !globalScene.currentBattle.trainer
? TrainerSlot.NONE
: this.fieldIndex % 2 === 0
? TrainerSlot.TRAINER
: TrainerSlot.TRAINER_PARTNER;
}
} }

View File

@ -1,10 +1,15 @@
import { allAbilities } from "#app/data/data-lists"; import { allAbilities } from "#app/data/data-lists";
import { getPokemonNameWithAffix } from "#app/messages";
import type CommandUiHandler from "#app/ui/command-ui-handler";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { Button } from "#enums/buttons";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { UiMode } from "#enums/ui-mode";
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 { afterEach, beforeAll, beforeEach, describe, it, expect, vi } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, it, expect } from "vitest";
describe("Abilities - Arena Trap", () => { describe("Abilities - Arena Trap", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
@ -23,68 +28,94 @@ describe("Abilities - Arena Trap", () => {
beforeEach(() => { beforeEach(() => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
game.override game.override
.moveset(Moves.SPLASH) .moveset([Moves.SPLASH, Moves.TELEPORT])
.ability(Abilities.ARENA_TRAP) .ability(Abilities.ARENA_TRAP)
.enemySpecies(Species.RALTS) .enemySpecies(Species.RALTS)
.enemyAbility(Abilities.BALL_FETCH) .enemyAbility(Abilities.ARENA_TRAP)
.enemyMoveset(Moves.TELEPORT); .enemyMoveset(Moves.SPLASH);
}); });
// TODO: Enable test when Issue #935 is addressed // NB: Since switching moves bypass trapping, the only way fleeing can occur is from the player
it.todo("should not allow grounded Pokémon to flee", async () => { // TODO: Implement once forced flee helper exists
it.todo("should interrupt player flee attempt and display message, unless user has Run Away", async () => {
game.override.battleStyle("single"); game.override.battleStyle("single");
await game.classicMode.startBattle([Species.DUGTRIO, Species.GOTHITELLE]);
await game.classicMode.startBattle(); const enemy = game.scene.getEnemyPokemon()!;
const enemy = game.scene.getEnemyPokemon(); game.onNextPrompt("CommandPhase", UiMode.COMMAND, () => {
// no switch out command should be queued due to arena trap
expect(game.scene.currentBattle.turnCommands[0]).toBeNull();
game.move.select(Moves.SPLASH); // back out and cancel the flee to avoid timeout
(game.scene.ui.getHandler() as CommandUiHandler).processInput(Button.CANCEL);
game.move.select(Moves.SPLASH);
});
await game.toNextTurn();
expect(game.textInterceptor.logs).toContain(
i18next.t("abilityTriggers:arenaTrap", {
pokemonNameWithAffix: getPokemonNameWithAffix(enemy),
abilityName: allAbilities[Abilities.ARENA_TRAP].name,
}),
);
game.override.ability(Abilities.RUN_AWAY);
// do switch stuff
await game.toNextTurn(); await game.toNextTurn();
expect(enemy).toBe(game.scene.getEnemyPokemon()); expect(game.scene.currentBattle.waveIndex).toBe(2);
});
it("should interrupt player switch attempt and display message", async () => {
game.override.battleStyle("single").enemyAbility(Abilities.ARENA_TRAP);
await game.classicMode.startBattle([Species.DUGTRIO, Species.GOTHITELLE]);
const enemy = game.scene.getEnemyPokemon()!;
game.doSwitchPokemon(1);
game.onNextPrompt("CommandPhase", UiMode.PARTY, () => {
// no switch out command should be queued due to arena trap
expect(game.scene.currentBattle.turnCommands[0]).toBeNull();
// back out and cancel the switch to avoid timeout
(game.scene.ui.getHandler() as CommandUiHandler).processInput(Button.CANCEL);
game.move.select(Moves.SPLASH);
});
await game.toNextTurn();
expect(game.textInterceptor.logs).toContain(
i18next.t("abilityTriggers:arenaTrap", {
pokemonNameWithAffix: getPokemonNameWithAffix(enemy),
abilityName: allAbilities[Abilities.ARENA_TRAP].name,
}),
);
}); });
it("should guarantee double battle with any one LURE", async () => { it("should guarantee double battle with any one LURE", async () => {
game.override.startingModifier([{ name: "LURE" }]).startingWave(2); game.override.startingModifier([{ name: "LURE" }]).startingWave(2);
await game.classicMode.startBattle([Species.DUGTRIO]);
await game.classicMode.startBattle(); expect(game.scene.getEnemyField()).toHaveLength(2);
expect(game.scene.getEnemyField().length).toBe(2);
}); });
/**
* This checks if the Player Pokemon is able to switch out/run away after the Enemy Pokemon with {@linkcode Abilities.ARENA_TRAP}
* is forcefully moved out of the field from moves such as Roar {@linkcode Moves.ROAR}
*
* Note: It should be able to switch out/run away
*/
it("should lift if pokemon with this ability leaves the field", async () => { it("should lift if pokemon with this ability leaves the field", async () => {
game.override game.override.battleStyle("single").enemyMoveset(Moves.SPLASH).moveset(Moves.ROAR);
.battleStyle("double") await game.classicMode.startBattle([Species.MAGIKARP]);
.enemyMoveset(Moves.SPLASH)
.moveset([Moves.ROAR, Moves.SPLASH])
.ability(Abilities.BALL_FETCH);
await game.classicMode.startBattle([Species.MAGIKARP, Species.SUDOWOODO, Species.LUNATONE]);
const [enemy1, enemy2] = game.scene.getEnemyField(); const player = game.scene.getPlayerPokemon()!;
const [player1, player2] = game.scene.getPlayerField(); const enemy = game.scene.getEnemyPokemon()!;
vi.spyOn(enemy1, "getAbility").mockReturnValue(allAbilities[Abilities.ARENA_TRAP]); expect(player.isTrapped()).toBe(true);
expect(enemy.isOnField()).toBe(true);
game.move.select(Moves.ROAR); game.move.select(Moves.ROAR);
game.move.select(Moves.SPLASH, 1); await game.phaseInterceptor.to("TurnEndPhase");
// This runs the fist command phase where the moves are selected expect(player.isTrapped()).toBe(false);
await game.toNextTurn(); expect(enemy.isOnField()).toBe(false);
// During the next command phase the player pokemons should not be trapped anymore
game.move.select(Moves.SPLASH);
game.move.select(Moves.SPLASH, 1);
await game.toNextTurn();
expect(player1.isTrapped()).toBe(false);
expect(player2.isTrapped()).toBe(false);
expect(enemy1.isOnField()).toBe(false);
expect(enemy2.isOnField()).toBe(true);
}); });
}); });

View File

@ -165,6 +165,8 @@ export default class GameManager {
* @param mode - The mode to wait for. * @param mode - The mode to wait for.
* @param callback - The callback function to execute on next prompt. * @param callback - The callback function to execute on next prompt.
* @param expireFn - Optional function to determine if the prompt has expired. * @param expireFn - Optional function to determine if the prompt has expired.
* @remarks
* If multiple callbacks are queued for the same phase, they will be executed in the order they were added.
*/ */
onNextPrompt( onNextPrompt(
phaseTarget: string, phaseTarget: string,