pokerogue/test/mystery-encounter/encounter-test-utils.ts
Dean 87e6095a00
[Misc/Feature] Add dynamic turn order (#6036)
* Add new priority queues

* Add dynamic queue manager

* Add timing modifier and fix post speed ordering

* Make `phaseQueue` private

* Fix `gameManager.setTurnOrder`

* Update `findPhase` to also check dynamic queues

* Modify existing phase manager methods to check dynamic queues

* Fix move order persisting through tests

* Fix magic coat/bounce

* Use append for magic coat/bounce

* Remove `getSpeedOrder` from `TurnStartPhase`, fix references to `getCommandOrder` in tests

* Fix round queuing last instead of next

* Add quick draw application

* Add quick claw activation

* Fix turn order tracking

* Add move header queue to fix ordering

* Fix abilities activating immediately on summon

* Fix `postsummonphases` being shuffled (need to handle speed ties differently here)

* Update speed order function

* Add `StaticSwitchSummonPhase`

* Fix magic coat/bounce error from conflict resolution

* Remove conditional queue

* Fix dancer and baton pass tests

* Automatically queue consecutive Pokémon phases as dynamic

* Move turn end phases queuing back to `TurnStartPhase`

* Fix `LearnMovePhase`

* Remove `PrependSplice`

* Move DQM to phase manager

* Fix various phases being pushed instead of unshifted

* Remove `StaticSwitchSummonPhase`

* Ensure the top queue is always at length - 1

* Fix encounter `PostSummonPhase`s and Revival Blessing

* Fix move headers

* Remove implicit ordering from DQM

* Fix `PostSummonPhase`s in encounters running too early

* Fix `tryRemovePhase` usages

* Add `MovePhase` after `MoveEndPhase` automatically

* Implement an `inSpeedOrder` function

* Merge fixes

* Fix encounter rewards

* Defer `FaintPhase`s where splice was used previously

* Separate speed order utils to avoid circular imports

* Temporarily disable lunar dance test

* Simplify deferral

* Remove move priority modifier

* Fix TS errors in code files

* Fix ts errors in tests

* Fix more test files

* Fix postsummon + checkswitch ability activations

* Fix `removeAll`

* Reposition `positionalTagPhase`

* Re-add `startCurrentPhase`

* Avoid overwriting `currentPhase` after `turnStart`

* Delete `switchSummonPhasePriorityQueue`

* Update `phase-manager.ts`

* Remove uses of `isNullOrUndefined`

* Rename deferral methods

* Update docs and use `getPlayerField(true)` in turn start phase

* Use `.getEnemyField(true)`

* Update docs for post summon phase priority queue (psppq)

* Update speed order utils

* Remove null from `nextPhase`

* Update move phase timing modifier docs

* Remove mention of phases from base priority queue class

* Remove and replace `applyInSpeedOrder`

* Don't sort weather effect phases

* Order priority queues before removing

- Add some `readonly` and `public` modifiers

- Remove unused `queuedPhases` field from `MoveEffectPhase`

* Fix linting in `phase-manager.ts`

* Remove unnecessary turn order modification in Rage Fist test

---------

Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com>
Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com>
Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
2025-09-20 17:49:40 -05:00

208 lines
7.0 KiB
TypeScript

import { Status } from "#data/status-effect";
import { Button } from "#enums/buttons";
import { StatusEffect } from "#enums/status-effect";
import { UiMode } from "#enums/ui-mode";
// biome-ignore lint/performance/noNamespaceImport: Necessary for mocks
import * as EncounterPhaseUtils from "#mystery-encounters/encounter-phase-utils";
import { CommandPhase } from "#phases/command-phase";
import { MessagePhase } from "#phases/message-phase";
import {
MysteryEncounterBattlePhase,
MysteryEncounterOptionSelectedPhase,
MysteryEncounterRewardsPhase,
} from "#phases/mystery-encounter-phases";
import { VictoryPhase } from "#phases/victory-phase";
import type { GameManager } from "#test/test-utils/game-manager";
import type { MessageUiHandler } from "#ui/message-ui-handler";
import type { MysteryEncounterUiHandler } from "#ui/mystery-encounter-ui-handler";
import type { OptionSelectUiHandler } from "#ui/option-select-ui-handler";
import type { PartyUiHandler } from "#ui/party-ui-handler";
import { expect, vi } from "vitest";
/**
* Runs a {@linkcode MysteryEncounter} to either the start of a battle, or to the {@linkcode MysteryEncounterRewardsPhase}, depending on the option selected
* @param game
* @param optionNo Human number, not index
* @param secondaryOptionSelect
* @param isBattle If selecting option should lead to battle, set to `true`
*/
export async function runMysteryEncounterToEnd(
game: GameManager,
optionNo: number,
secondaryOptionSelect?: { pokemonNo: number; optionNo?: number },
isBattle = false,
) {
vi.spyOn(EncounterPhaseUtils, "selectPokemonForOption");
await runSelectMysteryEncounterOption(game, optionNo, secondaryOptionSelect);
// run the selected options phase
game.onNextPrompt(
"MysteryEncounterOptionSelectedPhase",
UiMode.MESSAGE,
() => {
const uiHandler = game.scene.ui.getHandler<MysteryEncounterUiHandler>();
uiHandler.processInput(Button.ACTION);
},
() => game.isCurrentPhase(MysteryEncounterBattlePhase) || game.isCurrentPhase(MysteryEncounterRewardsPhase),
);
if (isBattle) {
game.onNextPrompt(
"CheckSwitchPhase",
UiMode.CONFIRM,
() => {
game.setMode(UiMode.MESSAGE);
game.endPhase();
},
() => game.isCurrentPhase(CommandPhase),
);
game.onNextPrompt(
"CheckSwitchPhase",
UiMode.MESSAGE,
() => {
game.setMode(UiMode.MESSAGE);
game.endPhase();
},
() => game.isCurrentPhase(CommandPhase),
);
// If a battle is started, fast forward to end of the battle
game.onNextPrompt("CommandPhase", UiMode.COMMAND, () => {
game.scene.phaseManager.clearPhaseQueue();
game.scene.phaseManager.unshiftPhase(new VictoryPhase(0));
game.endPhase();
});
// Handle end of battle trainer messages
game.onNextPrompt("TrainerVictoryPhase", UiMode.MESSAGE, () => {
const uiHandler = game.scene.ui.getHandler<MessageUiHandler>();
uiHandler.processInput(Button.ACTION);
});
// Handle egg hatch dialogue
game.onNextPrompt("EggLapsePhase", UiMode.MESSAGE, () => {
const uiHandler = game.scene.ui.getHandler<MessageUiHandler>();
uiHandler.processInput(Button.ACTION);
});
await game.toNextTurn();
} else {
await game.phaseInterceptor.to("MysteryEncounterRewardsPhase");
}
}
export async function runSelectMysteryEncounterOption(
game: GameManager,
optionNo: number,
secondaryOptionSelect?: { pokemonNo: number; optionNo?: number },
) {
// Handle any eventual queued messages (e.g. weather phase, etc.)
game.onNextPrompt(
"MessagePhase",
UiMode.MESSAGE,
() => {
const uiHandler = game.scene.ui.getHandler<MessageUiHandler>();
uiHandler.processInput(Button.ACTION);
},
() => game.isCurrentPhase(MysteryEncounterOptionSelectedPhase),
);
if (game.isCurrentPhase(MessagePhase)) {
await game.phaseInterceptor.to("MessagePhase");
}
// dispose of intro messages
game.onNextPrompt(
"MysteryEncounterPhase",
UiMode.MESSAGE,
() => {
const uiHandler = game.scene.ui.getHandler<MysteryEncounterUiHandler>();
uiHandler.processInput(Button.ACTION);
},
() => game.isCurrentPhase(MysteryEncounterOptionSelectedPhase),
);
await game.phaseInterceptor.to("MysteryEncounterPhase", true);
// select the desired option
const uiHandler = game.scene.ui.getHandler<MysteryEncounterUiHandler>();
uiHandler.unblockInput(); // input are blocked by 1s to prevent accidental input. Tests need to handle that
switch (optionNo) {
case 2:
uiHandler.processInput(Button.RIGHT);
break;
case 3:
uiHandler.processInput(Button.DOWN);
break;
case 4:
uiHandler.processInput(Button.RIGHT);
uiHandler.processInput(Button.DOWN);
break;
default:
// no movement needed. Default cursor position
break;
}
if (secondaryOptionSelect?.pokemonNo != null) {
await handleSecondaryOptionSelect(game, secondaryOptionSelect.pokemonNo, secondaryOptionSelect.optionNo);
} else {
uiHandler.processInput(Button.ACTION);
}
}
async function handleSecondaryOptionSelect(game: GameManager, pokemonNo: number, optionNo?: number) {
// Handle secondary option selections
const partyUiHandler = game.scene.ui.handlers[UiMode.PARTY] as PartyUiHandler;
vi.spyOn(partyUiHandler, "show");
const encounterUiHandler = game.scene.ui.getHandler<MysteryEncounterUiHandler>();
encounterUiHandler.processInput(Button.ACTION);
await vi.waitFor(() => expect(partyUiHandler.show).toHaveBeenCalled());
for (let i = 1; i < pokemonNo; i++) {
partyUiHandler.processInput(Button.DOWN);
}
// Open options on Pokemon
partyUiHandler.processInput(Button.ACTION);
// Click "Select" on Pokemon options
partyUiHandler.processInput(Button.ACTION);
// If there is a second choice to make after selecting a Pokemon
if (optionNo != null) {
// Wait for Summary menu to close and second options to spawn
const secondOptionUiHandler = game.scene.ui.handlers[UiMode.OPTION_SELECT] as OptionSelectUiHandler;
vi.spyOn(secondOptionUiHandler, "show");
await vi.waitFor(() => expect(secondOptionUiHandler.show).toHaveBeenCalled());
// Navigate down to the correct option
for (let i = 1; i < optionNo!; i++) {
secondOptionUiHandler.processInput(Button.DOWN);
}
// Select the option
secondOptionUiHandler.processInput(Button.ACTION);
}
}
/**
* For any {@linkcode MysteryEncounter} that has a battle, can call this to skip battle and proceed to {@linkcode MysteryEncounterRewardsPhase}
* @param game
* @param runRewardsPhase
*/
export async function skipBattleRunMysteryEncounterRewardsPhase(game: GameManager, runRewardsPhase = true) {
game.scene.phaseManager.clearPhaseQueue();
game.scene.getEnemyParty().forEach(p => {
p.hp = 0;
p.status = new Status(StatusEffect.FAINT);
game.scene.field.remove(p);
});
game.scene.phaseManager.pushPhase(new VictoryPhase(0));
game.endPhase();
game.setMode(UiMode.MESSAGE);
await game.phaseInterceptor.to("MysteryEncounterRewardsPhase", runRewardsPhase);
}