mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-09-23 06:53:27 +02:00
Merge remote-tracking branch 'upstream/beta' into todo-test-enable
This commit is contained in:
commit
b6c4e4ed80
@ -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 { ObjectValues } from "#types/type-helpers";
|
||||
|
||||
@ -24,3 +26,14 @@ export type PhaseClass = ObjectValues<PhaseConstructorMap>;
|
||||
* Union type of all phase names as strings.
|
||||
*/
|
||||
export type PhaseString = keyof PhaseMap;
|
||||
|
||||
/** Type for predicate functions operating on a specific type of {@linkcode Phase}. */
|
||||
|
||||
export type PhaseConditionFunc<T extends PhaseString> = (phase: PhaseMap[T]) => boolean;
|
||||
|
||||
/**
|
||||
* Interface type representing the assumption that all phases with pokemon associated are dynamic
|
||||
*/
|
||||
export interface DynamicPhase extends Phase {
|
||||
getPokemon(): Pokemon;
|
||||
}
|
||||
|
@ -104,7 +104,6 @@ import {
|
||||
import { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
|
||||
import { MysteryEncounterSaveData } from "#mystery-encounters/mystery-encounter-save-data";
|
||||
import { allMysteryEncounters, mysteryEncountersByBiome } from "#mystery-encounters/mystery-encounters";
|
||||
import type { MovePhase } from "#phases/move-phase";
|
||||
import { expSpriteKeys } from "#sprites/sprite-keys";
|
||||
import { hasExpSprite } from "#sprites/sprite-utils";
|
||||
import type { Variant } from "#sprites/variant";
|
||||
@ -787,12 +786,14 @@ export class BattleScene extends SceneBase {
|
||||
|
||||
/**
|
||||
* Returns an array of EnemyPokemon of length 1 or 2 depending on if in a double battle or not.
|
||||
* Does not actually check if the pokemon are on the field or not.
|
||||
* @param active - (Default `false`) Whether to consider only {@linkcode Pokemon.isActive | active} on-field pokemon
|
||||
* @returns array of {@linkcode EnemyPokemon}
|
||||
*/
|
||||
public getEnemyField(): EnemyPokemon[] {
|
||||
public getEnemyField(active = false): EnemyPokemon[] {
|
||||
const party = this.getEnemyParty();
|
||||
return party.slice(0, Math.min(party.length, this.currentBattle?.double ? 2 : 1));
|
||||
return party
|
||||
.slice(0, Math.min(party.length, this.currentBattle?.double ? 2 : 1))
|
||||
.filter(p => !active || p.isActive());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -817,25 +818,7 @@ export class BattleScene extends SceneBase {
|
||||
* @param allyPokemon - The {@linkcode Pokemon} allied with the removed Pokemon; will have moves redirected to it
|
||||
*/
|
||||
redirectPokemonMoves(removedPokemon: Pokemon, allyPokemon: Pokemon): void {
|
||||
// failsafe: if not a double battle just return
|
||||
if (this.currentBattle.double === false) {
|
||||
return;
|
||||
}
|
||||
if (allyPokemon?.isActive(true)) {
|
||||
let targetingMovePhase: MovePhase;
|
||||
do {
|
||||
targetingMovePhase = this.phaseManager.findPhase(
|
||||
mp =>
|
||||
mp.is("MovePhase")
|
||||
&& mp.targets.length === 1
|
||||
&& mp.targets[0] === removedPokemon.getBattlerIndex()
|
||||
&& mp.pokemon.isPlayer() !== allyPokemon.isPlayer(),
|
||||
) as MovePhase;
|
||||
if (targetingMovePhase && targetingMovePhase.targets[0] !== allyPokemon.getBattlerIndex()) {
|
||||
targetingMovePhase.targets[0] = allyPokemon.getBattlerIndex();
|
||||
}
|
||||
} while (targetingMovePhase);
|
||||
}
|
||||
this.phaseManager.redirectMoves(removedPokemon, allyPokemon);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1433,7 +1416,7 @@ export class BattleScene extends SceneBase {
|
||||
}
|
||||
|
||||
if (lastBattle?.double && !newDouble) {
|
||||
this.phaseManager.tryRemovePhase((p: Phase) => p.is("SwitchPhase"));
|
||||
this.phaseManager.tryRemovePhase("SwitchPhase");
|
||||
for (const p of this.getPlayerField()) {
|
||||
p.lapseTag(BattlerTagType.COMMANDED);
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ import { CommonAnim } from "#enums/move-anims-common";
|
||||
import { MoveCategory } from "#enums/move-category";
|
||||
import { MoveFlags } from "#enums/move-flags";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { MoveTarget } from "#enums/move-target";
|
||||
import { MoveUseMode } from "#enums/move-use-mode";
|
||||
@ -2558,7 +2559,7 @@ export class PostIntimidateStatStageChangeAbAttr extends AbAttr {
|
||||
|
||||
override apply({ pokemon, simulated, cancelled }: AbAttrParamsWithCancel): void {
|
||||
if (!simulated) {
|
||||
globalScene.phaseManager.pushNew(
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"StatStageChangePhase",
|
||||
pokemon.getBattlerIndex(),
|
||||
false,
|
||||
@ -3243,6 +3244,7 @@ export class CommanderAbAttr extends AbAttr {
|
||||
return (
|
||||
globalScene.currentBattle?.double
|
||||
&& ally != null
|
||||
&& ally.isActive(true)
|
||||
&& ally.species.speciesId === SpeciesId.DONDOZO
|
||||
&& !(ally.isFainted() || ally.getTag(BattlerTagType.COMMANDED))
|
||||
);
|
||||
@ -3257,7 +3259,7 @@ export class CommanderAbAttr extends AbAttr {
|
||||
// Apply boosts from this effect to the ally Dondozo
|
||||
pokemon.getAlly()?.addTag(BattlerTagType.COMMANDED, 0, MoveId.NONE, pokemon.id);
|
||||
// Cancel the source Pokemon's next move (if a move is queued)
|
||||
globalScene.phaseManager.tryRemovePhase(phase => phase.is("MovePhase") && phase.pokemon === pokemon);
|
||||
globalScene.phaseManager.tryRemovePhase("MovePhase", phase => phase.pokemon === pokemon);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5007,7 +5009,14 @@ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr {
|
||||
// If the move is an AttackMove or a StatusMove the Dancer must replicate the move on the source of the Dance
|
||||
if (move.getMove().is("AttackMove") || move.getMove().is("StatusMove")) {
|
||||
const target = this.getTarget(pokemon, source, targets);
|
||||
globalScene.phaseManager.unshiftNew("MovePhase", pokemon, target, move, MoveUseMode.INDIRECT);
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"MovePhase",
|
||||
pokemon,
|
||||
target,
|
||||
move,
|
||||
MoveUseMode.INDIRECT,
|
||||
MovePhaseTimingModifier.FIRST,
|
||||
);
|
||||
} else if (move.getMove().is("SelfStatusMove")) {
|
||||
// If the move is a SelfStatusMove (ie. Swords Dance) the Dancer should replicate it on itself
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
@ -5016,6 +5025,7 @@ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr {
|
||||
[pokemon.getBattlerIndex()],
|
||||
move,
|
||||
MoveUseMode.INDIRECT,
|
||||
MovePhaseTimingModifier.FIRST,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -6040,11 +6050,6 @@ export class IllusionPostBattleAbAttr extends PostBattleAbAttr {
|
||||
}
|
||||
}
|
||||
|
||||
export interface BypassSpeedChanceAbAttrParams extends AbAttrBaseParams {
|
||||
/** Holds whether the speed check is bypassed after ability application */
|
||||
bypass: BooleanHolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* If a Pokémon with this Ability selects a damaging move, it has a 30% chance of going first in its priority bracket. If the Ability activates, this is announced at the start of the turn (after move selection).
|
||||
* @sealed
|
||||
@ -6060,26 +6065,28 @@ export class BypassSpeedChanceAbAttr extends AbAttr {
|
||||
this.chance = chance;
|
||||
}
|
||||
|
||||
override canApply({ bypass, simulated, pokemon }: BypassSpeedChanceAbAttrParams): boolean {
|
||||
override canApply({ simulated, pokemon }: AbAttrBaseParams): boolean {
|
||||
// TODO: Consider whether we can move the simulated check to the `apply` method
|
||||
// May be difficult as we likely do not want to modify the randBattleSeed
|
||||
const turnCommand = globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()];
|
||||
const isCommandFight = turnCommand?.command === Command.FIGHT;
|
||||
const move = turnCommand?.move?.move ? allMoves[turnCommand.move.move] : null;
|
||||
const isDamageMove = move?.category === MoveCategory.PHYSICAL || move?.category === MoveCategory.SPECIAL;
|
||||
return (
|
||||
!simulated && !bypass.value && pokemon.randBattleSeedInt(100) < this.chance && isCommandFight && isDamageMove
|
||||
!simulated
|
||||
&& pokemon.randBattleSeedInt(100) < this.chance
|
||||
&& isDamageMove
|
||||
&& pokemon.canAddTag(BattlerTagType.BYPASS_SPEED)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* bypass move order in their priority bracket when pokemon choose damaging move
|
||||
*/
|
||||
override apply({ bypass }: BypassSpeedChanceAbAttrParams): void {
|
||||
bypass.value = true;
|
||||
override apply({ pokemon }: AbAttrBaseParams): void {
|
||||
pokemon.addTag(BattlerTagType.BYPASS_SPEED);
|
||||
}
|
||||
|
||||
override getTriggerMessage({ pokemon }: BypassSpeedChanceAbAttrParams, _abilityName: string): string {
|
||||
override getTriggerMessage({ pokemon }: AbAttrBaseParams, _abilityName: string): string {
|
||||
return i18next.t("abilityTriggers:quickDraw", { pokemonName: getPokemonNameWithAffix(pokemon) });
|
||||
}
|
||||
}
|
||||
@ -6087,8 +6094,6 @@ export class BypassSpeedChanceAbAttr extends AbAttr {
|
||||
export interface PreventBypassSpeedChanceAbAttrParams extends AbAttrBaseParams {
|
||||
/** Holds whether the speed check is bypassed after ability application */
|
||||
bypass: BooleanHolder;
|
||||
/** Holds whether the Pokemon can check held items for Quick Claw's effects */
|
||||
canCheckHeldItems: BooleanHolder;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -6115,9 +6120,8 @@ export class PreventBypassSpeedChanceAbAttr extends AbAttr {
|
||||
return isCommandFight && this.condition(pokemon, move!);
|
||||
}
|
||||
|
||||
override apply({ bypass, canCheckHeldItems }: PreventBypassSpeedChanceAbAttrParams): void {
|
||||
override apply({ bypass }: PreventBypassSpeedChanceAbAttrParams): void {
|
||||
bypass.value = false;
|
||||
canCheckHeldItems.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -6215,8 +6219,7 @@ class ForceSwitchOutHelper {
|
||||
|
||||
if (switchOutTarget.hp > 0) {
|
||||
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
|
||||
globalScene.phaseManager.prependNewToPhase(
|
||||
"MoveEndPhase",
|
||||
globalScene.phaseManager.queueDeferred(
|
||||
"SwitchPhase",
|
||||
this.switchType,
|
||||
switchOutTarget.getFieldIndex(),
|
||||
@ -6238,8 +6241,7 @@ class ForceSwitchOutHelper {
|
||||
const summonIndex = globalScene.currentBattle.trainer
|
||||
? globalScene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot)
|
||||
: 0;
|
||||
globalScene.phaseManager.prependNewToPhase(
|
||||
"MoveEndPhase",
|
||||
globalScene.phaseManager.queueDeferred(
|
||||
"SwitchSummonPhase",
|
||||
this.switchType,
|
||||
switchOutTarget.getFieldIndex(),
|
||||
@ -7173,7 +7175,7 @@ export function initAbilities() {
|
||||
new Ability(AbilityId.ANALYTIC, 5)
|
||||
.attr(MovePowerBoostAbAttr, (user) =>
|
||||
// Boost power if all other Pokemon have already moved (no other moves are slated to execute)
|
||||
!globalScene.phaseManager.findPhase((phase) => phase.is("MovePhase") && phase.pokemon.id !== user?.id),
|
||||
!globalScene.phaseManager.hasPhaseOfType("MovePhase", phase => phase.pokemon.id !== user?.id),
|
||||
1.3),
|
||||
new Ability(AbilityId.ILLUSION, 5)
|
||||
// The Pokemon generate an illusion if it's available
|
||||
|
@ -74,7 +74,6 @@ function applyAbAttrsInternal<T extends CallableAbAttrString>(
|
||||
for (const passive of [false, true]) {
|
||||
params.passive = passive;
|
||||
applySingleAbAttrs(attrType, params, gainedMidTurn, messages);
|
||||
globalScene.phaseManager.clearPhaseQueueSplice();
|
||||
}
|
||||
// We need to restore passive to its original state in the case that it was undefined on entry
|
||||
// this is necessary in case this method is called with an object that is reused.
|
||||
|
@ -609,17 +609,7 @@ export class ShellTrapTag extends BattlerTag {
|
||||
|
||||
// Trap should only be triggered by opponent's Physical moves
|
||||
if (phaseData?.move.category === MoveCategory.PHYSICAL && pokemon.isOpponent(phaseData.attacker)) {
|
||||
const shellTrapPhaseIndex = globalScene.phaseManager.phaseQueue.findIndex(
|
||||
phase => phase.is("MovePhase") && phase.pokemon === pokemon,
|
||||
);
|
||||
const firstMovePhaseIndex = globalScene.phaseManager.phaseQueue.findIndex(phase => phase.is("MovePhase"));
|
||||
|
||||
// Only shift MovePhase timing if it's not already next up
|
||||
if (shellTrapPhaseIndex !== -1 && shellTrapPhaseIndex !== firstMovePhaseIndex) {
|
||||
const shellTrapMovePhase = globalScene.phaseManager.phaseQueue.splice(shellTrapPhaseIndex, 1)[0];
|
||||
globalScene.phaseManager.prependToPhase(shellTrapMovePhase, "MovePhase");
|
||||
}
|
||||
|
||||
globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === pokemon);
|
||||
this.activated = true;
|
||||
}
|
||||
|
||||
@ -1295,8 +1285,8 @@ export class EncoreTag extends MoveRestrictionBattlerTag {
|
||||
|
||||
// If the target has not moved yet,
|
||||
// replace their upcoming move with the encored move against randomized targets
|
||||
const movePhase = globalScene.phaseManager.findPhase(
|
||||
(m): m is MovePhase => m.is("MovePhase") && m.pokemon === pokemon,
|
||||
const movePhase = globalScene.phaseManager.getMovePhase(
|
||||
m => m.pokemon === pokemon,
|
||||
);
|
||||
if (!movePhase) {
|
||||
return;
|
||||
@ -1314,7 +1304,7 @@ export class EncoreTag extends MoveRestrictionBattlerTag {
|
||||
? moveTargets.targets
|
||||
: [moveTargets.targets[pokemon.randBattleSeedInt(moveTargets.targets.length)]];
|
||||
|
||||
globalScene.phaseManager.tryReplacePhase(
|
||||
globalScene.phaseManager.changePhaseMove(
|
||||
m => m.is("MovePhase") && m.pokemon === pokemon,
|
||||
globalScene.phaseManager.create(
|
||||
"MovePhase",
|
||||
@ -3609,6 +3599,25 @@ export class GrudgeTag extends SerializableBattlerTag {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
public override readonly tagType = BattlerTagType.BYPASS_SPEED;
|
||||
|
||||
constructor() {
|
||||
super(BattlerTagType.BYPASS_SPEED, BattlerTagLapseType.TURN_END, 1);
|
||||
}
|
||||
|
||||
override canAdd(pokemon: Pokemon): boolean {
|
||||
const bypass = new BooleanHolder(true);
|
||||
applyAbAttrs("PreventBypassSpeedChanceAbAttr", { pokemon, bypass });
|
||||
return bypass.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
|
||||
*/
|
||||
@ -3911,6 +3920,8 @@ export function getBattlerTag(
|
||||
return new MagicCoatTag();
|
||||
case BattlerTagType.SUPREME_OVERLORD:
|
||||
return new SupremeOverlordTag();
|
||||
case BattlerTagType.BYPASS_SPEED:
|
||||
return new BypassSpeedTag();
|
||||
}
|
||||
}
|
||||
|
||||
@ -4046,4 +4057,5 @@ export type BattlerTagTypeMap = {
|
||||
[BattlerTagType.PSYCHO_SHIFT]: PsychoShiftTag;
|
||||
[BattlerTagType.MAGIC_COAT]: MagicCoatTag;
|
||||
[BattlerTagType.SUPREME_OVERLORD]: SupremeOverlordTag;
|
||||
[BattlerTagType.BYPASS_SPEED]: BypassSpeedTag;
|
||||
};
|
||||
|
@ -343,5 +343,5 @@ export const invalidInstructMoves: ReadonlySet<MoveId> = new Set([
|
||||
MoveId.TRANSFORM,
|
||||
MoveId.MIMIC,
|
||||
MoveId.STRUGGLE,
|
||||
// TODO: Add Max/G-Max/Z-Move blockage if or when they are implemented
|
||||
// NB: Add Max/G-Max/Z-Move blockage if or when they are implemented
|
||||
]);
|
||||
|
@ -82,10 +82,8 @@ import { applyMoveAttrs } from "#moves/apply-attrs";
|
||||
import { invalidAssistMoves, invalidCopycatMoves, invalidInstructMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSketchMoves, invalidSleepTalkMoves } from "#moves/invalid-moves";
|
||||
import { frenzyMissFunc, getMoveTargets } from "#moves/move-utils";
|
||||
import { PokemonMove } from "#moves/pokemon-move";
|
||||
import { MoveEndPhase } from "#phases/move-end-phase";
|
||||
import { MovePhase } from "#phases/move-phase";
|
||||
import { PokemonHealPhase } from "#phases/pokemon-heal-phase";
|
||||
import { SwitchSummonPhase } from "#phases/switch-summon-phase";
|
||||
import type { AttackMoveResult } from "#types/attack-move-result";
|
||||
import type { Localizable } from "#types/locales";
|
||||
import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString, MoveMessageFunc } from "#types/move-types";
|
||||
@ -95,6 +93,7 @@ import { getEnumValues } from "#utils/enums";
|
||||
import { toCamelCase, toTitleCase } from "#utils/strings";
|
||||
import i18next from "i18next";
|
||||
import { applyChallenges } from "#utils/challenge-utils";
|
||||
import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
|
||||
import type { AbstractConstructor } from "#types/type-helpers";
|
||||
|
||||
/**
|
||||
@ -881,6 +880,10 @@ export abstract class Move implements Localizable {
|
||||
applyMoveAttrs("IncrementMovePriorityAttr", user, null, this, priority);
|
||||
applyAbAttrs("ChangeMovePriorityAbAttr", {pokemon: user, simulated, move: this, priority});
|
||||
|
||||
if (user.getTag(BattlerTagType.BYPASS_SPEED)) {
|
||||
priority.value += 0.2;
|
||||
}
|
||||
|
||||
return priority.value;
|
||||
}
|
||||
|
||||
@ -3288,7 +3291,7 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr {
|
||||
|
||||
const overridden = args[0] as BooleanHolder;
|
||||
|
||||
const allyMovePhase = globalScene.phaseManager.findPhase<MovePhase>((phase) => phase.is("MovePhase") && phase.pokemon.isPlayer() === user.isPlayer());
|
||||
const allyMovePhase = globalScene.phaseManager.getMovePhase((phase) => phase.pokemon.isPlayer() === user.isPlayer());
|
||||
if (allyMovePhase) {
|
||||
const allyMove = allyMovePhase.move.getMove();
|
||||
if (allyMove !== move && allyMove.hasAttr("AwaitCombinedPledgeAttr")) {
|
||||
@ -3301,11 +3304,7 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr {
|
||||
}));
|
||||
|
||||
// Move the ally's MovePhase (if needed) so that the ally moves next
|
||||
const allyMovePhaseIndex = globalScene.phaseManager.phaseQueue.indexOf(allyMovePhase);
|
||||
const firstMovePhaseIndex = globalScene.phaseManager.phaseQueue.findIndex((phase) => phase.is("MovePhase"));
|
||||
if (allyMovePhaseIndex !== firstMovePhaseIndex) {
|
||||
globalScene.phaseManager.prependToPhase(globalScene.phaseManager.phaseQueue.splice(allyMovePhaseIndex, 1)[0], "MovePhase");
|
||||
}
|
||||
globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === user.getAlly());
|
||||
|
||||
overridden.value = true;
|
||||
return true;
|
||||
@ -4540,28 +4539,7 @@ export class LastMoveDoublePowerAttr extends VariablePowerAttr {
|
||||
*/
|
||||
apply(user: Pokemon, _target: Pokemon, _move: Move, args: any[]): boolean {
|
||||
const power = args[0] as NumberHolder;
|
||||
const enemy = user.getOpponent(0);
|
||||
const pokemonActed: Pokemon[] = [];
|
||||
|
||||
if (enemy?.turnData.acted) {
|
||||
pokemonActed.push(enemy);
|
||||
}
|
||||
|
||||
if (globalScene.currentBattle.double) {
|
||||
const userAlly = user.getAlly();
|
||||
const enemyAlly = enemy?.getAlly();
|
||||
|
||||
if (userAlly?.turnData.acted) {
|
||||
pokemonActed.push(userAlly);
|
||||
}
|
||||
if (enemyAlly?.turnData.acted) {
|
||||
pokemonActed.push(enemyAlly);
|
||||
}
|
||||
}
|
||||
|
||||
pokemonActed.sort((a, b) => b.turnData.order - a.turnData.order);
|
||||
|
||||
for (const p of pokemonActed) {
|
||||
for (const p of globalScene.phaseManager.dynamicQueueManager.getLastTurnOrder().slice(0, -1).reverse()) {
|
||||
const [ lastMove ] = p.getLastXMoves(1);
|
||||
if (lastMove.result !== MoveResult.FAIL) {
|
||||
if ((lastMove.result === MoveResult.SUCCESS) && (lastMove.move === this.move)) {
|
||||
@ -4643,20 +4621,13 @@ export class CueNextRoundAttr extends MoveEffectAttr {
|
||||
}
|
||||
|
||||
override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean {
|
||||
const nextRoundPhase = globalScene.phaseManager.findPhase<MovePhase>(phase =>
|
||||
phase.is("MovePhase") && phase.move.moveId === MoveId.ROUND
|
||||
);
|
||||
const nextRoundPhase = globalScene.phaseManager.getMovePhase(phase => phase.move.moveId === MoveId.ROUND);
|
||||
|
||||
if (!nextRoundPhase) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update the phase queue so that the next Pokemon using Round moves next
|
||||
const nextRoundIndex = globalScene.phaseManager.phaseQueue.indexOf(nextRoundPhase);
|
||||
const nextMoveIndex = globalScene.phaseManager.phaseQueue.findIndex(phase => phase.is("MovePhase"));
|
||||
if (nextRoundIndex !== nextMoveIndex) {
|
||||
globalScene.phaseManager.prependToPhase(globalScene.phaseManager.phaseQueue.splice(nextRoundIndex, 1)[0], "MovePhase");
|
||||
}
|
||||
globalScene.phaseManager.forceMoveNext(phase => phase.move.moveId === MoveId.ROUND);
|
||||
|
||||
// Mark the corresponding Pokemon as having "joined the Round" (for doubling power later)
|
||||
nextRoundPhase.pokemon.turnData.joinedRound = true;
|
||||
@ -6309,11 +6280,11 @@ export class RevivalBlessingAttr extends MoveEffectAttr {
|
||||
// Handle cases where revived pokemon needs to get switched in on same turn
|
||||
if (allyPokemon.isFainted() || allyPokemon === pokemon) {
|
||||
// Enemy switch phase should be removed and replaced with the revived pkmn switching in
|
||||
globalScene.phaseManager.tryRemovePhase((phase: SwitchSummonPhase) => phase.is("SwitchSummonPhase") && phase.getPokemon() === pokemon);
|
||||
globalScene.phaseManager.tryRemovePhase("SwitchSummonPhase", phase => phase.getFieldIndex() === slotIndex);
|
||||
// If the pokemon being revived was alive earlier in the turn, cancel its move
|
||||
// (revived pokemon can't move in the turn they're brought back)
|
||||
// TODO: might make sense to move this to `FaintPhase` after checking for Rev Seed (rather than handling it in the move)
|
||||
globalScene.phaseManager.findPhase((phase: MovePhase) => phase.pokemon === pokemon)?.cancel();
|
||||
globalScene.phaseManager.getMovePhase((phase: MovePhase) => phase.pokemon === pokemon)?.cancel();
|
||||
if (user.fieldPosition === FieldPosition.CENTER) {
|
||||
user.setFieldPosition(FieldPosition.LEFT);
|
||||
}
|
||||
@ -6394,8 +6365,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
||||
if (this.switchType === SwitchType.FORCE_SWITCH) {
|
||||
switchOutTarget.leaveField(true);
|
||||
const slotIndex = eligibleNewIndices[user.randBattleSeedInt(eligibleNewIndices.length)];
|
||||
globalScene.phaseManager.prependNewToPhase(
|
||||
"MoveEndPhase",
|
||||
globalScene.phaseManager.queueDeferred(
|
||||
"SwitchSummonPhase",
|
||||
this.switchType,
|
||||
switchOutTarget.getFieldIndex(),
|
||||
@ -6405,7 +6375,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
||||
);
|
||||
} else {
|
||||
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
|
||||
globalScene.phaseManager.prependNewToPhase("MoveEndPhase",
|
||||
globalScene.phaseManager.queueDeferred(
|
||||
"SwitchPhase",
|
||||
this.switchType,
|
||||
switchOutTarget.getFieldIndex(),
|
||||
@ -6434,7 +6404,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
||||
if (this.switchType === SwitchType.FORCE_SWITCH) {
|
||||
switchOutTarget.leaveField(true);
|
||||
const slotIndex = eligibleNewIndices[user.randBattleSeedInt(eligibleNewIndices.length)];
|
||||
globalScene.phaseManager.prependNewToPhase("MoveEndPhase",
|
||||
globalScene.phaseManager.queueDeferred(
|
||||
"SwitchSummonPhase",
|
||||
this.switchType,
|
||||
switchOutTarget.getFieldIndex(),
|
||||
@ -6444,7 +6414,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
||||
);
|
||||
} else {
|
||||
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
|
||||
globalScene.phaseManager.prependNewToPhase("MoveEndPhase",
|
||||
globalScene.phaseManager.queueDeferred(
|
||||
"SwitchSummonPhase",
|
||||
this.switchType,
|
||||
switchOutTarget.getFieldIndex(),
|
||||
@ -6875,7 +6845,7 @@ class CallMoveAttr extends OverrideMoveEffectAttr {
|
||||
: moveTargets.targets[user.randBattleSeedInt(moveTargets.targets.length)]];
|
||||
|
||||
globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", move.id);
|
||||
globalScene.phaseManager.unshiftNew("MovePhase", user, targets, new PokemonMove(move.id), MoveUseMode.FOLLOW_UP);
|
||||
globalScene.phaseManager.unshiftNew("MovePhase", user, targets, new PokemonMove(move.id), MoveUseMode.FOLLOW_UP, MovePhaseTimingModifier.FIRST);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -7107,7 +7077,7 @@ export class NaturePowerAttr extends OverrideMoveEffectAttr {
|
||||
|
||||
// Load the move's animation if we didn't already and unshift a new usage phase
|
||||
globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", moveId);
|
||||
globalScene.phaseManager.unshiftNew("MovePhase", user, [ target.getBattlerIndex() ], new PokemonMove(moveId), MoveUseMode.FOLLOW_UP);
|
||||
globalScene.phaseManager.unshiftNew("MovePhase", user, [ target.getBattlerIndex() ], new PokemonMove(moveId), MoveUseMode.FOLLOW_UP, MovePhaseTimingModifier.FIRST);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -7196,7 +7166,7 @@ export class RepeatMoveAttr extends MoveEffectAttr {
|
||||
targetPokemonName: getPokemonNameWithAffix(target)
|
||||
}));
|
||||
target.turnData.extraTurns++;
|
||||
globalScene.phaseManager.appendNewToPhase("MoveEndPhase", "MovePhase", target, moveTargets, this.movesetMove, MoveUseMode.NORMAL);
|
||||
globalScene.phaseManager.unshiftNew("MovePhase", target, moveTargets, movesetMove, MoveUseMode.NORMAL, MovePhaseTimingModifier.FIRST);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -7907,12 +7877,7 @@ export class AfterYouAttr extends MoveEffectAttr {
|
||||
*/
|
||||
override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
|
||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:afterYou", { targetName: getPokemonNameWithAffix(target) }));
|
||||
|
||||
// Will find next acting phase of the targeted pokémon, delete it and queue it right after us.
|
||||
const targetNextPhase = globalScene.phaseManager.findPhase((phase): phase is MovePhase => phase.is("MovePhase") && phase.pokemon === target);
|
||||
if (targetNextPhase && globalScene.phaseManager.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) {
|
||||
globalScene.phaseManager.prependToPhase(targetNextPhase, "MovePhase");
|
||||
}
|
||||
globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === target);
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -7935,45 +7900,11 @@ export class ForceLastAttr extends MoveEffectAttr {
|
||||
override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
|
||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:forceLast", { targetPokemonName: getPokemonNameWithAffix(target) }));
|
||||
|
||||
// TODO: Refactor this to be more readable and less janky
|
||||
const targetMovePhase = globalScene.phaseManager.findPhase((phase): phase is MovePhase => phase.is("MovePhase") && phase.pokemon === target);
|
||||
if (targetMovePhase && !targetMovePhase.isForcedLast() && globalScene.phaseManager.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) {
|
||||
// Finding the phase to insert the move in front of -
|
||||
// Either the end of the turn or in front of another, slower move which has also been forced last
|
||||
const prependPhase = globalScene.phaseManager.findPhase((phase) =>
|
||||
[ MovePhase, MoveEndPhase ].every(cls => !(phase instanceof cls))
|
||||
|| (phase.is("MovePhase")) && phaseForcedSlower(phase, target, !!globalScene.arena.getTag(ArenaTagType.TRICK_ROOM))
|
||||
);
|
||||
if (prependPhase) {
|
||||
globalScene.phaseManager.phaseQueue.splice(
|
||||
globalScene.phaseManager.phaseQueue.indexOf(prependPhase),
|
||||
0,
|
||||
globalScene.phaseManager.create("MovePhase", target, [ ...targetMovePhase.targets ], targetMovePhase.move, targetMovePhase.useMode, true)
|
||||
);
|
||||
}
|
||||
}
|
||||
globalScene.phaseManager.forceMoveLast((phase: MovePhase) => phase.pokemon === target);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a {@linkcode MovePhase} has been forced last and the corresponding pokemon is slower than {@linkcode target}.
|
||||
|
||||
* TODO:
|
||||
- Make this a class method
|
||||
- Make this look at speed order from TurnStartPhase
|
||||
*/
|
||||
const phaseForcedSlower = (phase: MovePhase, target: Pokemon, trickRoom: boolean): boolean => {
|
||||
let slower: boolean;
|
||||
// quashed pokemon still have speed ties
|
||||
if (phase.pokemon.getEffectiveStat(Stat.SPD) === target.getEffectiveStat(Stat.SPD)) {
|
||||
slower = !!target.randBattleSeedInt(2);
|
||||
} else {
|
||||
slower = !trickRoom ? phase.pokemon.getEffectiveStat(Stat.SPD) < target.getEffectiveStat(Stat.SPD) : phase.pokemon.getEffectiveStat(Stat.SPD) > target.getEffectiveStat(Stat.SPD);
|
||||
}
|
||||
return phase.isForcedLast() && slower;
|
||||
};
|
||||
|
||||
const failOnGravityCondition: MoveConditionFunc = (user, target, move) => !globalScene.arena.getTag(ArenaTagType.GRAVITY);
|
||||
|
||||
const failOnBossCondition: MoveConditionFunc = (user, target, move) => !target.isBossImmune();
|
||||
@ -7997,7 +7928,7 @@ const userSleptOrComatoseCondition: MoveConditionFunc = (user) => user.status?.e
|
||||
|
||||
const targetSleptOrComatoseCondition: MoveConditionFunc = (_user: Pokemon, target: Pokemon, _move: Move) => target.status?.effect === StatusEffect.SLEEP || target.hasAbility(AbilityId.COMATOSE);
|
||||
|
||||
const failIfLastCondition: MoveConditionFunc = () => globalScene.phaseManager.findPhase(phase => phase.is("MovePhase")) !== undefined;
|
||||
const failIfLastCondition: MoveConditionFunc = () => globalScene.phaseManager.hasPhaseOfType("MovePhase");
|
||||
|
||||
const failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => {
|
||||
const party: Pokemon[] = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty();
|
||||
|
@ -414,7 +414,7 @@ function summonPlayerPokemonAnimation(pokemon: PlayerPokemon): Promise<void> {
|
||||
pokemon.resetTurnData();
|
||||
|
||||
globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true);
|
||||
globalScene.phaseManager.pushNew("PostSummonPhase", pokemon.getBattlerIndex());
|
||||
globalScene.phaseManager.unshiftNew("PostSummonPhase", pokemon.getBattlerIndex());
|
||||
resolve();
|
||||
});
|
||||
},
|
||||
|
@ -669,7 +669,6 @@ function onGameOver() {
|
||||
|
||||
// Clear any leftover battle phases
|
||||
globalScene.phaseManager.clearPhaseQueue();
|
||||
globalScene.phaseManager.clearPhaseQueueSplice();
|
||||
|
||||
// Return enemy Pokemon
|
||||
const pokemon = globalScene.getEnemyPokemon();
|
||||
|
@ -738,7 +738,7 @@ export function setEncounterRewards(
|
||||
if (customShopRewards) {
|
||||
globalScene.phaseManager.unshiftNew("SelectModifierPhase", 0, undefined, customShopRewards);
|
||||
} else {
|
||||
globalScene.phaseManager.tryRemovePhase(p => p.is("MysteryEncounterRewardsPhase"));
|
||||
globalScene.phaseManager.removeAllPhasesOfType("MysteryEncounterRewardsPhase");
|
||||
}
|
||||
|
||||
if (eggRewards) {
|
||||
@ -812,8 +812,7 @@ export function leaveEncounterWithoutBattle(
|
||||
encounterMode: MysteryEncounterMode = MysteryEncounterMode.NO_BATTLE,
|
||||
) {
|
||||
globalScene.currentBattle.mysteryEncounter!.encounterMode = encounterMode;
|
||||
globalScene.phaseManager.clearPhaseQueue();
|
||||
globalScene.phaseManager.clearPhaseQueueSplice();
|
||||
globalScene.phaseManager.clearPhaseQueue(true);
|
||||
handleMysteryEncounterVictory(addHealPhase);
|
||||
}
|
||||
|
||||
@ -826,7 +825,7 @@ export function handleMysteryEncounterVictory(addHealPhase = false, doNotContinu
|
||||
const allowedPkm = globalScene.getPlayerParty().filter(pkm => pkm.isAllowedInBattle());
|
||||
|
||||
if (allowedPkm.length === 0) {
|
||||
globalScene.phaseManager.clearPhaseQueue();
|
||||
globalScene.phaseManager.clearPhaseQueue(true);
|
||||
globalScene.phaseManager.unshiftNew("GameOverPhase");
|
||||
return;
|
||||
}
|
||||
@ -869,7 +868,7 @@ export function handleMysteryEncounterBattleFailed(addHealPhase = false, doNotCo
|
||||
const allowedPkm = globalScene.getPlayerParty().filter(pkm => pkm.isAllowedInBattle());
|
||||
|
||||
if (allowedPkm.length === 0) {
|
||||
globalScene.phaseManager.clearPhaseQueue();
|
||||
globalScene.phaseManager.clearPhaseQueue(true);
|
||||
globalScene.phaseManager.unshiftNew("GameOverPhase");
|
||||
return;
|
||||
}
|
||||
|
@ -1,125 +0,0 @@
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import type { Phase } from "#app/phase";
|
||||
import { TrickRoomTag } from "#data/arena-tag";
|
||||
import { DynamicPhaseType } from "#enums/dynamic-phase-type";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { ActivatePriorityQueuePhase } from "#phases/activate-priority-queue-phase";
|
||||
import { PostSummonActivateAbilityPhase } from "#phases/post-summon-activate-ability-phase";
|
||||
import type { PostSummonPhase } from "#phases/post-summon-phase";
|
||||
import { BooleanHolder } from "#utils/common";
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to remove one or more Phases from the current queue.
|
||||
* @param phaseFilter - The function to select phases for removal
|
||||
* @param removeCount - The maximum number of phases to remove, or `all` to remove all matching phases;
|
||||
* default `1`
|
||||
* @returns The number of successfully removed phases
|
||||
* @todo Remove this eventually once the patchwork bug this is used for is fixed
|
||||
*/
|
||||
public tryRemovePhase(phaseFilter: (phase: Phase) => boolean, removeCount: number | "all" = 1): number {
|
||||
if (removeCount === "all") {
|
||||
removeCount = this.queue.length;
|
||||
} else if (removeCount < 1) {
|
||||
return 0;
|
||||
}
|
||||
let numRemoved = 0;
|
||||
|
||||
do {
|
||||
const phaseIndex = this.queue.findIndex(phaseFilter);
|
||||
if (phaseIndex === -1) {
|
||||
break;
|
||||
}
|
||||
this.queue.splice(phaseIndex, 1);
|
||||
numRemoved++;
|
||||
} while (numRemoved < removeCount && this.queue.length > 0);
|
||||
|
||||
return numRemoved;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
182
src/dynamic-queue-manager.ts
Normal file
182
src/dynamic-queue-manager.ts
Normal file
@ -0,0 +1,182 @@
|
||||
import type { DynamicPhase, PhaseConditionFunc, PhaseString } 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 type { MovePhase } from "#app/phases/move-phase";
|
||||
import { MovePhasePriorityQueue } from "#app/queues/move-phase-priority-queue";
|
||||
import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-queue";
|
||||
import { PostSummonPhasePriorityQueue } from "#app/queues/post-summon-phase-priority-queue";
|
||||
import type { PriorityQueue } from "#app/queues/priority-queue";
|
||||
import type { BattlerIndex } from "#enums/battler-index";
|
||||
import type { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
|
||||
|
||||
// TODO: might be easier to define which phases should be dynamic instead
|
||||
/** All phases which have defined a `getPokemon` method but should not be sorted dynamically */
|
||||
const nonDynamicPokemonPhases: readonly PhaseString[] = [
|
||||
"SummonPhase",
|
||||
"CommandPhase",
|
||||
"LearnMovePhase",
|
||||
"MoveEffectPhase",
|
||||
"MoveEndPhase",
|
||||
"FaintPhase",
|
||||
"DamageAnimPhase",
|
||||
"VictoryPhase",
|
||||
"PokemonHealPhase",
|
||||
"WeatherEffectPhase",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* The dynamic queue manager holds priority queues for phases which are queued as dynamic.
|
||||
*
|
||||
* Dynamic phases are generally those which hold a pokemon and are unshifted, not pushed. \
|
||||
* Queues work by sorting their entries in speed order (and possibly with more complex ordering) before each time a phase is popped.
|
||||
*
|
||||
* As the holder, this structure is also used to access and modify queued phases.
|
||||
* This is mostly used in redirection, cancellation, etc. of {@linkcode MovePhase}s.
|
||||
*/
|
||||
export class DynamicQueueManager {
|
||||
/** Maps phase types to their corresponding queues */
|
||||
private readonly dynamicPhaseMap: Map<PhaseString, PriorityQueue<Phase>>;
|
||||
|
||||
constructor() {
|
||||
this.dynamicPhaseMap = new Map();
|
||||
// PostSummon and Move phases have specialized queues
|
||||
this.dynamicPhaseMap.set("PostSummonPhase", new PostSummonPhasePriorityQueue());
|
||||
this.dynamicPhaseMap.set("MovePhase", new MovePhasePriorityQueue());
|
||||
}
|
||||
|
||||
/** Removes all phases from the manager */
|
||||
public clearQueues(): void {
|
||||
for (const queue of this.dynamicPhaseMap.values()) {
|
||||
queue.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new phase to the manager and creates the priority queue for it if one does not exist.
|
||||
* @param phase - The {@linkcode Phase} to add
|
||||
* @returns `true` if the phase was added, or `false` if it is not dynamic
|
||||
*/
|
||||
public queueDynamicPhase<T extends Phase>(phase: T): boolean {
|
||||
if (!this.isDynamicPhase(phase)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.dynamicPhaseMap.has(phase.phaseName)) {
|
||||
// TS can't figure out that T is dynamic at this point, but it does know that `typeof phase` is
|
||||
this.dynamicPhaseMap.set(phase.phaseName, new PokemonPhasePriorityQueue<typeof phase>());
|
||||
}
|
||||
this.dynamicPhaseMap.get(phase.phaseName)?.push(phase);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the highest-priority (generally by speed) {@linkcode Phase} of the specified type
|
||||
* @param type - The {@linkcode PhaseString | type} to pop
|
||||
* @returns The popped {@linkcode Phase}, or `undefined` if none of the specified type exist
|
||||
*/
|
||||
public popNextPhase(type: PhaseString): Phase | undefined {
|
||||
return this.dynamicPhaseMap.get(type)?.pop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if there is a queued dynamic {@linkcode Phase} meeting the conditions
|
||||
* @param type - The {@linkcode PhaseString | type} of phase to search for
|
||||
* @param condition - An optional {@linkcode PhaseConditionFunc} to add conditions to the search
|
||||
* @returns Whether a matching phase exists
|
||||
*/
|
||||
public exists<T extends PhaseString>(type: T, condition?: PhaseConditionFunc<T>): boolean {
|
||||
return !!this.dynamicPhaseMap.get(type)?.has(condition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and removes a single queued {@linkcode Phase}
|
||||
* @param type - The {@linkcode PhaseString | type} of phase to search for
|
||||
* @param phaseFilter - A {@linkcode PhaseConditionFunc} to specify conditions for the phase
|
||||
* @returns Whether a removal occurred
|
||||
*/
|
||||
public removePhase<T extends PhaseString>(type: T, condition?: PhaseConditionFunc<T>): boolean {
|
||||
return !!this.dynamicPhaseMap.get(type)?.remove(condition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the timing modifier of a move (i.e. to force it first or last)
|
||||
* @param condition - A {@linkcode PhaseConditionFunc} to specify conditions for the move
|
||||
* @param modifier - The {@linkcode MovePhaseTimingModifier} to switch the move to
|
||||
*/
|
||||
public setMoveTimingModifier(condition: PhaseConditionFunc<"MovePhase">, modifier: MovePhaseTimingModifier): void {
|
||||
this.getMovePhaseQueue().setTimingModifier(condition, modifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the {@linkcode MovePhase} meeting the condition and changes its move
|
||||
* @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function
|
||||
* @param move - The {@linkcode PokemonMove | move} to use in replacement
|
||||
*/
|
||||
public setMoveForPhase(condition: PhaseConditionFunc<"MovePhase">, move: PokemonMove): void {
|
||||
this.getMovePhaseQueue().setMoveForPhase(condition, move);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects moves which were targeted at a {@linkcode Pokemon} that has been removed
|
||||
* @param removedPokemon - The removed {@linkcode Pokemon}
|
||||
* @param allyPokemon - The ally of the removed pokemon
|
||||
*/
|
||||
public redirectMoves(removedPokemon: Pokemon, allyPokemon: Pokemon): void {
|
||||
this.getMovePhaseQueue().redirectMoves(removedPokemon, allyPokemon);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a {@linkcode MovePhase} meeting the condition
|
||||
* @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function
|
||||
* @returns The MovePhase, or `undefined` if it does not exist
|
||||
*/
|
||||
public getMovePhase(condition: PhaseConditionFunc<"MovePhase">): MovePhase | undefined {
|
||||
return this.getMovePhaseQueue().find(condition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and cancels a {@linkcode MovePhase} meeting the condition
|
||||
* @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function
|
||||
*/
|
||||
public cancelMovePhase(condition: PhaseConditionFunc<"MovePhase">): void {
|
||||
this.getMovePhaseQueue().cancelMove(condition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the move order to a static array rather than a dynamic queue
|
||||
* @param order - The order of {@linkcode BattlerIndex}s
|
||||
*/
|
||||
public setMoveOrder(order: BattlerIndex[]): void {
|
||||
this.getMovePhaseQueue().setMoveOrder(order);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns An in-order array of {@linkcode Pokemon}, representing the turn order as played out in the most recent turn
|
||||
*/
|
||||
public getLastTurnOrder(): Pokemon[] {
|
||||
return this.getMovePhaseQueue().getTurnOrder();
|
||||
}
|
||||
|
||||
/** Clears the stored `Move` turn order */
|
||||
public clearLastTurnOrder(): void {
|
||||
this.getMovePhaseQueue().clearTurnOrder();
|
||||
}
|
||||
|
||||
/** Internal helper to get the {@linkcode MovePhasePriorityQueue} */
|
||||
private getMovePhaseQueue(): MovePhasePriorityQueue {
|
||||
return this.dynamicPhaseMap.get("MovePhase") as MovePhasePriorityQueue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal helper to determine if a phase is dynamic.
|
||||
* @param phase - The {@linkcode Phase} to check
|
||||
* @returns Whether `phase` is dynamic
|
||||
* @privateRemarks
|
||||
* Currently, this checks that `phase` has a `getPokemon` method
|
||||
* and is not blacklisted in `nonDynamicPokemonPhases`.
|
||||
*/
|
||||
private isDynamicPhase(phase: Phase): phase is DynamicPhase {
|
||||
return typeof (phase as any).getPokemon === "function" && !nonDynamicPokemonPhases.includes(phase.phaseName);
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
// TODO: rename to something else (this isn't used only for arena tags)
|
||||
export enum ArenaTagSide {
|
||||
BOTH,
|
||||
PLAYER,
|
||||
|
@ -95,4 +95,5 @@ export enum BattlerTagType {
|
||||
POWDER = "POWDER",
|
||||
MAGIC_COAT = "MAGIC_COAT",
|
||||
SUPREME_OVERLORD = "SUPREME_OVERLORD",
|
||||
BYPASS_SPEED = "BYPASS_SPEED",
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
/**
|
||||
* Enum representation of the phase types held by implementations of {@linkcode PhasePriorityQueue}.
|
||||
*/
|
||||
// TODO: We currently assume these are in order
|
||||
export enum DynamicPhaseType {
|
||||
POST_SUMMON,
|
||||
}
|
16
src/enums/move-phase-timing-modifier.ts
Normal file
16
src/enums/move-phase-timing-modifier.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import type { ObjectValues } from "#types/type-helpers";
|
||||
|
||||
/**
|
||||
* Enum representing modifiers for the timing of MovePhases.
|
||||
*
|
||||
* @remarks
|
||||
* This system is entirely independent of and takes precedence over move priority
|
||||
*/
|
||||
export const MovePhaseTimingModifier = Object.freeze({
|
||||
/** Used when moves go last regardless of speed and priority (i.e. Quash) */
|
||||
LAST: 0,
|
||||
NORMAL: 1,
|
||||
/** Used to trigger moves immediately (i.e. ones that were called through Instruct). */
|
||||
FIRST: 2,
|
||||
});
|
||||
export type MovePhaseTimingModifier = ObjectValues<typeof MovePhaseTimingModifier>;
|
@ -371,9 +371,15 @@ export class Arena {
|
||||
|
||||
/**
|
||||
* Function to trigger all weather based form changes
|
||||
* @param source - The Pokemon causing the changes by removing itself from the field
|
||||
*/
|
||||
triggerWeatherBasedFormChanges(): void {
|
||||
triggerWeatherBasedFormChanges(source?: Pokemon): void {
|
||||
globalScene.getField(true).forEach(p => {
|
||||
// TODO - This is a bandaid. Abilities leaving the field needs a better approach than
|
||||
// calling this method for every switch out that happens
|
||||
if (p === source) {
|
||||
return;
|
||||
}
|
||||
const isCastformWithForecast = p.hasAbility(AbilityId.FORECAST) && p.species.speciesId === SpeciesId.CASTFORM;
|
||||
const isCherrimWithFlowerGift = p.hasAbility(AbilityId.FLOWER_GIFT) && p.species.speciesId === SpeciesId.CHERRIM;
|
||||
|
||||
|
@ -3891,15 +3891,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
damage = Math.min(damage, this.hp);
|
||||
this.hp = this.hp - damage;
|
||||
if (this.isFainted() && !ignoreFaintPhase) {
|
||||
/**
|
||||
* When adding the FaintPhase, want to toggle future unshiftPhase() and queueMessage() calls
|
||||
* to appear before the FaintPhase (as FaintPhase will potentially end the encounter and add Phases such as
|
||||
* GameOverPhase, VictoryPhase, etc.. that will interfere with anything else that happens during this MoveEffectPhase)
|
||||
*
|
||||
* Once the MoveEffectPhase is over (and calls it's .end() function, shiftPhase() will reset the PhaseQueueSplice via clearPhaseQueueSplice() )
|
||||
*/
|
||||
globalScene.phaseManager.setPhaseQueueSplice();
|
||||
globalScene.phaseManager.unshiftNew("FaintPhase", this.getBattlerIndex(), preventEndure);
|
||||
globalScene.phaseManager.queueFaintPhase(this.getBattlerIndex(), preventEndure);
|
||||
this.destroySubstitute();
|
||||
this.lapseTag(BattlerTagType.COMMANDED);
|
||||
}
|
||||
@ -3951,11 +3943,6 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
damage = 0;
|
||||
}
|
||||
damage = this.damage(damage, ignoreSegments, isIndirectDamage, ignoreFaintPhase);
|
||||
// Ensure the battle-info bar's HP is updated, though only if the battle info is visible
|
||||
// TODO: When battle-info UI is refactored, make this only update the HP bar
|
||||
if (this.battleInfo.visible) {
|
||||
this.updateInfo();
|
||||
}
|
||||
// Damage amount may have changed, but needed to be queued before calling damage function
|
||||
damagePhase.updateAmount(damage);
|
||||
/**
|
||||
@ -5844,8 +5831,7 @@ export class PlayerPokemon extends Pokemon {
|
||||
this.getFieldIndex(),
|
||||
(slotIndex: number, _option: PartyOption) => {
|
||||
if (slotIndex >= globalScene.currentBattle.getBattlerCount() && slotIndex < 6) {
|
||||
globalScene.phaseManager.prependNewToPhase(
|
||||
"MoveEndPhase",
|
||||
globalScene.phaseManager.queueDeferred(
|
||||
"SwitchSummonPhase",
|
||||
switchType,
|
||||
this.getFieldIndex(),
|
||||
|
@ -13,7 +13,6 @@ import { getStatusEffectHealText } from "#data/status-effect";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { BerryType } from "#enums/berry-type";
|
||||
import { Color, ShadowColor } from "#enums/color";
|
||||
import { Command } from "#enums/command";
|
||||
import type { FormChangeItem } from "#enums/form-change-item";
|
||||
import { LearnMoveType } from "#enums/learn-move-type";
|
||||
import type { MoveId } from "#enums/move-id";
|
||||
@ -1542,30 +1541,16 @@ export class BypassSpeedChanceModifier extends PokemonHeldItemModifier {
|
||||
return new BypassSpeedChanceModifier(this.type, this.pokemonId, this.stackCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if {@linkcode BypassSpeedChanceModifier} should be applied
|
||||
* @param pokemon the {@linkcode Pokemon} that holds the item
|
||||
* @param doBypassSpeed {@linkcode BooleanHolder} that is `true` if speed should be bypassed
|
||||
* @returns `true` if {@linkcode BypassSpeedChanceModifier} should be applied
|
||||
*/
|
||||
override shouldApply(pokemon?: Pokemon, doBypassSpeed?: BooleanHolder): boolean {
|
||||
return super.shouldApply(pokemon, doBypassSpeed) && !!doBypassSpeed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies {@linkcode BypassSpeedChanceModifier}
|
||||
* @param pokemon the {@linkcode Pokemon} that holds the item
|
||||
* @param doBypassSpeed {@linkcode BooleanHolder} that is `true` if speed should be bypassed
|
||||
* @returns `true` if {@linkcode BypassSpeedChanceModifier} has been applied
|
||||
*/
|
||||
override apply(pokemon: Pokemon, doBypassSpeed: BooleanHolder): boolean {
|
||||
if (!doBypassSpeed.value && pokemon.randBattleSeedInt(10) < this.getStackCount()) {
|
||||
doBypassSpeed.value = true;
|
||||
const isCommandFight =
|
||||
globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()]?.command === Command.FIGHT;
|
||||
override apply(pokemon: Pokemon): boolean {
|
||||
if (pokemon.randBattleSeedInt(10) < this.getStackCount() && pokemon.addTag(BattlerTagType.BYPASS_SPEED)) {
|
||||
const hasQuickClaw = this.type.is("PokemonHeldItemModifierType") && this.type.id === "QUICK_CLAW";
|
||||
|
||||
if (isCommandFight && hasQuickClaw) {
|
||||
if (hasQuickClaw) {
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("modifier:bypassSpeedChanceApply", {
|
||||
pokemonName: getPokemonNameWithAffix(pokemon),
|
||||
|
@ -8,12 +8,14 @@
|
||||
*/
|
||||
|
||||
import { PHASE_START_COLOR } from "#app/constants/colors";
|
||||
import { DynamicQueueManager } from "#app/dynamic-queue-manager";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import type { Phase } from "#app/phase";
|
||||
import { type PhasePriorityQueue, PostSummonPhasePriorityQueue } from "#data/phase-priority-queue";
|
||||
import type { DynamicPhaseType } from "#enums/dynamic-phase-type";
|
||||
import { PhaseTree } from "#app/phase-tree";
|
||||
import { BattleType } from "#enums/battle-type";
|
||||
import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
import { ActivatePriorityQueuePhase } from "#phases/activate-priority-queue-phase";
|
||||
import type { PokemonMove } from "#moves/pokemon-move";
|
||||
import { AddEnemyBuffModifierPhase } from "#phases/add-enemy-buff-modifier-phase";
|
||||
import { AttemptCapturePhase } from "#phases/attempt-capture-phase";
|
||||
import { AttemptRunPhase } from "#phases/attempt-run-phase";
|
||||
@ -25,6 +27,7 @@ import { CheckSwitchPhase } from "#phases/check-switch-phase";
|
||||
import { CommandPhase } from "#phases/command-phase";
|
||||
import { CommonAnimPhase } from "#phases/common-anim-phase";
|
||||
import { DamageAnimPhase } from "#phases/damage-anim-phase";
|
||||
import { DynamicPhaseMarker } from "#phases/dynamic-phase-marker";
|
||||
import { EggHatchPhase } from "#phases/egg-hatch-phase";
|
||||
import { EggLapsePhase } from "#phases/egg-lapse-phase";
|
||||
import { EggSummaryPhase } from "#phases/egg-summary-phase";
|
||||
@ -110,8 +113,7 @@ import { UnavailablePhase } from "#phases/unavailable-phase";
|
||||
import { UnlockPhase } from "#phases/unlock-phase";
|
||||
import { VictoryPhase } from "#phases/victory-phase";
|
||||
import { WeatherEffectPhase } from "#phases/weather-effect-phase";
|
||||
import type { PhaseMap, PhaseString } from "#types/phase-types";
|
||||
import { type Constructor, coerceArray } from "#utils/common";
|
||||
import type { PhaseConditionFunc, PhaseMap, PhaseString } from "#types/phase-types";
|
||||
|
||||
/**
|
||||
* Object that holds all of the phase constructors.
|
||||
@ -122,7 +124,6 @@ import { type Constructor, coerceArray } from "#utils/common";
|
||||
* This allows for easy creation of new phases without needing to import each phase individually.
|
||||
*/
|
||||
const PHASES = Object.freeze({
|
||||
ActivatePriorityQueuePhase,
|
||||
AddEnemyBuffModifierPhase,
|
||||
AttemptCapturePhase,
|
||||
AttemptRunPhase,
|
||||
@ -134,6 +135,7 @@ const PHASES = Object.freeze({
|
||||
CommandPhase,
|
||||
CommonAnimPhase,
|
||||
DamageAnimPhase,
|
||||
DynamicPhaseMarker,
|
||||
EggHatchPhase,
|
||||
EggLapsePhase,
|
||||
EggSummaryPhase,
|
||||
@ -223,32 +225,30 @@ const PHASES = Object.freeze({
|
||||
/** Maps Phase strings to their constructors */
|
||||
export type PhaseConstructorMap = typeof PHASES;
|
||||
|
||||
/** Phases pushed at the end of each {@linkcode TurnStartPhase} */
|
||||
const turnEndPhases: readonly PhaseString[] = [
|
||||
"WeatherEffectPhase",
|
||||
"PositionalTagPhase",
|
||||
"BerryPhase",
|
||||
"CheckStatusEffectPhase",
|
||||
"TurnEndPhase",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* PhaseManager is responsible for managing the phases in the battle scene
|
||||
*/
|
||||
export class PhaseManager {
|
||||
/** PhaseQueue: dequeue/remove the first element to get the next phase */
|
||||
public phaseQueue: Phase[] = [];
|
||||
public conditionalQueue: Array<[() => boolean, Phase]> = [];
|
||||
/** PhaseQueuePrepend: is a temp storage of what will be added to PhaseQueue */
|
||||
private phaseQueuePrepend: Phase[] = [];
|
||||
private readonly phaseQueue: PhaseTree = new PhaseTree();
|
||||
|
||||
/** overrides default of inserting phases to end of phaseQueuePrepend array. Useful for inserting Phases "out of order" */
|
||||
private phaseQueuePrependSpliceIndex = -1;
|
||||
|
||||
/** Storage for {@linkcode PhasePriorityQueue}s which hold phases whose order dynamically changes */
|
||||
private dynamicPhaseQueues: PhasePriorityQueue[];
|
||||
/** Parallel array to {@linkcode dynamicPhaseQueues} - matches phase types to their queues */
|
||||
private dynamicPhaseTypes: Constructor<Phase>[];
|
||||
/** Holds priority queues for dynamically ordered phases */
|
||||
public dynamicQueueManager = new DynamicQueueManager();
|
||||
|
||||
/** The currently-running phase */
|
||||
private currentPhase: Phase;
|
||||
/** The phase put on standby if {@linkcode overridePhase} is called */
|
||||
private standbyPhase: Phase | null = null;
|
||||
|
||||
constructor() {
|
||||
this.dynamicPhaseQueues = [new PostSummonPhasePriorityQueue()];
|
||||
this.dynamicPhaseTypes = [PostSummonPhase];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all previously set phases, then add a new {@linkcode TitlePhase} to transition to the title screen.
|
||||
* @param addLogin - Whether to add a new {@linkcode LoginPhase} before the {@linkcode TitlePhase}
|
||||
@ -276,122 +276,76 @@ export class PhaseManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Adds a phase to the end of the queue
|
||||
* @param phase - The {@linkcode Phase} to add
|
||||
*/
|
||||
pushConditionalPhase(phase: Phase, condition: () => boolean): void {
|
||||
this.conditionalQueue.push([condition, phase]);
|
||||
public pushPhase(phase: Phase): void {
|
||||
this.phaseQueue.pushPhase(this.checkDynamic(phase));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a phase to nextCommandPhaseQueue, as long as boolean passed in is false
|
||||
* @param phase {@linkcode Phase} the phase to add
|
||||
* Queue a phase to be run immediately after the current phase finishes. \
|
||||
* Unshifted phases are run in FIFO order if multiple are queued during a single phase's execution.
|
||||
* @param phase - The {@linkcode Phase} to add
|
||||
*/
|
||||
pushPhase(phase: Phase): void {
|
||||
if (this.getDynamicPhaseType(phase) !== undefined) {
|
||||
this.pushDynamicPhase(phase);
|
||||
} else {
|
||||
this.phaseQueue.push(phase);
|
||||
}
|
||||
public unshiftPhase(phase: Phase): void {
|
||||
const toAdd = this.checkDynamic(phase);
|
||||
phase.is("MovePhase") ? this.phaseQueue.addAfter(toAdd, "MoveEndPhase") : this.phaseQueue.addPhase(toAdd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds Phase(s) to the end of phaseQueuePrepend, or at phaseQueuePrependSpliceIndex
|
||||
* @param phases {@linkcode Phase} the phase(s) to add
|
||||
* Helper method to queue a phase as dynamic if necessary
|
||||
* @param phase - The phase to check
|
||||
* @returns The {@linkcode Phase} or a {@linkcode DynamicPhaseMarker} to be used in its place
|
||||
*/
|
||||
unshiftPhase(...phases: Phase[]): void {
|
||||
if (this.phaseQueuePrependSpliceIndex === -1) {
|
||||
this.phaseQueuePrepend.push(...phases);
|
||||
} else {
|
||||
this.phaseQueuePrepend.splice(this.phaseQueuePrependSpliceIndex, 0, ...phases);
|
||||
private checkDynamic(phase: Phase): Phase {
|
||||
if (this.dynamicQueueManager.queueDynamicPhase(phase)) {
|
||||
return new DynamicPhaseMarker(phase.phaseName);
|
||||
}
|
||||
return phase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the phaseQueue
|
||||
* @param leaveUnshifted - If `true`, leaves the top level of the tree intact; default `false`
|
||||
*/
|
||||
clearPhaseQueue(): void {
|
||||
this.phaseQueue.splice(0, this.phaseQueue.length);
|
||||
public clearPhaseQueue(leaveUnshifted = false): void {
|
||||
this.phaseQueue.clear(leaveUnshifted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all phase-related stuff, including all phase queues, the current and standby phases, and a splice index
|
||||
*/
|
||||
clearAllPhases(): void {
|
||||
for (const queue of [this.phaseQueue, this.phaseQueuePrepend, this.conditionalQueue]) {
|
||||
queue.splice(0, queue.length);
|
||||
}
|
||||
this.dynamicPhaseQueues.forEach(queue => queue.clear());
|
||||
/** Clears all phase queues and the standby phase */
|
||||
public clearAllPhases(): void {
|
||||
this.clearPhaseQueue();
|
||||
this.dynamicQueueManager.clearQueues();
|
||||
this.standbyPhase = null;
|
||||
this.clearPhaseQueueSplice();
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by function unshiftPhase(), sets index to start inserting at current length instead of the end of the array, useful if phaseQueuePrepend gets longer with Phases
|
||||
* Determines the next phase to run and starts it.
|
||||
* @privateRemarks
|
||||
* This is called by {@linkcode Phase.end} by default, and should not be called by other methods.
|
||||
*/
|
||||
setPhaseQueueSplice(): void {
|
||||
this.phaseQueuePrependSpliceIndex = this.phaseQueuePrepend.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets phaseQueuePrependSpliceIndex to -1, implies that calls to unshiftPhase will insert at end of phaseQueuePrepend
|
||||
*/
|
||||
clearPhaseQueueSplice(): void {
|
||||
this.phaseQueuePrependSpliceIndex = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is called by each Phase implementations "end()" by default
|
||||
* We dump everything from phaseQueuePrepend to the start of of phaseQueue
|
||||
* then removes first Phase and starts it
|
||||
*/
|
||||
shiftPhase(): void {
|
||||
public shiftPhase(): void {
|
||||
if (this.standbyPhase) {
|
||||
this.currentPhase = this.standbyPhase;
|
||||
this.standbyPhase = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.phaseQueuePrependSpliceIndex > -1) {
|
||||
this.clearPhaseQueueSplice();
|
||||
}
|
||||
this.phaseQueue.unshift(...this.phaseQueuePrepend);
|
||||
this.phaseQueuePrepend.splice(0);
|
||||
let nextPhase = this.phaseQueue.getNextPhase();
|
||||
|
||||
const unactivatedConditionalPhases: [() => boolean, Phase][] = [];
|
||||
// Check if there are any conditional phases queued
|
||||
for (const [condition, phase] of this.conditionalQueue) {
|
||||
// Evaluate the condition associated with the phase
|
||||
if (condition()) {
|
||||
// If the condition is met, add the phase to the phase queue
|
||||
this.pushPhase(phase);
|
||||
} else {
|
||||
// If the condition is not met, re-add the phase back to the end of the conditional queue
|
||||
unactivatedConditionalPhases.push([condition, phase]);
|
||||
}
|
||||
if (nextPhase?.is("DynamicPhaseMarker")) {
|
||||
nextPhase = this.dynamicQueueManager.popNextPhase(nextPhase.phaseType);
|
||||
}
|
||||
|
||||
this.conditionalQueue = unactivatedConditionalPhases;
|
||||
|
||||
// If no phases are left, unshift phases to start a new turn.
|
||||
if (this.phaseQueue.length === 0) {
|
||||
this.populatePhaseQueue();
|
||||
// Clear the conditionalQueue if there are no phases left in the phaseQueue
|
||||
this.conditionalQueue = [];
|
||||
if (nextPhase == null) {
|
||||
this.turnStart();
|
||||
} else {
|
||||
this.currentPhase = nextPhase;
|
||||
}
|
||||
|
||||
// Bang is justified as `populatePhaseQueue` ensures we always have _something_ in the queue at all times
|
||||
this.currentPhase = this.phaseQueue.shift()!;
|
||||
|
||||
this.startCurrentPhase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to start and log the current phase.
|
||||
*/
|
||||
@ -400,7 +354,14 @@ export class PhaseManager {
|
||||
this.currentPhase.start();
|
||||
}
|
||||
|
||||
overridePhase(phase: Phase): boolean {
|
||||
/**
|
||||
* Overrides the currently running phase with another
|
||||
* @param phase - The {@linkcode Phase} to override the current one with
|
||||
* @returns If the override succeeded
|
||||
*
|
||||
* @todo This is antithetical to the phase structure and used a single time. Remove it.
|
||||
*/
|
||||
public overridePhase(phase: Phase): boolean {
|
||||
if (this.standbyPhase) {
|
||||
return false;
|
||||
}
|
||||
@ -413,175 +374,47 @@ export class PhaseManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a specific {@linkcode Phase} in the phase queue.
|
||||
* Determine if there is a queued {@linkcode Phase} meeting the specified conditions.
|
||||
* @param type - The {@linkcode PhaseString | type} of phase to search for
|
||||
* @param condition - An optional {@linkcode PhaseConditionFunc} to add conditions to the search
|
||||
* @returns Whether a matching phase exists
|
||||
*/
|
||||
public hasPhaseOfType<T extends PhaseString>(type: T, condition?: PhaseConditionFunc<T>): boolean {
|
||||
return this.dynamicQueueManager.exists(type, condition) || this.phaseQueue.exists(type, condition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to find and remove the first queued {@linkcode Phase} matching the given conditions.
|
||||
* @param type - The {@linkcode PhaseString | type} of phase to search for
|
||||
* @param phaseFilter - An optional {@linkcode PhaseConditionFunc} to add conditions to the search
|
||||
* @returns Whether a phase was successfully removed
|
||||
*/
|
||||
public tryRemovePhase<T extends PhaseString>(type: T, phaseFilter?: PhaseConditionFunc<T>): boolean {
|
||||
if (this.dynamicQueueManager.removePhase(type, phaseFilter)) {
|
||||
return true;
|
||||
}
|
||||
return this.phaseQueue.remove(type, phaseFilter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all {@linkcode Phase}s of the given type from the queue
|
||||
* @param phaseType - The {@linkcode PhaseString | type} of phase to search for
|
||||
*
|
||||
* @param phaseFilter filter function to use to find the wanted phase
|
||||
* @returns the found phase or undefined if none found
|
||||
* @remarks
|
||||
* This is not intended to be used with dynamically ordered phases, and does not operate on the dynamic queue. \
|
||||
* However, it does remove {@linkcode DynamicPhaseMarker}s and so would prevent such phases from activating.
|
||||
*/
|
||||
findPhase<P extends Phase = Phase>(phaseFilter: (phase: Phase) => phase is P): P | undefined;
|
||||
findPhase<P extends Phase = Phase>(phaseFilter: (phase: P) => boolean): P | undefined;
|
||||
findPhase<P extends Phase = Phase>(phaseFilter: (phase: P) => boolean): P | undefined {
|
||||
return this.phaseQueue.find(phaseFilter) as P | undefined;
|
||||
}
|
||||
|
||||
tryReplacePhase(phaseFilter: (phase: Phase) => boolean, phase: Phase): boolean {
|
||||
const phaseIndex = this.phaseQueue.findIndex(phaseFilter);
|
||||
if (phaseIndex > -1) {
|
||||
this.phaseQueue[phaseIndex] = phase;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
tryRemovePhase(phaseFilter: (phase: Phase) => boolean): boolean {
|
||||
const phaseIndex = this.phaseQueue.findIndex(phaseFilter);
|
||||
if (phaseIndex > -1) {
|
||||
this.phaseQueue.splice(phaseIndex, 1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
public removeAllPhasesOfType(type: PhaseString): void {
|
||||
this.phaseQueue.removeAll(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Will search for a specific phase in {@linkcode phaseQueuePrepend} via filter, and remove the first result if a match is found.
|
||||
* @param phaseFilter filter function
|
||||
*/
|
||||
tryRemoveUnshiftedPhase(phaseFilter: (phase: Phase) => boolean): boolean {
|
||||
const phaseIndex = this.phaseQueuePrepend.findIndex(phaseFilter);
|
||||
if (phaseIndex > -1) {
|
||||
this.phaseQueuePrepend.splice(phaseIndex, 1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to add the input phase to index before target phase in the phaseQueue, else simply calls unshiftPhase()
|
||||
* @param phase - The phase to be added
|
||||
* @param targetPhase - The phase to search for in phaseQueue
|
||||
* @returns boolean if a targetPhase was found and added
|
||||
*/
|
||||
prependToPhase(phase: Phase | Phase[], targetPhase: PhaseString): boolean {
|
||||
phase = coerceArray(phase);
|
||||
const target = PHASES[targetPhase];
|
||||
const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof target);
|
||||
|
||||
if (targetIndex !== -1) {
|
||||
this.phaseQueue.splice(targetIndex, 0, ...phase);
|
||||
return true;
|
||||
}
|
||||
this.unshiftPhase(...phase);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to add the input phase(s) to index after target phase in the {@linkcode phaseQueue}, else simply calls {@linkcode unshiftPhase()}
|
||||
* @param phase {@linkcode Phase} the phase(s) to be added
|
||||
* @param targetPhase {@linkcode Phase} the type of phase to search for in {@linkcode phaseQueue}
|
||||
* @param condition Condition the target phase must meet to be appended to
|
||||
* @returns `true` if a `targetPhase` was found to append to
|
||||
*/
|
||||
appendToPhase(phase: Phase | Phase[], targetPhase: PhaseString, condition?: (p: Phase) => boolean): boolean {
|
||||
phase = coerceArray(phase);
|
||||
const target = PHASES[targetPhase];
|
||||
const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof target && (!condition || condition(ph)));
|
||||
|
||||
if (targetIndex !== -1 && this.phaseQueue.length > targetIndex) {
|
||||
this.phaseQueue.splice(targetIndex + 1, 0, ...phase);
|
||||
return true;
|
||||
}
|
||||
this.unshiftPhase(...phase);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks a phase and returns the matching {@linkcode DynamicPhaseType}, or undefined if it does not match one
|
||||
* @param phase The phase to check
|
||||
* @returns The corresponding {@linkcode DynamicPhaseType} or `undefined`
|
||||
*/
|
||||
public getDynamicPhaseType(phase: Phase | null): DynamicPhaseType | undefined {
|
||||
let phaseType: DynamicPhaseType | undefined;
|
||||
this.dynamicPhaseTypes.forEach((cls, index) => {
|
||||
if (phase instanceof cls) {
|
||||
phaseType = index;
|
||||
}
|
||||
});
|
||||
|
||||
return phaseType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes a phase onto its corresponding dynamic queue and marks the activation point in {@linkcode phaseQueue}
|
||||
*
|
||||
* The {@linkcode ActivatePriorityQueuePhase} will run the top phase in the dynamic queue (not necessarily {@linkcode phase})
|
||||
* @param phase The phase to push
|
||||
*/
|
||||
public pushDynamicPhase(phase: Phase): void {
|
||||
const type = this.getDynamicPhaseType(phase);
|
||||
if (type === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.pushPhase(new ActivatePriorityQueuePhase(type));
|
||||
this.dynamicPhaseQueues[type].push(phase);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to remove one or more Phases from the given DynamicPhaseQueue, removing the equivalent amount of {@linkcode ActivatePriorityQueuePhase}s from the queue.
|
||||
* @param type - The {@linkcode DynamicPhaseType} to check
|
||||
* @param phaseFilter - The function to select phases for removal
|
||||
* @param removeCount - The maximum number of phases to remove, or `all` to remove all matching phases;
|
||||
* default `1`
|
||||
* @todo Remove this eventually once the patchwork bug this is used for is fixed
|
||||
*/
|
||||
public tryRemoveDynamicPhase(
|
||||
type: DynamicPhaseType,
|
||||
phaseFilter: (phase: Phase) => boolean,
|
||||
removeCount: number | "all" = 1,
|
||||
): void {
|
||||
const numRemoved = this.dynamicPhaseQueues[type].tryRemovePhase(phaseFilter, removeCount);
|
||||
for (let x = 0; x < numRemoved; x++) {
|
||||
this.tryRemovePhase(p => p.is("ActivatePriorityQueuePhase"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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` to the queue
|
||||
* @param message - string for MessagePhase
|
||||
* @param callbackDelay - optional param for MessagePhase constructor
|
||||
* @param prompt - optional param for MessagePhase constructor
|
||||
* @param promptDelay - optional param for MessagePhase constructor
|
||||
* @param defer - Whether to allow the phase to be deferred
|
||||
* @param defer - If `true`, push the phase instead of unshifting; default `false`
|
||||
*
|
||||
* @see {@linkcode MessagePhase} for more details on the parameters
|
||||
*/
|
||||
@ -593,20 +426,18 @@ export class PhaseManager {
|
||||
defer?: boolean | null,
|
||||
) {
|
||||
const phase = new MessagePhase(message, callbackDelay, prompt, promptDelay);
|
||||
if (!defer) {
|
||||
// adds to the end of PhaseQueuePrepend
|
||||
this.unshiftPhase(phase);
|
||||
} else {
|
||||
//remember that pushPhase adds it to nextCommandPhaseQueue
|
||||
if (defer) {
|
||||
this.pushPhase(phase);
|
||||
} else {
|
||||
this.unshiftPhase(phase);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue a phase to show or hide the ability flyout bar.
|
||||
* Queues an ability bar flyout phase via {@linkcode unshiftPhase}
|
||||
* @param pokemon - The {@linkcode Pokemon} whose ability is being activated
|
||||
* @param passive - Whether the ability is a passive
|
||||
* @param show - Whether to show or hide the bar
|
||||
* @param show - If `true`, show the bar. Otherwise, hide it
|
||||
*/
|
||||
public queueAbilityDisplay(pokemon: Pokemon, passive: boolean, show: boolean): void {
|
||||
this.unshiftPhase(show ? new ShowAbilityPhase(pokemon.getBattlerIndex(), passive) : new HideAbilityPhase());
|
||||
@ -622,10 +453,12 @@ export class PhaseManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves everything from nextCommandPhaseQueue to phaseQueue (keeping order)
|
||||
* Clear all dynamic queues and begin a new {@linkcode TurnInitPhase} for the new turn.
|
||||
* Called whenever the current phase queue is empty.
|
||||
*/
|
||||
private populatePhaseQueue(): void {
|
||||
this.phaseQueue.push(new TurnInitPhase());
|
||||
private turnStart(): void {
|
||||
this.dynamicQueueManager.clearQueues();
|
||||
this.currentPhase = new TurnInitPhase();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -667,50 +500,119 @@ export class PhaseManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new phase and immediately prepend it to an existing phase in the phase queue.
|
||||
* Equivalent to calling {@linkcode create} followed by {@linkcode prependToPhase}.
|
||||
* @param targetPhase - The phase to search for in phaseQueue
|
||||
* @param phase - The name of the phase to create
|
||||
* Add a {@linkcode FaintPhase} to the queue
|
||||
* @param args - The arguments to pass to the phase constructor
|
||||
* @returns `true` if a `targetPhase` was found to prepend to
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* Faint phases are ordered in a special way to allow battle effects to settle before the pokemon faints.
|
||||
* @see {@linkcode PhaseTree.addPhase}
|
||||
*/
|
||||
public prependNewToPhase<T extends PhaseString>(
|
||||
targetPhase: PhaseString,
|
||||
phase: T,
|
||||
...args: ConstructorParameters<PhaseConstructorMap[T]>
|
||||
): boolean {
|
||||
return this.prependToPhase(this.create(phase, ...args), targetPhase);
|
||||
public queueFaintPhase(...args: ConstructorParameters<PhaseConstructorMap["FaintPhase"]>): void {
|
||||
this.phaseQueue.addPhase(this.create("FaintPhase", ...args), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new phase and immediately append it to an existing phase the phase queue.
|
||||
* Equivalent to calling {@linkcode create} followed by {@linkcode appendToPhase}.
|
||||
* @param targetPhase - The phase to search for in phaseQueue
|
||||
* @param phase - The name of the phase to create
|
||||
* @param args - The arguments to pass to the phase constructor
|
||||
* @returns `true` if a `targetPhase` was found to append to
|
||||
* Attempts to add {@linkcode PostSummonPhase}s for the enemy pokemon
|
||||
*
|
||||
* This is used to ensure that wild pokemon (which have no {@linkcode SummonPhase}) do not queue a {@linkcode PostSummonPhase}
|
||||
* until all pokemon are on the field.
|
||||
*/
|
||||
public appendNewToPhase<T extends PhaseString>(
|
||||
targetPhase: PhaseString,
|
||||
phase: T,
|
||||
...args: ConstructorParameters<PhaseConstructorMap[T]>
|
||||
): boolean {
|
||||
return this.appendToPhase(this.create(phase, ...args), targetPhase);
|
||||
public tryAddEnemyPostSummonPhases(): void {
|
||||
if (
|
||||
![BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(globalScene.currentBattle.battleType)
|
||||
&& !this.phaseQueue.exists("SummonPhase")
|
||||
) {
|
||||
globalScene.getEnemyField().forEach(p => {
|
||||
this.pushPhase(new PostSummonPhase(p.getBattlerIndex(), "SummonPhase"));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public startNewDynamicPhase<T extends PhaseString>(
|
||||
/**
|
||||
* Create a new phase and queue it to run after all others queued by the currently running phase.
|
||||
* @param phase - The name of the phase to create
|
||||
* @param args - The arguments to pass to the phase constructor
|
||||
*
|
||||
* @deprecated Only used for switches and should be phased out eventually.
|
||||
*/
|
||||
public queueDeferred<const T extends "SwitchPhase" | "SwitchSummonPhase">(
|
||||
phase: T,
|
||||
...args: ConstructorParameters<PhaseConstructorMap[T]>
|
||||
): void {
|
||||
this.startDynamicPhase(this.create(phase, ...args));
|
||||
this.phaseQueue.unshiftToCurrent(this.create(phase, ...args));
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the first {@linkcode MovePhase} meeting the condition
|
||||
* @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function
|
||||
* @returns The MovePhase, or `undefined` if it does not exist
|
||||
*/
|
||||
public getMovePhase(phaseCondition: PhaseConditionFunc<"MovePhase">): MovePhase | undefined {
|
||||
return this.dynamicQueueManager.getMovePhase(phaseCondition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and cancels the first {@linkcode MovePhase} meeting the condition
|
||||
* @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function
|
||||
*/
|
||||
public cancelMove(phaseCondition: PhaseConditionFunc<"MovePhase">): void {
|
||||
this.dynamicQueueManager.cancelMovePhase(phaseCondition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the first {@linkcode MovePhase} meeting the condition and forces it next
|
||||
* @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function
|
||||
*/
|
||||
public forceMoveNext(phaseCondition: PhaseConditionFunc<"MovePhase">): void {
|
||||
this.dynamicQueueManager.setMoveTimingModifier(phaseCondition, MovePhaseTimingModifier.FIRST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the first {@linkcode MovePhase} meeting the condition and forces it last
|
||||
* @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function
|
||||
*/
|
||||
public forceMoveLast(phaseCondition: PhaseConditionFunc<"MovePhase">): void {
|
||||
this.dynamicQueueManager.setMoveTimingModifier(phaseCondition, MovePhaseTimingModifier.LAST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the first {@linkcode MovePhase} meeting the condition and changes its move
|
||||
* @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function
|
||||
* @param move - The {@linkcode PokemonMove | move} to use in replacement
|
||||
*/
|
||||
public changePhaseMove(phaseCondition: PhaseConditionFunc<"MovePhase">, move: PokemonMove): void {
|
||||
this.dynamicQueueManager.setMoveForPhase(phaseCondition, move);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects moves which were targeted at a {@linkcode Pokemon} that has been removed
|
||||
* @param removedPokemon - The removed {@linkcode Pokemon}
|
||||
* @param allyPokemon - The ally of the removed pokemon
|
||||
*/
|
||||
public redirectMoves(removedPokemon: Pokemon, allyPokemon: Pokemon): void {
|
||||
this.dynamicQueueManager.redirectMoves(removedPokemon, allyPokemon);
|
||||
}
|
||||
|
||||
/** Queues phases which run at the end of each turn */
|
||||
public queueTurnEndPhases(): void {
|
||||
turnEndPhases.forEach(p => {
|
||||
this.pushNew(p);
|
||||
});
|
||||
}
|
||||
|
||||
/** Prevents end of turn effects from triggering when transitioning to a new biome on a X0 wave */
|
||||
public onInterlude(): void {
|
||||
const phasesToRemove = ["WeatherEffectPhase", "BerryPhase", "CheckStatusEffectPhase"];
|
||||
this.phaseQueue = this.phaseQueue.filter(p => !phasesToRemove.includes(p.phaseName));
|
||||
const phasesToRemove: readonly PhaseString[] = [
|
||||
"WeatherEffectPhase",
|
||||
"BerryPhase",
|
||||
"CheckStatusEffectPhase",
|
||||
] as const;
|
||||
for (const phaseType of phasesToRemove) {
|
||||
this.phaseQueue.removeAll(phaseType);
|
||||
}
|
||||
|
||||
const turnEndPhase = this.findPhase<TurnEndPhase>(p => p.phaseName === "TurnEndPhase");
|
||||
const turnEndPhase = this.phaseQueue.find("TurnEndPhase");
|
||||
if (turnEndPhase) {
|
||||
turnEndPhase.upcomingInterlude = true;
|
||||
}
|
||||
|
206
src/phase-tree.ts
Normal file
206
src/phase-tree.ts
Normal file
@ -0,0 +1,206 @@
|
||||
// biome-ignore-start lint/correctness/noUnusedImports: TSDoc imports
|
||||
import type { PhaseManager } from "#app/@types/phase-types";
|
||||
import type { DynamicPhaseMarker } from "#phases/dynamic-phase-marker";
|
||||
|
||||
// biome-ignore-end lint/correctness/noUnusedImports: TSDoc imports
|
||||
|
||||
import type { PhaseMap, PhaseString } from "#app/@types/phase-types";
|
||||
import type { Phase } from "#app/phase";
|
||||
import type { PhaseConditionFunc } from "#types/phase-types";
|
||||
|
||||
/**
|
||||
* The PhaseTree is the central storage location for {@linkcode Phase}s by the {@linkcode PhaseManager}.
|
||||
*
|
||||
* It has a tiered structure, where unshifted phases are added one level above the currently running Phase. Phases are generally popped from the Tree in FIFO order.
|
||||
*
|
||||
* Dynamically ordered phases are queued into the Tree only as {@linkcode DynamicPhaseMarker | Marker}s and as such are not guaranteed to run FIFO (otherwise, they would not be dynamic)
|
||||
*/
|
||||
export class PhaseTree {
|
||||
/** Storage for all levels in the tree. This is a simple array because only one Phase may have "children" at a time. */
|
||||
private levels: Phase[][] = [[]];
|
||||
/** The level of the currently running {@linkcode Phase} in the Tree (note that such phase is not actually in the Tree while it is running) */
|
||||
private currentLevel = 0;
|
||||
/**
|
||||
* True if a "deferred" level exists
|
||||
* @see {@linkcode addPhase}
|
||||
*/
|
||||
private deferredActive = false;
|
||||
|
||||
/**
|
||||
* Adds a {@linkcode Phase} to the specified level
|
||||
* @param phase - The phase to add
|
||||
* @param level - The numeric level to add the phase
|
||||
* @throws Error if `level` is out of legal bounds
|
||||
*/
|
||||
private add(phase: Phase, level: number): void {
|
||||
const addLevel = this.levels[level];
|
||||
if (addLevel == null) {
|
||||
throw new Error("Attempted to add a phase to a nonexistent level of the PhaseTree!\nLevel: " + level.toString());
|
||||
}
|
||||
this.levels[level].push(phase);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by the {@linkcode PhaseManager} to add phases to the Tree
|
||||
* @param phase - The {@linkcode Phase} to be added
|
||||
* @param defer - Whether to defer the execution of this phase by allowing subsequently-added phases to run before it
|
||||
*
|
||||
* @privateRemarks
|
||||
* Deferral is implemented by moving the queue at {@linkcode currentLevel} up one level and inserting the new phase below it.
|
||||
* {@linkcode deferredActive} is set until the moved queue (and anything added to it) is exhausted.
|
||||
*
|
||||
* If {@linkcode deferredActive} is `true` when a deferred phase is added, the phase will be pushed to the second-highest level queue.
|
||||
* That is, it will execute after the originally deferred phase, but there is no possibility for nesting with deferral.
|
||||
*
|
||||
* @todo `setPhaseQueueSplice` had strange behavior. This is simpler, but there are probably some remnant edge cases with the current implementation
|
||||
*/
|
||||
public addPhase(phase: Phase, defer = false): void {
|
||||
if (defer && !this.deferredActive) {
|
||||
this.deferredActive = true;
|
||||
this.levels.splice(-1, 0, []);
|
||||
}
|
||||
this.add(phase, this.levels.length - 1 - +defer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a {@linkcode Phase} after the first occurence of the given type, or to the top of the Tree if no such phase exists
|
||||
* @param phase - The {@linkcode Phase} to be added
|
||||
* @param type - A {@linkcode PhaseString} representing the type to search for
|
||||
*/
|
||||
public addAfter(phase: Phase, type: PhaseString): void {
|
||||
for (let i = this.levels.length - 1; i >= 0; i--) {
|
||||
const insertIdx = this.levels[i].findIndex(p => p.is(type)) + 1;
|
||||
if (insertIdx !== 0) {
|
||||
this.levels[i].splice(insertIdx, 0, phase);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.addPhase(phase);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unshifts a {@linkcode Phase} to the current level.
|
||||
* This is effectively the same as if the phase were added immediately after the currently-running phase, before it started.
|
||||
* @param phase - The {@linkcode Phase} to be added
|
||||
*/
|
||||
public unshiftToCurrent(phase: Phase): void {
|
||||
this.levels[this.currentLevel].unshift(phase);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes a {@linkcode Phase} to the last level of the queue. It will run only after all previously queued phases have been executed.
|
||||
* @param phase - The {@linkcode Phase} to be added
|
||||
*/
|
||||
public pushPhase(phase: Phase): void {
|
||||
this.add(phase, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes and returns the first {@linkcode Phase} from the topmost level of the tree
|
||||
* @returns - The next {@linkcode Phase}, or `undefined` if the Tree is empty
|
||||
*/
|
||||
public getNextPhase(): Phase | undefined {
|
||||
this.currentLevel = this.levels.length - 1;
|
||||
while (this.currentLevel > 0 && this.levels[this.currentLevel].length === 0) {
|
||||
this.deferredActive = false;
|
||||
this.levels.pop();
|
||||
this.currentLevel--;
|
||||
}
|
||||
|
||||
// TODO: right now, this is preventing properly marking when one set of unshifted phases ends
|
||||
this.levels.push([]);
|
||||
return this.levels[this.currentLevel].shift();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a particular {@linkcode Phase} in the Tree by searching in pop order
|
||||
* @param phaseType - The {@linkcode PhaseString | type} of phase to search for
|
||||
* @param phaseFilter - A {@linkcode PhaseConditionFunc} to specify conditions for the phase
|
||||
* @returns The matching {@linkcode Phase}, or `undefined` if none exists
|
||||
*/
|
||||
public find<P extends PhaseString>(phaseType: P, phaseFilter?: PhaseConditionFunc<P>): PhaseMap[P] | undefined {
|
||||
for (let i = this.levels.length - 1; i >= 0; i--) {
|
||||
const level = this.levels[i];
|
||||
const phase = level.find((p): p is PhaseMap[P] => p.is(phaseType) && (!phaseFilter || phaseFilter(p)));
|
||||
if (phase) {
|
||||
return phase;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a particular {@linkcode Phase} in the Tree by searching in pop order
|
||||
* @param phaseType - The {@linkcode PhaseString | type} of phase to search for
|
||||
* @param phaseFilter - A {@linkcode PhaseConditionFunc} to specify conditions for the phase
|
||||
* @returns The matching {@linkcode Phase}, or `undefined` if none exists
|
||||
*/
|
||||
public findAll<P extends PhaseString>(phaseType: P, phaseFilter?: PhaseConditionFunc<P>): PhaseMap[P][] {
|
||||
const phases: PhaseMap[P][] = [];
|
||||
for (let i = this.levels.length - 1; i >= 0; i--) {
|
||||
const level = this.levels[i];
|
||||
const levelPhases = level.filter((p): p is PhaseMap[P] => p.is(phaseType) && (!phaseFilter || phaseFilter(p)));
|
||||
phases.push(...levelPhases);
|
||||
}
|
||||
return phases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the Tree
|
||||
* @param leaveFirstLevel - If `true`, leaves the top level of the tree intact
|
||||
*
|
||||
* @privateremarks
|
||||
* The parameter on this method exists because {@linkcode PhaseManager.clearPhaseQueue} previously (probably by mistake) ignored `phaseQueuePrepend`.
|
||||
*
|
||||
* This is (probably by mistake) relied upon by certain ME functions.
|
||||
*/
|
||||
public clear(leaveFirstLevel = false) {
|
||||
this.levels = [leaveFirstLevel ? (this.levels.at(-1) ?? []) : []];
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and removes a single {@linkcode Phase} from the Tree
|
||||
* @param phaseType - The {@linkcode PhaseString | type} of phase to search for
|
||||
* @param phaseFilter - A {@linkcode PhaseConditionFunc} to specify conditions for the phase
|
||||
* @returns Whether a removal occurred
|
||||
*/
|
||||
public remove<P extends PhaseString>(phaseType: P, phaseFilter?: PhaseConditionFunc<P>): boolean {
|
||||
for (let i = this.levels.length - 1; i >= 0; i--) {
|
||||
const level = this.levels[i];
|
||||
const phaseIndex = level.findIndex(p => p.is(phaseType) && (!phaseFilter || phaseFilter(p)));
|
||||
if (phaseIndex !== -1) {
|
||||
level.splice(phaseIndex, 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all occurrences of {@linkcode Phase}s of the given type
|
||||
* @param phaseType - The {@linkcode PhaseString | type} of phase to search for
|
||||
*/
|
||||
public removeAll(phaseType: PhaseString): void {
|
||||
for (let i = 0; i < this.levels.length; i++) {
|
||||
const level = this.levels[i].filter(phase => !phase.is(phaseType));
|
||||
this.levels[i] = level;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a particular phase exists in the Tree
|
||||
* @param phaseType - The {@linkcode PhaseString | type} of phase to search for
|
||||
* @param phaseFilter - A {@linkcode PhaseConditionFunc} to specify conditions for the phase
|
||||
* @returns Whether a matching phase exists
|
||||
*/
|
||||
public exists<P extends PhaseString>(phaseType: P, phaseFilter?: PhaseConditionFunc<P>): boolean {
|
||||
for (const level of this.levels) {
|
||||
for (const phase of level) {
|
||||
if (phase.is(phaseType) && (!phaseFilter || phaseFilter(phase))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { Phase } from "#app/phase";
|
||||
import type { DynamicPhaseType } from "#enums/dynamic-phase-type";
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -18,23 +18,11 @@ export class BattleEndPhase extends BattlePhase {
|
||||
super.start();
|
||||
|
||||
// cull any extra `BattleEnd` phases from the queue.
|
||||
globalScene.phaseManager.phaseQueue = globalScene.phaseManager.phaseQueue.filter(phase => {
|
||||
if (phase.is("BattleEndPhase")) {
|
||||
this.isVictory ||= phase.isVictory;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
// `phaseQueuePrepend` is private, so we have to use this inefficient loop.
|
||||
while (
|
||||
globalScene.phaseManager.tryRemoveUnshiftedPhase(phase => {
|
||||
if (phase.is("BattleEndPhase")) {
|
||||
this.isVictory ||= phase.isVictory;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
) {}
|
||||
this.isVictory ||= globalScene.phaseManager.hasPhaseOfType(
|
||||
"BattleEndPhase",
|
||||
(phase: BattleEndPhase) => phase.isVictory,
|
||||
);
|
||||
globalScene.phaseManager.removeAllPhasesOfType("BattleEndPhase");
|
||||
|
||||
globalScene.gameData.gameStats.battles++;
|
||||
if (
|
||||
|
@ -1,20 +1,14 @@
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { Phase } from "#app/phase";
|
||||
import type { BattlerIndex } from "#enums/battler-index";
|
||||
|
||||
export class CheckStatusEffectPhase extends Phase {
|
||||
public readonly phaseName = "CheckStatusEffectPhase";
|
||||
private order: BattlerIndex[];
|
||||
constructor(order: BattlerIndex[]) {
|
||||
super();
|
||||
this.order = order;
|
||||
}
|
||||
|
||||
start() {
|
||||
const field = globalScene.getField();
|
||||
for (const o of this.order) {
|
||||
if (field[o].status?.isPostTurn()) {
|
||||
globalScene.phaseManager.unshiftNew("PostTurnStatusEffectPhase", o);
|
||||
for (const p of field) {
|
||||
if (p?.status?.isPostTurn()) {
|
||||
globalScene.phaseManager.unshiftNew("PostTurnStatusEffectPhase", p.getBattlerIndex());
|
||||
}
|
||||
}
|
||||
this.end();
|
||||
|
@ -28,7 +28,8 @@ export class CheckSwitchPhase extends BattlePhase {
|
||||
|
||||
// ...if the user is playing in Set Mode
|
||||
if (globalScene.battleStyle === BattleStyle.SET) {
|
||||
return super.end();
|
||||
this.end(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// ...if the checked Pokemon is somehow not on the field
|
||||
@ -44,7 +45,8 @@ export class CheckSwitchPhase extends BattlePhase {
|
||||
.slice(1)
|
||||
.filter(p => p.isActive()).length === 0
|
||||
) {
|
||||
return super.end();
|
||||
this.end(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// ...or if any player Pokemon has an effect that prevents the checked Pokemon from switching
|
||||
@ -53,7 +55,8 @@ export class CheckSwitchPhase extends BattlePhase {
|
||||
|| pokemon.isTrapped()
|
||||
|| globalScene.getPlayerField().some(p => p.getTag(BattlerTagType.COMMANDED))
|
||||
) {
|
||||
return super.end();
|
||||
this.end(true);
|
||||
return;
|
||||
}
|
||||
|
||||
globalScene.ui.showText(
|
||||
@ -71,10 +74,17 @@ export class CheckSwitchPhase extends BattlePhase {
|
||||
},
|
||||
() => {
|
||||
globalScene.ui.setMode(UiMode.MESSAGE);
|
||||
this.end();
|
||||
this.end(true);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public override end(queuePostSummon = false): void {
|
||||
if (queuePostSummon) {
|
||||
globalScene.phaseManager.unshiftNew("PostSummonPhase", this.fieldIndex);
|
||||
}
|
||||
super.end();
|
||||
}
|
||||
}
|
||||
|
17
src/phases/dynamic-phase-marker.ts
Normal file
17
src/phases/dynamic-phase-marker.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import type { PhaseString } from "#app/@types/phase-types";
|
||||
import { Phase } from "#app/phase";
|
||||
|
||||
/**
|
||||
* This phase exists for the sole purpose of marking the location and type of a dynamic phase for the phase manager
|
||||
*/
|
||||
export class DynamicPhaseMarker extends Phase {
|
||||
public override readonly phaseName = "DynamicPhaseMarker";
|
||||
|
||||
/** The type of phase which this phase is a marker for */
|
||||
public phaseType: PhaseString;
|
||||
|
||||
constructor(type: PhaseString) {
|
||||
super();
|
||||
this.phaseType = type;
|
||||
}
|
||||
}
|
@ -225,7 +225,7 @@ export class EggHatchPhase extends Phase {
|
||||
}
|
||||
|
||||
end() {
|
||||
if (globalScene.phaseManager.findPhase(p => p.is("EggHatchPhase"))) {
|
||||
if (globalScene.phaseManager.hasPhaseOfType("EggHatchPhase")) {
|
||||
this.eggHatchHandler.clear();
|
||||
} else {
|
||||
globalScene.time.delayedCall(250, () => globalScene.setModifiersVisible(true));
|
||||
|
@ -565,29 +565,6 @@ export class EncounterPhase extends BattlePhase {
|
||||
});
|
||||
|
||||
if (![BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(globalScene.currentBattle.battleType)) {
|
||||
enemyField.map(p =>
|
||||
globalScene.phaseManager.pushConditionalPhase(
|
||||
globalScene.phaseManager.create("PostSummonPhase", p.getBattlerIndex()),
|
||||
() => {
|
||||
// if there is not a player party, we can't continue
|
||||
if (globalScene.getPlayerParty().length === 0) {
|
||||
return false;
|
||||
}
|
||||
// how many player pokemon are on the field ?
|
||||
const pokemonsOnFieldCount = globalScene.getPlayerParty().filter(p => p.isOnField()).length;
|
||||
// if it's a 2vs1, there will never be a 2nd pokemon on our field even
|
||||
const requiredPokemonsOnField = Math.min(
|
||||
globalScene.getPlayerParty().filter(p => !p.isFainted()).length,
|
||||
2,
|
||||
);
|
||||
// if it's a double, there should be 2, otherwise 1
|
||||
if (globalScene.currentBattle.double) {
|
||||
return pokemonsOnFieldCount === requiredPokemonsOnField;
|
||||
}
|
||||
return pokemonsOnFieldCount === 1;
|
||||
},
|
||||
),
|
||||
);
|
||||
const ivScannerModifier = globalScene.findModifier(m => m instanceof IvScannerModifier);
|
||||
if (ivScannerModifier) {
|
||||
enemyField.map(p => globalScene.phaseManager.pushNew("ScanIvsPhase", p.getBattlerIndex()));
|
||||
@ -596,36 +573,30 @@ export class EncounterPhase extends BattlePhase {
|
||||
|
||||
if (!this.loaded) {
|
||||
const availablePartyMembers = globalScene.getPokemonAllowedInBattle();
|
||||
const minPartySize = globalScene.currentBattle.double ? 2 : 1;
|
||||
const currentBattle = globalScene.currentBattle;
|
||||
const checkSwitch =
|
||||
currentBattle.battleType !== BattleType.TRAINER
|
||||
&& (currentBattle.waveIndex > 1 || !globalScene.gameMode.isDaily)
|
||||
&& availablePartyMembers.length > minPartySize;
|
||||
|
||||
const phaseManager = globalScene.phaseManager;
|
||||
if (!availablePartyMembers[0].isOnField()) {
|
||||
globalScene.phaseManager.pushNew("SummonPhase", 0);
|
||||
phaseManager.pushNew("SummonPhase", 0, true, false, checkSwitch);
|
||||
}
|
||||
|
||||
if (globalScene.currentBattle.double) {
|
||||
if (currentBattle.double) {
|
||||
if (availablePartyMembers.length > 1) {
|
||||
globalScene.phaseManager.pushNew("ToggleDoublePositionPhase", true);
|
||||
phaseManager.pushNew("ToggleDoublePositionPhase", true);
|
||||
if (!availablePartyMembers[1].isOnField()) {
|
||||
globalScene.phaseManager.pushNew("SummonPhase", 1);
|
||||
phaseManager.pushNew("SummonPhase", 1, true, false, checkSwitch);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (availablePartyMembers.length > 1 && availablePartyMembers[1].isOnField()) {
|
||||
globalScene.phaseManager.pushNew("ReturnPhase", 1);
|
||||
}
|
||||
globalScene.phaseManager.pushNew("ToggleDoublePositionPhase", false);
|
||||
}
|
||||
|
||||
if (
|
||||
globalScene.currentBattle.battleType !== BattleType.TRAINER
|
||||
&& (globalScene.currentBattle.waveIndex > 1 || !globalScene.gameMode.isDaily)
|
||||
) {
|
||||
const minPartySize = globalScene.currentBattle.double ? 2 : 1;
|
||||
if (availablePartyMembers.length > minPartySize) {
|
||||
globalScene.phaseManager.pushNew("CheckSwitchPhase", 0, globalScene.currentBattle.double);
|
||||
if (globalScene.currentBattle.double) {
|
||||
globalScene.phaseManager.pushNew("CheckSwitchPhase", 1, globalScene.currentBattle.double);
|
||||
}
|
||||
}
|
||||
phaseManager.pushNew("ToggleDoublePositionPhase", false);
|
||||
}
|
||||
}
|
||||
handleTutorial(Tutorial.Access_Menu).then(() => super.end());
|
||||
|
@ -84,19 +84,12 @@ export class GameOverPhase extends BattlePhase {
|
||||
globalScene.phaseManager.pushNew("EncounterPhase", true);
|
||||
|
||||
const availablePartyMembers = globalScene.getPokemonAllowedInBattle().length;
|
||||
|
||||
globalScene.phaseManager.pushNew("SummonPhase", 0);
|
||||
if (globalScene.currentBattle.double && availablePartyMembers > 1) {
|
||||
globalScene.phaseManager.pushNew("SummonPhase", 1);
|
||||
}
|
||||
if (
|
||||
const checkSwitch =
|
||||
globalScene.currentBattle.waveIndex > 1
|
||||
&& globalScene.currentBattle.battleType !== BattleType.TRAINER
|
||||
) {
|
||||
globalScene.phaseManager.pushNew("CheckSwitchPhase", 0, globalScene.currentBattle.double);
|
||||
if (globalScene.currentBattle.double && availablePartyMembers > 1) {
|
||||
globalScene.phaseManager.pushNew("CheckSwitchPhase", 1, globalScene.currentBattle.double);
|
||||
}
|
||||
&& globalScene.currentBattle.battleType !== BattleType.TRAINER;
|
||||
globalScene.phaseManager.pushNew("SummonPhase", 0, true, false, checkSwitch);
|
||||
if (globalScene.currentBattle.double && availablePartyMembers > 1) {
|
||||
globalScene.phaseManager.pushNew("SummonPhase", 1, true, false, checkSwitch);
|
||||
}
|
||||
|
||||
globalScene.ui.fadeIn(1250);
|
||||
@ -267,7 +260,6 @@ export class GameOverPhase extends BattlePhase {
|
||||
.then(success => doGameOver(!globalScene.gameMode.isDaily || !!success))
|
||||
.catch(_err => {
|
||||
globalScene.phaseManager.clearPhaseQueue();
|
||||
globalScene.phaseManager.clearPhaseQueueSplice();
|
||||
globalScene.phaseManager.unshiftNew("MessagePhase", i18next.t("menu:serverCommunicationFailed"), 2500);
|
||||
// force the game to reload after 2 seconds.
|
||||
setTimeout(() => {
|
||||
|
@ -187,7 +187,7 @@ export class LearnMovePhase extends PlayerPartyMemberPokemonPhase {
|
||||
pokemon.usedTMs = [];
|
||||
}
|
||||
pokemon.usedTMs.push(this.moveId);
|
||||
globalScene.phaseManager.tryRemovePhase(phase => phase.is("SelectModifierPhase"));
|
||||
globalScene.phaseManager.tryRemovePhase("SelectModifierPhase");
|
||||
} else if (this.learnMoveType === LearnMoveType.MEMORY) {
|
||||
if (this.cost !== -1) {
|
||||
if (!Overrides.WAIVE_ROLL_FEE_OVERRIDE) {
|
||||
@ -197,7 +197,7 @@ export class LearnMovePhase extends PlayerPartyMemberPokemonPhase {
|
||||
}
|
||||
globalScene.playSound("se/buy");
|
||||
} else {
|
||||
globalScene.phaseManager.tryRemovePhase(phase => phase.is("SelectModifierPhase"));
|
||||
globalScene.phaseManager.tryRemovePhase("SelectModifierPhase");
|
||||
}
|
||||
}
|
||||
pokemon.setMove(index, this.moveId);
|
||||
|
@ -75,7 +75,7 @@ export class MoveChargePhase extends PokemonPhase {
|
||||
// Otherwise, add the attack portion to the user's move queue to execute next turn.
|
||||
// TODO: This checks status twice for a single-turn usage...
|
||||
if (instantCharge.value) {
|
||||
globalScene.phaseManager.tryRemovePhase(phase => phase.is("MoveEndPhase") && phase.getPokemon() === user);
|
||||
globalScene.phaseManager.tryRemovePhase("MoveEndPhase", phase => phase.getPokemon() === user);
|
||||
globalScene.phaseManager.unshiftNew("MovePhase", user, [this.targetIndex], this.move, this.useMode);
|
||||
} else {
|
||||
user.pushMoveQueue({ move: move.id, targets: [this.targetIndex], useMode: this.useMode });
|
||||
|
@ -16,6 +16,7 @@ import { MoveCategory } from "#enums/move-category";
|
||||
import { MoveEffectTrigger } from "#enums/move-effect-trigger";
|
||||
import { MoveFlags } from "#enums/move-flags";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { MoveTarget } from "#enums/move-target";
|
||||
import { isReflected, MoveUseMode } from "#enums/move-use-mode";
|
||||
@ -318,7 +319,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
applyMoveAttrs("MissEffectAttr", user, target, this.move);
|
||||
break;
|
||||
case HitCheckResult.REFLECTED:
|
||||
globalScene.phaseManager.appendNewToPhase("MoveEndPhase", "MoveReflectPhase", target, user, this.move);
|
||||
globalScene.phaseManager.unshiftNew("MoveReflectPhase", target, user, this.move);
|
||||
break;
|
||||
case HitCheckResult.PENDING:
|
||||
case HitCheckResult.ERROR:
|
||||
@ -747,10 +748,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
* @param target - The {@linkcode Pokemon} that fainted
|
||||
*/
|
||||
protected onFaintTarget(user: Pokemon, target: Pokemon): void {
|
||||
// set splice index here, so future scene queues happen before FaintedPhase
|
||||
globalScene.phaseManager.setPhaseQueueSplice();
|
||||
|
||||
globalScene.phaseManager.unshiftNew("FaintPhase", target.getBattlerIndex(), false, user);
|
||||
globalScene.phaseManager.queueFaintPhase(target.getBattlerIndex(), false, user);
|
||||
|
||||
target.destroySubstitute();
|
||||
target.lapseTag(BattlerTagType.COMMANDED);
|
||||
|
@ -5,8 +5,8 @@ import { BattlePhase } from "#phases/battle-phase";
|
||||
|
||||
export class MoveHeaderPhase extends BattlePhase {
|
||||
public readonly phaseName = "MoveHeaderPhase";
|
||||
public pokemon: Pokemon;
|
||||
public move: PokemonMove;
|
||||
public pokemon: Pokemon;
|
||||
|
||||
constructor(pokemon: Pokemon, move: PokemonMove) {
|
||||
super();
|
||||
@ -15,6 +15,10 @@ export class MoveHeaderPhase extends BattlePhase {
|
||||
this.move = move;
|
||||
}
|
||||
|
||||
public getPokemon(): Pokemon {
|
||||
return this.pokemon;
|
||||
}
|
||||
|
||||
canMove(): boolean {
|
||||
return this.pokemon.isActive(true) && this.move.isUsable(this.pokemon);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import { MOVE_COLOR } from "#app/constants/colors";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import Overrides from "#app/overrides";
|
||||
import { PokemonPhase } from "#app/phases/pokemon-phase";
|
||||
import { CenterOfAttentionTag } from "#data/battler-tags";
|
||||
import { SpeciesFormChangePreMoveTrigger } from "#data/form-change-triggers";
|
||||
import { getStatusEffectActivationText, getStatusEffectHealText } from "#data/status-effect";
|
||||
@ -15,6 +16,7 @@ import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { CommonAnim } from "#enums/move-anims-common";
|
||||
import { MoveFlags } from "#enums/move-flags";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { isIgnorePP, isIgnoreStatus, isReflected, isVirtual, MoveUseMode } from "#enums/move-use-mode";
|
||||
import { PokemonType } from "#enums/pokemon-type";
|
||||
@ -24,20 +26,19 @@ import type { Pokemon } from "#field/pokemon";
|
||||
import { applyMoveAttrs } from "#moves/apply-attrs";
|
||||
import { frenzyMissFunc } from "#moves/move-utils";
|
||||
import type { PokemonMove } from "#moves/pokemon-move";
|
||||
import { BattlePhase } from "#phases/battle-phase";
|
||||
import type { TurnMove } from "#types/turn-move";
|
||||
import { NumberHolder } from "#utils/common";
|
||||
import { enumValueToKey } from "#utils/enums";
|
||||
import i18next from "i18next";
|
||||
|
||||
export class MovePhase extends BattlePhase {
|
||||
export class MovePhase extends PokemonPhase {
|
||||
public readonly phaseName = "MovePhase";
|
||||
protected _pokemon: Pokemon;
|
||||
protected _move: PokemonMove;
|
||||
public move: PokemonMove;
|
||||
protected _targets: BattlerIndex[];
|
||||
public readonly useMode: MoveUseMode; // Made public for quash
|
||||
/** Whether the current move is forced last (used for Quash). */
|
||||
protected forcedLast: boolean;
|
||||
/** The timing modifier of the move (used by Quash and to force called moves to the front of their queue) */
|
||||
public timingModifier: MovePhaseTimingModifier;
|
||||
/** Whether the current move should fail but still use PP. */
|
||||
protected failed = false;
|
||||
/** Whether the current move should fail and retain PP. */
|
||||
@ -59,14 +60,6 @@ export class MovePhase extends BattlePhase {
|
||||
this._pokemon = pokemon;
|
||||
}
|
||||
|
||||
public get move(): PokemonMove {
|
||||
return this._move;
|
||||
}
|
||||
|
||||
protected set move(move: PokemonMove) {
|
||||
this._move = move;
|
||||
}
|
||||
|
||||
public get targets(): BattlerIndex[] {
|
||||
return this._targets;
|
||||
}
|
||||
@ -81,16 +74,22 @@ export class MovePhase extends BattlePhase {
|
||||
* @param move - The {@linkcode PokemonMove} to use
|
||||
* @param useMode - The {@linkcode MoveUseMode} corresponding to this move's means of execution (usually `MoveUseMode.NORMAL`).
|
||||
* 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 timingModifier - The {@linkcode MovePhaseTimingModifier} for the move; Default {@linkcode MovePhaseTimingModifier.NORMAL}
|
||||
*/
|
||||
constructor(pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, useMode: MoveUseMode, forcedLast = false) {
|
||||
super();
|
||||
constructor(
|
||||
pokemon: Pokemon,
|
||||
targets: BattlerIndex[],
|
||||
move: PokemonMove,
|
||||
useMode: MoveUseMode,
|
||||
timingModifier: MovePhaseTimingModifier = MovePhaseTimingModifier.NORMAL,
|
||||
) {
|
||||
super(pokemon.getBattlerIndex());
|
||||
|
||||
this.pokemon = pokemon;
|
||||
this.targets = targets;
|
||||
this.move = move;
|
||||
this.useMode = useMode;
|
||||
this.forcedLast = forcedLast;
|
||||
this.timingModifier = timingModifier;
|
||||
this.moveHistoryEntry = {
|
||||
move: MoveId.NONE,
|
||||
targets,
|
||||
@ -121,14 +120,6 @@ export class MovePhase extends BattlePhase {
|
||||
this.cancelled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows whether the current move has been forced to the end of the turn
|
||||
* Needed for speed order, see {@linkcode MoveId.QUASH}
|
||||
*/
|
||||
public isForcedLast(): boolean {
|
||||
return this.forcedLast;
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
super.start();
|
||||
|
||||
|
@ -48,7 +48,6 @@ export class MysteryEncounterPhase extends Phase {
|
||||
|
||||
// Clears out queued phases that are part of standard battle
|
||||
globalScene.phaseManager.clearPhaseQueue();
|
||||
globalScene.phaseManager.clearPhaseQueueSplice();
|
||||
|
||||
const encounter = globalScene.currentBattle.mysteryEncounter!;
|
||||
encounter.updateSeedOffset();
|
||||
@ -233,9 +232,7 @@ export class MysteryEncounterBattleStartCleanupPhase extends Phase {
|
||||
});
|
||||
|
||||
// Remove any status tick phases
|
||||
while (globalScene.phaseManager.findPhase(p => p.is("PostTurnStatusEffectPhase"))) {
|
||||
globalScene.phaseManager.tryRemovePhase(p => p.is("PostTurnStatusEffectPhase"));
|
||||
}
|
||||
globalScene.phaseManager.removeAllPhasesOfType("PostTurnStatusEffectPhase");
|
||||
|
||||
// The total number of Pokemon in the player's party that can legally fight
|
||||
const legalPlayerPokemon = globalScene.getPokemonAllowedInBattle();
|
||||
@ -412,16 +409,21 @@ export class MysteryEncounterBattlePhase extends Phase {
|
||||
}
|
||||
|
||||
const availablePartyMembers = globalScene.getPlayerParty().filter(p => p.isAllowedInBattle());
|
||||
const minPartySize = globalScene.currentBattle.double ? 2 : 1;
|
||||
const checkSwitch =
|
||||
encounterMode !== MysteryEncounterMode.TRAINER_BATTLE
|
||||
&& !this.disableSwitch
|
||||
&& availablePartyMembers.length > minPartySize;
|
||||
|
||||
if (!availablePartyMembers[0].isOnField()) {
|
||||
globalScene.phaseManager.pushNew("SummonPhase", 0);
|
||||
globalScene.phaseManager.pushNew("SummonPhase", 0, true, false, checkSwitch);
|
||||
}
|
||||
|
||||
if (globalScene.currentBattle.double) {
|
||||
if (availablePartyMembers.length > 1) {
|
||||
globalScene.phaseManager.pushNew("ToggleDoublePositionPhase", true);
|
||||
if (!availablePartyMembers[1].isOnField()) {
|
||||
globalScene.phaseManager.pushNew("SummonPhase", 1);
|
||||
globalScene.phaseManager.pushNew("SummonPhase", 1, true, false, checkSwitch);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -432,16 +434,6 @@ export class MysteryEncounterBattlePhase extends Phase {
|
||||
globalScene.phaseManager.pushNew("ToggleDoublePositionPhase", false);
|
||||
}
|
||||
|
||||
if (encounterMode !== MysteryEncounterMode.TRAINER_BATTLE && !this.disableSwitch) {
|
||||
const minPartySize = globalScene.currentBattle.double ? 2 : 1;
|
||||
if (availablePartyMembers.length > minPartySize) {
|
||||
globalScene.phaseManager.pushNew("CheckSwitchPhase", 0, globalScene.currentBattle.double);
|
||||
if (globalScene.currentBattle.double) {
|
||||
globalScene.phaseManager.pushNew("CheckSwitchPhase", 1, globalScene.currentBattle.double);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.end();
|
||||
}
|
||||
|
||||
@ -540,7 +532,7 @@ export class MysteryEncounterRewardsPhase extends Phase {
|
||||
if (encounter.doEncounterRewards) {
|
||||
encounter.doEncounterRewards();
|
||||
} else if (this.addHealPhase) {
|
||||
globalScene.phaseManager.tryRemovePhase(p => p.is("SelectModifierPhase"));
|
||||
globalScene.phaseManager.removeAllPhasesOfType("SelectModifierPhase");
|
||||
globalScene.phaseManager.unshiftNew("SelectModifierPhase", 0, undefined, {
|
||||
fillRemaining: false,
|
||||
rerollMultiplier: -1,
|
||||
|
@ -6,12 +6,7 @@ export class NewBattlePhase extends BattlePhase {
|
||||
start() {
|
||||
super.start();
|
||||
|
||||
// cull any extra `NewBattle` phases from the queue.
|
||||
globalScene.phaseManager.phaseQueue = globalScene.phaseManager.phaseQueue.filter(
|
||||
phase => !phase.is("NewBattlePhase"),
|
||||
);
|
||||
// `phaseQueuePrepend` is private, so we have to use this inefficient loop.
|
||||
while (globalScene.phaseManager.tryRemoveUnshiftedPhase(phase => phase.is("NewBattlePhase"))) {}
|
||||
globalScene.phaseManager.removeAllPhasesOfType("NewBattlePhase");
|
||||
|
||||
globalScene.newBattle();
|
||||
|
||||
|
@ -22,4 +22,8 @@ export abstract class PartyMemberPokemonPhase extends FieldPhase {
|
||||
getPokemon(): Pokemon {
|
||||
return this.getParty()[this.partyMemberIndex];
|
||||
}
|
||||
|
||||
isPlayer(): boolean {
|
||||
return this.player;
|
||||
}
|
||||
}
|
||||
|
@ -6,8 +6,8 @@ import { PostSummonPhase } from "#phases/post-summon-phase";
|
||||
* Helper to {@linkcode PostSummonPhase} which applies abilities
|
||||
*/
|
||||
export class PostSummonActivateAbilityPhase extends PostSummonPhase {
|
||||
private priority: number;
|
||||
private passive: boolean;
|
||||
private readonly priority: number;
|
||||
private readonly passive: boolean;
|
||||
|
||||
constructor(battlerIndex: BattlerIndex, priority: number, passive: boolean) {
|
||||
super(battlerIndex);
|
||||
|
@ -1,19 +1,29 @@
|
||||
import { applyAbAttrs } from "#abilities/apply-ab-attrs";
|
||||
import type { PhaseString } from "#app/@types/phase-types";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { EntryHazardTag } from "#data/arena-tag";
|
||||
import { MysteryEncounterPostSummonTag } from "#data/battler-tags";
|
||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||
import type { BattlerIndex } from "#enums/battler-index";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { PokemonPhase } from "#phases/pokemon-phase";
|
||||
|
||||
export class PostSummonPhase extends PokemonPhase {
|
||||
public readonly phaseName = "PostSummonPhase";
|
||||
/** Used to determine whether to push or unshift {@linkcode PostSummonActivateAbilityPhase}s */
|
||||
public readonly source: PhaseString;
|
||||
|
||||
constructor(battlerIndex?: BattlerIndex | number, source: PhaseString = "SwitchSummonPhase") {
|
||||
super(battlerIndex);
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
start() {
|
||||
super.start();
|
||||
|
||||
const pokemon = this.getPokemon();
|
||||
|
||||
console.log("Ran PSP for:", pokemon.name);
|
||||
if (pokemon.status?.effect === StatusEffect.TOXIC) {
|
||||
pokemon.status.toxicTurnCount = 0;
|
||||
}
|
||||
@ -29,8 +39,7 @@ export class PostSummonPhase extends PokemonPhase {
|
||||
) {
|
||||
pokemon.lapseTag(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON);
|
||||
}
|
||||
|
||||
const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField();
|
||||
const field = pokemon.isPlayer() ? globalScene.getPlayerField(true) : globalScene.getEnemyField(true);
|
||||
for (const p of field) {
|
||||
applyAbAttrs("CommanderAbAttr", { pokemon: p });
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ import { BattleSpec } from "#enums/battle-spec";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
import { BattlePhase } from "#phases/battle-phase";
|
||||
import type { MovePhase } from "#phases/move-phase";
|
||||
|
||||
export class QuietFormChangePhase extends BattlePhase {
|
||||
public readonly phaseName = "QuietFormChangePhase";
|
||||
@ -170,12 +169,7 @@ export class QuietFormChangePhase extends BattlePhase {
|
||||
this.pokemon.initBattleInfo();
|
||||
this.pokemon.cry();
|
||||
|
||||
const movePhase = globalScene.phaseManager.findPhase(
|
||||
p => p.is("MovePhase") && p.pokemon === this.pokemon,
|
||||
) as MovePhase;
|
||||
if (movePhase) {
|
||||
movePhase.cancel();
|
||||
}
|
||||
globalScene.phaseManager.cancelMove(p => p.pokemon === this.pokemon);
|
||||
}
|
||||
if (this.formChange.trigger instanceof SpeciesFormChangeTeraTrigger) {
|
||||
const params = { pokemon: this.pokemon };
|
||||
|
@ -223,10 +223,7 @@ export class StatStageChangePhase extends PokemonPhase {
|
||||
});
|
||||
|
||||
// Look for any other stat change phases; if this is the last one, do White Herb check
|
||||
const existingPhase = globalScene.phaseManager.findPhase(
|
||||
p => p.is("StatStageChangePhase") && p.battlerIndex === this.battlerIndex,
|
||||
);
|
||||
if (!existingPhase?.is("StatStageChangePhase")) {
|
||||
if (!globalScene.phaseManager.hasPhaseOfType("StatStageChangePhase", p => p.battlerIndex === this.battlerIndex)) {
|
||||
// Apply White Herb if needed
|
||||
const whiteHerb = globalScene.applyModifier(
|
||||
ResetNegativeStatStageModifier,
|
||||
@ -297,49 +294,6 @@ export class StatStageChangePhase extends PokemonPhase {
|
||||
}
|
||||
}
|
||||
|
||||
aggregateStatStageChanges(): void {
|
||||
const accEva: BattleStat[] = [Stat.ACC, Stat.EVA];
|
||||
const isAccEva = accEva.some(s => this.stats.includes(s));
|
||||
let existingPhase: StatStageChangePhase;
|
||||
if (this.stats.length === 1) {
|
||||
while (
|
||||
(existingPhase = globalScene.phaseManager.findPhase(
|
||||
p =>
|
||||
p.is("StatStageChangePhase")
|
||||
&& p.battlerIndex === this.battlerIndex
|
||||
&& p.stats.length === 1
|
||||
&& p.stats[0] === this.stats[0]
|
||||
&& p.selfTarget === this.selfTarget
|
||||
&& p.showMessage === this.showMessage
|
||||
&& p.ignoreAbilities === this.ignoreAbilities,
|
||||
) as StatStageChangePhase)
|
||||
) {
|
||||
this.stages += existingPhase.stages;
|
||||
|
||||
if (!globalScene.phaseManager.tryRemovePhase(p => p === existingPhase)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
while (
|
||||
(existingPhase = globalScene.phaseManager.findPhase(
|
||||
p =>
|
||||
p.is("StatStageChangePhase")
|
||||
&& p.battlerIndex === this.battlerIndex
|
||||
&& p.selfTarget === this.selfTarget
|
||||
&& accEva.some(s => p.stats.includes(s)) === isAccEva
|
||||
&& p.stages === this.stages
|
||||
&& p.showMessage === this.showMessage
|
||||
&& p.ignoreAbilities === this.ignoreAbilities,
|
||||
) as StatStageChangePhase)
|
||||
) {
|
||||
this.stats.push(...existingPhase.stats);
|
||||
if (!globalScene.phaseManager.tryRemovePhase(p => p === existingPhase)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getStatStageChangeMessages(stats: BattleStat[], stages: number, relStages: number[]): string[] {
|
||||
const messages: string[] = [];
|
||||
|
||||
|
@ -16,12 +16,14 @@ import i18next from "i18next";
|
||||
export class SummonPhase extends PartyMemberPokemonPhase {
|
||||
// The union type is needed to keep typescript happy as these phases extend from SummonPhase
|
||||
public readonly phaseName: "SummonPhase" | "SummonMissingPhase" | "SwitchSummonPhase" | "ReturnPhase" = "SummonPhase";
|
||||
private loaded: boolean;
|
||||
private readonly loaded: boolean;
|
||||
private readonly checkSwitch: boolean;
|
||||
|
||||
constructor(fieldIndex: number, player = true, loaded = false) {
|
||||
constructor(fieldIndex: number, player = true, loaded = false, checkSwitch = false) {
|
||||
super(fieldIndex, player);
|
||||
|
||||
this.loaded = loaded;
|
||||
this.checkSwitch = checkSwitch;
|
||||
}
|
||||
|
||||
start() {
|
||||
@ -288,7 +290,17 @@ export class SummonPhase extends PartyMemberPokemonPhase {
|
||||
}
|
||||
|
||||
queuePostSummon(): void {
|
||||
globalScene.phaseManager.pushNew("PostSummonPhase", this.getPokemon().getBattlerIndex());
|
||||
if (this.checkSwitch) {
|
||||
globalScene.phaseManager.pushNew(
|
||||
"CheckSwitchPhase",
|
||||
this.getPokemon().getFieldIndex(),
|
||||
globalScene.currentBattle.double,
|
||||
);
|
||||
} else {
|
||||
globalScene.phaseManager.pushNew("PostSummonPhase", this.getPokemon().getBattlerIndex(), this.phaseName);
|
||||
}
|
||||
|
||||
globalScene.phaseManager.tryAddEnemyPostSummonPhases();
|
||||
}
|
||||
|
||||
end() {
|
||||
@ -296,4 +308,8 @@ export class SummonPhase extends PartyMemberPokemonPhase {
|
||||
|
||||
super.end();
|
||||
}
|
||||
|
||||
public getFieldIndex(): number {
|
||||
return this.fieldIndex;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { DynamicPhaseType } from "#enums/dynamic-phase-type";
|
||||
import { SwitchType } from "#enums/switch-type";
|
||||
import { UiMode } from "#enums/ui-mode";
|
||||
import { BattlePhase } from "#phases/battle-phase";
|
||||
@ -77,14 +76,6 @@ export class SwitchPhase extends BattlePhase {
|
||||
fieldIndex,
|
||||
(slotIndex: number, option: PartyOption) => {
|
||||
if (slotIndex >= globalScene.currentBattle.getBattlerCount() && slotIndex < 6) {
|
||||
// Remove any pre-existing PostSummonPhase under the same field index.
|
||||
// Pre-existing PostSummonPhases may occur when this phase is invoked during a prompt to switch at the start of a wave.
|
||||
// TODO: Separate the animations from `SwitchSummonPhase` and co. into another phase and use that on initial switch - this is a band-aid fix
|
||||
globalScene.phaseManager.tryRemoveDynamicPhase(
|
||||
DynamicPhaseType.POST_SUMMON,
|
||||
p => p.is("PostSummonPhase") && p.player && p.fieldIndex === this.fieldIndex,
|
||||
"all",
|
||||
);
|
||||
const switchType = option === PartyOption.PASS_BATON ? SwitchType.BATON_PASS : this.switchType;
|
||||
globalScene.phaseManager.unshiftNew("SwitchSummonPhase", switchType, fieldIndex, slotIndex, this.doReturn);
|
||||
}
|
||||
|
@ -241,11 +241,11 @@ export class SwitchSummonPhase extends SummonPhase {
|
||||
|
||||
globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true);
|
||||
// Reverts to weather-based forms when weather suppressors (Cloud Nine/Air Lock) are switched out
|
||||
globalScene.arena.triggerWeatherBasedFormChanges();
|
||||
globalScene.arena.triggerWeatherBasedFormChanges(pokemon);
|
||||
}
|
||||
|
||||
queuePostSummon(): void {
|
||||
globalScene.phaseManager.startNewDynamicPhase("PostSummonPhase", this.getPokemon().getBattlerIndex());
|
||||
globalScene.phaseManager.unshiftNew("PostSummonPhase", this.getPokemon().getBattlerIndex());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -315,23 +315,15 @@ export class TitlePhase extends Phase {
|
||||
|
||||
if (this.loaded) {
|
||||
const availablePartyMembers = globalScene.getPokemonAllowedInBattle().length;
|
||||
|
||||
globalScene.phaseManager.pushNew("SummonPhase", 0, true, true);
|
||||
if (globalScene.currentBattle.double && availablePartyMembers > 1) {
|
||||
globalScene.phaseManager.pushNew("SummonPhase", 1, true, true);
|
||||
}
|
||||
|
||||
if (
|
||||
const minPartySize = globalScene.currentBattle.double ? 2 : 1;
|
||||
const checkSwitch =
|
||||
globalScene.currentBattle.battleType !== BattleType.TRAINER
|
||||
&& (globalScene.currentBattle.waveIndex > 1 || !globalScene.gameMode.isDaily)
|
||||
) {
|
||||
const minPartySize = globalScene.currentBattle.double ? 2 : 1;
|
||||
if (availablePartyMembers > minPartySize) {
|
||||
globalScene.phaseManager.pushNew("CheckSwitchPhase", 0, globalScene.currentBattle.double);
|
||||
if (globalScene.currentBattle.double) {
|
||||
globalScene.phaseManager.pushNew("CheckSwitchPhase", 1, globalScene.currentBattle.double);
|
||||
}
|
||||
}
|
||||
&& availablePartyMembers > minPartySize;
|
||||
|
||||
globalScene.phaseManager.pushNew("SummonPhase", 0, true, true, checkSwitch);
|
||||
if (globalScene.currentBattle.double && availablePartyMembers > 1) {
|
||||
globalScene.phaseManager.pushNew("SummonPhase", 1, true, true, checkSwitch);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,6 +25,7 @@ export class TurnEndPhase extends FieldPhase {
|
||||
|
||||
globalScene.currentBattle.incrementTurn();
|
||||
globalScene.eventTarget.dispatchEvent(new TurnEndEvent(globalScene.currentBattle.turn));
|
||||
globalScene.phaseManager.dynamicQueueManager.clearLastTurnOrder();
|
||||
|
||||
globalScene.phaseManager.hideAbilityBar();
|
||||
|
||||
|
@ -1,89 +1,31 @@
|
||||
import { applyAbAttrs } from "#abilities/apply-ab-attrs";
|
||||
import type { TurnCommand } from "#app/battle";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { TrickRoomTag } from "#data/arena-tag";
|
||||
import { allMoves } from "#data/data-lists";
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
||||
import type { BattlerIndex } from "#enums/battler-index";
|
||||
import { Command } from "#enums/command";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { SwitchType } from "#enums/switch-type";
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
import { BypassSpeedChanceModifier } from "#modifiers/modifier";
|
||||
import { PokemonMove } from "#moves/pokemon-move";
|
||||
import { FieldPhase } from "#phases/field-phase";
|
||||
import { BooleanHolder, randSeedShuffle } from "#utils/common";
|
||||
import { inSpeedOrder } from "#utils/speed-order-generator";
|
||||
|
||||
export class TurnStartPhase extends FieldPhase {
|
||||
public readonly phaseName = "TurnStartPhase";
|
||||
|
||||
/**
|
||||
* Helper method to retrieve the current speed order of the combattants.
|
||||
* It also checks for Trick Room and reverses the array if it is present.
|
||||
* @returns The {@linkcode BattlerIndex}es of all on-field Pokemon, sorted in speed order.
|
||||
* @todo Make this private
|
||||
*/
|
||||
getSpeedOrder(): BattlerIndex[] {
|
||||
const playerField = globalScene.getPlayerField().filter(p => p.isActive());
|
||||
const enemyField = globalScene.getEnemyField().filter(p => p.isActive());
|
||||
|
||||
// Shuffle the list before sorting so speed ties produce random results
|
||||
// This is seeded with the current turn to prevent turn order varying
|
||||
// based on how long since you last reloaded.
|
||||
let orderedTargets = (playerField as Pokemon[]).concat(enemyField);
|
||||
globalScene.executeWithSeedOffset(
|
||||
() => {
|
||||
orderedTargets = randSeedShuffle(orderedTargets);
|
||||
},
|
||||
globalScene.currentBattle.turn,
|
||||
globalScene.waveSeed,
|
||||
);
|
||||
|
||||
// Check for Trick Room and reverse sort order if active.
|
||||
// Notably, Pokerogue does NOT have the "outspeed trick room" glitch at >1809 spd.
|
||||
const speedReversed = new BooleanHolder(false);
|
||||
globalScene.arena.applyTags(TrickRoomTag, false, speedReversed);
|
||||
|
||||
orderedTargets.sort((a: Pokemon, b: Pokemon) => {
|
||||
const aSpeed = a.getEffectiveStat(Stat.SPD);
|
||||
const bSpeed = b.getEffectiveStat(Stat.SPD);
|
||||
|
||||
return speedReversed.value ? aSpeed - bSpeed : bSpeed - aSpeed;
|
||||
});
|
||||
|
||||
return orderedTargets.map(t => t.getFieldIndex() + (t.isEnemy() ? BattlerIndex.ENEMY : BattlerIndex.PLAYER));
|
||||
}
|
||||
|
||||
/**
|
||||
* This takes the result of {@linkcode 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 The `BattlerIndex`es of all on-field Pokemon sorted in action order.
|
||||
* Returns an ordering of the current field based on command priority
|
||||
* @returns The sequence of commands for this turn
|
||||
*/
|
||||
getCommandOrder(): BattlerIndex[] {
|
||||
let moveOrder = this.getSpeedOrder();
|
||||
// The creation of the battlerBypassSpeed object contains checks for the ability Quick Draw and the held item Quick Claw
|
||||
// The ability Mycelium Might disables Quick Claw's activation when using a status move
|
||||
// This occurs before the main loop because of battles with more than two Pokemon
|
||||
const battlerBypassSpeed = {};
|
||||
|
||||
globalScene.getField(true).forEach(p => {
|
||||
const bypassSpeed = new BooleanHolder(false);
|
||||
const canCheckHeldItems = new BooleanHolder(true);
|
||||
applyAbAttrs("BypassSpeedChanceAbAttr", { pokemon: p, bypass: bypassSpeed });
|
||||
applyAbAttrs("PreventBypassSpeedChanceAbAttr", {
|
||||
pokemon: p,
|
||||
bypass: bypassSpeed,
|
||||
canCheckHeldItems,
|
||||
});
|
||||
if (canCheckHeldItems.value) {
|
||||
globalScene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p, bypassSpeed);
|
||||
}
|
||||
battlerBypassSpeed[p.getBattlerIndex()] = bypassSpeed;
|
||||
});
|
||||
const playerField = globalScene.getPlayerField(true).map(p => p.getBattlerIndex());
|
||||
const enemyField = globalScene.getEnemyField(true).map(p => p.getBattlerIndex());
|
||||
const orderedTargets: BattlerIndex[] = playerField.concat(enemyField);
|
||||
|
||||
// The function begins sorting orderedTargets based on command priority, move priority, and possible speed bypasses.
|
||||
// Non-FIGHT commands (SWITCH, BALL, RUN) have a higher command priority and will always occur before any FIGHT commands.
|
||||
moveOrder = moveOrder.slice(0);
|
||||
moveOrder.sort((a, b) => {
|
||||
orderedTargets.sort((a, b) => {
|
||||
const aCommand = globalScene.currentBattle.turnCommands[a];
|
||||
const bCommand = globalScene.currentBattle.turnCommands[b];
|
||||
|
||||
@ -94,41 +36,14 @@ export class TurnStartPhase extends FieldPhase {
|
||||
if (bCommand?.command === Command.FIGHT) {
|
||||
return -1;
|
||||
}
|
||||
} else if (aCommand?.command === Command.FIGHT) {
|
||||
const aMove = allMoves[aCommand.move!.move];
|
||||
const bMove = allMoves[bCommand!.move!.move];
|
||||
|
||||
const aUser = globalScene.getField(true).find(p => p.getBattlerIndex() === a)!;
|
||||
const bUser = globalScene.getField(true).find(p => p.getBattlerIndex() === b)!;
|
||||
|
||||
const aPriority = aMove.getPriority(aUser, false);
|
||||
const bPriority = bMove.getPriority(bUser, false);
|
||||
|
||||
// The game now checks for differences in priority levels.
|
||||
// If the moves share the same original priority bracket, it can check for differences in battlerBypassSpeed and return the result.
|
||||
// This conditional is used to ensure that Quick Claw can still activate with abilities like Stall and Mycelium Might (attack moves only)
|
||||
// Otherwise, the game returns the user of the move with the highest priority.
|
||||
const isSameBracket = Math.ceil(aPriority) - Math.ceil(bPriority) === 0;
|
||||
if (aPriority !== bPriority) {
|
||||
if (isSameBracket && battlerBypassSpeed[a].value !== battlerBypassSpeed[b].value) {
|
||||
return battlerBypassSpeed[a].value ? -1 : 1;
|
||||
}
|
||||
return aPriority < bPriority ? 1 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
// If there is no difference between the move's calculated priorities,
|
||||
// check for differences in battlerBypassSpeed and returns the result.
|
||||
if (battlerBypassSpeed[a].value !== battlerBypassSpeed[b].value) {
|
||||
return battlerBypassSpeed[a].value ? -1 : 1;
|
||||
}
|
||||
|
||||
const aIndex = moveOrder.indexOf(a);
|
||||
const bIndex = moveOrder.indexOf(b);
|
||||
const aIndex = orderedTargets.indexOf(a);
|
||||
const bIndex = orderedTargets.indexOf(b);
|
||||
|
||||
return aIndex < bIndex ? -1 : aIndex > bIndex ? 1 : 0;
|
||||
});
|
||||
return moveOrder;
|
||||
return orderedTargets;
|
||||
}
|
||||
|
||||
// TODO: Refactor this alongside `CommandPhase.handleCommand` to use SEPARATE METHODS
|
||||
@ -139,9 +54,8 @@ export class TurnStartPhase extends FieldPhase {
|
||||
const field = globalScene.getField();
|
||||
const moveOrder = this.getCommandOrder();
|
||||
|
||||
for (const o of this.getSpeedOrder()) {
|
||||
const pokemon = field[o];
|
||||
const preTurnCommand = globalScene.currentBattle.preTurnCommands[o];
|
||||
for (const pokemon of inSpeedOrder(ArenaTagSide.BOTH)) {
|
||||
const preTurnCommand = globalScene.currentBattle.preTurnCommands[pokemon.getBattlerIndex()];
|
||||
|
||||
if (preTurnCommand?.skip) {
|
||||
continue;
|
||||
@ -154,6 +68,10 @@ export class TurnStartPhase extends FieldPhase {
|
||||
}
|
||||
|
||||
const phaseManager = globalScene.phaseManager;
|
||||
for (const pokemon of inSpeedOrder(ArenaTagSide.BOTH)) {
|
||||
applyAbAttrs("BypassSpeedChanceAbAttr", { pokemon });
|
||||
globalScene.applyModifiers(BypassSpeedChanceModifier, pokemon.isPlayer(), pokemon);
|
||||
}
|
||||
|
||||
moveOrder.forEach((o, index) => {
|
||||
const pokemon = field[o];
|
||||
@ -178,13 +96,8 @@ export class TurnStartPhase extends FieldPhase {
|
||||
// TODO: Re-order these phases to be consistent with mainline turn order:
|
||||
// https://www.smogon.com/forums/threads/sword-shield-battle-mechanics-research.3655528/page-64#post-9244179
|
||||
|
||||
phaseManager.pushNew("WeatherEffectPhase");
|
||||
phaseManager.pushNew("PositionalTagPhase");
|
||||
phaseManager.pushNew("BerryPhase");
|
||||
|
||||
phaseManager.pushNew("CheckStatusEffectPhase", moveOrder);
|
||||
|
||||
phaseManager.pushNew("TurnEndPhase");
|
||||
// TODO: In an ideal world, this is handled by the phase manager. The change is nontrivial due to the ordering of post-turn phases like those queued by VictoryPhase
|
||||
globalScene.phaseManager.queueTurnEndPhases();
|
||||
|
||||
/*
|
||||
* `this.end()` will call `PhaseManager#shiftPhase()`, which dumps everything from `phaseQueuePrepend`
|
||||
|
103
src/queues/move-phase-priority-queue.ts
Normal file
103
src/queues/move-phase-priority-queue.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import type { PokemonMove } from "#app/data/moves/pokemon-move";
|
||||
import type { Pokemon } from "#app/field/pokemon";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import type { MovePhase } from "#app/phases/move-phase";
|
||||
import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-queue";
|
||||
import type { BattlerIndex } from "#enums/battler-index";
|
||||
import type { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
|
||||
import type { PhaseConditionFunc } from "#types/phase-types";
|
||||
|
||||
/** A priority queue responsible for the ordering of {@linkcode MovePhase}s */
|
||||
export class MovePhasePriorityQueue extends PokemonPhasePriorityQueue<MovePhase> {
|
||||
private lastTurnOrder: Pokemon[] = [];
|
||||
|
||||
protected override reorder(): void {
|
||||
super.reorder();
|
||||
this.sortPostSpeed();
|
||||
}
|
||||
|
||||
public cancelMove(condition: PhaseConditionFunc<"MovePhase">): void {
|
||||
this.queue.find(p => condition(p))?.cancel();
|
||||
}
|
||||
|
||||
public setTimingModifier(condition: PhaseConditionFunc<"MovePhase">, modifier: MovePhaseTimingModifier): void {
|
||||
const phase = this.queue.find(p => condition(p));
|
||||
if (phase != null) {
|
||||
phase.timingModifier = modifier;
|
||||
}
|
||||
}
|
||||
|
||||
public setMoveForPhase(condition: PhaseConditionFunc<"MovePhase">, move: PokemonMove) {
|
||||
const phase = this.queue.find(p => condition(p));
|
||||
if (phase != null) {
|
||||
phase.move = move;
|
||||
}
|
||||
}
|
||||
|
||||
public redirectMoves(removedPokemon: Pokemon, allyPokemon: Pokemon): void {
|
||||
// failsafe: if not a double battle just return
|
||||
if (!globalScene.currentBattle.double) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: simplify later
|
||||
if (allyPokemon?.isActive(true)) {
|
||||
this.queue
|
||||
.filter(
|
||||
mp =>
|
||||
mp.targets.length === 1
|
||||
&& mp.targets[0] === removedPokemon.getBattlerIndex()
|
||||
&& mp.pokemon.isPlayer() !== allyPokemon.isPlayer(),
|
||||
)
|
||||
.forEach(targetingMovePhase => {
|
||||
if (targetingMovePhase && targetingMovePhase.targets[0] !== allyPokemon.getBattlerIndex()) {
|
||||
targetingMovePhase.targets[0] = allyPokemon.getBattlerIndex();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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 timingModifiers = [a, b].map(movePhase => movePhase.timingModifier);
|
||||
|
||||
if (timingModifiers[0] !== timingModifiers[1]) {
|
||||
return timingModifiers[1] - timingModifiers[0];
|
||||
}
|
||||
|
||||
return priority[1] - priority[0];
|
||||
});
|
||||
}
|
||||
}
|
20
src/queues/pokemon-phase-priority-queue.ts
Normal file
20
src/queues/pokemon-phase-priority-queue.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import type { DynamicPhase } from "#app/@types/phase-types";
|
||||
import { PriorityQueue } from "#app/queues/priority-queue";
|
||||
import { sortInSpeedOrder } from "#app/utils/speed-order";
|
||||
import type { BattlerIndex } from "#enums/battler-index";
|
||||
|
||||
/** A generic speed-based priority queue of {@linkcode DynamicPhase}s */
|
||||
export class PokemonPhasePriorityQueue<T extends DynamicPhase> extends PriorityQueue<T> {
|
||||
protected setOrder: BattlerIndex[] | undefined;
|
||||
protected override reorder(): void {
|
||||
const setOrder = this.setOrder;
|
||||
if (setOrder) {
|
||||
this.queue.sort(
|
||||
(a, b) =>
|
||||
setOrder.indexOf(a.getPokemon().getBattlerIndex()) - setOrder.indexOf(b.getPokemon().getBattlerIndex()),
|
||||
);
|
||||
} else {
|
||||
this.queue = sortInSpeedOrder(this.queue);
|
||||
}
|
||||
}
|
||||
}
|
10
src/queues/pokemon-priority-queue.ts
Normal file
10
src/queues/pokemon-priority-queue.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import type { Pokemon } from "#app/field/pokemon";
|
||||
import { PriorityQueue } from "#app/queues/priority-queue";
|
||||
import { sortInSpeedOrder } from "#app/utils/speed-order";
|
||||
|
||||
/** A priority queue of {@linkcode Pokemon}s */
|
||||
export class PokemonPriorityQueue extends PriorityQueue<Pokemon> {
|
||||
protected override reorder(): void {
|
||||
this.queue = sortInSpeedOrder(this.queue);
|
||||
}
|
||||
}
|
45
src/queues/post-summon-phase-priority-queue.ts
Normal file
45
src/queues/post-summon-phase-priority-queue.ts
Normal file
@ -0,0 +1,45 @@
|
||||
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> {
|
||||
protected override reorder(): void {
|
||||
this.queue = sortInSpeedOrder(this.queue, false);
|
||||
this.queue.sort((phaseA, phaseB) => 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 {
|
||||
if (phase instanceof PostSummonActivateAbilityPhase) {
|
||||
return;
|
||||
}
|
||||
|
||||
const phasePokemon = phase.getPokemon();
|
||||
|
||||
phasePokemon.getAbilityPriorities().forEach((priority, idx) => {
|
||||
const activateAbilityPhase = new PostSummonActivateAbilityPhase(
|
||||
phasePokemon.getBattlerIndex(),
|
||||
priority,
|
||||
idx !== 0,
|
||||
);
|
||||
phase.source === "SummonPhase"
|
||||
? globalScene.phaseManager.pushPhase(activateAbilityPhase)
|
||||
: globalScene.phaseManager.unshiftPhase(activateAbilityPhase);
|
||||
});
|
||||
}
|
||||
}
|
78
src/queues/priority-queue.ts
Normal file
78
src/queues/priority-queue.ts
Normal file
@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Stores a list of elements.
|
||||
*
|
||||
* Dynamically updates ordering to always pop the highest "priority", based on implementation of {@linkcode reorder}.
|
||||
*/
|
||||
export abstract class PriorityQueue<T> {
|
||||
protected queue: T[] = [];
|
||||
|
||||
/**
|
||||
* Sorts the elements in the queue
|
||||
*/
|
||||
protected abstract reorder(): void;
|
||||
|
||||
/**
|
||||
* Calls {@linkcode reorder} and shifts the queue
|
||||
* @returns The front element of the queue after sorting, or `undefined` if the queue is empty
|
||||
* @sealed
|
||||
*/
|
||||
public pop(): T | undefined {
|
||||
if (this.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.reorder();
|
||||
return this.queue.shift();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an element to the queue
|
||||
* @param element The element to add
|
||||
*/
|
||||
public push(element: T): void {
|
||||
this.queue.push(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all elements from the queue
|
||||
* @sealed
|
||||
*/
|
||||
public clear(): void {
|
||||
this.queue.splice(0, this.queue.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns Whether the queue is empty
|
||||
* @sealed
|
||||
*/
|
||||
public isEmpty(): boolean {
|
||||
return this.queue.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the first element matching the condition
|
||||
* @param condition - An optional condition function (defaults to a function that always returns `true`)
|
||||
* @returns Whether a removal occurred
|
||||
*/
|
||||
public remove(condition: (t: T) => boolean = () => true): boolean {
|
||||
// Reorder to remove the first element
|
||||
this.reorder();
|
||||
const index = this.queue.findIndex(condition);
|
||||
if (index === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.queue.splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @returns An element matching the condition function */
|
||||
public find(condition?: (t: T) => boolean): T | undefined {
|
||||
return this.queue.find(e => !condition || condition(e));
|
||||
}
|
||||
|
||||
/** @returns Whether an element matching the condition function exists */
|
||||
public has(condition?: (t: T) => boolean): boolean {
|
||||
return this.queue.some(e => !condition || condition(e));
|
||||
}
|
||||
}
|
39
src/utils/speed-order-generator.ts
Normal file
39
src/utils/speed-order-generator.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { PokemonPriorityQueue } from "#app/queues/pokemon-priority-queue";
|
||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
|
||||
/**
|
||||
* A generator function which uses a priority queue to yield each pokemon from a given side of the field in speed order.
|
||||
* @param side - The {@linkcode ArenaTagSide | side} of the field to use
|
||||
* @returns A {@linkcode Generator} of {@linkcode Pokemon}
|
||||
*
|
||||
* @remarks
|
||||
* This should almost always be used by iteration in a `for...of` loop
|
||||
*/
|
||||
export function* inSpeedOrder(side: ArenaTagSide = ArenaTagSide.BOTH): Generator<Pokemon, number> {
|
||||
let pokemonList: Pokemon[];
|
||||
switch (side) {
|
||||
case ArenaTagSide.PLAYER:
|
||||
pokemonList = globalScene.getPlayerField(true);
|
||||
break;
|
||||
case ArenaTagSide.ENEMY:
|
||||
pokemonList = globalScene.getEnemyField(true);
|
||||
break;
|
||||
default:
|
||||
pokemonList = globalScene.getField(true);
|
||||
}
|
||||
|
||||
const queue = new PokemonPriorityQueue();
|
||||
let i = 0;
|
||||
pokemonList.forEach(p => {
|
||||
queue.push(p);
|
||||
});
|
||||
while (!queue.isEmpty()) {
|
||||
// If the queue is not empty, this can never be undefined
|
||||
i++;
|
||||
yield queue.pop()!;
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
57
src/utils/speed-order.ts
Normal file
57
src/utils/speed-order.ts
Normal file
@ -0,0 +1,57 @@
|
||||
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";
|
||||
|
||||
/** Interface representing an object associated with a specific Pokemon */
|
||||
interface hasPokemon {
|
||||
getPokemon(): Pokemon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts an array of {@linkcode Pokemon} by speed, taking Trick Room into account.
|
||||
* @param pokemonList - The list of Pokemon or objects containing Pokemon
|
||||
* @param shuffleFirst - Whether to shuffle the list before sorting (to handle speed ties). Default `true`.
|
||||
* @returns The sorted array of {@linkcode Pokemon}
|
||||
*/
|
||||
export function sortInSpeedOrder<T extends Pokemon | hasPokemon>(pokemonList: T[], shuffleFirst = true): T[] {
|
||||
pokemonList = shuffleFirst ? shufflePokemonList(pokemonList) : pokemonList;
|
||||
sortBySpeed(pokemonList);
|
||||
return pokemonList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param pokemonList - The array of Pokemon or objects containing Pokemon
|
||||
* @returns The shuffled array
|
||||
*/
|
||||
function shufflePokemonList<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;
|
||||
}
|
||||
|
||||
/** Sorts an array of {@linkcode Pokemon} by speed (without shuffling) */
|
||||
function sortBySpeed<T extends Pokemon | hasPokemon>(pokemonList: T[]): void {
|
||||
pokemonList.sort((a, b) => {
|
||||
const aSpeed = (a instanceof Pokemon ? a : a.getPokemon()).getEffectiveStat(Stat.SPD);
|
||||
const bSpeed = (b instanceof Pokemon ? b : b.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();
|
||||
}
|
||||
}
|
@ -34,7 +34,7 @@ describe("Abilities - Dancer", () => {
|
||||
game.override.enemyAbility(AbilityId.DANCER).enemySpecies(SpeciesId.MAGIKARP).enemyMoveset(MoveId.VICTORY_DANCE);
|
||||
await game.classicMode.startBattle([SpeciesId.ORICORIO, SpeciesId.FEEBAS]);
|
||||
|
||||
const [oricorio, feebas] = game.scene.getPlayerField();
|
||||
const [oricorio, feebas, magikarp1] = game.scene.getField();
|
||||
game.move.changeMoveset(oricorio, [MoveId.SWORDS_DANCE, MoveId.VICTORY_DANCE, MoveId.SPLASH]);
|
||||
game.move.changeMoveset(feebas, [MoveId.SWORDS_DANCE, MoveId.SPLASH]);
|
||||
|
||||
@ -44,8 +44,9 @@ describe("Abilities - Dancer", () => {
|
||||
await game.phaseInterceptor.to("MovePhase"); // feebas uses swords dance
|
||||
await game.phaseInterceptor.to("MovePhase", false); // oricorio copies swords dance
|
||||
|
||||
// Dancer order will be Magikarp, Oricorio, Magikarp based on set turn order
|
||||
let currentPhase = game.scene.phaseManager.getCurrentPhase() as MovePhase;
|
||||
expect(currentPhase.pokemon).toBe(oricorio);
|
||||
expect(currentPhase.pokemon).toBe(magikarp1);
|
||||
expect(currentPhase.move.moveId).toBe(MoveId.SWORDS_DANCE);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEndPhase"); // end oricorio's move
|
||||
|
@ -2,8 +2,6 @@ import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { TurnEndPhase } from "#phases/turn-end-phase";
|
||||
import { TurnStartPhase } from "#phases/turn-start-phase";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
@ -45,65 +43,50 @@ describe("Abilities - Mycelium Might", () => {
|
||||
it("should move last in its priority bracket and ignore protective abilities", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.REGIELEKI]);
|
||||
|
||||
const enemyPokemon = game.field.getEnemyPokemon();
|
||||
const playerIndex = game.field.getPlayerPokemon().getBattlerIndex();
|
||||
const enemyIndex = enemyPokemon.getBattlerIndex();
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
const player = game.field.getPlayerPokemon();
|
||||
|
||||
game.move.select(MoveId.BABY_DOLL_EYES);
|
||||
|
||||
await game.phaseInterceptor.to(TurnStartPhase, false);
|
||||
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
|
||||
const speedOrder = phase.getSpeedOrder();
|
||||
const commandOrder = phase.getCommandOrder();
|
||||
await game.phaseInterceptor.to("MoveEndPhase", false);
|
||||
// The opponent Pokemon (without Mycelium Might) goes first despite having lower speed than the player Pokemon.
|
||||
// The player Pokemon (with Mycelium Might) goes last despite having higher speed than the opponent.
|
||||
expect(speedOrder).toEqual([playerIndex, enemyIndex]);
|
||||
expect(commandOrder).toEqual([enemyIndex, playerIndex]);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
expect(player.hp).not.toEqual(player.getMaxHp());
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
// Despite the opponent's ability (Clear Body), its ATK stat stage is still reduced.
|
||||
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1);
|
||||
expect(enemy.getStatStage(Stat.ATK)).toBe(-1);
|
||||
});
|
||||
|
||||
it("should still go first if a status move that is in a higher priority bracket than the opponent's move is used", async () => {
|
||||
game.override.enemyMoveset(MoveId.TACKLE);
|
||||
await game.classicMode.startBattle([SpeciesId.REGIELEKI]);
|
||||
|
||||
const enemyPokemon = game.field.getEnemyPokemon();
|
||||
const playerIndex = game.field.getPlayerPokemon().getBattlerIndex();
|
||||
const enemyIndex = enemyPokemon.getBattlerIndex();
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
const player = game.field.getPlayerPokemon();
|
||||
|
||||
game.move.select(MoveId.BABY_DOLL_EYES);
|
||||
|
||||
await game.phaseInterceptor.to(TurnStartPhase, false);
|
||||
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
|
||||
const speedOrder = phase.getSpeedOrder();
|
||||
const commandOrder = phase.getCommandOrder();
|
||||
await game.phaseInterceptor.to("MoveEndPhase", false);
|
||||
// The player Pokemon (with M.M.) goes first because its move is still within a higher priority bracket than its opponent.
|
||||
// The enemy Pokemon goes second because its move is in a lower priority bracket.
|
||||
expect(speedOrder).toEqual([playerIndex, enemyIndex]);
|
||||
expect(commandOrder).toEqual([playerIndex, enemyIndex]);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
expect(player.hp).toEqual(player.getMaxHp());
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
// Despite the opponent's ability (Clear Body), its ATK stat stage is still reduced.
|
||||
expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1);
|
||||
expect(enemy.getStatStage(Stat.ATK)).toBe(-1);
|
||||
});
|
||||
|
||||
it("should not affect non-status moves", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.REGIELEKI]);
|
||||
|
||||
const playerIndex = game.field.getPlayerPokemon().getBattlerIndex();
|
||||
const enemyIndex = game.field.getEnemyPokemon().getBattlerIndex();
|
||||
const player = game.field.getPlayerPokemon();
|
||||
|
||||
game.move.select(MoveId.QUICK_ATTACK);
|
||||
|
||||
await game.phaseInterceptor.to(TurnStartPhase, false);
|
||||
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
|
||||
const speedOrder = phase.getSpeedOrder();
|
||||
const commandOrder = phase.getCommandOrder();
|
||||
await game.phaseInterceptor.to("MoveEndPhase", false);
|
||||
// The player Pokemon (with M.M.) goes first because it has a higher speed and did not use a status move.
|
||||
// The enemy Pokemon (without M.M.) goes second because its speed is lower.
|
||||
// This means that the commandOrder should be identical to the speedOrder
|
||||
expect(speedOrder).toEqual([playerIndex, enemyIndex]);
|
||||
expect(commandOrder).toEqual([playerIndex, enemyIndex]);
|
||||
expect(player.hp).toEqual(player.getMaxHp());
|
||||
});
|
||||
});
|
||||
|
@ -59,7 +59,7 @@ describe("Abilities - Neutralizing Gas", () => {
|
||||
expect(game.field.getPlayerPokemon().getStatStage(Stat.ATK)).toBe(1);
|
||||
});
|
||||
|
||||
it.todo("should activate before other abilities", async () => {
|
||||
it("should activate before other abilities", async () => {
|
||||
game.override.enemySpecies(SpeciesId.ACCELGOR).enemyLevel(100).enemyAbility(AbilityId.INTIMIDATE);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
@ -5,7 +5,7 @@ import { SpeciesId } from "#enums/species-id";
|
||||
import { FaintPhase } from "#phases/faint-phase";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("Abilities - Quick Draw", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
@ -25,7 +25,6 @@ describe("Abilities - Quick Draw", () => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.battleStyle("single")
|
||||
.starterSpecies(SpeciesId.MAGIKARP)
|
||||
.ability(AbilityId.QUICK_DRAW)
|
||||
.moveset([MoveId.TACKLE, MoveId.TAIL_WHIP])
|
||||
.enemyLevel(100)
|
||||
@ -40,8 +39,8 @@ describe("Abilities - Quick Draw", () => {
|
||||
).mockReturnValue(100);
|
||||
});
|
||||
|
||||
test("makes pokemon going first in its priority bracket", async () => {
|
||||
await game.classicMode.startBattle();
|
||||
it("makes pokemon go first in its priority bracket", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
const pokemon = game.field.getPlayerPokemon();
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
@ -57,33 +56,27 @@ describe("Abilities - Quick Draw", () => {
|
||||
expect(pokemon.waveData.abilitiesApplied).toContain(AbilityId.QUICK_DRAW);
|
||||
});
|
||||
|
||||
test(
|
||||
"does not triggered by non damage moves",
|
||||
{
|
||||
retry: 5,
|
||||
},
|
||||
async () => {
|
||||
await game.classicMode.startBattle();
|
||||
it("is not triggered by non damaging moves", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
const pokemon = game.field.getPlayerPokemon();
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
const pokemon = game.field.getPlayerPokemon();
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
|
||||
pokemon.hp = 1;
|
||||
enemy.hp = 1;
|
||||
pokemon.hp = 1;
|
||||
enemy.hp = 1;
|
||||
|
||||
game.move.select(MoveId.TAIL_WHIP);
|
||||
await game.phaseInterceptor.to(FaintPhase, false);
|
||||
game.move.select(MoveId.TAIL_WHIP);
|
||||
await game.phaseInterceptor.to(FaintPhase, false);
|
||||
|
||||
expect(pokemon.isFainted()).toBe(true);
|
||||
expect(enemy.isFainted()).toBe(false);
|
||||
expect(pokemon.waveData.abilitiesApplied).not.contain(AbilityId.QUICK_DRAW);
|
||||
},
|
||||
);
|
||||
expect(pokemon.isFainted()).toBe(true);
|
||||
expect(enemy.isFainted()).toBe(false);
|
||||
expect(pokemon.waveData.abilitiesApplied).not.contain(AbilityId.QUICK_DRAW);
|
||||
});
|
||||
|
||||
test("does not increase priority", async () => {
|
||||
it("does not increase priority", async () => {
|
||||
game.override.enemyMoveset([MoveId.EXTREME_SPEED]);
|
||||
|
||||
await game.classicMode.startBattle();
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
const pokemon = game.field.getPlayerPokemon();
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { TurnStartPhase } from "#phases/turn-start-phase";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
@ -40,56 +39,41 @@ describe("Abilities - Stall", () => {
|
||||
it("Pokemon with Stall should move last in its priority bracket regardless of speed", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.SHUCKLE]);
|
||||
|
||||
const playerIndex = game.field.getPlayerPokemon().getBattlerIndex();
|
||||
const enemyIndex = game.field.getEnemyPokemon().getBattlerIndex();
|
||||
const player = game.field.getPlayerPokemon();
|
||||
|
||||
game.move.select(MoveId.QUICK_ATTACK);
|
||||
|
||||
await game.phaseInterceptor.to(TurnStartPhase, false);
|
||||
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
|
||||
const speedOrder = phase.getSpeedOrder();
|
||||
const commandOrder = phase.getCommandOrder();
|
||||
await game.phaseInterceptor.to("MoveEndPhase", false);
|
||||
// The player Pokemon (without Stall) goes first despite having lower speed than the opponent.
|
||||
// The opponent Pokemon (with Stall) goes last despite having higher speed than the player Pokemon.
|
||||
expect(speedOrder).toEqual([enemyIndex, playerIndex]);
|
||||
expect(commandOrder).toEqual([playerIndex, enemyIndex]);
|
||||
expect(player).toHaveFullHp();
|
||||
});
|
||||
|
||||
it("Pokemon with Stall will go first if a move that is in a higher priority bracket than the opponent's move is used", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.SHUCKLE]);
|
||||
|
||||
const playerIndex = game.field.getPlayerPokemon().getBattlerIndex();
|
||||
const enemyIndex = game.field.getEnemyPokemon().getBattlerIndex();
|
||||
const player = game.field.getPlayerPokemon();
|
||||
|
||||
game.move.select(MoveId.TACKLE);
|
||||
|
||||
await game.phaseInterceptor.to(TurnStartPhase, false);
|
||||
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
|
||||
const speedOrder = phase.getSpeedOrder();
|
||||
const commandOrder = phase.getCommandOrder();
|
||||
await game.phaseInterceptor.to("MoveEndPhase", false);
|
||||
// The opponent Pokemon (with Stall) goes first because its move is still within a higher priority bracket than its opponent.
|
||||
// The player Pokemon goes second because its move is in a lower priority bracket.
|
||||
expect(speedOrder).toEqual([enemyIndex, playerIndex]);
|
||||
expect(commandOrder).toEqual([enemyIndex, playerIndex]);
|
||||
expect(player).not.toHaveFullHp();
|
||||
});
|
||||
|
||||
it("If both Pokemon have stall and use the same move, speed is used to determine who goes first.", async () => {
|
||||
game.override.ability(AbilityId.STALL);
|
||||
await game.classicMode.startBattle([SpeciesId.SHUCKLE]);
|
||||
|
||||
const playerIndex = game.field.getPlayerPokemon().getBattlerIndex();
|
||||
const enemyIndex = game.field.getEnemyPokemon().getBattlerIndex();
|
||||
const player = game.field.getPlayerPokemon();
|
||||
|
||||
game.move.select(MoveId.TACKLE);
|
||||
|
||||
await game.phaseInterceptor.to(TurnStartPhase, false);
|
||||
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
|
||||
const speedOrder = phase.getSpeedOrder();
|
||||
const commandOrder = phase.getCommandOrder();
|
||||
await game.phaseInterceptor.to("MoveEndPhase", false);
|
||||
|
||||
// The opponent Pokemon (with Stall) goes first because it has a higher speed.
|
||||
// The player Pokemon (with Stall) goes second because its speed is lower.
|
||||
expect(speedOrder).toEqual([enemyIndex, playerIndex]);
|
||||
expect(commandOrder).toEqual([enemyIndex, playerIndex]);
|
||||
expect(player).not.toHaveFullHp();
|
||||
});
|
||||
});
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import type { TurnStartPhase } from "#phases/turn-start-phase";
|
||||
import type { MovePhase } from "#phases/move-phase";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
@ -34,38 +35,34 @@ describe("Battle order", () => {
|
||||
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
|
||||
|
||||
const playerPokemon = game.field.getPlayerPokemon();
|
||||
const playerStartHp = playerPokemon.hp;
|
||||
const enemyPokemon = game.field.getEnemyPokemon();
|
||||
const enemyStartHp = enemyPokemon.hp;
|
||||
|
||||
vi.spyOn(playerPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 50]); // set playerPokemon's speed to 50
|
||||
vi.spyOn(enemyPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set enemyPokemon's speed to 150
|
||||
|
||||
game.move.select(MoveId.TACKLE);
|
||||
await game.phaseInterceptor.to("TurnStartPhase", false);
|
||||
|
||||
const playerPokemonIndex = playerPokemon.getBattlerIndex();
|
||||
const enemyPokemonIndex = enemyPokemon.getBattlerIndex();
|
||||
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
|
||||
const order = phase.getCommandOrder();
|
||||
expect(order[0]).toBe(enemyPokemonIndex);
|
||||
expect(order[1]).toBe(playerPokemonIndex);
|
||||
await game.phaseInterceptor.to("MoveEndPhase", false);
|
||||
expect(playerPokemon.hp).not.toEqual(playerStartHp);
|
||||
expect(enemyPokemon.hp).toEqual(enemyStartHp);
|
||||
});
|
||||
|
||||
it("Player faster than opponent 150 vs 50", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
|
||||
|
||||
const playerPokemon = game.field.getPlayerPokemon();
|
||||
const playerStartHp = playerPokemon.hp;
|
||||
const enemyPokemon = game.field.getEnemyPokemon();
|
||||
const enemyStartHp = enemyPokemon.hp;
|
||||
vi.spyOn(playerPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set playerPokemon's speed to 150
|
||||
vi.spyOn(enemyPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 50]); // set enemyPokemon's speed to 50
|
||||
|
||||
game.move.select(MoveId.TACKLE);
|
||||
await game.phaseInterceptor.to("TurnStartPhase", false);
|
||||
|
||||
const playerPokemonIndex = playerPokemon.getBattlerIndex();
|
||||
const enemyPokemonIndex = enemyPokemon.getBattlerIndex();
|
||||
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
|
||||
const order = phase.getCommandOrder();
|
||||
expect(order[0]).toBe(playerPokemonIndex);
|
||||
expect(order[1]).toBe(enemyPokemonIndex);
|
||||
await game.phaseInterceptor.to("MoveEndPhase", false);
|
||||
expect(playerPokemon.hp).toEqual(playerStartHp);
|
||||
expect(enemyPokemon.hp).not.toEqual(enemyStartHp);
|
||||
});
|
||||
|
||||
it("double - both opponents faster than player 50/50 vs 150/150", async () => {
|
||||
@ -73,23 +70,24 @@ describe("Battle order", () => {
|
||||
await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.BLASTOISE]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerField();
|
||||
const playerHps = playerPokemon.map(p => p.hp);
|
||||
const enemyPokemon = game.scene.getEnemyField();
|
||||
const enemyHps = enemyPokemon.map(p => p.hp);
|
||||
|
||||
playerPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 50])); // set both playerPokemons' speed to 50
|
||||
enemyPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150])); // set both enemyPokemons' speed to 150
|
||||
const playerIndices = playerPokemon.map(p => p?.getBattlerIndex());
|
||||
const enemyIndices = enemyPokemon.map(p => p?.getBattlerIndex());
|
||||
|
||||
game.move.select(MoveId.TACKLE);
|
||||
game.move.select(MoveId.TACKLE, 1);
|
||||
await game.phaseInterceptor.to("TurnStartPhase", false);
|
||||
await game.move.selectEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER);
|
||||
await game.move.selectEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER_2);
|
||||
|
||||
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
|
||||
const order = phase.getCommandOrder();
|
||||
expect(order.slice(0, 2).includes(enemyIndices[0])).toBe(true);
|
||||
expect(order.slice(0, 2).includes(enemyIndices[1])).toBe(true);
|
||||
expect(order.slice(2, 4).includes(playerIndices[0])).toBe(true);
|
||||
expect(order.slice(2, 4).includes(playerIndices[1])).toBe(true);
|
||||
await game.phaseInterceptor.to("MoveEndPhase", true);
|
||||
await game.phaseInterceptor.to("MoveEndPhase", false);
|
||||
for (let i = 0; i < 2; i++) {
|
||||
expect(playerPokemon[i].hp).not.toEqual(playerHps[i]);
|
||||
expect(enemyPokemon[i].hp).toEqual(enemyHps[i]);
|
||||
}
|
||||
});
|
||||
|
||||
it("double - speed tie except 1 - 100/100 vs 100/150", async () => {
|
||||
@ -101,18 +99,13 @@ describe("Battle order", () => {
|
||||
playerPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 100])); //set both playerPokemons' speed to 100
|
||||
vi.spyOn(enemyPokemon[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 100]); // set enemyPokemon's speed to 100
|
||||
vi.spyOn(enemyPokemon[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set enemyPokemon's speed to 150
|
||||
const playerIndices = playerPokemon.map(p => p?.getBattlerIndex());
|
||||
const enemyIndices = enemyPokemon.map(p => p?.getBattlerIndex());
|
||||
|
||||
game.move.select(MoveId.TACKLE);
|
||||
game.move.select(MoveId.TACKLE, 1);
|
||||
await game.phaseInterceptor.to("TurnStartPhase", false);
|
||||
await game.phaseInterceptor.to("MovePhase", false);
|
||||
|
||||
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
|
||||
const order = phase.getCommandOrder();
|
||||
// enemy 2 should be first, followed by some other assortment of the other 3 pokemon
|
||||
expect(order[0]).toBe(enemyIndices[1]);
|
||||
expect(order.slice(1, 4)).toEqual(expect.arrayContaining([enemyIndices[0], ...playerIndices]));
|
||||
const phase = game.scene.phaseManager.getCurrentPhase() as MovePhase;
|
||||
expect(phase.pokemon).toEqual(enemyPokemon[1]);
|
||||
});
|
||||
|
||||
it("double - speed tie 100/150 vs 100/150", async () => {
|
||||
@ -125,17 +118,13 @@ describe("Battle order", () => {
|
||||
vi.spyOn(playerPokemon[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set other playerPokemon's speed to 150
|
||||
vi.spyOn(enemyPokemon[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 100]); // set one enemyPokemon's speed to 100
|
||||
vi.spyOn(enemyPokemon[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set other enemyPokemon's speed to 150
|
||||
const playerIndices = playerPokemon.map(p => p?.getBattlerIndex());
|
||||
const enemyIndices = enemyPokemon.map(p => p?.getBattlerIndex());
|
||||
|
||||
game.move.select(MoveId.TACKLE);
|
||||
game.move.select(MoveId.TACKLE, 1);
|
||||
await game.phaseInterceptor.to("TurnStartPhase", false);
|
||||
|
||||
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
|
||||
const order = phase.getCommandOrder();
|
||||
// P2/E2 should be randomly first/second, then P1/E1 randomly 3rd/4th
|
||||
expect(order.slice(0, 2)).toStrictEqual(expect.arrayContaining([playerIndices[1], enemyIndices[1]]));
|
||||
expect(order.slice(2, 4)).toStrictEqual(expect.arrayContaining([playerIndices[0], enemyIndices[0]]));
|
||||
await game.phaseInterceptor.to("MovePhase", false);
|
||||
|
||||
const phase = game.scene.phaseManager.getCurrentPhase() as MovePhase;
|
||||
expect(enemyPokemon[1] === phase.pokemon || playerPokemon[1] === phase.pokemon);
|
||||
});
|
||||
});
|
||||
|
@ -76,12 +76,7 @@ describe("Moves - Baton Pass", () => {
|
||||
expect(game.field.getEnemyPokemon().getStatStage(Stat.SPATK)).toEqual(2);
|
||||
// confirm that a switch actually happened. can't use species because I
|
||||
// can't find a way to override trainer parties with more than 1 pokemon species
|
||||
expect(game.phaseInterceptor.log.slice(-4)).toEqual([
|
||||
"MoveEffectPhase",
|
||||
"SwitchSummonPhase",
|
||||
"SummonPhase",
|
||||
"PostSummonPhase",
|
||||
]);
|
||||
expect(game.field.getEnemyPokemon().summonData.moveHistory).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("doesn't transfer effects that aren't transferrable", async () => {
|
||||
|
@ -193,7 +193,7 @@ describe("Moves - Delayed Attacks", () => {
|
||||
// All attacks have concluded at this point, unshifting new `MoveEffectPhase`s to the queue.
|
||||
expectFutureSightActive(0);
|
||||
|
||||
const MEPs = game.scene.phaseManager.phaseQueue.filter(p => p.is("MoveEffectPhase"));
|
||||
const MEPs = game.scene.phaseManager["phaseQueue"].findAll("MoveEffectPhase");
|
||||
expect(MEPs).toHaveLength(4);
|
||||
expect(MEPs.map(mep => mep.getPokemon())).toEqual(oldOrder);
|
||||
});
|
||||
|
@ -3,7 +3,6 @@ import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { BerryPhase } from "#phases/berry-phase";
|
||||
import { MessagePhase } from "#phases/message-phase";
|
||||
import { MoveHeaderPhase } from "#phases/move-header-phase";
|
||||
import { SwitchSummonPhase } from "#phases/switch-summon-phase";
|
||||
import { TurnStartPhase } from "#phases/turn-start-phase";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
@ -116,7 +115,7 @@ describe("Moves - Focus Punch", () => {
|
||||
await game.phaseInterceptor.to(TurnStartPhase);
|
||||
|
||||
expect(game.scene.phaseManager.getCurrentPhase() instanceof SwitchSummonPhase).toBeTruthy();
|
||||
expect(game.scene.phaseManager.phaseQueue.find(phase => phase instanceof MoveHeaderPhase)).toBeDefined();
|
||||
expect(game.scene.phaseManager.hasPhaseOfType("MoveHeaderPhase")).toBe(true);
|
||||
});
|
||||
it("should replace the 'but it failed' text when the user gets hit", async () => {
|
||||
game.override.enemyMoveset([MoveId.TACKLE]);
|
||||
|
@ -166,7 +166,6 @@ describe("Moves - Rage Fist", () => {
|
||||
|
||||
// Charizard hit
|
||||
game.move.select(MoveId.SPLASH);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
await game.toNextTurn();
|
||||
expect(getPartyHitCount()).toEqual([1, 0]);
|
||||
|
||||
|
@ -119,17 +119,16 @@ describe("Moves - Revival Blessing", () => {
|
||||
game.override
|
||||
.battleStyle("double")
|
||||
.enemyMoveset([MoveId.REVIVAL_BLESSING])
|
||||
.moveset([MoveId.SPLASH])
|
||||
.moveset([MoveId.SPLASH, MoveId.JUDGMENT])
|
||||
.startingLevel(100)
|
||||
.startingWave(25); // 2nd rival battle - must have 3+ pokemon
|
||||
await game.classicMode.startBattle([SpeciesId.ARCEUS, SpeciesId.GIRATINA]);
|
||||
|
||||
const enemyFainting = game.scene.getEnemyField()[0];
|
||||
|
||||
game.move.select(MoveId.SPLASH, 0);
|
||||
game.move.use(MoveId.JUDGMENT, 0, BattlerIndex.ENEMY);
|
||||
game.move.select(MoveId.SPLASH, 1);
|
||||
await game.killPokemon(enemyFainting);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
await game.toNextTurn();
|
||||
// If there are incorrectly two switch phases into this slot, the fainted pokemon will end up in slot 3
|
||||
// Make sure it's still in slot 1
|
||||
|
@ -48,7 +48,7 @@ describe("Moves - Shell Trap", () => {
|
||||
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER, BattlerIndex.PLAYER_2]);
|
||||
|
||||
await game.phaseInterceptor.to(MoveEndPhase);
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
|
||||
const movePhase = game.scene.phaseManager.getCurrentPhase();
|
||||
expect(movePhase instanceof MovePhase).toBeTruthy();
|
||||
|
@ -5,10 +5,10 @@ import { BattlerIndex } from "#enums/battler-index";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { TurnStartPhase } from "#phases/turn-start-phase";
|
||||
import { WeatherType } from "#enums/weather-type";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Move - Trick Room", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
@ -56,13 +56,11 @@ describe("Move - Trick Room", () => {
|
||||
turnCount: 4, // The 5 turn limit _includes_ the current turn!
|
||||
});
|
||||
|
||||
// Now, check that speed was indeed reduced
|
||||
const turnOrderSpy = vi.spyOn(TurnStartPhase.prototype, "getSpeedOrder");
|
||||
|
||||
game.move.use(MoveId.SPLASH);
|
||||
game.move.use(MoveId.SUNNY_DAY);
|
||||
await game.move.forceEnemyMove(MoveId.RAIN_DANCE);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(turnOrderSpy).toHaveLastReturnedWith([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
expect(game.scene.arena.getWeatherType()).toBe(WeatherType.SUNNY);
|
||||
});
|
||||
|
||||
it("should be removed when overlapped", async () => {
|
||||
|
@ -135,7 +135,7 @@ describe("Move - Wish", () => {
|
||||
// all wishes have activated and added healing phases
|
||||
expect(game).toHavePositionalTag(PositionalTagType.WISH, 0);
|
||||
|
||||
const healPhases = game.scene.phaseManager.phaseQueue.filter(p => p.is("PokemonHealPhase"));
|
||||
const healPhases = game.scene.phaseManager["phaseQueue"].findAll("PokemonHealPhase");
|
||||
expect(healPhases).toHaveLength(4);
|
||||
expect.soft(healPhases.map(php => php.getPokemon())).toEqual(oldOrder);
|
||||
|
||||
|
@ -70,7 +70,6 @@ export async function runMysteryEncounterToEnd(
|
||||
// If a battle is started, fast forward to end of the battle
|
||||
game.onNextPrompt("CommandPhase", UiMode.COMMAND, () => {
|
||||
game.scene.phaseManager.clearPhaseQueue();
|
||||
game.scene.phaseManager.clearPhaseQueueSplice();
|
||||
game.scene.phaseManager.unshiftPhase(new VictoryPhase(0));
|
||||
game.endPhase();
|
||||
});
|
||||
@ -196,7 +195,6 @@ async function handleSecondaryOptionSelect(game: GameManager, pokemonNo: number,
|
||||
*/
|
||||
export async function skipBattleRunMysteryEncounterRewardsPhase(game: GameManager, runRewardsPhase = true) {
|
||||
game.scene.phaseManager.clearPhaseQueue();
|
||||
game.scene.phaseManager.clearPhaseQueueSplice();
|
||||
game.scene.getEnemyParty().forEach(p => {
|
||||
p.hp = 0;
|
||||
p.status = new Status(StatusEffect.FAINT);
|
||||
|
@ -355,7 +355,6 @@ describe("The Winstrate Challenge - Mystery Encounter", () => {
|
||||
*/
|
||||
async function skipBattleToNextBattle(game: GameManager, isFinalBattle = false) {
|
||||
game.scene.phaseManager.clearPhaseQueue();
|
||||
game.scene.phaseManager.clearPhaseQueueSplice();
|
||||
const commandUiHandler = game.scene.ui.handlers[UiMode.COMMAND];
|
||||
commandUiHandler.clear();
|
||||
game.scene.getEnemyParty().forEach(p => {
|
||||
|
@ -468,6 +468,9 @@ export class GameManager {
|
||||
* Faint a player or enemy pokemon instantly by setting their HP to 0.
|
||||
* @param pokemon - The player/enemy pokemon being fainted
|
||||
* @returns A Promise that resolves once the fainted pokemon's FaintPhase finishes running.
|
||||
* @remarks
|
||||
* This method *pushes* a FaintPhase and runs until it's finished. This may cause a turn to play out unexpectedly
|
||||
* @todo Consider whether running the faint phase immediately can be done
|
||||
*/
|
||||
async killPokemon(pokemon: PlayerPokemon | EnemyPokemon) {
|
||||
pokemon.hp = 0;
|
||||
@ -537,7 +540,7 @@ export 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.
|
||||
*
|
||||
* @param order - The turn order to set, as an array of {@linkcode BattlerIndex}es
|
||||
@ -551,7 +554,7 @@ export class GameManager {
|
||||
async setTurnOrder(order: BattlerIndex[]): Promise<void> {
|
||||
await this.phaseInterceptor.to("TurnStartPhase", false);
|
||||
|
||||
vi.spyOn(this.scene.phaseManager.getCurrentPhase() as TurnStartPhase, "getSpeedOrder").mockReturnValue(order);
|
||||
this.scene.phaseManager.dynamicQueueManager.setMoveOrder(order);
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user