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"; import type { PhaseConstructorMap } from "#app/phase-manager";
// Intentionally export the types of everything in phase-manager, as this file is meant to be // 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. * Union type of all phase names as strings.
*/ */
export type PhaseString = keyof PhaseMap; 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; let targetingMovePhase: MovePhase;
do { do {
targetingMovePhase = this.phaseManager.findPhase( targetingMovePhase = this.phaseManager.findPhase(
"MovePhase",
mp => mp =>
mp.is("MovePhase") &&
mp.targets.length === 1 && mp.targets.length === 1 &&
mp.targets[0] === removedPokemon.getBattlerIndex() && mp.targets[0] === removedPokemon.getBattlerIndex() &&
mp.pokemon.isPlayer() !== allyPokemon.isPlayer(), 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 type { Localizable } from "#app/@types/locales";
import { applyAbAttrs } from "./apply-ab-attrs"; import { applyAbAttrs } from "./apply-ab-attrs";
import type { Closed, Exact } from "#app/@types/type-helpers"; 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 // biome-ignore-start lint/correctness/noUnusedImports: Used in TSDoc
import type BattleScene from "#app/battle-scene"; import type BattleScene from "#app/battle-scene";
@ -3181,6 +3184,7 @@ export class CommanderAbAttr extends AbAttr {
return ( return (
globalScene.currentBattle?.double && globalScene.currentBattle?.double &&
!isNullOrUndefined(ally) && !isNullOrUndefined(ally) &&
ally.isActive(true) &&
ally.species.speciesId === SpeciesId.DONDOZO && ally.species.speciesId === SpeciesId.DONDOZO &&
!(ally.isFainted() || ally.getTag(BattlerTagType.COMMANDED)) !(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 { export class IgnoreContactAbAttr extends AbAttr {
private declare readonly _: never; 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 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")) { if (move.getMove().is("AttackMove") || move.getMove().is("StatusMove")) {
const target = this.getTarget(pokemon, source, targets); 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")) { } else if (move.getMove().is("SelfStatusMove")) {
// If the move is a SelfStatusMove (ie. Swords Dance) the Dancer should replicate it on itself // If the move is a SelfStatusMove (ie. Swords Dance) the Dancer should replicate it on itself
globalScene.phaseManager.unshiftNew( globalScene.phaseManager.pushNew(
"MovePhase", "MovePhase",
pokemon, pokemon,
[pokemon.getBattlerIndex()], [pokemon.getBattlerIndex()],
move, move,
MoveUseMode.INDIRECT, 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). * 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 * @sealed
@ -5972,26 +5998,28 @@ export class BypassSpeedChanceAbAttr extends AbAttr {
this.chance = chance; 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 // 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 // May be difficult as we likely do not want to modify the randBattleSeed
const turnCommand = globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()]; const turnCommand = globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()];
const isCommandFight = turnCommand?.command === Command.FIGHT;
const move = turnCommand?.move?.move ? allMoves[turnCommand.move.move] : null; const move = turnCommand?.move?.move ? allMoves[turnCommand.move.move] : null;
const isDamageMove = move?.category === MoveCategory.PHYSICAL || move?.category === MoveCategory.SPECIAL; const isDamageMove = move?.category === MoveCategory.PHYSICAL || move?.category === MoveCategory.SPECIAL;
return ( 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 * bypass move order in their priority bracket when pokemon choose damaging move
*/ */
override apply({ bypass }: BypassSpeedChanceAbAttrParams): void { override apply({ pokemon }: AbAttrBaseParams): void {
bypass.value = true; 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) }); return i18next.t("abilityTriggers:quickDraw", { pokemonName: getPokemonNameWithAffix(pokemon) });
} }
} }
@ -6027,9 +6055,8 @@ export class PreventBypassSpeedChanceAbAttr extends AbAttr {
return isCommandFight && this.condition(pokemon, move!); return isCommandFight && this.condition(pokemon, move!);
} }
override apply({ bypass, canCheckHeldItems }: PreventBypassSpeedChanceAbAttrParams): void { override apply({ bypass }: PreventBypassSpeedChanceAbAttrParams): void {
bypass.value = false; bypass.value = false;
canCheckHeldItems.value = false;
} }
} }
@ -6150,7 +6177,7 @@ class ForceSwitchOutHelper {
: 0; : 0;
globalScene.phaseManager.prependNewToPhase( globalScene.phaseManager.prependNewToPhase(
"MoveEndPhase", "MoveEndPhase",
"SwitchSummonPhase", "StaticSwitchSummonPhase",
this.switchType, this.switchType,
switchOutTarget.getFieldIndex(), switchOutTarget.getFieldIndex(),
summonIndex, summonIndex,
@ -6518,6 +6545,7 @@ const AbilityAttrs = Object.freeze({
BlockStatusDamageAbAttr, BlockStatusDamageAbAttr,
BlockOneHitKOAbAttr, BlockOneHitKOAbAttr,
ChangeMovePriorityAbAttr, ChangeMovePriorityAbAttr,
ChangeMovePriorityModifierAbAttr,
IgnoreContactAbAttr, IgnoreContactAbAttr,
PreWeatherEffectAbAttr, PreWeatherEffectAbAttr,
PreWeatherDamageAbAttr, PreWeatherDamageAbAttr,
@ -6935,7 +6963,7 @@ export function initAbilities() {
.attr(AlwaysHitAbAttr) .attr(AlwaysHitAbAttr)
.attr(DoubleBattleChanceAbAttr), .attr(DoubleBattleChanceAbAttr),
new Ability(AbilityId.STALL, 4) 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) new Ability(AbilityId.TECHNICIAN, 4)
.attr(MovePowerBoostAbAttr, (user, target, move) => { .attr(MovePowerBoostAbAttr, (user, target, move) => {
const power = new NumberHolder(move.power); const power = new NumberHolder(move.power);
@ -7085,7 +7113,7 @@ export function initAbilities() {
new Ability(AbilityId.ANALYTIC, 5) new Ability(AbilityId.ANALYTIC, 5)
.attr(MovePowerBoostAbAttr, (user) => .attr(MovePowerBoostAbAttr, (user) =>
// Boost power if all other Pokemon have already moved (no other moves are slated to execute) // 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), 1.3),
new Ability(AbilityId.ILLUSION, 5) new Ability(AbilityId.ILLUSION, 5)
// The Pokemon generate an illusion if it's available // The Pokemon generate an illusion if it's available
@ -7673,7 +7701,7 @@ export function initAbilities() {
.attr(TypeImmunityHealAbAttr, PokemonType.GROUND) .attr(TypeImmunityHealAbAttr, PokemonType.GROUND)
.ignorable(), .ignorable(),
new Ability(AbilityId.MYCELIUM_MIGHT, 9) 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(PreventBypassSpeedChanceAbAttr, (_pokemon, move) => move.category === MoveCategory.STATUS)
.attr(MoveAbilityBypassAbAttr, (_pokemon, move: Move) => move.category === MoveCategory.STATUS), .attr(MoveAbilityBypassAbAttr, (_pokemon, move: Move) => move.category === MoveCategory.STATUS),
new Ability(AbilityId.MINDS_EYE, 9) 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 // Trap should only be triggered by opponent's Physical moves
if (phaseData?.move.category === MoveCategory.PHYSICAL && pokemon.isOpponent(phaseData.attacker)) { if (phaseData?.move.category === MoveCategory.PHYSICAL && pokemon.isOpponent(phaseData.attacker)) {
const shellTrapPhaseIndex = globalScene.phaseManager.phaseQueue.findIndex( globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === pokemon);
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");
}
this.activated = true; this.activated = true;
} }
@ -1165,22 +1155,9 @@ export class EncoreTag extends MoveRestrictionBattlerTag {
}), }),
); );
const movePhase = globalScene.phaseManager.findPhase(m => m.is("MovePhase") && m.pokemon === pokemon); const movesetMove = pokemon.getMoveset().find(m => m.moveId === this.moveId);
if (movePhase) { if (movesetMove) {
const movesetMove = pokemon.getMoveset().find(m => m.moveId === this.moveId); globalScene.phaseManager.changePhaseMove((phase: MovePhase) => phase.pokemon === pokemon, movesetMove);
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,
),
);
}
} }
} }
@ -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 * 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(); return new PsychoShiftTag();
case BattlerTagType.MAGIC_COAT: case BattlerTagType.MAGIC_COAT:
return new MagicCoatTag(); return new MagicCoatTag();
case BattlerTagType.BYPASS_SPEED:
return new BypassSpeedTag();
case BattlerTagType.NONE: case BattlerTagType.NONE:
default: default:
return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId); 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 { applyMoveAttrs } from "./apply-attrs";
import { frenzyMissFunc, getMoveTargets } from "./move-utils"; import { frenzyMissFunc, getMoveTargets } from "./move-utils";
import { AbAttrBaseParams, AbAttrParamsWithCancel, PreAttackModifyPowerAbAttrParams } from "../abilities/ability"; 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}. * 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; 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 * 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 * 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 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) { if (allyMovePhase) {
const allyMove = allyMovePhase.move.getMove(); const allyMove = allyMovePhase.move.getMove();
if (allyMove !== move && allyMove.hasAttr("AwaitCombinedPledgeAttr")) { 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 // Move the ally's MovePhase (if needed) so that the ally moves next
const allyMovePhaseIndex = globalScene.phaseManager.phaseQueue.indexOf(allyMovePhase); globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === user.getAlly());
const firstMovePhaseIndex = globalScene.phaseManager.phaseQueue.findIndex((phase) => phase.is("MovePhase"));
if (allyMovePhaseIndex !== firstMovePhaseIndex) {
globalScene.phaseManager.prependToPhase(globalScene.phaseManager.phaseQueue.splice(allyMovePhaseIndex, 1)[0], "MovePhase");
}
overridden.value = true; overridden.value = true;
return true; return true;
@ -4455,28 +4460,7 @@ export class LastMoveDoublePowerAttr extends VariablePowerAttr {
*/ */
apply(user: Pokemon, _target: Pokemon, _move: Move, args: any[]): boolean { apply(user: Pokemon, _target: Pokemon, _move: Move, args: any[]): boolean {
const power = args[0] as NumberHolder; const power = args[0] as NumberHolder;
const enemy = user.getOpponent(0); for (const p of globalScene.phaseManager.dynamicQueueManager.getLastTurnOrder().slice(0, -1).reverse()) {
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) {
const [ lastMove ] = p.getLastXMoves(1); const [ lastMove ] = p.getLastXMoves(1);
if (lastMove.result !== MoveResult.FAIL) { if (lastMove.result !== MoveResult.FAIL) {
if ((lastMove.result === MoveResult.SUCCESS) && (lastMove.move === this.move)) { 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 { override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean {
const nextRoundPhase = globalScene.phaseManager.findPhase<MovePhase>(phase => const nextRoundPhase = globalScene.phaseManager.findPhase("MovePhase", phase => phase.move.moveId === MoveId.ROUND
phase.is("MovePhase") && phase.move.moveId === MoveId.ROUND
); );
if (!nextRoundPhase) { if (!nextRoundPhase) {
return false; return false;
} }
// Update the phase queue so that the next Pokemon using Round moves next globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.is("MovePhase") && phase.move.moveId === MoveId.ROUND);
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");
}
// Mark the corresponding Pokemon as having "joined the Round" (for doubling power later) // Mark the corresponding Pokemon as having "joined the Round" (for doubling power later)
nextRoundPhase.pokemon.turnData.joinedRound = true; 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 // Handle cases where revived pokemon needs to get switched in on same turn
if (allyPokemon.isFainted() || allyPokemon === pokemon) { if (allyPokemon.isFainted() || allyPokemon === pokemon) {
// Enemy switch phase should be removed and replaced with the revived pkmn switching in // 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 // 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) // (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) // 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) { if (user.fieldPosition === FieldPosition.CENTER) {
user.setFieldPosition(FieldPosition.LEFT); 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; return true;
@ -6341,7 +6319,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
const slotIndex = eligibleNewIndices[user.randBattleSeedInt(eligibleNewIndices.length)]; const slotIndex = eligibleNewIndices[user.randBattleSeedInt(eligibleNewIndices.length)];
globalScene.phaseManager.prependNewToPhase( globalScene.phaseManager.prependNewToPhase(
"MoveEndPhase", "MoveEndPhase",
"SwitchSummonPhase", "StaticSwitchSummonPhase",
this.switchType, this.switchType,
switchOutTarget.getFieldIndex(), switchOutTarget.getFieldIndex(),
slotIndex, slotIndex,
@ -6380,7 +6358,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
switchOutTarget.leaveField(true); switchOutTarget.leaveField(true);
const slotIndex = eligibleNewIndices[user.randBattleSeedInt(eligibleNewIndices.length)]; const slotIndex = eligibleNewIndices[user.randBattleSeedInt(eligibleNewIndices.length)];
globalScene.phaseManager.prependNewToPhase("MoveEndPhase", globalScene.phaseManager.prependNewToPhase("MoveEndPhase",
"SwitchSummonPhase", "StaticSwitchSummonPhase",
this.switchType, this.switchType,
switchOutTarget.getFieldIndex(), switchOutTarget.getFieldIndex(),
slotIndex, slotIndex,
@ -6390,7 +6368,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
} else { } else {
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH); switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
globalScene.phaseManager.prependNewToPhase("MoveEndPhase", globalScene.phaseManager.prependNewToPhase("MoveEndPhase",
"SwitchSummonPhase", "StaticSwitchSummonPhase",
this.switchType, this.switchType,
switchOutTarget.getFieldIndex(), switchOutTarget.getFieldIndex(),
(globalScene.currentBattle.trainer ? globalScene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0), (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)]]; : moveTargets.targets[user.randBattleSeedInt(moveTargets.targets.length)]];
globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", move.id); 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; 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 // Load the move's animation if we didn't already and unshift a new usage phase
globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", moveId); 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; return true;
} }
} }
@ -7132,7 +7110,7 @@ export class RepeatMoveAttr extends MoveEffectAttr {
targetPokemonName: getPokemonNameWithAffix(target) targetPokemonName: getPokemonNameWithAffix(target)
})); }));
target.turnData.extraTurns++; 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; return true;
} }
@ -7920,12 +7898,7 @@ export class AfterYouAttr extends MoveEffectAttr {
*/ */
override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean { override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:afterYou", { targetName: getPokemonNameWithAffix(target) })); globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:afterYou", { targetName: getPokemonNameWithAffix(target) }));
globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === 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");
}
return true; return true;
} }
@ -7949,45 +7922,11 @@ export class ForceLastAttr extends MoveEffectAttr {
override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean { override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:forceLast", { targetPokemonName: getPokemonNameWithAffix(target) })); globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:forceLast", { targetPokemonName: getPokemonNameWithAffix(target) }));
// TODO: Refactor this to be more readable and less janky globalScene.phaseManager.forceMoveLast((phase: MovePhase) => phase.pokemon === target);
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)
);
}
}
return true; 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 failOnGravityCondition: MoveConditionFunc = (user, target, move) => !globalScene.arena.getTag(ArenaTagType.GRAVITY);
const failOnBossCondition: MoveConditionFunc = (user, target, move) => !target.isBossImmune(); 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 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 failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => {
const party: Pokemon[] = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty(); 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", ENDURE_TOKEN = "ENDURE_TOKEN",
POWDER = "POWDER", POWDER = "POWDER",
MAGIC_COAT = "MAGIC_COAT", 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 * 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 => { 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 isCastformWithForecast = p.hasAbility(AbilityId.FORECAST) && p.species.speciesId === SpeciesId.CASTFORM;
const isCherrimWithFlowerGift = p.hasAbility(AbilityId.FLOWER_GIFT) && p.species.speciesId === SpeciesId.CHERRIM; 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 { TurnMove } from "#app/@types/turn-move";
import type { DamageCalculationResult, DamageResult } from "#app/@types/damage-result"; import type { DamageCalculationResult, DamageResult } from "#app/@types/damage-result";
import type { AbAttrMap, AbAttrString, TypeMultiplierAbAttrParams } from "#app/@types/ability-types"; import type { AbAttrMap, AbAttrString, TypeMultiplierAbAttrParams } from "#app/@types/ability-types";
import type { TurnCommand } from "#app/battle";
import { getTerrainBlockMessage } from "#app/data/terrain"; import { getTerrainBlockMessage } from "#app/data/terrain";
import { LearnMoveSituation } from "#enums/learn-move-situation"; import { LearnMoveSituation } from "#enums/learn-move-situation";
@ -5731,7 +5732,7 @@ export class PlayerPokemon extends Pokemon {
if (slotIndex >= globalScene.currentBattle.getBattlerCount() && slotIndex < 6) { if (slotIndex >= globalScene.currentBattle.getBattlerCount() && slotIndex < 6) {
globalScene.phaseManager.prependNewToPhase( globalScene.phaseManager.prependNewToPhase(
"MoveEndPhase", "MoveEndPhase",
"SwitchSummonPhase", "StaticSwitchSummonPhase",
switchType, switchType,
this.getFieldIndex(), this.getFieldIndex(),
slotIndex, slotIndex,

View File

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

View File

@ -1,8 +1,7 @@
import type { Phase } from "#app/phase"; import type { Phase } from "#app/phase";
import type { default as Pokemon } from "#app/field/pokemon"; 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 { 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 { AddEnemyBuffModifierPhase } from "#app/phases/add-enemy-buff-modifier-phase";
import { AttemptCapturePhase } from "#app/phases/attempt-capture-phase"; import { AttemptCapturePhase } from "#app/phases/attempt-capture-phase";
import { AttemptRunPhase } from "#app/phases/attempt-run-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 { CheckSwitchPhase } from "#app/phases/check-switch-phase";
import { CommandPhase } from "#app/phases/command-phase"; import { CommandPhase } from "#app/phases/command-phase";
import { CommonAnimPhase } from "#app/phases/common-anim-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 { DamageAnimPhase } from "#app/phases/damage-anim-phase";
import type { DynamicPhaseType } from "#enums/dynamic-phase-type";
import { EggHatchPhase } from "#app/phases/egg-hatch-phase"; import { EggHatchPhase } from "#app/phases/egg-hatch-phase";
import { EggLapsePhase } from "#app/phases/egg-lapse-phase"; import { EggLapsePhase } from "#app/phases/egg-lapse-phase";
import { EggSummaryPhase } from "#app/phases/egg-summary-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 { ObtainStatusEffectPhase } from "#app/phases/obtain-status-effect-phase";
import { PartyExpPhase } from "#app/phases/party-exp-phase"; import { PartyExpPhase } from "#app/phases/party-exp-phase";
import { PartyHealPhase } from "#app/phases/party-heal-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 { PokemonAnimPhase } from "#app/phases/pokemon-anim-phase";
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase"; import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
import { PokemonTransformPhase } from "#app/phases/pokemon-transform-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 { UnlockPhase } from "#app/phases/unlock-phase";
import { VictoryPhase } from "#app/phases/victory-phase"; import { VictoryPhase } from "#app/phases/victory-phase";
import { WeatherEffectPhase } from "#app/phases/weather-effect-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. * 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. * This allows for easy creation of new phases without needing to import each phase individually.
*/ */
const PHASES = Object.freeze({ const PHASES = Object.freeze({
ActivatePriorityQueuePhase,
AddEnemyBuffModifierPhase, AddEnemyBuffModifierPhase,
AttemptCapturePhase, AttemptCapturePhase,
AttemptRunPhase, AttemptRunPhase,
@ -190,6 +190,7 @@ const PHASES = Object.freeze({
ShowAbilityPhase, ShowAbilityPhase,
ShowPartyExpBarPhase, ShowPartyExpBarPhase,
ShowTrainerPhase, ShowTrainerPhase,
StaticSwitchSummonPhase,
StatStageChangePhase, StatStageChangePhase,
SummonMissingPhase, SummonMissingPhase,
SummonPhase, SummonPhase,
@ -213,13 +214,18 @@ const PHASES = Object.freeze({
/** Maps Phase strings to their constructors */ /** Maps Phase strings to their constructors */
export type PhaseConstructorMap = typeof PHASES; 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 * PhaseManager is responsible for managing the phases in the battle scene
*/ */
export class PhaseManager { export class PhaseManager {
/** PhaseQueue: dequeue/remove the first element to get the next phase */ /** PhaseQueue: dequeue/remove the first element to get the next phase */
public phaseQueue: Phase[] = []; private phaseQueue: Phase[] = [];
public conditionalQueue: Array<[() => boolean, Phase]> = [];
/** PhaseQueuePrepend: is a temp storage of what will be added to PhaseQueue */ /** PhaseQueuePrepend: is a temp storage of what will be added to PhaseQueue */
private phaseQueuePrepend: Phase[] = []; private phaseQueuePrepend: Phase[] = [];
@ -227,18 +233,12 @@ export class PhaseManager {
private phaseQueuePrependSpliceIndex = -1; private phaseQueuePrependSpliceIndex = -1;
private nextCommandPhaseQueue: Phase[] = []; private nextCommandPhaseQueue: Phase[] = [];
/** Storage for {@linkcode PhasePriorityQueue}s which hold phases whose order dynamically changes */ public dynamicQueueManager = new DynamicQueueManager();
private dynamicPhaseQueues: PhasePriorityQueue[];
/** Parallel array to {@linkcode dynamicPhaseQueues} - matches phase types to their queues */
private dynamicPhaseTypes: Constructor<Phase>[];
private currentPhase: Phase | null = null; private currentPhase: Phase | null = null;
private standbyPhase: Phase | null = null; private standbyPhase: Phase | null = null;
constructor() { public turnEnded = false;
this.dynamicPhaseQueues = [new PostSummonPhasePriorityQueue()];
this.dynamicPhaseTypes = [PostSummonPhase];
}
/* Phase Functions */ /* Phase Functions */
getCurrentPhase(): Phase | null { getCurrentPhase(): Phase | null {
@ -249,31 +249,17 @@ export class PhaseManager {
return this.standbyPhase; 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 * Adds a phase to nextCommandPhaseQueue, as long as boolean passed in is false
* @param phase {@linkcode Phase} the phase to add * @param phase {@linkcode Phase} the phase to add
* @param defer boolean on which queue to add to, defaults to false, and adds to phaseQueue * @param defer boolean on which queue to add to, defaults to false, and adds to phaseQueue
*/ */
pushPhase(phase: Phase, defer = false): void { pushPhase(phase: Phase, defer = false): void {
if (this.getDynamicPhaseType(phase) !== undefined) { if (this.isDynamicPhase(phase) && this.dynamicQueueManager.activeQueueExists(phase.phaseName)) {
this.pushDynamicPhase(phase); this.dynamicQueueManager.queueDynamicPhase(phase);
} else { return;
(!defer ? this.phaseQueue : this.nextCommandPhaseQueue).push(phase);
} }
(!defer ? this.phaseQueue : this.nextCommandPhaseQueue).push(phase);
} }
/** /**
@ -281,6 +267,10 @@ export class PhaseManager {
* @param phases {@linkcode Phase} the phase(s) to add * @param phases {@linkcode Phase} the phase(s) to add
*/ */
unshiftPhase(...phases: Phase[]): void { 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) { if (this.phaseQueuePrependSpliceIndex === -1) {
this.phaseQueuePrepend.push(...phases); this.phaseQueuePrepend.push(...phases);
} else { } 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 * Clears all phase-related stuff, including all phase queues, the current and standby phases, and a splice index
*/ */
clearAllPhases(): void { 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); queue.splice(0, queue.length);
} }
this.dynamicPhaseQueues.forEach(queue => queue.clear()); this.dynamicQueueManager.clearQueues();
this.currentPhase = null; this.currentPhase = null;
this.standbyPhase = null; this.standbyPhase = null;
this.turnEnded = false;
this.clearPhaseQueueSplice(); this.clearPhaseQueueSplice();
} }
@ -345,32 +336,21 @@ export class PhaseManager {
} }
} }
} }
if (!this.phaseQueue.length) {
this.populatePhaseQueue(); this.queueDynamicPhasesAtFront();
// Clear the conditionalQueue if there are no phases left in the phaseQueue
this.conditionalQueue = []; 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; 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) { if (this.currentPhase) {
console.log(`%cStart Phase ${this.currentPhase.constructor.name}`, "color:green;"); console.log(`%cStart Phase ${this.currentPhase.constructor.name}`, "color:green;");
this.currentPhase.start(); this.currentPhase.start();
@ -390,17 +370,32 @@ export class PhaseManager {
return true; 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. * 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 * @param phaseFilter filter function to use to find the wanted phase
* @returns the found phase or undefined if none found * @returns the found phase or undefined if none found
*/ */
findPhase<P extends Phase = Phase>(phaseFilter: (phase: P) => boolean): P | undefined { findPhase<P extends PhaseString>(
return this.phaseQueue.find(phaseFilter) as P | undefined; 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); const phaseIndex = this.phaseQueue.findIndex(phaseFilter);
if (phaseIndex > -1) { if (phaseIndex > -1) {
this.phaseQueue[phaseIndex] = phase; this.phaseQueue[phaseIndex] = phase;
@ -409,20 +404,23 @@ export class PhaseManager {
return false; 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); const phaseIndex = this.phaseQueue.findIndex(phaseFilter);
if (phaseIndex > -1) { if (phaseIndex > -1) {
this.phaseQueue.splice(phaseIndex, 1); this.phaseQueue.splice(phaseIndex, 1);
return true; 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. * 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 * @param phaseFilter filter function
*/ */
tryRemoveUnshiftedPhase(phaseFilter: (phase: Phase) => boolean): boolean { tryRemoveUnshiftedPhase(phaseFilter: PhaseConditionFunc): boolean {
const phaseIndex = this.phaseQueuePrepend.findIndex(phaseFilter); const phaseIndex = this.phaseQueuePrepend.findIndex(phaseFilter);
if (phaseIndex > -1) { if (phaseIndex > -1) {
this.phaseQueuePrepend.splice(phaseIndex, 1); this.phaseQueuePrepend.splice(phaseIndex, 1);
@ -431,22 +429,27 @@ export class PhaseManager {
return false; 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() * 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 phase - The phase to be added
* @param targetPhase - The phase to search for in phaseQueue * @param targetPhase - The phase to search for in phaseQueue
* @returns boolean if a targetPhase was found and added * @returns boolean if a targetPhase was found and added
*/ */
prependToPhase(phase: Phase | Phase[], targetPhase: PhaseString): boolean { prependToPhase(phase: Phase, targetPhase: StaticPhaseString): boolean {
phase = coerceArray(phase);
const target = PHASES[targetPhase]; const target = PHASES[targetPhase];
const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof target); const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof target);
if (targetIndex !== -1) { if (targetIndex !== -1) {
this.phaseQueue.splice(targetIndex, 0, ...phase); this.phaseQueue.splice(targetIndex, 0, phase);
return true; return true;
} }
this.unshiftPhase(...phase); this.unshiftPhase(phase);
return false; return false;
} }
@ -457,81 +460,18 @@ export class PhaseManager {
* @param condition Condition the target phase must meet to be appended to * @param condition Condition the target phase must meet to be appended to
* @returns `true` if a `targetPhase` was found to append to * @returns `true` if a `targetPhase` was found to append to
*/ */
appendToPhase(phase: Phase | Phase[], targetPhase: PhaseString, condition?: (p: Phase) => boolean): boolean { appendToPhase(phase: Phase, targetPhase: StaticPhaseString, condition?: PhaseConditionFunc): boolean {
phase = coerceArray(phase);
const target = PHASES[targetPhase]; const target = PHASES[targetPhase];
const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof target && (!condition || condition(ph))); const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof target && (!condition || condition(ph)));
if (targetIndex !== -1 && this.phaseQueue.length > targetIndex) { if (targetIndex !== -1 && this.phaseQueue.length > targetIndex) {
this.phaseQueue.splice(targetIndex + 1, 0, ...phase); this.phaseQueue.splice(targetIndex + 1, 0, phase);
return true; return true;
} }
this.unshiftPhase(...phase); this.unshiftPhase(phase);
return false; 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 * Adds a MessagePhase, either to PhaseQueuePrepend or nextCommandPhaseQueue
* @param message - string for MessagePhase * @param message - string for MessagePhase
@ -582,7 +522,10 @@ export class PhaseManager {
/** /**
* Moves everything from nextCommandPhaseQueue to phaseQueue (keeping order) * 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) { if (this.nextCommandPhaseQueue.length) {
this.phaseQueue.push(...this.nextCommandPhaseQueue); this.phaseQueue.push(...this.nextCommandPhaseQueue);
this.nextCommandPhaseQueue.splice(0, this.nextCommandPhaseQueue.length); this.nextCommandPhaseQueue.splice(0, this.nextCommandPhaseQueue.length);
@ -637,7 +580,7 @@ export class PhaseManager {
* @returns `true` if a `targetPhase` was found to prepend to * @returns `true` if a `targetPhase` was found to prepend to
*/ */
public prependNewToPhase<T extends PhaseString>( public prependNewToPhase<T extends PhaseString>(
targetPhase: PhaseString, targetPhase: StaticPhaseString,
phase: T, phase: T,
...args: ConstructorParameters<PhaseConstructorMap[T]> ...args: ConstructorParameters<PhaseConstructorMap[T]>
): boolean { ): boolean {
@ -653,17 +596,62 @@ export class PhaseManager {
* @returns `true` if a `targetPhase` was found to append to * @returns `true` if a `targetPhase` was found to append to
*/ */
public appendNewToPhase<T extends PhaseString>( public appendNewToPhase<T extends PhaseString>(
targetPhase: PhaseString, targetPhase: StaticPhaseString,
phase: T, phase: T,
...args: ConstructorParameters<PhaseConstructorMap[T]> ...args: ConstructorParameters<PhaseConstructorMap[T]>
): boolean { ): boolean {
return this.appendToPhase(this.create(phase, ...args), targetPhase); return this.appendToPhase(this.create(phase, ...args), targetPhase);
} }
public startNewDynamicPhase<T extends PhaseString>( public forceMoveNext(phaseCondition: PhaseConditionFunc) {
phase: T, this.dynamicQueueManager.setMoveTimingModifier(phaseCondition, MovePhaseTimingModifier.FIRST);
...args: ConstructorParameters<PhaseConstructorMap[T]> }
): void {
this.startDynamicPhase(this.create(phase, ...args)); 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(); super.start();
// cull any extra `BattleEnd` phases from the queue. // cull any extra `BattleEnd` phases from the queue.
globalScene.phaseManager.phaseQueue = globalScene.phaseManager.phaseQueue.filter(phase => { this.isVictory ||= globalScene.phaseManager.hasPhaseOfType(
if (phase.is("BattleEndPhase")) { "BattleEndPhase",
this.isVictory ||= phase.isVictory; (phase: BattleEndPhase) => phase.isVictory,
return false; );
} globalScene.phaseManager.removeAllPhasesOfType("BattleEndPhase");
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;
})
) {}
globalScene.gameData.gameStats.battles++; globalScene.gameData.gameStats.battles++;
if ( if (

View File

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

View File

@ -225,7 +225,7 @@ export class EggHatchPhase extends Phase {
} }
end() { end() {
if (globalScene.phaseManager.findPhase(p => p.is("EggHatchPhase"))) { if (globalScene.phaseManager.findPhase("EggHatchPhase")) {
this.eggHatchHandler.clear(); this.eggHatchHandler.clear();
} else { } else {
globalScene.time.delayedCall(250, () => globalScene.setModifiersVisible(true)); 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)) { 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); const ivScannerModifier = globalScene.findModifier(m => m instanceof IvScannerModifier);
if (ivScannerModifier) { if (ivScannerModifier) {
enemyField.map(p => globalScene.phaseManager.pushNew("ScanIvsPhase", p.getBattlerIndex())); 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()); 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) .filter(p => p.isActive() && !p.isOnField() && p.trainerSlot === (pokemon as EnemyPokemon).trainerSlot)
.length; .length;
if (hasReservePartyMember) { 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 { isFieldTargeted } from "#app/data/moves/move-utils";
import { DamageAchv } from "#app/system/achv"; import { DamageAchv } from "#app/system/achv";
import { isVirtual, isReflected, MoveUseMode } from "#enums/move-use-mode"; import { isVirtual, isReflected, MoveUseMode } from "#enums/move-use-mode";
import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
export type HitCheckEntry = [HitCheckResult, TypeDamageMultiplier]; 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 user - The {@linkcode Pokemon} using this phase's invoked move
* @param target - The {@linkcode Pokemon} that is reflecting the move * @param target - The {@linkcode Pokemon} that is reflecting the move
* TODO: Rework this to use `onApply` of Magic Coat * TODO: Rework this to use `onApply` of Magic Coat
@ -167,24 +168,21 @@ export class MoveEffectPhase extends PokemonPhase {
: [user.getBattlerIndex()]; : [user.getBattlerIndex()];
// TODO: ability displays should be handled by the ability // TODO: ability displays should be handled by the ability
if (!target.getTag(BattlerTagType.MAGIC_COAT)) { if (!target.getTag(BattlerTagType.MAGIC_COAT)) {
this.queuedPhases.push( globalScene.phaseManager.unshiftNew(
globalScene.phaseManager.create( "ShowAbilityPhase",
"ShowAbilityPhase", target.getBattlerIndex(),
target.getBattlerIndex(), target.getPassiveAbility().hasAttr("ReflectStatusMoveAbAttr"),
target.getPassiveAbility().hasAttr("ReflectStatusMoveAbAttr"),
),
); );
this.queuedPhases.push(globalScene.phaseManager.create("HideAbilityPhase")); globalScene.phaseManager.unshiftNew("HideAbilityPhase");
} }
this.queuedPhases.push( globalScene.phaseManager.pushNew(
globalScene.phaseManager.create( "MovePhase",
"MovePhase", target,
target, newTargets,
newTargets, new PokemonMove(this.move.id),
new PokemonMove(this.move.id), MoveUseMode.REFLECTED,
MoveUseMode.REFLECTED, MovePhaseTimingModifier.FIRST,
),
); );
} }
@ -376,9 +374,6 @@ export class MoveEffectPhase extends PokemonPhase {
return; return;
} }
if (this.queuedPhases.length) {
globalScene.phaseManager.appendToPhase(this.queuedPhases, "MoveEndPhase");
}
const moveType = user.getMoveType(this.move, true); const moveType = user.getMoveType(this.move, true);
if (this.move.category !== MoveCategory.STATUS && !user.stellarTypesBoosted.includes(moveType)) { if (this.move.category !== MoveCategory.STATUS && !user.stellarTypesBoosted.includes(moveType)) {
user.stellarTypesBoosted.push(moveType); user.stellarTypesBoosted.push(moveType);

View File

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

View File

@ -18,7 +18,6 @@ import type Pokemon from "#app/field/pokemon";
import { MoveResult } from "#enums/move-result"; import { MoveResult } from "#enums/move-result";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import Overrides from "#app/overrides"; import Overrides from "#app/overrides";
import { BattlePhase } from "#app/phases/battle-phase";
import { enumValueToKey, NumberHolder } from "#app/utils/common"; import { enumValueToKey, NumberHolder } from "#app/utils/common";
import { AbilityId } from "#enums/ability-id"; import { AbilityId } from "#enums/ability-id";
import { ArenaTagType } from "#enums/arena-tag-type"; import { ArenaTagType } from "#enums/arena-tag-type";
@ -29,14 +28,16 @@ import i18next from "i18next";
import { getTerrainBlockMessage } from "#app/data/terrain"; import { getTerrainBlockMessage } from "#app/data/terrain";
import { isVirtual, isIgnorePP, isReflected, MoveUseMode, isIgnoreStatus } from "#enums/move-use-mode"; import { isVirtual, isIgnorePP, isReflected, MoveUseMode, isIgnoreStatus } from "#enums/move-use-mode";
import { frenzyMissFunc } from "#app/data/moves/move-utils"; 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"; public readonly phaseName = "MovePhase";
protected _pokemon: Pokemon; protected _pokemon: Pokemon;
protected _move: PokemonMove; protected _move: PokemonMove;
protected _targets: BattlerIndex[]; protected _targets: BattlerIndex[];
public readonly useMode: MoveUseMode; // Made public for quash public readonly useMode: MoveUseMode; // Made public for quash
protected forcedLast: boolean; protected _timingModifier: MovePhaseTimingModifier;
/** Whether the current move should fail but still use PP */ /** Whether the current move should fail but still use PP */
protected failed = false; protected failed = false;
@ -56,7 +57,7 @@ export class MovePhase extends BattlePhase {
return this._move; return this._move;
} }
protected set move(move: PokemonMove) { public set move(move: PokemonMove) {
this._move = move; this._move = move;
} }
@ -68,6 +69,14 @@ export class MovePhase extends BattlePhase {
this._targets = targets; 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. * Create a new MovePhase for using moves.
* @param pokemon - The {@linkcode Pokemon} using the move * @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`. * 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` * @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) { constructor(pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, useMode: MoveUseMode, timingModifier = MovePhaseTimingModifier.NORMAL) {
super(); super(pokemon.getBattlerIndex());
this.pokemon = pokemon; this.pokemon = pokemon;
this.targets = targets; this.targets = targets;
this.move = move; this.move = move;
this.useMode = useMode; this.useMode = useMode;
this.forcedLast = forcedLast; this.timingModifier = timingModifier;
} }
/** /**
@ -109,14 +118,6 @@ export class MovePhase extends BattlePhase {
this.cancelled = true; 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 { public start(): void {
super.start(); super.start();

View File

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

View File

@ -6,12 +6,7 @@ export class NewBattlePhase extends BattlePhase {
start() { start() {
super.start(); super.start();
// cull any extra `NewBattle` phases from the queue. globalScene.phaseManager.removeAllPhasesOfType("NewBattlePhase");
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.newBattle(); globalScene.newBattle();

View File

@ -22,4 +22,8 @@ export abstract class PartyMemberPokemonPhase extends FieldPhase {
getPokemon(): Pokemon { getPokemon(): Pokemon {
return this.getParty()[this.partyMemberIndex]; 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(); const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField();
for (const p of field) { for (const p of field) {
applyAbAttrs("CommanderAbAttr", { pokemon: p }); if (p.isActive(true)) {
applyAbAttrs("CommanderAbAttr", { pokemon: p });
}
} }
this.end(); this.end();

View File

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

View File

@ -50,7 +50,7 @@ export class RevivalBlessingPhase extends BattlePhase {
if (slotIndex <= 1) { if (slotIndex <= 1) {
// Revived ally pokemon // Revived ally pokemon
globalScene.phaseManager.unshiftNew( globalScene.phaseManager.unshiftNew(
"SwitchSummonPhase", "StaticSwitchSummonPhase",
SwitchType.SWITCH, SwitchType.SWITCH,
pokemon.getFieldIndex(), pokemon.getFieldIndex(),
slotIndex, slotIndex,
@ -61,7 +61,7 @@ export class RevivalBlessingPhase extends BattlePhase {
} else if (allyPokemon.isFainted()) { } else if (allyPokemon.isFainted()) {
// Revived party pokemon, and ally pokemon is fainted // Revived party pokemon, and ally pokemon is fainted
globalScene.phaseManager.unshiftNew( globalScene.phaseManager.unshiftNew(
"SwitchSummonPhase", "StaticSwitchSummonPhase",
SwitchType.SWITCH, SwitchType.SWITCH,
allyPokemon.getFieldIndex(), allyPokemon.getFieldIndex(),
slotIndex, 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 // Look for any other stat change phases; if this is the last one, do White Herb check
const existingPhase = globalScene.phaseManager.findPhase( const existingPhase = globalScene.phaseManager.findPhase(
p => p.is("StatStageChangePhase") && p.battlerIndex === this.battlerIndex, "StatStageChangePhase",
p => p.battlerIndex === this.battlerIndex,
); );
if (!existingPhase?.is("StatStageChangePhase")) { if (!existingPhase?.is("StatStageChangePhase")) {
// Apply White Herb if needed // Apply White Herb if needed
@ -311,8 +312,8 @@ export class StatStageChangePhase extends PokemonPhase {
if (this.stats.length === 1) { if (this.stats.length === 1) {
while ( while (
(existingPhase = globalScene.phaseManager.findPhase( (existingPhase = globalScene.phaseManager.findPhase(
"StatStageChangePhase",
p => p =>
p.is("StatStageChangePhase") &&
p.battlerIndex === this.battlerIndex && p.battlerIndex === this.battlerIndex &&
p.stats.length === 1 && p.stats.length === 1 &&
p.stats[0] === this.stats[0] && p.stats[0] === this.stats[0] &&
@ -330,8 +331,8 @@ export class StatStageChangePhase extends PokemonPhase {
} }
while ( while (
(existingPhase = globalScene.phaseManager.findPhase( (existingPhase = globalScene.phaseManager.findPhase(
"StatStageChangePhase",
p => p =>
p.is("StatStageChangePhase") &&
p.battlerIndex === this.battlerIndex && p.battlerIndex === this.battlerIndex &&
p.selfTarget === this.selfTarget && p.selfTarget === this.selfTarget &&
accEva.some(s => p.stats.includes(s)) === isAccEva && 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 { export class SummonPhase extends PartyMemberPokemonPhase {
// The union type is needed to keep typescript happy as these phases extend from SummonPhase // 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; private loaded: boolean;
constructor(fieldIndex: number, player = true, loaded = false) { constructor(fieldIndex: number, player = true, loaded = false) {
@ -296,4 +301,8 @@ export class SummonPhase extends PartyMemberPokemonPhase {
super.end(); 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, p => p.is("PostSummonPhase") && p.player && p.fieldIndex === this.fieldIndex,
); );
const switchType = option === PartyOption.PASS_BATON ? SwitchType.BATON_PASS : this.switchType; 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()); 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"; import { SwitchType } from "#enums/switch-type";
export class SwitchSummonPhase extends SummonPhase { export class SwitchSummonPhase extends SummonPhase {
public readonly phaseName: "SwitchSummonPhase" | "ReturnPhase" = "SwitchSummonPhase"; public readonly phaseName: "SwitchSummonPhase" | "ReturnPhase" | "StaticSwitchSummonPhase" = "SwitchSummonPhase";
private readonly switchType: SwitchType; private readonly switchType: SwitchType;
private readonly slotIndex: number; private readonly slotIndex: number;
private readonly doReturn: boolean; private readonly doReturn: boolean;
@ -241,11 +241,11 @@ export class SwitchSummonPhase extends SummonPhase {
globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true); globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true);
// Reverts to weather-based forms when weather suppressors (Cloud Nine/Air Lock) are switched out // Reverts to weather-based forms when weather suppressors (Cloud Nine/Air Lock) are switched out
globalScene.arena.triggerWeatherBasedFormChanges(); globalScene.arena.triggerWeatherBasedFormChanges(pokemon);
} }
queuePostSummon(): void { 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.currentBattle.incrementTurn();
globalScene.eventTarget.dispatchEvent(new TurnEndEvent(globalScene.currentBattle.turn)); globalScene.eventTarget.dispatchEvent(new TurnEndEvent(globalScene.currentBattle.turn));
globalScene.phaseManager.dynamicQueueManager.clearLastTurnOrder();
globalScene.phaseManager.hideAbilityBar(); globalScene.phaseManager.hideAbilityBar();

View File

@ -14,6 +14,7 @@ export class TurnInitPhase extends FieldPhase {
start() { start() {
super.start(); super.start();
globalScene.phaseManager.turnEnded = false;
globalScene.getPlayerField().forEach(p => { globalScene.getPlayerField().forEach(p => {
// If this pokemon is in play and evolved into something illegal under the current challenge, force a switch // If this pokemon is in play and evolved into something illegal under the current challenge, force a switch
if (p.isOnField() && !p.isAllowedInBattle()) { 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 { PokemonMove } from "#app/data/moves/pokemon-move";
import { BypassSpeedChanceModifier } from "#app/modifier/modifier";
import { Command } from "#enums/command"; import { Command } from "#enums/command";
import { randSeedShuffle, BooleanHolder } from "#app/utils/common";
import { FieldPhase } from "./field-phase"; import { FieldPhase } from "./field-phase";
import { BattlerIndex } from "#enums/battler-index"; import type { BattlerIndex } from "#enums/battler-index";
import { TrickRoomTag } from "#app/data/arena-tag";
import { SwitchType } from "#enums/switch-type"; import { SwitchType } from "#enums/switch-type";
import { globalScene } from "#app/global-scene"; 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 { export class TurnStartPhase extends FieldPhase {
public readonly phaseName = "TurnStartPhase"; public readonly phaseName = "TurnStartPhase";
/** /**
* This orders the active Pokemon on the field by speed into an BattlerIndex array and returns that array. * Returns an ordering of the current field based on command priority
* It also checks for Trick Room and reverses the array if it is present. * @returns {@linkcode BattlerIndex[]} the sequence of commands for this turn
* @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
*/ */
getCommandOrder(): BattlerIndex[] { getCommandOrder(): BattlerIndex[] {
let moveOrder = this.getSpeedOrder(); const playerField = globalScene
// The creation of the battlerBypassSpeed object contains checks for the ability Quick Draw and the held item Quick Claw .getPlayerField()
// The ability Mycelium Might disables Quick Claw's activation when using a status move .filter(p => p.isActive())
// This occurs before the main loop because of battles with more than two Pokemon .map(p => p.getBattlerIndex());
const battlerBypassSpeed = {}; const enemyField = globalScene
.getEnemyField()
globalScene.getField(true).forEach(p => { .filter(p => p.isActive())
const bypassSpeed = new BooleanHolder(false); .map(p => p.getBattlerIndex());
const canCheckHeldItems = new BooleanHolder(true); const orderedTargets: BattlerIndex[] = playerField.concat(enemyField);
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;
});
// The function begins sorting orderedTargets based on command priority, move priority, and possible speed bypasses. // 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. // Non-FIGHT commands (SWITCH, BALL, RUN) have a higher command priority and will always occur before any FIGHT commands.
moveOrder = moveOrder.slice(0); orderedTargets.sort((a, b) => {
moveOrder.sort((a, b) => {
const aCommand = globalScene.currentBattle.turnCommands[a]; const aCommand = globalScene.currentBattle.turnCommands[a];
const bCommand = globalScene.currentBattle.turnCommands[b]; const bCommand = globalScene.currentBattle.turnCommands[b];
@ -91,40 +39,14 @@ export class TurnStartPhase extends FieldPhase {
if (bCommand?.command === Command.FIGHT) { if (bCommand?.command === Command.FIGHT) {
return -1; 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. const aIndex = orderedTargets.indexOf(a);
if (battlerBypassSpeed[a].value !== battlerBypassSpeed[b].value) { const bIndex = orderedTargets.indexOf(b);
return battlerBypassSpeed[a].value ? -1 : 1;
}
const aIndex = moveOrder.indexOf(a);
const bIndex = moveOrder.indexOf(b);
return aIndex < bIndex ? -1 : aIndex > bIndex ? 1 : 0; return aIndex < bIndex ? -1 : aIndex > bIndex ? 1 : 0;
}); });
return moveOrder; return orderedTargets;
} }
// TODO: Refactor this alongside `CommandPhase.handleCommand` to use SEPARATE METHODS // TODO: Refactor this alongside `CommandPhase.handleCommand` to use SEPARATE METHODS
@ -133,25 +55,27 @@ export class TurnStartPhase extends FieldPhase {
super.start(); super.start();
const field = globalScene.getField(); const field = globalScene.getField();
const activeField = globalScene.getField(true);
const moveOrder = this.getCommandOrder(); const moveOrder = this.getCommandOrder();
let orderIndex = 0; applyInSpeedOrder(activeField, (p: Pokemon) => {
const preTurnCommand = globalScene.currentBattle.preTurnCommands[p.getBattlerIndex()];
for (const o of this.getSpeedOrder()) {
const pokemon = field[o];
const preTurnCommand = globalScene.currentBattle.preTurnCommands[o];
if (preTurnCommand?.skip) { if (preTurnCommand?.skip) {
continue; return;
} }
switch (preTurnCommand?.command) { switch (preTurnCommand?.command) {
case Command.TERA: case Command.TERA:
globalScene.phaseManager.pushNew("TeraPhase", pokemon); globalScene.phaseManager.pushNew("TeraPhase", p);
} }
} });
const phaseManager = globalScene.phaseManager; const phaseManager = globalScene.phaseManager;
applyInSpeedOrder(activeField, (p: Pokemon) => {
applyAbAttrs("BypassSpeedChanceAbAttr", { pokemon: p });
globalScene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p);
});
for (const o of moveOrder) { for (const o of moveOrder) {
const pokemon = field[o]; const pokemon = field[o];
@ -164,7 +88,6 @@ export class TurnStartPhase extends FieldPhase {
switch (turnCommand?.command) { switch (turnCommand?.command) {
case Command.FIGHT: { case Command.FIGHT: {
const queuedMove = turnCommand.move; const queuedMove = turnCommand.move;
pokemon.turnData.order = orderIndex++;
if (!queuedMove) { if (!queuedMove) {
continue; continue;
} }
@ -172,7 +95,7 @@ export class TurnStartPhase extends FieldPhase {
pokemon.getMoveset().find(m => m.moveId === queuedMove.move && m.ppUsed < m.getMovePp()) ?? pokemon.getMoveset().find(m => m.moveId === queuedMove.move && m.ppUsed < m.getMovePp()) ??
new PokemonMove(queuedMove.move); new PokemonMove(queuedMove.move);
if (move.getMove().hasAttr("MoveHeaderAttr")) { if (move.getMove().hasAttr("MoveHeaderAttr")) {
phaseManager.unshiftNew("MoveHeaderPhase", pokemon, move); phaseManager.pushNew("MoveHeaderPhase", pokemon.getBattlerIndex(), move);
} }
if (pokemon.isPlayer() && turnCommand.cursor === -1) { if (pokemon.isPlayer() && turnCommand.cursor === -1) {
@ -200,7 +123,7 @@ export class TurnStartPhase extends FieldPhase {
case Command.POKEMON: case Command.POKEMON:
{ {
const switchType = turnCommand.args?.[0] ? SwitchType.BATON_PASS : SwitchType.SWITCH; const switchType = turnCommand.args?.[0] ? SwitchType.BATON_PASS : SwitchType.SWITCH;
phaseManager.unshiftNew( phaseManager.pushNew(
"SwitchSummonPhase", "SwitchSummonPhase",
switchType, switchType,
pokemon.getFieldIndex(), 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 * 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 * 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]); await game.classicMode.startBattle([SpeciesId.ORICORIO, SpeciesId.FEEBAS]);
const [oricorio, feebas] = game.scene.getPlayerField(); 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(oricorio, [MoveId.SWORDS_DANCE, MoveId.VICTORY_DANCE, MoveId.SPLASH]);
game.move.changeMoveset(feebas, [MoveId.SWORDS_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"); // feebas uses swords dance
await game.phaseInterceptor.to("MovePhase", false); // oricorio copies 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; 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); expect(currentPhase.move.moveId).toBe(MoveId.SWORDS_DANCE);
await game.phaseInterceptor.to("MoveEndPhase"); // end oricorio's move 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 GameManager from "#test/testUtils/gameManager";
import { AbilityId } from "#enums/ability-id"; import { AbilityId } from "#enums/ability-id";
import { Stat } from "#enums/stat"; 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 () => { it("will move last in its priority bracket and ignore protective abilities", async () => {
await game.classicMode.startBattle([SpeciesId.REGIELEKI]); await game.classicMode.startBattle([SpeciesId.REGIELEKI]);
const enemyPokemon = game.scene.getEnemyPokemon(); const enemy = game.scene.getEnemyPokemon()!;
const playerIndex = game.scene.getPlayerPokemon()?.getBattlerIndex(); const player = game.scene.getPlayerPokemon()!;
const enemyIndex = enemyPokemon?.getBattlerIndex();
game.move.select(MoveId.BABY_DOLL_EYES); game.move.select(MoveId.BABY_DOLL_EYES);
await game.phaseInterceptor.to(TurnStartPhase, false); await game.phaseInterceptor.to("MoveEndPhase", false);
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
const speedOrder = phase.getSpeedOrder();
const commandOrder = phase.getCommandOrder();
// The opponent Pokemon (without Mycelium Might) goes first despite having lower speed than the player Pokemon. // 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. // The player Pokemon (with Mycelium Might) goes last despite having higher speed than the opponent.
expect(speedOrder).toEqual([playerIndex, enemyIndex]); expect(player.hp).not.toEqual(player.getMaxHp());
expect(commandOrder).toEqual([enemyIndex, playerIndex]); await game.phaseInterceptor.to("TurnEndPhase");
await game.phaseInterceptor.to(TurnEndPhase);
// Despite the opponent's ability (Clear Body), its ATK stat stage is still reduced. // 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 () => { 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); game.override.enemyMoveset(MoveId.TACKLE);
await game.classicMode.startBattle([SpeciesId.REGIELEKI]); await game.classicMode.startBattle([SpeciesId.REGIELEKI]);
const enemyPokemon = game.scene.getEnemyPokemon(); const enemy = game.scene.getEnemyPokemon()!;
const playerIndex = game.scene.getPlayerPokemon()?.getBattlerIndex(); const player = game.scene.getPlayerPokemon()!;
const enemyIndex = enemyPokemon?.getBattlerIndex();
game.move.select(MoveId.BABY_DOLL_EYES); game.move.select(MoveId.BABY_DOLL_EYES);
await game.phaseInterceptor.to(TurnStartPhase, false); await game.phaseInterceptor.to("MoveEndPhase", false);
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
const speedOrder = phase.getSpeedOrder();
const commandOrder = phase.getCommandOrder();
// The player Pokemon (with M.M.) goes first because its move is still within a higher priority bracket than its opponent. // 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. // The enemy Pokemon goes second because its move is in a lower priority bracket.
expect(speedOrder).toEqual([playerIndex, enemyIndex]); expect(player.hp).toEqual(player.getMaxHp());
expect(commandOrder).toEqual([playerIndex, enemyIndex]); await game.phaseInterceptor.to("TurnEndPhase");
await game.phaseInterceptor.to(TurnEndPhase);
// Despite the opponent's ability (Clear Body), its ATK stat stage is still reduced. // 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 () => { it("will not affect non-status moves", async () => {
await game.classicMode.startBattle([SpeciesId.REGIELEKI]); await game.classicMode.startBattle([SpeciesId.REGIELEKI]);
const playerIndex = game.scene.getPlayerPokemon()!.getBattlerIndex(); const player = game.scene.getPlayerPokemon()!;
const enemyIndex = game.scene.getEnemyPokemon()!.getBattlerIndex();
game.move.select(MoveId.QUICK_ATTACK); game.move.select(MoveId.QUICK_ATTACK);
await game.phaseInterceptor.to(TurnStartPhase, false); await game.phaseInterceptor.to("MoveEndPhase", false);
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
const speedOrder = phase.getSpeedOrder();
const commandOrder = phase.getCommandOrder();
// The player Pokemon (with M.M.) goes first because it has a higher speed and did not use a status move. // 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. // The enemy Pokemon (without M.M.) goes second because its speed is lower.
// This means that the commandOrder should be identical to the speedOrder // This means that the commandOrder should be identical to the speedOrder
expect(speedOrder).toEqual([playerIndex, enemyIndex]); expect(player.hp).toEqual(player.getMaxHp());
expect(commandOrder).toEqual([playerIndex, enemyIndex]);
}); });
}); });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -166,7 +166,7 @@ describe("Moves - Rage Fist", () => {
// Charizard hit // Charizard hit
game.move.select(MoveId.SPLASH); game.move.select(MoveId.SPLASH);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.toNextTurn(); await game.toNextTurn();
expect(getPartyHitCount()).toEqual([1, 0]); 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. * Used to manually modify Pokemon turn order.
* Note: This *DOES NOT* account for priority. * Note: This *DOES NOT* account for priority.
* @param order - The turn order to set as an array of {@linkcode BattlerIndex}es. * @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> { async setTurnOrder(order: BattlerIndex[]): Promise<void> {
await this.phaseInterceptor.to(TurnStartPhase, false); await this.phaseInterceptor.to(TurnStartPhase, false);
vi.spyOn(this.scene.phaseManager.getCurrentPhase() as TurnStartPhase, "getSpeedOrder").mockReturnValue(order); this.scene.phaseManager.dynamicQueueManager.setMoveOrder(order);
} }
/** /**