This commit is contained in:
Dean 2025-07-03 02:02:41 +00:00 committed by GitHub
commit 5d80d74fc1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
55 changed files with 846 additions and 793 deletions

View File

@ -0,0 +1,3 @@
import type { Phase } from "#app/phase";
export type PhaseConditionFunc = (phase: Phase) => boolean;

View File

@ -1,3 +1,5 @@
import type Pokemon from "#app/field/pokemon";
import type { Phase } from "#app/phase";
import type { PhaseConstructorMap } from "#app/phase-manager";
// Intentionally export the types of everything in phase-manager, as this file is meant to be
@ -23,3 +25,11 @@ export type PhaseClass = PhaseConstructorMap[keyof PhaseConstructorMap];
* Union type of all phase names as strings.
*/
export type PhaseString = keyof PhaseMap;
export type DynamicPhaseString = "PostSummonPhase" | "SwitchSummonPhase" | "MovePhase" | "MoveHeaderPhase";
export type StaticPhaseString = Exclude<PhaseString, DynamicPhaseString>;
export interface DynamicPhase extends Phase {
getPokemon(): Pokemon;
}

View File

@ -861,8 +861,8 @@ export default class BattleScene extends SceneBase {
let targetingMovePhase: MovePhase;
do {
targetingMovePhase = this.phaseManager.findPhase(
"MovePhase",
mp =>
mp.is("MovePhase") &&
mp.targets.length === 1 &&
mp.targets[0] === removedPokemon.getBattlerIndex() &&
mp.pokemon.isPlayer() !== allyPokemon.isPlayer(),

View File

@ -82,6 +82,9 @@ import type { Constructor } from "#app/utils/common";
import type { Localizable } from "#app/@types/locales";
import { applyAbAttrs } from "./apply-ab-attrs";
import type { Closed, Exact } from "#app/@types/type-helpers";
import { MovePriorityModifier } from "#enums/move-priority-modifier";
import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
import type { MovePhase } from "#app/phases/move-phase";
// biome-ignore-start lint/correctness/noUnusedImports: Used in TSDoc
import type BattleScene from "#app/battle-scene";
@ -3181,6 +3184,7 @@ export class CommanderAbAttr extends AbAttr {
return (
globalScene.currentBattle?.double &&
!isNullOrUndefined(ally) &&
ally.isActive(true) &&
ally.species.speciesId === SpeciesId.DONDOZO &&
!(ally.isFainted() || ally.getTag(BattlerTagType.COMMANDED))
);
@ -4014,6 +4018,25 @@ export class ChangeMovePriorityAbAttr extends AbAttr {
}
}
export class ChangeMovePriorityModifierAbAttr extends AbAttr {
private newModifier: MovePriorityModifier;
private moveFunc: (pokemon: Pokemon, move: Move) => boolean;
constructor(moveFunc: (pokemon: Pokemon, move: Move) => boolean, newModifier: MovePriorityModifier) {
super(false);
this.newModifier = newModifier;
this.moveFunc = moveFunc;
}
override canApply({ pokemon, move }: ChangeMovePriorityAbAttrParams): boolean {
return this.moveFunc(pokemon, move);
}
override apply({ priority }: ChangeMovePriorityAbAttrParams): void {
priority.value = this.newModifier;
}
}
export class IgnoreContactAbAttr extends AbAttr {
private declare readonly _: never;
}
@ -4955,15 +4978,23 @@ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr {
// If the move is an AttackMove or a StatusMove the Dancer must replicate the move on the source of the Dance
if (move.getMove().is("AttackMove") || move.getMove().is("StatusMove")) {
const target = this.getTarget(pokemon, source, targets);
globalScene.phaseManager.unshiftNew("MovePhase", pokemon, target, move, MoveUseMode.INDIRECT);
globalScene.phaseManager.pushNew(
"MovePhase",
pokemon,
target,
move,
MoveUseMode.INDIRECT,
MovePhaseTimingModifier.FIRST,
);
} else if (move.getMove().is("SelfStatusMove")) {
// If the move is a SelfStatusMove (ie. Swords Dance) the Dancer should replicate it on itself
globalScene.phaseManager.unshiftNew(
globalScene.phaseManager.pushNew(
"MovePhase",
pokemon,
[pokemon.getBattlerIndex()],
move,
MoveUseMode.INDIRECT,
MovePhaseTimingModifier.FIRST,
);
}
}
@ -5952,11 +5983,6 @@ export class IllusionPostBattleAbAttr extends PostBattleAbAttr {
}
}
export interface BypassSpeedChanceAbAttrParams extends AbAttrBaseParams {
/** Holds whether the speed check is bypassed after ability application */
bypass: BooleanHolder;
}
/**
* If a Pokémon with this Ability selects a damaging move, it has a 30% chance of going first in its priority bracket. If the Ability activates, this is announced at the start of the turn (after move selection).
* @sealed
@ -5972,26 +5998,28 @@ export class BypassSpeedChanceAbAttr extends AbAttr {
this.chance = chance;
}
override canApply({ bypass, simulated, pokemon }: BypassSpeedChanceAbAttrParams): boolean {
override canApply({ simulated, pokemon }: AbAttrBaseParams): boolean {
// TODO: Consider whether we can move the simulated check to the `apply` method
// May be difficult as we likely do not want to modify the randBattleSeed
const turnCommand = globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()];
const isCommandFight = turnCommand?.command === Command.FIGHT;
const move = turnCommand?.move?.move ? allMoves[turnCommand.move.move] : null;
const isDamageMove = move?.category === MoveCategory.PHYSICAL || move?.category === MoveCategory.SPECIAL;
return (
!simulated && !bypass.value && pokemon.randBattleSeedInt(100) < this.chance && isCommandFight && isDamageMove
!simulated &&
pokemon.randBattleSeedInt(100) < this.chance &&
isDamageMove &&
pokemon.canAddTag(BattlerTagType.BYPASS_SPEED)
);
}
/**
* bypass move order in their priority bracket when pokemon choose damaging move
*/
override apply({ bypass }: BypassSpeedChanceAbAttrParams): void {
bypass.value = true;
override apply({ pokemon }: AbAttrBaseParams): void {
pokemon.addTag(BattlerTagType.BYPASS_SPEED);
}
override getTriggerMessage({ pokemon }: BypassSpeedChanceAbAttrParams, _abilityName: string): string {
override getTriggerMessage({ pokemon }: AbAttrBaseParams, _abilityName: string): string {
return i18next.t("abilityTriggers:quickDraw", { pokemonName: getPokemonNameWithAffix(pokemon) });
}
}
@ -6027,9 +6055,8 @@ export class PreventBypassSpeedChanceAbAttr extends AbAttr {
return isCommandFight && this.condition(pokemon, move!);
}
override apply({ bypass, canCheckHeldItems }: PreventBypassSpeedChanceAbAttrParams): void {
override apply({ bypass }: PreventBypassSpeedChanceAbAttrParams): void {
bypass.value = false;
canCheckHeldItems.value = false;
}
}
@ -6150,7 +6177,7 @@ class ForceSwitchOutHelper {
: 0;
globalScene.phaseManager.prependNewToPhase(
"MoveEndPhase",
"SwitchSummonPhase",
"StaticSwitchSummonPhase",
this.switchType,
switchOutTarget.getFieldIndex(),
summonIndex,
@ -6518,6 +6545,7 @@ const AbilityAttrs = Object.freeze({
BlockStatusDamageAbAttr,
BlockOneHitKOAbAttr,
ChangeMovePriorityAbAttr,
ChangeMovePriorityModifierAbAttr,
IgnoreContactAbAttr,
PreWeatherEffectAbAttr,
PreWeatherDamageAbAttr,
@ -6935,7 +6963,7 @@ export function initAbilities() {
.attr(AlwaysHitAbAttr)
.attr(DoubleBattleChanceAbAttr),
new Ability(AbilityId.STALL, 4)
.attr(ChangeMovePriorityAbAttr, (_pokemon, _move: Move) => true, -0.2),
.attr(ChangeMovePriorityModifierAbAttr, (_pokemon, _move: Move) => true, MovePriorityModifier.LAST_IN_BRACKET),
new Ability(AbilityId.TECHNICIAN, 4)
.attr(MovePowerBoostAbAttr, (user, target, move) => {
const power = new NumberHolder(move.power);
@ -7085,7 +7113,7 @@ export function initAbilities() {
new Ability(AbilityId.ANALYTIC, 5)
.attr(MovePowerBoostAbAttr, (user) =>
// Boost power if all other Pokemon have already moved (no other moves are slated to execute)
!globalScene.phaseManager.findPhase((phase) => phase.is("MovePhase") && phase.pokemon.id !== user?.id),
!globalScene.phaseManager.hasPhaseOfType("MovePhase", (phase: MovePhase) => phase.pokemon.id !== user?.id),
1.3),
new Ability(AbilityId.ILLUSION, 5)
// The Pokemon generate an illusion if it's available
@ -7673,7 +7701,7 @@ export function initAbilities() {
.attr(TypeImmunityHealAbAttr, PokemonType.GROUND)
.ignorable(),
new Ability(AbilityId.MYCELIUM_MIGHT, 9)
.attr(ChangeMovePriorityAbAttr, (_pokemon, move) => move.category === MoveCategory.STATUS, -0.2)
.attr(ChangeMovePriorityModifierAbAttr, (_pokemon, move) => move.category === MoveCategory.STATUS, MovePriorityModifier.LAST_IN_BRACKET)
.attr(PreventBypassSpeedChanceAbAttr, (_pokemon, move) => move.category === MoveCategory.STATUS)
.attr(MoveAbilityBypassAbAttr, (_pokemon, move: Move) => move.category === MoveCategory.STATUS),
new Ability(AbilityId.MINDS_EYE, 9)

View File

@ -507,17 +507,7 @@ export class ShellTrapTag extends BattlerTag {
// Trap should only be triggered by opponent's Physical moves
if (phaseData?.move.category === MoveCategory.PHYSICAL && pokemon.isOpponent(phaseData.attacker)) {
const shellTrapPhaseIndex = globalScene.phaseManager.phaseQueue.findIndex(
phase => phase.is("MovePhase") && phase.pokemon === pokemon,
);
const firstMovePhaseIndex = globalScene.phaseManager.phaseQueue.findIndex(phase => phase.is("MovePhase"));
// Only shift MovePhase timing if it's not already next up
if (shellTrapPhaseIndex !== -1 && shellTrapPhaseIndex !== firstMovePhaseIndex) {
const shellTrapMovePhase = globalScene.phaseManager.phaseQueue.splice(shellTrapPhaseIndex, 1)[0];
globalScene.phaseManager.prependToPhase(shellTrapMovePhase, "MovePhase");
}
globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === pokemon);
this.activated = true;
}
@ -1165,22 +1155,9 @@ export class EncoreTag extends MoveRestrictionBattlerTag {
}),
);
const movePhase = globalScene.phaseManager.findPhase(m => m.is("MovePhase") && m.pokemon === pokemon);
if (movePhase) {
const movesetMove = pokemon.getMoveset().find(m => m.moveId === this.moveId);
if (movesetMove) {
const lastMove = pokemon.getLastXMoves(1)[0];
globalScene.phaseManager.tryReplacePhase(
m => m.is("MovePhase") && m.pokemon === pokemon,
globalScene.phaseManager.create(
"MovePhase",
pokemon,
lastMove.targets ?? [],
movesetMove,
MoveUseMode.NORMAL,
),
);
}
const movesetMove = pokemon.getMoveset().find(m => m.moveId === this.moveId);
if (movesetMove) {
globalScene.phaseManager.changePhaseMove((phase: MovePhase) => phase.pokemon === pokemon, movesetMove);
}
}
@ -3481,6 +3458,23 @@ export class GrudgeTag extends BattlerTag {
}
}
/**
* Tag to allow the affected Pokemon's move to go first in its priority bracket.
* Used for {@link https://bulbapedia.bulbagarden.net/wiki/Quick_Draw_(Ability) Quick Draw}
* and {@link https://bulbapedia.bulbagarden.net/wiki/Quick_Claw Quick Claw}.
*/
export class BypassSpeedTag extends BattlerTag {
constructor() {
super(BattlerTagType.BYPASS_SPEED, BattlerTagLapseType.TURN_END, 1);
}
override canAdd(pokemon: Pokemon): boolean {
const cancelled = new BooleanHolder(false);
applyAbAttrs("PreventBypassSpeedChanceAbAttr", pokemon, null, false, cancelled);
return !cancelled.value;
}
}
/**
* Tag used to heal the user of Psycho Shift of its status effect if Psycho Shift succeeds in transferring its status effect to the target Pokemon
*/
@ -3728,6 +3722,8 @@ export function getBattlerTag(
return new PsychoShiftTag();
case BattlerTagType.MAGIC_COAT:
return new MagicCoatTag();
case BattlerTagType.BYPASS_SPEED:
return new BypassSpeedTag();
case BattlerTagType.NONE:
default:
return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId);

View File

@ -90,6 +90,8 @@ import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveKindString, MoveCla
import { applyMoveAttrs } from "./apply-attrs";
import { frenzyMissFunc, getMoveTargets } from "./move-utils";
import { AbAttrBaseParams, AbAttrParamsWithCancel, PreAttackModifyPowerAbAttrParams } from "../abilities/ability";
import { MovePriorityModifier } from "#enums/move-priority-modifier";
import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
/**
* A function used to conditionally determine execution of a given {@linkcode MoveAttr}.
@ -870,6 +872,13 @@ export default abstract class Move implements Localizable {
return priority.value;
}
getPriorityModifier(user: Pokemon, simulated = true): MovePriorityModifier {
const modifierHolder = new NumberHolder(MovePriorityModifier.NORMAL);
applyAbAttrs("ChangeMovePriorityModifierAbAttr", {pokemon: user, simulated: simulated, move: this, priority: modifierHolder});
modifierHolder.value = user.getTag(BattlerTagType.BYPASS_SPEED) ? MovePriorityModifier.FIRST_IN_BRACKET : modifierHolder.value;
return modifierHolder.value;
}
/**
* Calculate the [Expected Power](https://en.wikipedia.org/wiki/Expected_value) per turn
* of this move, taking into account multi hit moves, accuracy, and the number of turns it
@ -3182,7 +3191,7 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr {
const overridden = args[0] as BooleanHolder;
const allyMovePhase = globalScene.phaseManager.findPhase<MovePhase>((phase) => phase.is("MovePhase") && phase.pokemon.isPlayer() === user.isPlayer());
const allyMovePhase = globalScene.phaseManager.findPhase("MovePhase", (phase) => phase.pokemon.isPlayer() === user.isPlayer());
if (allyMovePhase) {
const allyMove = allyMovePhase.move.getMove();
if (allyMove !== move && allyMove.hasAttr("AwaitCombinedPledgeAttr")) {
@ -3195,11 +3204,7 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr {
}));
// Move the ally's MovePhase (if needed) so that the ally moves next
const allyMovePhaseIndex = globalScene.phaseManager.phaseQueue.indexOf(allyMovePhase);
const firstMovePhaseIndex = globalScene.phaseManager.phaseQueue.findIndex((phase) => phase.is("MovePhase"));
if (allyMovePhaseIndex !== firstMovePhaseIndex) {
globalScene.phaseManager.prependToPhase(globalScene.phaseManager.phaseQueue.splice(allyMovePhaseIndex, 1)[0], "MovePhase");
}
globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === user.getAlly());
overridden.value = true;
return true;
@ -4455,28 +4460,7 @@ export class LastMoveDoublePowerAttr extends VariablePowerAttr {
*/
apply(user: Pokemon, _target: Pokemon, _move: Move, args: any[]): boolean {
const power = args[0] as NumberHolder;
const enemy = user.getOpponent(0);
const pokemonActed: Pokemon[] = [];
if (enemy?.turnData.acted) {
pokemonActed.push(enemy);
}
if (globalScene.currentBattle.double) {
const userAlly = user.getAlly();
const enemyAlly = enemy?.getAlly();
if (userAlly?.turnData.acted) {
pokemonActed.push(userAlly);
}
if (enemyAlly?.turnData.acted) {
pokemonActed.push(enemyAlly);
}
}
pokemonActed.sort((a, b) => b.turnData.order - a.turnData.order);
for (const p of pokemonActed) {
for (const p of globalScene.phaseManager.dynamicQueueManager.getLastTurnOrder().slice(0, -1).reverse()) {
const [ lastMove ] = p.getLastXMoves(1);
if (lastMove.result !== MoveResult.FAIL) {
if ((lastMove.result === MoveResult.SUCCESS) && (lastMove.move === this.move)) {
@ -4561,20 +4545,14 @@ export class CueNextRoundAttr extends MoveEffectAttr {
}
override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean {
const nextRoundPhase = globalScene.phaseManager.findPhase<MovePhase>(phase =>
phase.is("MovePhase") && phase.move.moveId === MoveId.ROUND
const nextRoundPhase = globalScene.phaseManager.findPhase("MovePhase", phase => phase.move.moveId === MoveId.ROUND
);
if (!nextRoundPhase) {
return false;
}
// Update the phase queue so that the next Pokemon using Round moves next
const nextRoundIndex = globalScene.phaseManager.phaseQueue.indexOf(nextRoundPhase);
const nextMoveIndex = globalScene.phaseManager.phaseQueue.findIndex(phase => phase.is("MovePhase"));
if (nextRoundIndex !== nextMoveIndex) {
globalScene.phaseManager.prependToPhase(globalScene.phaseManager.phaseQueue.splice(nextRoundIndex, 1)[0], "MovePhase");
}
globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.is("MovePhase") && phase.move.moveId === MoveId.ROUND);
// Mark the corresponding Pokemon as having "joined the Round" (for doubling power later)
nextRoundPhase.pokemon.turnData.joinedRound = true;
@ -6253,15 +6231,15 @@ export class RevivalBlessingAttr extends MoveEffectAttr {
// Handle cases where revived pokemon needs to get switched in on same turn
if (allyPokemon.isFainted() || allyPokemon === pokemon) {
// Enemy switch phase should be removed and replaced with the revived pkmn switching in
globalScene.phaseManager.tryRemovePhase((phase: SwitchSummonPhase) => phase.is("SwitchSummonPhase") && phase.getPokemon() === pokemon);
globalScene.phaseManager.tryRemovePhase((phase: SwitchSummonPhase) => phase.is("StaticSwitchSummonPhase") && phase.getPokemon() === pokemon);
// If the pokemon being revived was alive earlier in the turn, cancel its move
// (revived pokemon can't move in the turn they're brought back)
// TODO: might make sense to move this to `FaintPhase` after checking for Rev Seed (rather than handling it in the move)
globalScene.phaseManager.findPhase((phase: MovePhase) => phase.pokemon === pokemon)?.cancel();
globalScene.phaseManager.findPhase("MovePhase", (phase: MovePhase) => phase.pokemon === pokemon)?.cancel();
if (user.fieldPosition === FieldPosition.CENTER) {
user.setFieldPosition(FieldPosition.LEFT);
}
globalScene.phaseManager.unshiftNew("SwitchSummonPhase", SwitchType.SWITCH, allyPokemon.getFieldIndex(), slotIndex, false, false);
globalScene.phaseManager.unshiftNew("StaticSwitchSummonPhase", SwitchType.SWITCH, allyPokemon.getFieldIndex(), slotIndex, false, false);
}
}
return true;
@ -6341,7 +6319,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
const slotIndex = eligibleNewIndices[user.randBattleSeedInt(eligibleNewIndices.length)];
globalScene.phaseManager.prependNewToPhase(
"MoveEndPhase",
"SwitchSummonPhase",
"StaticSwitchSummonPhase",
this.switchType,
switchOutTarget.getFieldIndex(),
slotIndex,
@ -6380,7 +6358,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
switchOutTarget.leaveField(true);
const slotIndex = eligibleNewIndices[user.randBattleSeedInt(eligibleNewIndices.length)];
globalScene.phaseManager.prependNewToPhase("MoveEndPhase",
"SwitchSummonPhase",
"StaticSwitchSummonPhase",
this.switchType,
switchOutTarget.getFieldIndex(),
slotIndex,
@ -6390,7 +6368,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
} else {
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
globalScene.phaseManager.prependNewToPhase("MoveEndPhase",
"SwitchSummonPhase",
"StaticSwitchSummonPhase",
this.switchType,
switchOutTarget.getFieldIndex(),
(globalScene.currentBattle.trainer ? globalScene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0),
@ -6817,7 +6795,7 @@ class CallMoveAttr extends OverrideMoveEffectAttr {
: moveTargets.targets[user.randBattleSeedInt(moveTargets.targets.length)]];
globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", move.id);
globalScene.phaseManager.unshiftNew("MovePhase", user, targets, new PokemonMove(move.id), MoveUseMode.FOLLOW_UP);
globalScene.phaseManager.pushNew("MovePhase", user, targets, new PokemonMove(move.id), MoveUseMode.FOLLOW_UP, MovePhaseTimingModifier.FIRST);
return true;
}
}
@ -7047,7 +7025,7 @@ export class NaturePowerAttr extends OverrideMoveEffectAttr {
// Load the move's animation if we didn't already and unshift a new usage phase
globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", moveId);
globalScene.phaseManager.unshiftNew("MovePhase", user, [ target.getBattlerIndex() ], new PokemonMove(moveId), MoveUseMode.FOLLOW_UP);
globalScene.phaseManager.pushNew("MovePhase", user, [ target.getBattlerIndex() ], new PokemonMove(moveId), MoveUseMode.FOLLOW_UP, MovePhaseTimingModifier.FIRST);
return true;
}
}
@ -7132,7 +7110,7 @@ export class RepeatMoveAttr extends MoveEffectAttr {
targetPokemonName: getPokemonNameWithAffix(target)
}));
target.turnData.extraTurns++;
globalScene.phaseManager.appendNewToPhase("MoveEndPhase", "MovePhase", target, moveTargets, movesetMove, MoveUseMode.NORMAL);
globalScene.phaseManager.pushNew("MovePhase", target, moveTargets, movesetMove, MoveUseMode.NORMAL, MovePhaseTimingModifier.FIRST);
return true;
}
@ -7920,12 +7898,7 @@ export class AfterYouAttr extends MoveEffectAttr {
*/
override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:afterYou", { targetName: getPokemonNameWithAffix(target) }));
// Will find next acting phase of the targeted pokémon, delete it and queue it right after us.
const targetNextPhase = globalScene.phaseManager.findPhase<MovePhase>(phase => phase.pokemon === target);
if (targetNextPhase && globalScene.phaseManager.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) {
globalScene.phaseManager.prependToPhase(targetNextPhase, "MovePhase");
}
globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === target);
return true;
}
@ -7949,45 +7922,11 @@ export class ForceLastAttr extends MoveEffectAttr {
override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:forceLast", { targetPokemonName: getPokemonNameWithAffix(target) }));
// TODO: Refactor this to be more readable and less janky
const targetMovePhase = globalScene.phaseManager.findPhase<MovePhase>((phase) => phase.pokemon === target);
if (targetMovePhase && !targetMovePhase.isForcedLast() && globalScene.phaseManager.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) {
// Finding the phase to insert the move in front of -
// Either the end of the turn or in front of another, slower move which has also been forced last
const prependPhase = globalScene.phaseManager.findPhase((phase) =>
[ MovePhase, MoveEndPhase ].every(cls => !(phase instanceof cls))
|| (phase.is("MovePhase")) && phaseForcedSlower(phase, target, !!globalScene.arena.getTag(ArenaTagType.TRICK_ROOM))
);
if (prependPhase) {
globalScene.phaseManager.phaseQueue.splice(
globalScene.phaseManager.phaseQueue.indexOf(prependPhase),
0,
globalScene.phaseManager.create("MovePhase", target, [ ...targetMovePhase.targets ], targetMovePhase.move, targetMovePhase.useMode, true)
);
}
}
globalScene.phaseManager.forceMoveLast((phase: MovePhase) => phase.pokemon === target);
return true;
}
}
/**
* Returns whether a {@linkcode MovePhase} has been forced last and the corresponding pokemon is slower than {@linkcode target}.
* TODO:
- Make this a class method
- Make this look at speed order from TurnStartPhase
*/
const phaseForcedSlower = (phase: MovePhase, target: Pokemon, trickRoom: boolean): boolean => {
let slower: boolean;
// quashed pokemon still have speed ties
if (phase.pokemon.getEffectiveStat(Stat.SPD) === target.getEffectiveStat(Stat.SPD)) {
slower = !!target.randBattleSeedInt(2);
} else {
slower = !trickRoom ? phase.pokemon.getEffectiveStat(Stat.SPD) < target.getEffectiveStat(Stat.SPD) : phase.pokemon.getEffectiveStat(Stat.SPD) > target.getEffectiveStat(Stat.SPD);
}
return phase.isForcedLast() && slower;
};
const failOnGravityCondition: MoveConditionFunc = (user, target, move) => !globalScene.arena.getTag(ArenaTagType.GRAVITY);
const failOnBossCondition: MoveConditionFunc = (user, target, move) => !target.isBossImmune();
@ -8008,7 +7947,7 @@ const userSleptOrComatoseCondition: MoveConditionFunc = (user: Pokemon, target:
const targetSleptOrComatoseCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => target.status?.effect === StatusEffect.SLEEP || target.hasAbility(AbilityId.COMATOSE);
const failIfLastCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => globalScene.phaseManager.phaseQueue.find(phase => phase.is("MovePhase")) !== undefined;
const failIfLastCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => globalScene.phaseManager.hasPhaseOfType("MovePhase");
const failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => {
const party: Pokemon[] = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty();

View File

@ -1,97 +0,0 @@
import { globalScene } from "#app/global-scene";
import type { Phase } from "#app/phase";
import { ActivatePriorityQueuePhase } from "#app/phases/activate-priority-queue-phase";
import type { PostSummonPhase } from "#app/phases/post-summon-phase";
import { PostSummonActivateAbilityPhase } from "#app/phases/post-summon-activate-ability-phase";
import { Stat } from "#enums/stat";
import { BooleanHolder } from "#app/utils/common";
import { TrickRoomTag } from "#app/data/arena-tag";
import { DynamicPhaseType } from "#enums/dynamic-phase-type";
/**
* Stores a list of {@linkcode Phase}s
*
* Dynamically updates ordering to always pop the highest "priority", based on implementation of {@linkcode reorder}
*/
export abstract class PhasePriorityQueue {
protected abstract queue: Phase[];
/**
* Sorts the elements in the queue
*/
public abstract reorder(): void;
/**
* Calls {@linkcode reorder} and shifts the queue
* @returns The front element of the queue after sorting
*/
public pop(): Phase | undefined {
this.reorder();
return this.queue.shift();
}
/**
* Adds a phase to the queue
* @param phase The phase to add
*/
public push(phase: Phase): void {
this.queue.push(phase);
}
/**
* Removes all phases from the queue
*/
public clear(): void {
this.queue.splice(0, this.queue.length);
}
}
/**
* Priority Queue for {@linkcode PostSummonPhase} and {@linkcode PostSummonActivateAbilityPhase}
*
* Orders phases first by ability priority, then by the {@linkcode Pokemon}'s effective speed
*/
export class PostSummonPhasePriorityQueue extends PhasePriorityQueue {
protected override queue: PostSummonPhase[] = [];
public override reorder(): void {
this.queue.sort((phaseA: PostSummonPhase, phaseB: PostSummonPhase) => {
if (phaseA.getPriority() === phaseB.getPriority()) {
return (
(phaseB.getPokemon().getEffectiveStat(Stat.SPD) - phaseA.getPokemon().getEffectiveStat(Stat.SPD)) *
(isTrickRoom() ? -1 : 1)
);
}
return phaseB.getPriority() - phaseA.getPriority();
});
}
public override push(phase: PostSummonPhase): void {
super.push(phase);
this.queueAbilityPhase(phase);
}
/**
* Queues all necessary {@linkcode PostSummonActivateAbilityPhase}s for each pushed {@linkcode PostSummonPhase}
* @param phase The {@linkcode PostSummonPhase} that was pushed onto the queue
*/
private queueAbilityPhase(phase: PostSummonPhase): void {
const phasePokemon = phase.getPokemon();
phasePokemon.getAbilityPriorities().forEach((priority, idx) => {
this.queue.push(new PostSummonActivateAbilityPhase(phasePokemon.getBattlerIndex(), priority, !!idx));
globalScene.phaseManager.appendToPhase(
new ActivatePriorityQueuePhase(DynamicPhaseType.POST_SUMMON),
"ActivatePriorityQueuePhase",
(p: ActivatePriorityQueuePhase) => p.getType() === DynamicPhaseType.POST_SUMMON,
);
});
}
}
function isTrickRoom(): boolean {
const speedReversed = new BooleanHolder(false);
globalScene.arena.applyTags(TrickRoomTag, false, speedReversed);
return speedReversed.value;
}

View File

@ -95,4 +95,5 @@ export enum BattlerTagType {
ENDURE_TOKEN = "ENDURE_TOKEN",
POWDER = "POWDER",
MAGIC_COAT = "MAGIC_COAT",
BYPASS_SPEED = "BYPASS_SPEED"
}

View File

@ -1,6 +0,0 @@
/**
* Enum representation of the phase types held by implementations of {@linkcode PhasePriorityQueue}
*/
export enum DynamicPhaseType {
POST_SUMMON
}

View File

@ -0,0 +1,5 @@
export enum MovePhaseTimingModifier {
LAST = 0,
NORMAL,
FIRST
}

View File

@ -0,0 +1,5 @@
export enum MovePriorityModifier {
LAST_IN_BRACKET = 0,
NORMAL,
FIRST_IN_BRACKET,
}

View File

@ -374,9 +374,15 @@ export class Arena {
/**
* Function to trigger all weather based form changes
* @param source - The Pokemon causing the changes by removing itself from the field
*/
triggerWeatherBasedFormChanges(): void {
triggerWeatherBasedFormChanges(source?: Pokemon): void {
globalScene.getField(true).forEach(p => {
// TODO - This is a bandaid. Abilities leaving the field needs a better approach than
// calling this method for every switch out that happens
if (p === source) {
return;
}
const isCastformWithForecast = p.hasAbility(AbilityId.FORECAST) && p.species.speciesId === SpeciesId.CASTFORM;
const isCherrimWithFlowerGift = p.hasAbility(AbilityId.FLOWER_GIFT) && p.species.speciesId === SpeciesId.CHERRIM;

View File

@ -182,6 +182,7 @@ import type { IllusionData } from "#app/@types/illusion-data";
import type { TurnMove } from "#app/@types/turn-move";
import type { DamageCalculationResult, DamageResult } from "#app/@types/damage-result";
import type { AbAttrMap, AbAttrString, TypeMultiplierAbAttrParams } from "#app/@types/ability-types";
import type { TurnCommand } from "#app/battle";
import { getTerrainBlockMessage } from "#app/data/terrain";
import { LearnMoveSituation } from "#enums/learn-move-situation";
@ -5731,7 +5732,7 @@ export class PlayerPokemon extends Pokemon {
if (slotIndex >= globalScene.currentBattle.getBattlerCount() && slotIndex < 6) {
globalScene.phaseManager.prependNewToPhase(
"MoveEndPhase",
"SwitchSummonPhase",
"StaticSwitchSummonPhase",
switchType,
this.getFieldIndex(),
slotIndex,

View File

@ -12,7 +12,6 @@ import { getPokemonNameWithAffix } from "#app/messages";
import Overrides from "#app/overrides";
import { LearnMoveType } from "#enums/learn-move-type";
import type { VoucherType } from "#app/system/voucher";
import { Command } from "#enums/command";
import { addTextObject, TextStyle } from "#app/ui/text";
import { BooleanHolder, hslToHex, isNullOrUndefined, NumberHolder, randSeedFloat, toDmgValue } from "#app/utils/common";
import { BattlerTagType } from "#enums/battler-tag-type";
@ -1574,30 +1573,16 @@ export class BypassSpeedChanceModifier extends PokemonHeldItemModifier {
return new BypassSpeedChanceModifier(this.type, this.pokemonId, this.stackCount);
}
/**
* Checks if {@linkcode BypassSpeedChanceModifier} should be applied
* @param pokemon the {@linkcode Pokemon} that holds the item
* @param doBypassSpeed {@linkcode BooleanHolder} that is `true` if speed should be bypassed
* @returns `true` if {@linkcode BypassSpeedChanceModifier} should be applied
*/
override shouldApply(pokemon?: Pokemon, doBypassSpeed?: BooleanHolder): boolean {
return super.shouldApply(pokemon, doBypassSpeed) && !!doBypassSpeed;
}
/**
* Applies {@linkcode BypassSpeedChanceModifier}
* @param pokemon the {@linkcode Pokemon} that holds the item
* @param doBypassSpeed {@linkcode BooleanHolder} that is `true` if speed should be bypassed
* @returns `true` if {@linkcode BypassSpeedChanceModifier} has been applied
*/
override apply(pokemon: Pokemon, doBypassSpeed: BooleanHolder): boolean {
if (!doBypassSpeed.value && pokemon.randBattleSeedInt(10) < this.getStackCount()) {
doBypassSpeed.value = true;
const isCommandFight =
globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()]?.command === Command.FIGHT;
override apply(pokemon: Pokemon): boolean {
if (pokemon.randBattleSeedInt(10) < this.getStackCount() && pokemon.addTag(BattlerTagType.BYPASS_SPEED)) {
const hasQuickClaw = this.type.is("PokemonHeldItemModifierType") && this.type.id === "QUICK_CLAW";
if (isCommandFight && hasQuickClaw) {
if (hasQuickClaw) {
globalScene.phaseManager.queueMessage(
i18next.t("modifier:bypassSpeedChanceApply", {
pokemonName: getPokemonNameWithAffix(pokemon),

View File

@ -1,8 +1,7 @@
import type { Phase } from "#app/phase";
import type { default as Pokemon } from "#app/field/pokemon";
import type { PhaseMap, PhaseString } from "./@types/phase-types";
import type { PhaseMap, PhaseString, DynamicPhase, StaticPhaseString } from "./@types/phase-types";
import { globalScene } from "#app/global-scene";
import { ActivatePriorityQueuePhase } from "#app/phases/activate-priority-queue-phase";
import { AddEnemyBuffModifierPhase } from "#app/phases/add-enemy-buff-modifier-phase";
import { AttemptCapturePhase } from "#app/phases/attempt-capture-phase";
import { AttemptRunPhase } from "#app/phases/attempt-run-phase";
@ -12,9 +11,7 @@ import { CheckStatusEffectPhase } from "#app/phases/check-status-effect-phase";
import { CheckSwitchPhase } from "#app/phases/check-switch-phase";
import { CommandPhase } from "#app/phases/command-phase";
import { CommonAnimPhase } from "#app/phases/common-anim-phase";
import { coerceArray, type Constructor } from "#app/utils/common";
import { DamageAnimPhase } from "#app/phases/damage-anim-phase";
import type { DynamicPhaseType } from "#enums/dynamic-phase-type";
import { EggHatchPhase } from "#app/phases/egg-hatch-phase";
import { EggLapsePhase } from "#app/phases/egg-lapse-phase";
import { EggSummaryPhase } from "#app/phases/egg-summary-phase";
@ -58,7 +55,6 @@ import { NextEncounterPhase } from "#app/phases/next-encounter-phase";
import { ObtainStatusEffectPhase } from "#app/phases/obtain-status-effect-phase";
import { PartyExpPhase } from "#app/phases/party-exp-phase";
import { PartyHealPhase } from "#app/phases/party-heal-phase";
import { type PhasePriorityQueue, PostSummonPhasePriorityQueue } from "#app/data/phase-priority-queue";
import { PokemonAnimPhase } from "#app/phases/pokemon-anim-phase";
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
import { PokemonTransformPhase } from "#app/phases/pokemon-transform-phase";
@ -99,6 +95,11 @@ import { UnavailablePhase } from "#app/phases/unavailable-phase";
import { UnlockPhase } from "#app/phases/unlock-phase";
import { VictoryPhase } from "#app/phases/victory-phase";
import { WeatherEffectPhase } from "#app/phases/weather-effect-phase";
import { DynamicQueueManager } from "#app/queues/dynamic-queue-manager";
import type { PhaseConditionFunc } from "#app/@types/phase-condition";
import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
import type { PokemonMove } from "#app/data/moves/pokemon-move";
import { StaticSwitchSummonPhase } from "#app/phases/static-switch-summon-phase";
/*
* Manager for phases used by battle scene.
@ -115,7 +116,6 @@ import { WeatherEffectPhase } from "#app/phases/weather-effect-phase";
* This allows for easy creation of new phases without needing to import each phase individually.
*/
const PHASES = Object.freeze({
ActivatePriorityQueuePhase,
AddEnemyBuffModifierPhase,
AttemptCapturePhase,
AttemptRunPhase,
@ -190,6 +190,7 @@ const PHASES = Object.freeze({
ShowAbilityPhase,
ShowPartyExpBarPhase,
ShowTrainerPhase,
StaticSwitchSummonPhase,
StatStageChangePhase,
SummonMissingPhase,
SummonPhase,
@ -213,13 +214,18 @@ const PHASES = Object.freeze({
/** Maps Phase strings to their constructors */
export type PhaseConstructorMap = typeof PHASES;
const turnEndPhases: PhaseString[] = ["WeatherEffectPhase", "BerryPhase", "CheckStatusEffectPhase", "TurnEndPhase"];
const ignorablePhases: PhaseString[] = ["ShowAbilityPhase", "HideAbilityPhase"];
// TODO might be easier to define which phases should be dynamic instead
const nonDynamicPokemonPhases: PhaseString[] = ["SummonPhase", "CommandPhase"];
/**
* PhaseManager is responsible for managing the phases in the battle scene
*/
export class PhaseManager {
/** PhaseQueue: dequeue/remove the first element to get the next phase */
public phaseQueue: Phase[] = [];
public conditionalQueue: Array<[() => boolean, Phase]> = [];
private phaseQueue: Phase[] = [];
/** PhaseQueuePrepend: is a temp storage of what will be added to PhaseQueue */
private phaseQueuePrepend: Phase[] = [];
@ -227,18 +233,12 @@ export class PhaseManager {
private phaseQueuePrependSpliceIndex = -1;
private nextCommandPhaseQueue: Phase[] = [];
/** Storage for {@linkcode PhasePriorityQueue}s which hold phases whose order dynamically changes */
private dynamicPhaseQueues: PhasePriorityQueue[];
/** Parallel array to {@linkcode dynamicPhaseQueues} - matches phase types to their queues */
private dynamicPhaseTypes: Constructor<Phase>[];
public dynamicQueueManager = new DynamicQueueManager();
private currentPhase: Phase | null = null;
private standbyPhase: Phase | null = null;
constructor() {
this.dynamicPhaseQueues = [new PostSummonPhasePriorityQueue()];
this.dynamicPhaseTypes = [PostSummonPhase];
}
public turnEnded = false;
/* Phase Functions */
getCurrentPhase(): Phase | null {
@ -249,31 +249,17 @@ export class PhaseManager {
return this.standbyPhase;
}
/**
* Adds a phase to the conditional queue and ensures it is executed only when the specified condition is met.
*
* This method allows deferring the execution of a phase until certain conditions are met, which is useful for handling
* situations like abilities and entry hazards that depend on specific game states.
*
* @param phase - The phase to be added to the conditional queue.
* @param condition - A function that returns a boolean indicating whether the phase should be executed.
*
*/
pushConditionalPhase(phase: Phase, condition: () => boolean): void {
this.conditionalQueue.push([condition, phase]);
}
/**
* Adds a phase to nextCommandPhaseQueue, as long as boolean passed in is false
* @param phase {@linkcode Phase} the phase to add
* @param defer boolean on which queue to add to, defaults to false, and adds to phaseQueue
*/
pushPhase(phase: Phase, defer = false): void {
if (this.getDynamicPhaseType(phase) !== undefined) {
this.pushDynamicPhase(phase);
} else {
(!defer ? this.phaseQueue : this.nextCommandPhaseQueue).push(phase);
if (this.isDynamicPhase(phase) && this.dynamicQueueManager.activeQueueExists(phase.phaseName)) {
this.dynamicQueueManager.queueDynamicPhase(phase);
return;
}
(!defer ? this.phaseQueue : this.nextCommandPhaseQueue).push(phase);
}
/**
@ -281,6 +267,10 @@ export class PhaseManager {
* @param phases {@linkcode Phase} the phase(s) to add
*/
unshiftPhase(...phases: Phase[]): void {
if (this.isDynamicPhase(phases[0]) && this.dynamicQueueManager.activeQueueExists(phases[0].phaseName)) {
phases.forEach((p: DynamicPhase) => this.dynamicQueueManager.queueDynamicPhase(p));
return;
}
if (this.phaseQueuePrependSpliceIndex === -1) {
this.phaseQueuePrepend.push(...phases);
} else {
@ -299,12 +289,13 @@ export class PhaseManager {
* Clears all phase-related stuff, including all phase queues, the current and standby phases, and a splice index
*/
clearAllPhases(): void {
for (const queue of [this.phaseQueue, this.phaseQueuePrepend, this.conditionalQueue, this.nextCommandPhaseQueue]) {
for (const queue of [this.phaseQueue, this.phaseQueuePrepend, this.nextCommandPhaseQueue]) {
queue.splice(0, queue.length);
}
this.dynamicPhaseQueues.forEach(queue => queue.clear());
this.dynamicQueueManager.clearQueues();
this.currentPhase = null;
this.standbyPhase = null;
this.turnEnded = false;
this.clearPhaseQueueSplice();
}
@ -345,32 +336,21 @@ export class PhaseManager {
}
}
}
if (!this.phaseQueue.length) {
this.populatePhaseQueue();
// Clear the conditionalQueue if there are no phases left in the phaseQueue
this.conditionalQueue = [];
this.queueDynamicPhasesAtFront();
if (this.phaseQueue.every(p => ignorablePhases.includes(p.phaseName))) {
this.startNextDynamicPhase();
}
if (!this.turnEnded && (!this.phaseQueue.length || this.phaseQueue[0].is("BattleEndPhase"))) {
if (!this.startNextDynamicPhase()) {
this.turnEndSequence();
}
}
this.currentPhase = this.phaseQueue.shift() ?? null;
const unactivatedConditionalPhases: [() => boolean, Phase][] = [];
// Check if there are any conditional phases queued
while (this.conditionalQueue?.length) {
// Retrieve the first conditional phase from the queue
const conditionalPhase = this.conditionalQueue.shift();
// Evaluate the condition associated with the phase
if (conditionalPhase?.[0]()) {
// If the condition is met, add the phase to the phase queue
this.pushPhase(conditionalPhase[1]);
} else if (conditionalPhase) {
// If the condition is not met, re-add the phase back to the front of the conditional queue
unactivatedConditionalPhases.push(conditionalPhase);
} else {
console.warn("condition phase is undefined/null!", conditionalPhase);
}
}
this.conditionalQueue.push(...unactivatedConditionalPhases);
if (this.currentPhase) {
console.log(`%cStart Phase ${this.currentPhase.constructor.name}`, "color:green;");
this.currentPhase.start();
@ -390,17 +370,32 @@ export class PhaseManager {
return true;
}
public hasPhaseOfType(type: PhaseString, condition?: PhaseConditionFunc): boolean {
if (this.dynamicQueueManager.exists(type, condition)) {
return true;
}
return [this.phaseQueue, this.phaseQueuePrepend].some((queue: Phase[]) =>
queue.find(phase => phase.is(type) && (!condition || condition(phase))),
);
}
/**
* Find a specific {@linkcode Phase} in the phase queue.
*
* @param phaseType - A {@linkcode PhaseString} representing which type to search for
* @param phaseFilter filter function to use to find the wanted phase
* @returns the found phase or undefined if none found
*/
findPhase<P extends Phase = Phase>(phaseFilter: (phase: P) => boolean): P | undefined {
return this.phaseQueue.find(phaseFilter) as P | undefined;
findPhase<P extends PhaseString>(
phaseType: P,
phaseFilter?: (phase: PhaseMap[P]) => boolean,
): PhaseMap[P] | undefined {
if (this.dynamicQueueManager.exists(phaseType, phaseFilter)) {
return this.dynamicQueueManager.findPhaseOfType(phaseType, phaseFilter) as PhaseMap[P];
}
return this.phaseQueue.find(phase => phase.is(phaseType) && (!phaseFilter || phaseFilter(phase))) as PhaseMap[P];
}
tryReplacePhase(phaseFilter: (phase: Phase) => boolean, phase: Phase): boolean {
tryReplacePhase(phaseFilter: PhaseConditionFunc, phase: Phase): boolean {
const phaseIndex = this.phaseQueue.findIndex(phaseFilter);
if (phaseIndex > -1) {
this.phaseQueue[phaseIndex] = phase;
@ -409,20 +404,23 @@ export class PhaseManager {
return false;
}
tryRemovePhase(phaseFilter: (phase: Phase) => boolean): boolean {
tryRemovePhase(phaseFilter: PhaseConditionFunc): boolean {
if (this.dynamicQueueManager.removePhase(phaseFilter)) {
return true;
}
const phaseIndex = this.phaseQueue.findIndex(phaseFilter);
if (phaseIndex > -1) {
this.phaseQueue.splice(phaseIndex, 1);
return true;
}
return false;
return this.dynamicQueueManager.removePhase(phaseFilter);
}
/**
* Will search for a specific phase in {@linkcode phaseQueuePrepend} via filter, and remove the first result if a match is found.
* @param phaseFilter filter function
*/
tryRemoveUnshiftedPhase(phaseFilter: (phase: Phase) => boolean): boolean {
tryRemoveUnshiftedPhase(phaseFilter: PhaseConditionFunc): boolean {
const phaseIndex = this.phaseQueuePrepend.findIndex(phaseFilter);
if (phaseIndex > -1) {
this.phaseQueuePrepend.splice(phaseIndex, 1);
@ -431,22 +429,27 @@ export class PhaseManager {
return false;
}
public removeAllPhasesOfType(type: PhaseString): void {
this.phaseQueue = this.phaseQueue.filter(phase => !phase.is(type));
this.phaseQueuePrepend = this.phaseQueuePrepend.filter(phase => !phase.is(type));
}
/**
* Tries to add the input phase to index before target phase in the phaseQueue, else simply calls unshiftPhase()
* @param phase - The phase to be added
* @param targetPhase - The phase to search for in phaseQueue
* @returns boolean if a targetPhase was found and added
*/
prependToPhase(phase: Phase | Phase[], targetPhase: PhaseString): boolean {
phase = coerceArray(phase);
prependToPhase(phase: Phase, targetPhase: StaticPhaseString): boolean {
const target = PHASES[targetPhase];
const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof target);
if (targetIndex !== -1) {
this.phaseQueue.splice(targetIndex, 0, ...phase);
this.phaseQueue.splice(targetIndex, 0, phase);
return true;
}
this.unshiftPhase(...phase);
this.unshiftPhase(phase);
return false;
}
@ -457,81 +460,18 @@ export class PhaseManager {
* @param condition Condition the target phase must meet to be appended to
* @returns `true` if a `targetPhase` was found to append to
*/
appendToPhase(phase: Phase | Phase[], targetPhase: PhaseString, condition?: (p: Phase) => boolean): boolean {
phase = coerceArray(phase);
appendToPhase(phase: Phase, targetPhase: StaticPhaseString, condition?: PhaseConditionFunc): boolean {
const target = PHASES[targetPhase];
const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof target && (!condition || condition(ph)));
if (targetIndex !== -1 && this.phaseQueue.length > targetIndex) {
this.phaseQueue.splice(targetIndex + 1, 0, ...phase);
this.phaseQueue.splice(targetIndex + 1, 0, phase);
return true;
}
this.unshiftPhase(...phase);
this.unshiftPhase(phase);
return false;
}
/**
* Checks a phase and returns the matching {@linkcode DynamicPhaseType}, or undefined if it does not match one
* @param phase The phase to check
* @returns The corresponding {@linkcode DynamicPhaseType} or `undefined`
*/
public getDynamicPhaseType(phase: Phase | null): DynamicPhaseType | undefined {
let phaseType: DynamicPhaseType | undefined;
this.dynamicPhaseTypes.forEach((cls, index) => {
if (phase instanceof cls) {
phaseType = index;
}
});
return phaseType;
}
/**
* Pushes a phase onto its corresponding dynamic queue and marks the activation point in {@linkcode phaseQueue}
*
* The {@linkcode ActivatePriorityQueuePhase} will run the top phase in the dynamic queue (not necessarily {@linkcode phase})
* @param phase The phase to push
*/
public pushDynamicPhase(phase: Phase): void {
const type = this.getDynamicPhaseType(phase);
if (type === undefined) {
return;
}
this.pushPhase(new ActivatePriorityQueuePhase(type));
this.dynamicPhaseQueues[type].push(phase);
}
/**
* Unshifts the top phase from the corresponding dynamic queue onto {@linkcode phaseQueue}
* @param type {@linkcode DynamicPhaseType} The type of dynamic phase to start
*/
public startDynamicPhaseType(type: DynamicPhaseType): void {
const phase = this.dynamicPhaseQueues[type].pop();
if (phase) {
this.unshiftPhase(phase);
}
}
/**
* Unshifts an {@linkcode ActivatePriorityQueuePhase} for {@linkcode phase}, then pushes {@linkcode phase} to its dynamic queue
*
* This is the same as {@linkcode pushDynamicPhase}, except the activation phase is unshifted
*
* {@linkcode phase} is not guaranteed to be the next phase from the queue to run (if the queue is not empty)
* @param phase The phase to add
* @returns
*/
public startDynamicPhase(phase: Phase): void {
const type = this.getDynamicPhaseType(phase);
if (type === undefined) {
return;
}
this.unshiftPhase(new ActivatePriorityQueuePhase(type));
this.dynamicPhaseQueues[type].push(phase);
}
/**
* Adds a MessagePhase, either to PhaseQueuePrepend or nextCommandPhaseQueue
* @param message - string for MessagePhase
@ -582,7 +522,10 @@ export class PhaseManager {
/**
* Moves everything from nextCommandPhaseQueue to phaseQueue (keeping order)
*/
private populatePhaseQueue(): void {
private turnEndSequence(): void {
this.turnEnded = true;
this.dynamicQueueManager.clearQueues();
this.queueTurnEndPhases();
if (this.nextCommandPhaseQueue.length) {
this.phaseQueue.push(...this.nextCommandPhaseQueue);
this.nextCommandPhaseQueue.splice(0, this.nextCommandPhaseQueue.length);
@ -637,7 +580,7 @@ export class PhaseManager {
* @returns `true` if a `targetPhase` was found to prepend to
*/
public prependNewToPhase<T extends PhaseString>(
targetPhase: PhaseString,
targetPhase: StaticPhaseString,
phase: T,
...args: ConstructorParameters<PhaseConstructorMap[T]>
): boolean {
@ -653,17 +596,62 @@ export class PhaseManager {
* @returns `true` if a `targetPhase` was found to append to
*/
public appendNewToPhase<T extends PhaseString>(
targetPhase: PhaseString,
targetPhase: StaticPhaseString,
phase: T,
...args: ConstructorParameters<PhaseConstructorMap[T]>
): boolean {
return this.appendToPhase(this.create(phase, ...args), targetPhase);
}
public startNewDynamicPhase<T extends PhaseString>(
phase: T,
...args: ConstructorParameters<PhaseConstructorMap[T]>
): void {
this.startDynamicPhase(this.create(phase, ...args));
public forceMoveNext(phaseCondition: PhaseConditionFunc) {
this.dynamicQueueManager.setMoveTimingModifier(phaseCondition, MovePhaseTimingModifier.FIRST);
}
public forceMoveLast(phaseCondition: PhaseConditionFunc) {
this.dynamicQueueManager.setMoveTimingModifier(phaseCondition, MovePhaseTimingModifier.LAST);
}
public changePhaseMove(phaseCondition: PhaseConditionFunc, move: PokemonMove) {
this.dynamicQueueManager.setMoveForPhase(phaseCondition, move);
}
public queueTurnEndPhases(): void {
turnEndPhases
.slice()
.reverse()
.forEach(p => this.phaseQueue.unshift(this.create(p)));
}
private consecutivePokemonPhases(): DynamicPhase[] | undefined {
if (this.phaseQueue.length < 1 || !this.isDynamicPhase(this.phaseQueue[0])) {
return;
}
let spliceLength = this.phaseQueue.findIndex(p => !p.is(this.phaseQueue[0].phaseName));
spliceLength = spliceLength !== -1 ? spliceLength : this.phaseQueue.length;
if (spliceLength > 1) {
return this.phaseQueue.splice(0, spliceLength) as DynamicPhase[];
}
}
private queueDynamicPhasesAtFront(): void {
const dynamicPhases = this.consecutivePokemonPhases();
if (dynamicPhases) {
dynamicPhases.forEach((p: DynamicPhase) => {
globalScene.phaseManager.dynamicQueueManager.queueDynamicPhase(p);
});
}
}
public startNextDynamicPhase(): boolean {
const dynamicPhase = this.dynamicQueueManager.popNextPhase();
if (dynamicPhase) {
this.phaseQueue.unshift(dynamicPhase);
}
return !!dynamicPhase;
}
private isDynamicPhase(phase: Phase): phase is DynamicPhase {
return typeof (phase as any).getPokemon === "function" && !nonDynamicPokemonPhases.includes(phase.phaseName);
}
}

View File

@ -1,23 +0,0 @@
import type { DynamicPhaseType } from "#enums/dynamic-phase-type";
import { globalScene } from "#app/global-scene";
import { Phase } from "#app/phase";
export class ActivatePriorityQueuePhase extends Phase {
public readonly phaseName = "ActivatePriorityQueuePhase";
private type: DynamicPhaseType;
constructor(type: DynamicPhaseType) {
super();
this.type = type;
}
override start() {
super.start();
globalScene.phaseManager.startDynamicPhaseType(this.type);
this.end();
}
public getType(): DynamicPhaseType {
return this.type;
}
}

View File

@ -18,23 +18,11 @@ export class BattleEndPhase extends BattlePhase {
super.start();
// cull any extra `BattleEnd` phases from the queue.
globalScene.phaseManager.phaseQueue = globalScene.phaseManager.phaseQueue.filter(phase => {
if (phase.is("BattleEndPhase")) {
this.isVictory ||= phase.isVictory;
return false;
}
return true;
});
// `phaseQueuePrepend` is private, so we have to use this inefficient loop.
while (
globalScene.phaseManager.tryRemoveUnshiftedPhase(phase => {
if (phase.is("BattleEndPhase")) {
this.isVictory ||= phase.isVictory;
return true;
}
return false;
})
) {}
this.isVictory ||= globalScene.phaseManager.hasPhaseOfType(
"BattleEndPhase",
(phase: BattleEndPhase) => phase.isVictory,
);
globalScene.phaseManager.removeAllPhasesOfType("BattleEndPhase");
globalScene.gameData.gameStats.battles++;
if (

View File

@ -1,20 +1,14 @@
import { Phase } from "#app/phase";
import type { BattlerIndex } from "#enums/battler-index";
import { globalScene } from "#app/global-scene";
export class CheckStatusEffectPhase extends Phase {
public readonly phaseName = "CheckStatusEffectPhase";
private order: BattlerIndex[];
constructor(order: BattlerIndex[]) {
super();
this.order = order;
}
start() {
const field = globalScene.getField();
for (const o of this.order) {
if (field[o].status?.isPostTurn()) {
globalScene.phaseManager.unshiftNew("PostTurnStatusEffectPhase", o);
for (const p of field) {
if (p?.status?.isPostTurn()) {
globalScene.phaseManager.unshiftNew("PostTurnStatusEffectPhase", p.getBattlerIndex());
}
}
this.end();

View File

@ -225,7 +225,7 @@ export class EggHatchPhase extends Phase {
}
end() {
if (globalScene.phaseManager.findPhase(p => p.is("EggHatchPhase"))) {
if (globalScene.phaseManager.findPhase("EggHatchPhase")) {
this.eggHatchHandler.clear();
} else {
globalScene.time.delayedCall(250, () => globalScene.setModifiersVisible(true));

View File

@ -562,29 +562,6 @@ export class EncounterPhase extends BattlePhase {
});
if (![BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(globalScene.currentBattle.battleType)) {
enemyField.map(p =>
globalScene.phaseManager.pushConditionalPhase(
globalScene.phaseManager.create("PostSummonPhase", p.getBattlerIndex()),
() => {
// if there is not a player party, we can't continue
if (!globalScene.getPlayerParty().length) {
return false;
}
// how many player pokemon are on the field ?
const pokemonsOnFieldCount = globalScene.getPlayerParty().filter(p => p.isOnField()).length;
// if it's a 2vs1, there will never be a 2nd pokemon on our field even
const requiredPokemonsOnField = Math.min(
globalScene.getPlayerParty().filter(p => !p.isFainted()).length,
2,
);
// if it's a double, there should be 2, otherwise 1
if (globalScene.currentBattle.double) {
return pokemonsOnFieldCount === requiredPokemonsOnField;
}
return pokemonsOnFieldCount === 1;
},
),
);
const ivScannerModifier = globalScene.findModifier(m => m instanceof IvScannerModifier);
if (ivScannerModifier) {
enemyField.map(p => globalScene.phaseManager.pushNew("ScanIvsPhase", p.getBattlerIndex()));
@ -625,6 +602,9 @@ export class EncounterPhase extends BattlePhase {
}
}
}
if (![BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(globalScene.currentBattle.battleType)) {
enemyField.map(p => globalScene.phaseManager.pushNew("PostSummonPhase", p.getBattlerIndex()));
}
handleTutorial(Tutorial.Access_Menu).then(() => super.end());
}

View File

@ -181,7 +181,14 @@ export class FaintPhase extends PokemonPhase {
.filter(p => p.isActive() && !p.isOnField() && p.trainerSlot === (pokemon as EnemyPokemon).trainerSlot)
.length;
if (hasReservePartyMember) {
globalScene.phaseManager.pushNew("SwitchSummonPhase", SwitchType.SWITCH, this.fieldIndex, -1, false, false);
globalScene.phaseManager.pushNew(
"StaticSwitchSummonPhase",
SwitchType.SWITCH,
this.fieldIndex,
-1,
false,
false,
);
}
}
}

View File

@ -50,6 +50,7 @@ import type Move from "#app/data/moves/move";
import { isFieldTargeted } from "#app/data/moves/move-utils";
import { DamageAchv } from "#app/system/achv";
import { isVirtual, isReflected, MoveUseMode } from "#enums/move-use-mode";
import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
export type HitCheckEntry = [HitCheckResult, TypeDamageMultiplier];
@ -156,7 +157,7 @@ export class MoveEffectPhase extends PokemonPhase {
}
/**
* Queue the phaes that should occur when the target reflects the move back to the user
* Queue the phases that should occur when the target reflects the move back to the user
* @param user - The {@linkcode Pokemon} using this phase's invoked move
* @param target - The {@linkcode Pokemon} that is reflecting the move
* TODO: Rework this to use `onApply` of Magic Coat
@ -167,24 +168,21 @@ export class MoveEffectPhase extends PokemonPhase {
: [user.getBattlerIndex()];
// TODO: ability displays should be handled by the ability
if (!target.getTag(BattlerTagType.MAGIC_COAT)) {
this.queuedPhases.push(
globalScene.phaseManager.create(
"ShowAbilityPhase",
target.getBattlerIndex(),
target.getPassiveAbility().hasAttr("ReflectStatusMoveAbAttr"),
),
globalScene.phaseManager.unshiftNew(
"ShowAbilityPhase",
target.getBattlerIndex(),
target.getPassiveAbility().hasAttr("ReflectStatusMoveAbAttr"),
);
this.queuedPhases.push(globalScene.phaseManager.create("HideAbilityPhase"));
globalScene.phaseManager.unshiftNew("HideAbilityPhase");
}
this.queuedPhases.push(
globalScene.phaseManager.create(
"MovePhase",
target,
newTargets,
new PokemonMove(this.move.id),
MoveUseMode.REFLECTED,
),
globalScene.phaseManager.pushNew(
"MovePhase",
target,
newTargets,
new PokemonMove(this.move.id),
MoveUseMode.REFLECTED,
MovePhaseTimingModifier.FIRST,
);
}
@ -376,9 +374,6 @@ export class MoveEffectPhase extends PokemonPhase {
return;
}
if (this.queuedPhases.length) {
globalScene.phaseManager.appendToPhase(this.queuedPhases, "MoveEndPhase");
}
const moveType = user.getMoveType(this.move, true);
if (this.move.category !== MoveCategory.STATUS && !user.stellarTypesBoosted.includes(moveType)) {
user.stellarTypesBoosted.push(moveType);

View File

@ -1,29 +1,27 @@
import { applyMoveAttrs } from "#app/data/moves/apply-attrs";
import type { PokemonMove } from "#app/data/moves/pokemon-move";
import type Pokemon from "#app/field/pokemon";
import { BattlePhase } from "./battle-phase";
import { PokemonPhase } from "#app/phases/pokemon-phase";
import type { BattlerIndex } from "#enums/battler-index";
export class MoveHeaderPhase extends BattlePhase {
export class MoveHeaderPhase extends PokemonPhase {
public readonly phaseName = "MoveHeaderPhase";
public pokemon: Pokemon;
public move: PokemonMove;
constructor(pokemon: Pokemon, move: PokemonMove) {
super();
constructor(battlerIndex: BattlerIndex, move: PokemonMove) {
super(battlerIndex);
this.pokemon = pokemon;
this.move = move;
}
canMove(): boolean {
return this.pokemon.isActive(true) && this.move.isUsable(this.pokemon);
return this.getPokemon().isActive(true) && this.move.isUsable(this.getPokemon());
}
start() {
super.start();
if (this.canMove()) {
applyMoveAttrs("MoveHeaderAttr", this.pokemon, null, this.move.getMove());
applyMoveAttrs("MoveHeaderAttr", this.getPokemon(), null, this.move.getMove());
}
this.end();
}

View File

@ -18,7 +18,6 @@ import type Pokemon from "#app/field/pokemon";
import { MoveResult } from "#enums/move-result";
import { getPokemonNameWithAffix } from "#app/messages";
import Overrides from "#app/overrides";
import { BattlePhase } from "#app/phases/battle-phase";
import { enumValueToKey, NumberHolder } from "#app/utils/common";
import { AbilityId } from "#enums/ability-id";
import { ArenaTagType } from "#enums/arena-tag-type";
@ -29,14 +28,16 @@ import i18next from "i18next";
import { getTerrainBlockMessage } from "#app/data/terrain";
import { isVirtual, isIgnorePP, isReflected, MoveUseMode, isIgnoreStatus } from "#enums/move-use-mode";
import { frenzyMissFunc } from "#app/data/moves/move-utils";
import { PokemonPhase } from "#app/phases/pokemon-phase";
import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
export class MovePhase extends BattlePhase {
export class MovePhase extends PokemonPhase {
public readonly phaseName = "MovePhase";
protected _pokemon: Pokemon;
protected _move: PokemonMove;
protected _targets: BattlerIndex[];
public readonly useMode: MoveUseMode; // Made public for quash
protected forcedLast: boolean;
protected _timingModifier: MovePhaseTimingModifier;
/** Whether the current move should fail but still use PP */
protected failed = false;
@ -56,7 +57,7 @@ export class MovePhase extends BattlePhase {
return this._move;
}
protected set move(move: PokemonMove) {
public set move(move: PokemonMove) {
this._move = move;
}
@ -68,6 +69,14 @@ export class MovePhase extends BattlePhase {
this._targets = targets;
}
public get timingModifier(): MovePhaseTimingModifier {
return this._timingModifier;
}
public set timingModifier(modifier: MovePhaseTimingModifier) {
this._timingModifier = modifier;
}
/**
* Create a new MovePhase for using moves.
* @param pokemon - The {@linkcode Pokemon} using the move
@ -76,14 +85,14 @@ export class MovePhase extends BattlePhase {
* Not marked optional to ensure callers correctly pass on `useModes`.
* @param forcedLast - Whether to force this phase to occur last in order (for {@linkcode MoveId.QUASH}); default `false`
*/
constructor(pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, useMode: MoveUseMode, forcedLast = false) {
super();
constructor(pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, useMode: MoveUseMode, timingModifier = MovePhaseTimingModifier.NORMAL) {
super(pokemon.getBattlerIndex());
this.pokemon = pokemon;
this.targets = targets;
this.move = move;
this.useMode = useMode;
this.forcedLast = forcedLast;
this.timingModifier = timingModifier;
}
/**
@ -109,14 +118,6 @@ export class MovePhase extends BattlePhase {
this.cancelled = true;
}
/**
* Shows whether the current move has been forced to the end of the turn
* Needed for speed order, see {@linkcode MoveId.QUASH}
*/
public isForcedLast(): boolean {
return this.forcedLast;
}
public start(): void {
super.start();

View File

@ -237,7 +237,7 @@ export class MysteryEncounterBattleStartCleanupPhase extends Phase {
});
// Remove any status tick phases
while (globalScene.phaseManager.findPhase(p => p.is("PostTurnStatusEffectPhase"))) {
while (globalScene.phaseManager.findPhase("PostTurnStatusEffectPhase")) {
globalScene.phaseManager.tryRemovePhase(p => p.is("PostTurnStatusEffectPhase"));
}

View File

@ -6,12 +6,7 @@ export class NewBattlePhase extends BattlePhase {
start() {
super.start();
// cull any extra `NewBattle` phases from the queue.
globalScene.phaseManager.phaseQueue = globalScene.phaseManager.phaseQueue.filter(
phase => !phase.is("NewBattlePhase"),
);
// `phaseQueuePrepend` is private, so we have to use this inefficient loop.
while (globalScene.phaseManager.tryRemoveUnshiftedPhase(phase => phase.is("NewBattlePhase"))) {}
globalScene.phaseManager.removeAllPhasesOfType("NewBattlePhase");
globalScene.newBattle();

View File

@ -22,4 +22,8 @@ export abstract class PartyMemberPokemonPhase extends FieldPhase {
getPokemon(): Pokemon {
return this.getParty()[this.partyMemberIndex];
}
isPlayer(): boolean {
return this.player;
}
}

View File

@ -28,7 +28,9 @@ export class PostSummonPhase extends PokemonPhase {
const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField();
for (const p of field) {
applyAbAttrs("CommanderAbAttr", { pokemon: p });
if (p.isActive(true)) {
applyAbAttrs("CommanderAbAttr", { pokemon: p });
}
}
this.end();

View File

@ -11,7 +11,6 @@ import { BattlerTagType } from "#app/enums/battler-tag-type";
import type Pokemon from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
import { BattlePhase } from "./battle-phase";
import type { MovePhase } from "./move-phase";
import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
export class QuietFormChangePhase extends BattlePhase {
@ -173,9 +172,7 @@ export class QuietFormChangePhase extends BattlePhase {
this.pokemon.initBattleInfo();
this.pokemon.cry();
const movePhase = globalScene.phaseManager.findPhase(
p => p.is("MovePhase") && p.pokemon === this.pokemon,
) as MovePhase;
const movePhase = globalScene.phaseManager.findPhase("MovePhase", p => p.pokemon === this.pokemon);
if (movePhase) {
movePhase.cancel();
}

View File

@ -50,7 +50,7 @@ export class RevivalBlessingPhase extends BattlePhase {
if (slotIndex <= 1) {
// Revived ally pokemon
globalScene.phaseManager.unshiftNew(
"SwitchSummonPhase",
"StaticSwitchSummonPhase",
SwitchType.SWITCH,
pokemon.getFieldIndex(),
slotIndex,
@ -61,7 +61,7 @@ export class RevivalBlessingPhase extends BattlePhase {
} else if (allyPokemon.isFainted()) {
// Revived party pokemon, and ally pokemon is fainted
globalScene.phaseManager.unshiftNew(
"SwitchSummonPhase",
"StaticSwitchSummonPhase",
SwitchType.SWITCH,
allyPokemon.getFieldIndex(),
slotIndex,

View File

@ -231,7 +231,8 @@ export class StatStageChangePhase extends PokemonPhase {
// Look for any other stat change phases; if this is the last one, do White Herb check
const existingPhase = globalScene.phaseManager.findPhase(
p => p.is("StatStageChangePhase") && p.battlerIndex === this.battlerIndex,
"StatStageChangePhase",
p => p.battlerIndex === this.battlerIndex,
);
if (!existingPhase?.is("StatStageChangePhase")) {
// Apply White Herb if needed
@ -311,8 +312,8 @@ export class StatStageChangePhase extends PokemonPhase {
if (this.stats.length === 1) {
while (
(existingPhase = globalScene.phaseManager.findPhase(
"StatStageChangePhase",
p =>
p.is("StatStageChangePhase") &&
p.battlerIndex === this.battlerIndex &&
p.stats.length === 1 &&
p.stats[0] === this.stats[0] &&
@ -330,8 +331,8 @@ export class StatStageChangePhase extends PokemonPhase {
}
while (
(existingPhase = globalScene.phaseManager.findPhase(
"StatStageChangePhase",
p =>
p.is("StatStageChangePhase") &&
p.battlerIndex === this.battlerIndex &&
p.selfTarget === this.selfTarget &&
accEva.some(s => p.stats.includes(s)) === isAccEva &&

View File

@ -0,0 +1,5 @@
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
export class StaticSwitchSummonPhase extends SwitchSummonPhase {
public readonly phaseName = "StaticSwitchSummonPhase";
}

View File

@ -15,7 +15,12 @@ import { globalScene } from "#app/global-scene";
export class SummonPhase extends PartyMemberPokemonPhase {
// The union type is needed to keep typescript happy as these phases extend from SummonPhase
public readonly phaseName: "SummonPhase" | "SummonMissingPhase" | "SwitchSummonPhase" | "ReturnPhase" = "SummonPhase";
public readonly phaseName:
| "SummonPhase"
| "SummonMissingPhase"
| "SwitchSummonPhase"
| "ReturnPhase"
| "StaticSwitchSummonPhase" = "SummonPhase";
private loaded: boolean;
constructor(fieldIndex: number, player = true, loaded = false) {
@ -296,4 +301,8 @@ export class SummonPhase extends PartyMemberPokemonPhase {
super.end();
}
public getFieldIndex(): number {
return this.fieldIndex;
}
}

View File

@ -79,7 +79,13 @@ export class SwitchPhase extends BattlePhase {
p => p.is("PostSummonPhase") && p.player && p.fieldIndex === this.fieldIndex,
);
const switchType = option === PartyOption.PASS_BATON ? SwitchType.BATON_PASS : this.switchType;
globalScene.phaseManager.unshiftNew("SwitchSummonPhase", switchType, fieldIndex, slotIndex, this.doReturn);
globalScene.phaseManager.unshiftNew(
"StaticSwitchSummonPhase",
switchType,
fieldIndex,
slotIndex,
this.doReturn,
);
}
globalScene.ui.setMode(UiMode.MESSAGE).then(() => super.end());
},

View File

@ -14,7 +14,7 @@ import { SubstituteTag } from "#app/data/battler-tags";
import { SwitchType } from "#enums/switch-type";
export class SwitchSummonPhase extends SummonPhase {
public readonly phaseName: "SwitchSummonPhase" | "ReturnPhase" = "SwitchSummonPhase";
public readonly phaseName: "SwitchSummonPhase" | "ReturnPhase" | "StaticSwitchSummonPhase" = "SwitchSummonPhase";
private readonly switchType: SwitchType;
private readonly slotIndex: number;
private readonly doReturn: boolean;
@ -241,11 +241,11 @@ export class SwitchSummonPhase extends SummonPhase {
globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true);
// Reverts to weather-based forms when weather suppressors (Cloud Nine/Air Lock) are switched out
globalScene.arena.triggerWeatherBasedFormChanges();
globalScene.arena.triggerWeatherBasedFormChanges(pokemon);
}
queuePostSummon(): void {
globalScene.phaseManager.startNewDynamicPhase("PostSummonPhase", this.getPokemon().getBattlerIndex());
globalScene.phaseManager.pushNew("PostSummonPhase", this.getPokemon().getBattlerIndex());
}
/**

View File

@ -23,6 +23,7 @@ export class TurnEndPhase extends FieldPhase {
globalScene.currentBattle.incrementTurn();
globalScene.eventTarget.dispatchEvent(new TurnEndEvent(globalScene.currentBattle.turn));
globalScene.phaseManager.dynamicQueueManager.clearLastTurnOrder();
globalScene.phaseManager.hideAbilityBar();

View File

@ -14,6 +14,7 @@ export class TurnInitPhase extends FieldPhase {
start() {
super.start();
globalScene.phaseManager.turnEnded = false;
globalScene.getPlayerField().forEach(p => {
// If this pokemon is in play and evolved into something illegal under the current challenge, force a switch
if (p.isOnField() && !p.isAllowedInBattle()) {

View File

@ -1,86 +1,34 @@
import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
import { allMoves } from "#app/data/data-lists";
import { Stat } from "#app/enums/stat";
import type Pokemon from "#app/field/pokemon";
import { PokemonMove } from "#app/data/moves/pokemon-move";
import { BypassSpeedChanceModifier } from "#app/modifier/modifier";
import { Command } from "#enums/command";
import { randSeedShuffle, BooleanHolder } from "#app/utils/common";
import { FieldPhase } from "./field-phase";
import { BattlerIndex } from "#enums/battler-index";
import { TrickRoomTag } from "#app/data/arena-tag";
import type { BattlerIndex } from "#enums/battler-index";
import { SwitchType } from "#enums/switch-type";
import { globalScene } from "#app/global-scene";
import { applyInSpeedOrder } from "#app/utils/speed-order";
import type Pokemon from "#app/field/pokemon";
import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
import { BypassSpeedChanceModifier } from "#app/modifier/modifier";
export class TurnStartPhase extends FieldPhase {
public readonly phaseName = "TurnStartPhase";
/**
* This orders the active Pokemon on the field by speed into an BattlerIndex array and returns that array.
* It also checks for Trick Room and reverses the array if it is present.
* @returns {@linkcode BattlerIndex[]} the battle indices of all pokemon on the field ordered by speed
*/
getSpeedOrder(): BattlerIndex[] {
const playerField = globalScene.getPlayerField().filter(p => p.isActive()) as Pokemon[];
const enemyField = globalScene.getEnemyField().filter(p => p.isActive()) as Pokemon[];
// We shuffle the list before sorting so speed ties produce random results
let orderedTargets: Pokemon[] = playerField.concat(enemyField);
// We seed it with the current turn to prevent an inconsistency where it
// was varying based on how long since you last reloaded
globalScene.executeWithSeedOffset(
() => {
orderedTargets = randSeedShuffle(orderedTargets);
},
globalScene.currentBattle.turn,
globalScene.waveSeed,
);
// Next, a check for Trick Room is applied to determine sort order.
const speedReversed = new BooleanHolder(false);
globalScene.arena.applyTags(TrickRoomTag, false, speedReversed);
// Adjust the sort function based on whether Trick Room is active.
orderedTargets.sort((a: Pokemon, b: Pokemon) => {
const aSpeed = a?.getEffectiveStat(Stat.SPD) ?? 0;
const bSpeed = b?.getEffectiveStat(Stat.SPD) ?? 0;
return speedReversed.value ? aSpeed - bSpeed : bSpeed - aSpeed;
});
return orderedTargets.map(t => t.getFieldIndex() + (!t.isPlayer() ? BattlerIndex.ENEMY : BattlerIndex.PLAYER));
}
/**
* This takes the result of getSpeedOrder and applies priority / bypass speed attributes to it.
* This also considers the priority levels of various commands and changes the result of getSpeedOrder based on such.
* @returns {@linkcode BattlerIndex[]} the final sequence of commands for this turn
* Returns an ordering of the current field based on command priority
* @returns {@linkcode BattlerIndex[]} the sequence of commands for this turn
*/
getCommandOrder(): BattlerIndex[] {
let moveOrder = this.getSpeedOrder();
// The creation of the battlerBypassSpeed object contains checks for the ability Quick Draw and the held item Quick Claw
// The ability Mycelium Might disables Quick Claw's activation when using a status move
// This occurs before the main loop because of battles with more than two Pokemon
const battlerBypassSpeed = {};
globalScene.getField(true).forEach(p => {
const bypassSpeed = new BooleanHolder(false);
const canCheckHeldItems = new BooleanHolder(true);
applyAbAttrs("BypassSpeedChanceAbAttr", { pokemon: p, bypass: bypassSpeed });
applyAbAttrs("PreventBypassSpeedChanceAbAttr", {
pokemon: p,
bypass: bypassSpeed,
canCheckHeldItems: canCheckHeldItems,
});
if (canCheckHeldItems.value) {
globalScene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p, bypassSpeed);
}
battlerBypassSpeed[p.getBattlerIndex()] = bypassSpeed;
});
const playerField = globalScene
.getPlayerField()
.filter(p => p.isActive())
.map(p => p.getBattlerIndex());
const enemyField = globalScene
.getEnemyField()
.filter(p => p.isActive())
.map(p => p.getBattlerIndex());
const orderedTargets: BattlerIndex[] = playerField.concat(enemyField);
// The function begins sorting orderedTargets based on command priority, move priority, and possible speed bypasses.
// Non-FIGHT commands (SWITCH, BALL, RUN) have a higher command priority and will always occur before any FIGHT commands.
moveOrder = moveOrder.slice(0);
moveOrder.sort((a, b) => {
orderedTargets.sort((a, b) => {
const aCommand = globalScene.currentBattle.turnCommands[a];
const bCommand = globalScene.currentBattle.turnCommands[b];
@ -91,40 +39,14 @@ export class TurnStartPhase extends FieldPhase {
if (bCommand?.command === Command.FIGHT) {
return -1;
}
} else if (aCommand?.command === Command.FIGHT) {
const aMove = allMoves[aCommand.move!.move];
const bMove = allMoves[bCommand!.move!.move];
const aUser = globalScene.getField(true).find(p => p.getBattlerIndex() === a)!;
const bUser = globalScene.getField(true).find(p => p.getBattlerIndex() === b)!;
const aPriority = aMove.getPriority(aUser, false);
const bPriority = bMove.getPriority(bUser, false);
// The game now checks for differences in priority levels.
// If the moves share the same original priority bracket, it can check for differences in battlerBypassSpeed and return the result.
// This conditional is used to ensure that Quick Claw can still activate with abilities like Stall and Mycelium Might (attack moves only)
// Otherwise, the game returns the user of the move with the highest priority.
const isSameBracket = Math.ceil(aPriority) - Math.ceil(bPriority) === 0;
if (aPriority !== bPriority) {
if (isSameBracket && battlerBypassSpeed[a].value !== battlerBypassSpeed[b].value) {
return battlerBypassSpeed[a].value ? -1 : 1;
}
return aPriority < bPriority ? 1 : -1;
}
}
// If there is no difference between the move's calculated priorities, the game checks for differences in battlerBypassSpeed and returns the result.
if (battlerBypassSpeed[a].value !== battlerBypassSpeed[b].value) {
return battlerBypassSpeed[a].value ? -1 : 1;
}
const aIndex = moveOrder.indexOf(a);
const bIndex = moveOrder.indexOf(b);
const aIndex = orderedTargets.indexOf(a);
const bIndex = orderedTargets.indexOf(b);
return aIndex < bIndex ? -1 : aIndex > bIndex ? 1 : 0;
});
return moveOrder;
return orderedTargets;
}
// TODO: Refactor this alongside `CommandPhase.handleCommand` to use SEPARATE METHODS
@ -133,25 +55,27 @@ export class TurnStartPhase extends FieldPhase {
super.start();
const field = globalScene.getField();
const activeField = globalScene.getField(true);
const moveOrder = this.getCommandOrder();
let orderIndex = 0;
for (const o of this.getSpeedOrder()) {
const pokemon = field[o];
const preTurnCommand = globalScene.currentBattle.preTurnCommands[o];
applyInSpeedOrder(activeField, (p: Pokemon) => {
const preTurnCommand = globalScene.currentBattle.preTurnCommands[p.getBattlerIndex()];
if (preTurnCommand?.skip) {
continue;
return;
}
switch (preTurnCommand?.command) {
case Command.TERA:
globalScene.phaseManager.pushNew("TeraPhase", pokemon);
globalScene.phaseManager.pushNew("TeraPhase", p);
}
}
});
const phaseManager = globalScene.phaseManager;
applyInSpeedOrder(activeField, (p: Pokemon) => {
applyAbAttrs("BypassSpeedChanceAbAttr", { pokemon: p });
globalScene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p);
});
for (const o of moveOrder) {
const pokemon = field[o];
@ -164,7 +88,6 @@ export class TurnStartPhase extends FieldPhase {
switch (turnCommand?.command) {
case Command.FIGHT: {
const queuedMove = turnCommand.move;
pokemon.turnData.order = orderIndex++;
if (!queuedMove) {
continue;
}
@ -172,7 +95,7 @@ export class TurnStartPhase extends FieldPhase {
pokemon.getMoveset().find(m => m.moveId === queuedMove.move && m.ppUsed < m.getMovePp()) ??
new PokemonMove(queuedMove.move);
if (move.getMove().hasAttr("MoveHeaderAttr")) {
phaseManager.unshiftNew("MoveHeaderPhase", pokemon, move);
phaseManager.pushNew("MoveHeaderPhase", pokemon.getBattlerIndex(), move);
}
if (pokemon.isPlayer() && turnCommand.cursor === -1) {
@ -200,7 +123,7 @@ export class TurnStartPhase extends FieldPhase {
case Command.POKEMON:
{
const switchType = turnCommand.args?.[0] ? SwitchType.BATON_PASS : SwitchType.SWITCH;
phaseManager.unshiftNew(
phaseManager.pushNew(
"SwitchSummonPhase",
switchType,
pokemon.getFieldIndex(),
@ -219,14 +142,6 @@ export class TurnStartPhase extends FieldPhase {
}
}
phaseManager.pushNew("WeatherEffectPhase");
phaseManager.pushNew("BerryPhase");
/** Add a new phase to check who should be taking status damage */
phaseManager.pushNew("CheckStatusEffectPhase", moveOrder);
phaseManager.pushNew("TurnEndPhase");
/**
* this.end() will call shiftPhase(), which dumps everything from PrependQueue (aka everything that is unshifted()) to the front
* of the queue and dequeues to start the next phase

View File

@ -0,0 +1,103 @@
import type { PhaseConditionFunc } from "#app/@types/phase-condition";
import type { PhaseString, DynamicPhase } from "#app/@types/phase-types";
import type { PokemonMove } from "#app/data/moves/pokemon-move";
import type Pokemon from "#app/field/pokemon";
import type { Phase } from "#app/phase";
import { MovePhasePriorityQueue } from "#app/queues/move-phase-priority-queue";
import type { PhasePriorityQueue } from "#app/queues/phase-priority-queue";
import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-queue";
import { PostSummonPhasePriorityQueue } from "#app/queues/post-summon-phase-priority-queue";
import { SwitchSummonPhasePriorityQueue } from "#app/queues/switch-summon-phase-priority-queue";
import type { BattlerIndex } from "#enums/battler-index";
import type { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
export class DynamicQueueManager {
private dynamicPhaseMap: Map<PhaseString, PhasePriorityQueue<Phase>>;
private alwaysDynamic: PhaseString[] = ["SwitchSummonPhase", "PostSummonPhase", "MovePhase"];
private popOrder: PhaseString[] = [];
constructor() {
this.dynamicPhaseMap = new Map();
this.dynamicPhaseMap.set("SwitchSummonPhase", new SwitchSummonPhasePriorityQueue());
this.dynamicPhaseMap.set("PostSummonPhase", new PostSummonPhasePriorityQueue());
this.dynamicPhaseMap.set("MovePhase", new MovePhasePriorityQueue());
}
public clearQueues(): void {
for (const queue of this.dynamicPhaseMap.values()) {
queue.clear();
}
this.popOrder.splice(0, this.popOrder.length);
}
public queueDynamicPhase<T extends DynamicPhase>(phase: T): void {
if (!this.dynamicPhaseMap.has(phase.phaseName)) {
this.dynamicPhaseMap.set(phase.phaseName, new PokemonPhasePriorityQueue<T>());
}
this.dynamicPhaseMap.get(phase.phaseName)?.push(phase);
this.popOrder.push(phase.phaseName);
}
public popNextPhase(): Phase | undefined {
const type = this.popOrder.pop();
if (!type) {
return;
}
if (!this.alwaysDynamic.includes(type)) {
return this.dynamicPhaseMap.get(type)?.pop();
}
return this.alwaysDynamic
.map((p: PhaseString) => this.dynamicPhaseMap.get(p))
.find(q => q && !q.isEmpty())
?.pop();
}
public findPhaseOfType(type: PhaseString, condition?: PhaseConditionFunc): Phase | undefined {
return this.dynamicPhaseMap.get(type)?.findPhase(condition);
}
public activeQueueExists(type: PhaseString) {
return this.alwaysDynamic.includes(type) || this.dynamicPhaseMap.get(type)?.isEmpty() === false;
}
public exists(type: PhaseString, condition?: PhaseConditionFunc): boolean {
return !!this.dynamicPhaseMap.get(type)?.hasPhaseWithCondition(condition);
}
public removePhase(condition: PhaseConditionFunc) {
for (const queue of this.dynamicPhaseMap.values()) {
if (queue.remove(condition)) {
return true;
}
}
return false;
}
public setMoveTimingModifier(condition: PhaseConditionFunc, modifier: MovePhaseTimingModifier) {
this.getMovePhaseQueue().setTimingModifier(condition, modifier);
}
public setMoveForPhase(condition: PhaseConditionFunc, move: PokemonMove) {
this.getMovePhaseQueue().setMoveForPhase(condition, move);
}
public setMoveOrder(order: BattlerIndex[]) {
this.getMovePhaseQueue().setMoveOrder(order);
}
public getLastTurnOrder(): Pokemon[] {
return this.getMovePhaseQueue().getTurnOrder();
}
public clearLastTurnOrder(): void {
this.getMovePhaseQueue().clearTurnOrder();
}
private getMovePhaseQueue(): MovePhasePriorityQueue {
return this.dynamicPhaseMap.get("MovePhase") as MovePhasePriorityQueue;
}
public addPopType(type: PhaseString): void {
this.popOrder.push(type);
}
}

View File

@ -0,0 +1,84 @@
import type { PhaseConditionFunc } from "#app/@types/phase-condition";
import type { PokemonMove } from "#app/data/moves/pokemon-move";
import type Pokemon from "#app/field/pokemon";
import type { MovePhase } from "#app/phases/move-phase";
import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-queue";
import { isNullOrUndefined } from "#app/utils/common";
import type { BattlerIndex } from "#enums/battler-index";
import type { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
export class MovePhasePriorityQueue extends PokemonPhasePriorityQueue<MovePhase> {
private lastTurnOrder: Pokemon[] = [];
public override reorder(): void {
super.reorder();
this.sortPostSpeed();
console.log(this.queue.map(p => p.getPokemon().name));
}
public setTimingModifier(condition: PhaseConditionFunc, modifier: MovePhaseTimingModifier): void {
const phase = this.queue.find(phase => condition(phase));
if (!isNullOrUndefined(phase)) {
phase.timingModifier = modifier;
}
}
public setMoveForPhase(condition: PhaseConditionFunc, move: PokemonMove) {
const phase = this.queue.find(phase => condition(phase));
if (!isNullOrUndefined(phase)) {
phase.move = move;
}
}
public setMoveOrder(order: BattlerIndex[]) {
this.setOrder = order;
}
public override pop(): MovePhase | undefined {
this.reorder();
const phase = this.queue.shift();
if (phase) {
this.lastTurnOrder.push(phase.pokemon);
}
return phase;
}
public getTurnOrder(): Pokemon[] {
return this.lastTurnOrder;
}
public clearTurnOrder(): void {
this.lastTurnOrder = [];
}
public override clear(): void {
this.setOrder = undefined;
this.lastTurnOrder = [];
super.clear();
}
private sortPostSpeed(): void {
this.queue.sort((a: MovePhase, b: MovePhase) => {
const priority = [a, b].map(movePhase => {
const move = movePhase.move.getMove();
return move.getPriority(movePhase.pokemon, true);
});
const priorityModifiers = [a, b].map(movePhase =>
movePhase.move.getMove().getPriorityModifier(movePhase.pokemon),
);
const timingModifiers = [a, b].map(movePhase => movePhase.timingModifier);
if (timingModifiers[0] !== timingModifiers[1]) {
return timingModifiers[1] - timingModifiers[0];
}
if (priority[0] === priority[1] && priorityModifiers[0] !== priorityModifiers[1]) {
return priorityModifiers[1] - priorityModifiers[0];
}
return priority[1] - priority[0];
});
}
}

View File

@ -0,0 +1,61 @@
import type { PhaseConditionFunc } from "#app/@types/phase-condition";
import type { Phase } from "#app/phase";
/**
* Stores a list of {@linkcode Phase}s
*
* Dynamically updates ordering to always pop the highest "priority", based on implementation of {@linkcode reorder}
*/
export abstract class PhasePriorityQueue<T extends Phase> {
protected queue: T[] = [];
/**
* Sorts the elements in the queue
*/
public abstract reorder(): void;
/**
* Calls {@linkcode reorder} and shifts the queue
* @returns The front element of the queue after sorting
*/
public pop(): T | undefined {
this.reorder();
return this.queue.shift();
}
/**
* Adds a phase to the queue
* @param phase The phase to add
*/
public push(phase: T): void {
this.queue.push(phase);
}
/**
* Removes all phases from the queue
*/
public clear(): void {
this.queue.splice(0, this.queue.length);
}
public isEmpty(): boolean {
return !this.queue.length;
}
public remove(condition: PhaseConditionFunc): boolean {
const phaseIndex = this.queue.findIndex(condition);
if (phaseIndex > -1) {
this.queue.splice(phaseIndex, 1);
return true;
}
return false;
}
public findPhase(condition?: PhaseConditionFunc): Phase | undefined {
return this.queue.find(phase => !condition || condition(phase));
}
public hasPhaseWithCondition(condition?: PhaseConditionFunc): boolean {
return this.queue.find(phase => !condition || condition(phase)) !== undefined;
}
}

View File

@ -0,0 +1,20 @@
import type { DynamicPhase } from "#app/@types/phase-types";
import { PhasePriorityQueue } from "#app/queues/phase-priority-queue";
import { sortInSpeedOrder } from "#app/utils/speed-order";
import type { BattlerIndex } from "#enums/battler-index";
export class PokemonPhasePriorityQueue<T extends DynamicPhase> extends PhasePriorityQueue<T> {
protected setOrder: BattlerIndex[] | undefined;
public override reorder(): void {
this.queue = this.queue.filter(phase => phase.getPokemon()?.isActive(true));
if (this.setOrder) {
this.queue.sort(
(a, b) =>
this.setOrder!.indexOf(a.getPokemon().getBattlerIndex()) -
this.setOrder!.indexOf(b.getPokemon().getBattlerIndex()),
);
} else {
this.queue = sortInSpeedOrder(this.queue);
}
}
}

View File

@ -0,0 +1,38 @@
import { globalScene } from "#app/global-scene";
import { PostSummonActivateAbilityPhase } from "#app/phases/post-summon-activate-ability-phase";
import type { PostSummonPhase } from "#app/phases/post-summon-phase";
import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-queue";
import { sortInSpeedOrder } from "#app/utils/speed-order";
/**
* Priority Queue for {@linkcode PostSummonPhase} and {@linkcode PostSummonActivateAbilityPhase}
*
* Orders phases first by ability priority, then by the {@linkcode Pokemon}'s effective speed
*/
export class PostSummonPhasePriorityQueue extends PokemonPhasePriorityQueue<PostSummonPhase> {
public override reorder(): void {
this.queue = sortInSpeedOrder(this.queue, false);
this.queue.sort((phaseA: PostSummonPhase, phaseB: PostSummonPhase) => {
return phaseB.getPriority() - phaseA.getPriority();
});
}
public override push(phase: PostSummonPhase): void {
super.push(phase);
this.queueAbilityPhase(phase);
}
/**
* Queues all necessary {@linkcode PostSummonActivateAbilityPhase}s for each pushed {@linkcode PostSummonPhase}
* @param phase The {@linkcode PostSummonPhase} that was pushed onto the queue
*/
private queueAbilityPhase(phase: PostSummonPhase): void {
const phasePokemon = phase.getPokemon();
phasePokemon.getAbilityPriorities().forEach((priority, idx) => {
this.queue.push(new PostSummonActivateAbilityPhase(phasePokemon.getBattlerIndex(), priority, !!idx));
globalScene.phaseManager.dynamicQueueManager.addPopType("PostSummonPhase");
});
}
}

View File

@ -0,0 +1,14 @@
import type { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-queue";
export class SwitchSummonPhasePriorityQueue extends PokemonPhasePriorityQueue<SwitchSummonPhase> {
public override push(phase: SwitchSummonPhase): void {
// The same pokemon or slot cannot be switched into at the same time
this.queue = this.queue.filter(
old =>
old.getPokemon() !== phase.getPokemon() &&
!(old.isPlayer() === phase.isPlayer() && old.getFieldIndex() === phase.getFieldIndex()),
);
super.push(phase);
}
}

50
src/utils/speed-order.ts Normal file
View File

@ -0,0 +1,50 @@
import Pokemon from "#app/field/pokemon";
import { globalScene } from "#app/global-scene";
import { BooleanHolder, randSeedShuffle } from "#app/utils/common";
import { ArenaTagType } from "#enums/arena-tag-type";
import { Stat } from "#enums/stat";
export interface hasPokemon {
getPokemon(): Pokemon;
}
export function applyInSpeedOrder<T extends Pokemon>(pokemonList: T[], callback: (pokemon: Pokemon) => void): void {
sortInSpeedOrder(pokemonList).forEach(pokemon => callback(pokemon));
}
export function sortInSpeedOrder<T extends Pokemon | hasPokemon>(pokemonList: T[], shuffleFirst = true): T[] {
pokemonList = shuffleFirst ? shuffle(pokemonList) : pokemonList;
sortBySpeed(pokemonList);
return pokemonList;
}
/** Randomly shuffles the queue. */
function shuffle<T extends Pokemon | hasPokemon>(pokemonList: T[]): T[] {
// This is seeded with the current turn to prevent an inconsistency where it
// was varying based on how long since you last reloaded
globalScene.executeWithSeedOffset(
() => {
pokemonList = randSeedShuffle(pokemonList);
},
globalScene.currentBattle.turn * 1000 + pokemonList.length,
globalScene.waveSeed,
);
return pokemonList;
}
function sortBySpeed<T extends Pokemon | hasPokemon>(pokemonList: T[]): void {
pokemonList.sort((a, b) => {
const [aSpeed, bSpeed] = [a, b].map(pkmn =>
pkmn instanceof Pokemon ? pkmn.getEffectiveStat(Stat.SPD) : pkmn.getPokemon().getEffectiveStat(Stat.SPD),
);
return bSpeed - aSpeed;
});
/** 'true' if Trick Room is on the field. */
const speedReversed = new BooleanHolder(false);
globalScene.arena.applyTags(ArenaTagType.TRICK_ROOM, false, speedReversed);
if (speedReversed.value) {
pokemonList.reverse();
}
}

View File

@ -35,6 +35,7 @@ describe("Abilities - Dancer", () => {
await game.classicMode.startBattle([SpeciesId.ORICORIO, SpeciesId.FEEBAS]);
const [oricorio, feebas] = game.scene.getPlayerField();
const [magikarp1] = game.scene.getEnemyField();
game.move.changeMoveset(oricorio, [MoveId.SWORDS_DANCE, MoveId.VICTORY_DANCE, MoveId.SPLASH]);
game.move.changeMoveset(feebas, [MoveId.SWORDS_DANCE, MoveId.SPLASH]);
@ -44,8 +45,9 @@ describe("Abilities - Dancer", () => {
await game.phaseInterceptor.to("MovePhase"); // feebas uses swords dance
await game.phaseInterceptor.to("MovePhase", false); // oricorio copies swords dance
// Dancer order will be Magikarp, Oricorio, Magikarp based on set turn order
let currentPhase = game.scene.phaseManager.getCurrentPhase() as MovePhase;
expect(currentPhase.pokemon).toBe(oricorio);
expect(currentPhase.pokemon).toBe(magikarp1);
expect(currentPhase.move.moveId).toBe(MoveId.SWORDS_DANCE);
await game.phaseInterceptor.to("MoveEndPhase"); // end oricorio's move

View File

@ -1,5 +1,3 @@
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { TurnStartPhase } from "#app/phases/turn-start-phase";
import GameManager from "#test/testUtils/gameManager";
import { AbilityId } from "#enums/ability-id";
import { Stat } from "#enums/stat";
@ -45,65 +43,50 @@ describe("Abilities - Mycelium Might", () => {
it("will move last in its priority bracket and ignore protective abilities", async () => {
await game.classicMode.startBattle([SpeciesId.REGIELEKI]);
const enemyPokemon = game.scene.getEnemyPokemon();
const playerIndex = game.scene.getPlayerPokemon()?.getBattlerIndex();
const enemyIndex = enemyPokemon?.getBattlerIndex();
const enemy = game.scene.getEnemyPokemon()!;
const player = game.scene.getPlayerPokemon()!;
game.move.select(MoveId.BABY_DOLL_EYES);
await game.phaseInterceptor.to(TurnStartPhase, false);
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
const speedOrder = phase.getSpeedOrder();
const commandOrder = phase.getCommandOrder();
await game.phaseInterceptor.to("MoveEndPhase", false);
// The opponent Pokemon (without Mycelium Might) goes first despite having lower speed than the player Pokemon.
// The player Pokemon (with Mycelium Might) goes last despite having higher speed than the opponent.
expect(speedOrder).toEqual([playerIndex, enemyIndex]);
expect(commandOrder).toEqual([enemyIndex, playerIndex]);
await game.phaseInterceptor.to(TurnEndPhase);
expect(player.hp).not.toEqual(player.getMaxHp());
await game.phaseInterceptor.to("TurnEndPhase");
// Despite the opponent's ability (Clear Body), its ATK stat stage is still reduced.
expect(enemyPokemon?.getStatStage(Stat.ATK)).toBe(-1);
expect(enemy.getStatStage(Stat.ATK)).toBe(-1);
});
it("will still go first if a status move that is in a higher priority bracket than the opponent's move is used", async () => {
game.override.enemyMoveset(MoveId.TACKLE);
await game.classicMode.startBattle([SpeciesId.REGIELEKI]);
const enemyPokemon = game.scene.getEnemyPokemon();
const playerIndex = game.scene.getPlayerPokemon()?.getBattlerIndex();
const enemyIndex = enemyPokemon?.getBattlerIndex();
const enemy = game.scene.getEnemyPokemon()!;
const player = game.scene.getPlayerPokemon()!;
game.move.select(MoveId.BABY_DOLL_EYES);
await game.phaseInterceptor.to(TurnStartPhase, false);
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
const speedOrder = phase.getSpeedOrder();
const commandOrder = phase.getCommandOrder();
await game.phaseInterceptor.to("MoveEndPhase", false);
// The player Pokemon (with M.M.) goes first because its move is still within a higher priority bracket than its opponent.
// The enemy Pokemon goes second because its move is in a lower priority bracket.
expect(speedOrder).toEqual([playerIndex, enemyIndex]);
expect(commandOrder).toEqual([playerIndex, enemyIndex]);
await game.phaseInterceptor.to(TurnEndPhase);
expect(player.hp).toEqual(player.getMaxHp());
await game.phaseInterceptor.to("TurnEndPhase");
// Despite the opponent's ability (Clear Body), its ATK stat stage is still reduced.
expect(enemyPokemon?.getStatStage(Stat.ATK)).toBe(-1);
expect(enemy.getStatStage(Stat.ATK)).toBe(-1);
});
it("will not affect non-status moves", async () => {
await game.classicMode.startBattle([SpeciesId.REGIELEKI]);
const playerIndex = game.scene.getPlayerPokemon()!.getBattlerIndex();
const enemyIndex = game.scene.getEnemyPokemon()!.getBattlerIndex();
const player = game.scene.getPlayerPokemon()!;
game.move.select(MoveId.QUICK_ATTACK);
await game.phaseInterceptor.to(TurnStartPhase, false);
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
const speedOrder = phase.getSpeedOrder();
const commandOrder = phase.getCommandOrder();
await game.phaseInterceptor.to("MoveEndPhase", false);
// The player Pokemon (with M.M.) goes first because it has a higher speed and did not use a status move.
// The enemy Pokemon (without M.M.) goes second because its speed is lower.
// This means that the commandOrder should be identical to the speedOrder
expect(speedOrder).toEqual([playerIndex, enemyIndex]);
expect(commandOrder).toEqual([playerIndex, enemyIndex]);
expect(player.hp).toEqual(player.getMaxHp());
});
});

View File

@ -59,7 +59,7 @@ describe("Abilities - Neutralizing Gas", () => {
expect(game.scene.getPlayerPokemon()?.getStatStage(Stat.ATK)).toBe(1);
});
it.todo("should activate before other abilities", async () => {
it("should activate before other abilities", async () => {
game.override.enemySpecies(SpeciesId.ACCELGOR).enemyLevel(100).enemyAbility(AbilityId.INTIMIDATE);
await game.classicMode.startBattle([SpeciesId.FEEBAS]);

View File

@ -5,7 +5,7 @@ import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id";
import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
describe("Abilities - Quick Draw", () => {
let phaserGame: Phaser.Game;
@ -25,7 +25,6 @@ describe("Abilities - Quick Draw", () => {
game = new GameManager(phaserGame);
game.override
.battleStyle("single")
.starterSpecies(SpeciesId.MAGIKARP)
.ability(AbilityId.QUICK_DRAW)
.moveset([MoveId.TACKLE, MoveId.TAIL_WHIP])
.enemyLevel(100)
@ -40,8 +39,8 @@ describe("Abilities - Quick Draw", () => {
).mockReturnValue(100);
});
test("makes pokemon going first in its priority bracket", async () => {
await game.classicMode.startBattle();
it("makes pokemon go first in its priority bracket", async () => {
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const pokemon = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
@ -57,33 +56,27 @@ describe("Abilities - Quick Draw", () => {
expect(pokemon.waveData.abilitiesApplied).toContain(AbilityId.QUICK_DRAW);
});
test(
"does not triggered by non damage moves",
{
retry: 5,
},
async () => {
await game.classicMode.startBattle();
it("is not triggered by non damaging moves", async () => {
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const pokemon = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
const pokemon = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
pokemon.hp = 1;
enemy.hp = 1;
pokemon.hp = 1;
enemy.hp = 1;
game.move.select(MoveId.TAIL_WHIP);
await game.phaseInterceptor.to(FaintPhase, false);
game.move.select(MoveId.TAIL_WHIP);
await game.phaseInterceptor.to(FaintPhase, false);
expect(pokemon.isFainted()).toBe(true);
expect(enemy.isFainted()).toBe(false);
expect(pokemon.waveData.abilitiesApplied).not.contain(AbilityId.QUICK_DRAW);
},
);
expect(pokemon.isFainted()).toBe(true);
expect(enemy.isFainted()).toBe(false);
expect(pokemon.waveData.abilitiesApplied).not.contain(AbilityId.QUICK_DRAW);
});
test("does not increase priority", async () => {
it("does not increase priority", async () => {
game.override.enemyMoveset([MoveId.EXTREME_SPEED]);
await game.classicMode.startBattle();
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const pokemon = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;

View File

@ -4,7 +4,6 @@ import { SpeciesId } from "#enums/species-id";
import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { TurnStartPhase } from "#app/phases/turn-start-phase";
describe("Abilities - Stall", () => {
let phaserGame: Phaser.Game;
@ -40,56 +39,41 @@ describe("Abilities - Stall", () => {
it("Pokemon with Stall should move last in its priority bracket regardless of speed", async () => {
await game.classicMode.startBattle([SpeciesId.SHUCKLE]);
const playerIndex = game.scene.getPlayerPokemon()!.getBattlerIndex();
const enemyIndex = game.scene.getEnemyPokemon()!.getBattlerIndex();
const player = game.scene.getPlayerPokemon()!;
game.move.select(MoveId.QUICK_ATTACK);
await game.phaseInterceptor.to(TurnStartPhase, false);
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
const speedOrder = phase.getSpeedOrder();
const commandOrder = phase.getCommandOrder();
await game.phaseInterceptor.to("MoveEndPhase", false);
// The player Pokemon (without Stall) goes first despite having lower speed than the opponent.
// The opponent Pokemon (with Stall) goes last despite having higher speed than the player Pokemon.
expect(speedOrder).toEqual([enemyIndex, playerIndex]);
expect(commandOrder).toEqual([playerIndex, enemyIndex]);
expect(player.hp).toEqual(player.getMaxHp());
});
it("Pokemon with Stall will go first if a move that is in a higher priority bracket than the opponent's move is used", async () => {
await game.classicMode.startBattle([SpeciesId.SHUCKLE]);
const playerIndex = game.scene.getPlayerPokemon()!.getBattlerIndex();
const enemyIndex = game.scene.getEnemyPokemon()!.getBattlerIndex();
const player = game.scene.getPlayerPokemon()!;
game.move.select(MoveId.TACKLE);
await game.phaseInterceptor.to(TurnStartPhase, false);
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
const speedOrder = phase.getSpeedOrder();
const commandOrder = phase.getCommandOrder();
await game.phaseInterceptor.to("MoveEndPhase", false);
// The opponent Pokemon (with Stall) goes first because its move is still within a higher priority bracket than its opponent.
// The player Pokemon goes second because its move is in a lower priority bracket.
expect(speedOrder).toEqual([enemyIndex, playerIndex]);
expect(commandOrder).toEqual([enemyIndex, playerIndex]);
expect(player.hp).not.toEqual(player.getMaxHp());
});
it("If both Pokemon have stall and use the same move, speed is used to determine who goes first.", async () => {
game.override.ability(AbilityId.STALL);
await game.classicMode.startBattle([SpeciesId.SHUCKLE]);
const playerIndex = game.scene.getPlayerPokemon()!.getBattlerIndex();
const enemyIndex = game.scene.getEnemyPokemon()!.getBattlerIndex();
const player = game.scene.getPlayerPokemon()!;
game.move.select(MoveId.TACKLE);
await game.phaseInterceptor.to(TurnStartPhase, false);
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
const speedOrder = phase.getSpeedOrder();
const commandOrder = phase.getCommandOrder();
await game.phaseInterceptor.to("MoveEndPhase", false);
// The opponent Pokemon (with Stall) goes first because it has a higher speed.
// The player Pokemon (with Stall) goes second because its speed is lower.
expect(speedOrder).toEqual([enemyIndex, playerIndex]);
expect(commandOrder).toEqual([enemyIndex, playerIndex]);
expect(player.hp).not.toEqual(player.getMaxHp());
});
});

View File

@ -1,7 +1,6 @@
import { EnemyCommandPhase } from "#app/phases/enemy-command-phase";
import { SelectTargetPhase } from "#app/phases/select-target-phase";
import { TurnStartPhase } from "#app/phases/turn-start-phase";
import type { MovePhase } from "#app/phases/move-phase";
import { AbilityId } from "#enums/ability-id";
import { BattlerIndex } from "#enums/battler-index";
import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id";
import GameManager from "#test/testUtils/gameManager";
@ -36,38 +35,34 @@ describe("Battle order", () => {
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const playerStartHp = playerPokemon.hp;
const enemyPokemon = game.scene.getEnemyPokemon()!;
const enemyStartHp = enemyPokemon.hp;
vi.spyOn(playerPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 50]); // set playerPokemon's speed to 50
vi.spyOn(enemyPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set enemyPokemon's speed to 150
game.move.select(MoveId.TACKLE);
await game.phaseInterceptor.run(EnemyCommandPhase);
const playerPokemonIndex = playerPokemon.getBattlerIndex();
const enemyPokemonIndex = enemyPokemon.getBattlerIndex();
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
const order = phase.getCommandOrder();
expect(order[0]).toBe(enemyPokemonIndex);
expect(order[1]).toBe(playerPokemonIndex);
await game.phaseInterceptor.to("MoveEndPhase", false);
expect(playerPokemon.hp).not.toEqual(playerStartHp);
expect(enemyPokemon.hp).toEqual(enemyStartHp);
});
it("Player faster than opponent 150 vs 50", async () => {
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const playerStartHp = playerPokemon.hp;
const enemyPokemon = game.scene.getEnemyPokemon()!;
const enemyStartHp = enemyPokemon.hp;
vi.spyOn(playerPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set playerPokemon's speed to 150
vi.spyOn(enemyPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 50]); // set enemyPokemon's speed to 50
game.move.select(MoveId.TACKLE);
await game.phaseInterceptor.run(EnemyCommandPhase);
const playerPokemonIndex = playerPokemon.getBattlerIndex();
const enemyPokemonIndex = enemyPokemon.getBattlerIndex();
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
const order = phase.getCommandOrder();
expect(order[0]).toBe(playerPokemonIndex);
expect(order[1]).toBe(enemyPokemonIndex);
await game.phaseInterceptor.to("MoveEndPhase", false);
expect(playerPokemon.hp).toEqual(playerStartHp);
expect(enemyPokemon.hp).not.toEqual(enemyStartHp);
});
it("double - both opponents faster than player 50/50 vs 150/150", async () => {
@ -75,23 +70,24 @@ describe("Battle order", () => {
await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.BLASTOISE]);
const playerPokemon = game.scene.getPlayerField();
const playerHps = playerPokemon.map(p => p.hp);
const enemyPokemon = game.scene.getEnemyField();
const enemyHps = enemyPokemon.map(p => p.hp);
playerPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 50])); // set both playerPokemons' speed to 50
enemyPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150])); // set both enemyPokemons' speed to 150
const playerIndices = playerPokemon.map(p => p?.getBattlerIndex());
const enemyIndices = enemyPokemon.map(p => p?.getBattlerIndex());
game.move.select(MoveId.TACKLE);
game.move.select(MoveId.TACKLE, 1);
await game.phaseInterceptor.runFrom(SelectTargetPhase).to(TurnStartPhase, false);
await game.move.selectEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER);
await game.move.selectEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER_2);
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
const order = phase.getCommandOrder();
expect(order.slice(0, 2).includes(enemyIndices[0])).toBe(true);
expect(order.slice(0, 2).includes(enemyIndices[1])).toBe(true);
expect(order.slice(2, 4).includes(playerIndices[0])).toBe(true);
expect(order.slice(2, 4).includes(playerIndices[1])).toBe(true);
await game.phaseInterceptor.to("MoveEndPhase", true);
await game.phaseInterceptor.to("MoveEndPhase", false);
for (let i = 0; i < 2; i++) {
expect(playerPokemon[i].hp).not.toEqual(playerHps[i]);
expect(enemyPokemon[i].hp).toEqual(enemyHps[i]);
}
});
it("double - speed tie except 1 - 100/100 vs 100/150", async () => {
@ -103,18 +99,13 @@ describe("Battle order", () => {
playerPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 100])); //set both playerPokemons' speed to 100
vi.spyOn(enemyPokemon[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 100]); // set enemyPokemon's speed to 100
vi.spyOn(enemyPokemon[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set enemyPokemon's speed to 150
const playerIndices = playerPokemon.map(p => p?.getBattlerIndex());
const enemyIndices = enemyPokemon.map(p => p?.getBattlerIndex());
game.move.select(MoveId.TACKLE);
game.move.select(MoveId.TACKLE, 1);
await game.phaseInterceptor.runFrom(SelectTargetPhase).to(TurnStartPhase, false);
await game.phaseInterceptor.to("MovePhase", false);
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
const order = phase.getCommandOrder();
// enemy 2 should be first, followed by some other assortment of the other 3 pokemon
expect(order[0]).toBe(enemyIndices[1]);
expect(order.slice(1, 4)).toEqual(expect.arrayContaining([enemyIndices[0], ...playerIndices]));
const phase = game.scene.phaseManager.getCurrentPhase() as MovePhase;
expect(phase.pokemon).toEqual(enemyPokemon[1]);
});
it("double - speed tie 100/150 vs 100/150", async () => {
@ -127,17 +118,13 @@ describe("Battle order", () => {
vi.spyOn(playerPokemon[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set other playerPokemon's speed to 150
vi.spyOn(enemyPokemon[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 100]); // set one enemyPokemon's speed to 100
vi.spyOn(enemyPokemon[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set other enemyPokemon's speed to 150
const playerIndices = playerPokemon.map(p => p?.getBattlerIndex());
const enemyIndices = enemyPokemon.map(p => p?.getBattlerIndex());
game.move.select(MoveId.TACKLE);
game.move.select(MoveId.TACKLE, 1);
await game.phaseInterceptor.runFrom(SelectTargetPhase).to(TurnStartPhase, false);
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
const order = phase.getCommandOrder();
// P2/E2 should be randomly first/second, then P1/E1 randomly 3rd/4th
expect(order.slice(0, 2)).toStrictEqual(expect.arrayContaining([playerIndices[1], enemyIndices[1]]));
expect(order.slice(2, 4)).toStrictEqual(expect.arrayContaining([playerIndices[0], enemyIndices[0]]));
await game.phaseInterceptor.to("MovePhase", false);
const phase = game.scene.phaseManager.getCurrentPhase() as MovePhase;
expect(enemyPokemon[1] === phase.pokemon || playerPokemon[1] === phase.pokemon);
});
});

View File

@ -76,12 +76,7 @@ describe("Moves - Baton Pass", () => {
expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.SPATK)).toEqual(2);
// confirm that a switch actually happened. can't use species because I
// can't find a way to override trainer parties with more than 1 pokemon species
expect(game.phaseInterceptor.log.slice(-4)).toEqual([
"MoveEffectPhase",
"SwitchSummonPhase",
"SummonPhase",
"PostSummonPhase",
]);
expect(game.scene.getEnemyPokemon()?.summonData.moveHistory.length).toEqual(0);
});
it("doesn't transfer effects that aren't transferrable", async () => {

View File

@ -1,6 +1,5 @@
import { BerryPhase } from "#app/phases/berry-phase";
import { MessagePhase } from "#app/phases/message-phase";
import { MoveHeaderPhase } from "#app/phases/move-header-phase";
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
import { TurnStartPhase } from "#app/phases/turn-start-phase";
import { AbilityId } from "#enums/ability-id";
@ -116,7 +115,7 @@ describe("Moves - Focus Punch", () => {
await game.phaseInterceptor.to(TurnStartPhase);
expect(game.scene.phaseManager.getCurrentPhase() instanceof SwitchSummonPhase).toBeTruthy();
expect(game.scene.phaseManager.phaseQueue.find(phase => phase instanceof MoveHeaderPhase)).toBeDefined();
expect(game.scene.phaseManager.hasPhaseOfType("MoveHeaderPhase")).toBeTruthy();
});
it("should replace the 'but it failed' text when the user gets hit", async () => {
game.override.enemyMoveset([MoveId.TACKLE]);

View File

@ -166,7 +166,7 @@ describe("Moves - Rage Fist", () => {
// Charizard hit
game.move.select(MoveId.SPLASH);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.toNextTurn();
expect(getPartyHitCount()).toEqual([1, 0]);

View File

@ -523,7 +523,7 @@ export default class GameManager {
}
/**
* Intercepts `TurnStartPhase` and mocks {@linkcode TurnStartPhase.getSpeedOrder}'s return value.
* Modifies the queue manager to return move phases in a particular order
* Used to manually modify Pokemon turn order.
* Note: This *DOES NOT* account for priority.
* @param order - The turn order to set as an array of {@linkcode BattlerIndex}es.
@ -535,7 +535,7 @@ export default class GameManager {
async setTurnOrder(order: BattlerIndex[]): Promise<void> {
await this.phaseInterceptor.to(TurnStartPhase, false);
vi.spyOn(this.scene.phaseManager.getCurrentPhase() as TurnStartPhase, "getSpeedOrder").mockReturnValue(order);
this.scene.phaseManager.dynamicQueueManager.setMoveOrder(order);
}
/**