Switch to priority queue approach

This commit is contained in:
Dean 2025-03-17 21:22:30 -07:00
parent 9ea26325ae
commit aca6d2ce8d
6 changed files with 213 additions and 125 deletions

View File

@ -170,6 +170,13 @@ import { StatusEffect } from "#enums/status-effect";
import { initGlobalScene } from "#app/global-scene";
import { ShowAbilityPhase } from "#app/phases/show-ability-phase";
import { HideAbilityPhase } from "#app/phases/hide-ability-phase";
import {
type DynamicPhaseType,
type PhasePriorityQueue,
PostSummonPhasePriorityQueue,
} from "#app/data/phase-priority-queue";
import { PostSummonPhase } from "#app/phases/post-summon-phase";
import { ActivatePriorityQueuePhase } from "#app/phases/activate-priority-queue-phase";
export const bypassLogin = import.meta.env.VITE_BYPASS_LOGIN === "1";
@ -302,6 +309,10 @@ export default class BattleScene extends SceneBase {
/** overrides default of inserting phases to end of phaseQueuePrepend array, useful or inserting Phases "out of order" */
private phaseQueuePrependSpliceIndex: number;
private nextCommandPhaseQueue: Phase[];
/** Storage for {@linkcode PhasePriorityQueue}s which hold phases whose order dynamically changes */
private dynamicPhaseQueues: PhasePriorityQueue[];
/** Parallel array to {@linkcode dynamicPhaseQueues} - matches phase types to their queues */
private dynamicPhaseTypes: Constructor<Phase>[];
private currentPhase: Phase | null;
private standbyPhase: Phase | null;
@ -397,6 +408,8 @@ export default class BattleScene extends SceneBase {
this.conditionalQueue = [];
this.phaseQueuePrependSpliceIndex = -1;
this.nextCommandPhaseQueue = [];
this.dynamicPhaseQueues = [new PostSummonPhasePriorityQueue()];
this.dynamicPhaseTypes = [PostSummonPhase];
this.eventManager = new TimedEventManager();
this.updateGameInfo();
initGlobalScene(this);
@ -2693,12 +2706,16 @@ export default class BattleScene extends SceneBase {
}
/**
* Adds a phase to nextCommandPhaseQueue, as long as boolean passed in is false
* Adds a phase to the end of the appropriate queue (dynamic or {@linkcode phaseQueue} / {@linkcode nextCommandPhaseQueue})
* @param phase {@linkcode Phase} the phase to add
* @param defer boolean on which queue to add to, defaults to false, and adds to phaseQueue
* @param defer If `true`, add to {@linkcode nextCommandPhaseQueue} instead of {@linkcode phaseQueue}
*/
pushPhase(phase: Phase, defer = false): void {
(!defer ? this.phaseQueue : this.nextCommandPhaseQueue).push(phase);
if (this.getDynamicPhaseType(phase) !== undefined) {
this.pushDynamicPhase(phase);
} else {
(!defer ? this.phaseQueue : this.nextCommandPhaseQueue).push(phase);
}
}
/**
@ -2867,13 +2884,14 @@ export default class BattleScene extends SceneBase {
* 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: Constructor<Phase>): boolean {
appendToPhase(phase: Phase | Phase[], targetPhase: Constructor<Phase>, condition?: (p: Phase) => boolean): boolean {
if (!Array.isArray(phase)) {
phase = [phase];
}
const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof targetPhase);
const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof targetPhase && (!condition || condition(ph)));
if (targetIndex !== -1 && this.phaseQueue.length > targetIndex) {
this.phaseQueue.splice(targetIndex + 1, 0, ...phase);
@ -2884,22 +2902,65 @@ export default class BattleScene extends SceneBase {
}
/**
* Sorts the first consecutive set of occurences of {@linkcode targetPhase} in {@linkcode phaseQueue}
* @param targetPhase The type of phase to search for and sort
* @param by A function to compare the phases with
* @see {@linkcode Array.sort} for the comparison function
* 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`
*/
sortPhaseType(targetPhase: Constructor<Phase>, by: (a: Phase, b: Phase) => number): void {
const startIndex = this.phaseQueue.findIndex(phase => phase instanceof targetPhase);
if (startIndex === -1) {
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;
}
const endIndex = this.phaseQueue.findIndex((phase, index) => index > startIndex && !(phase instanceof targetPhase));
const sortedSubset = this.phaseQueue
.slice(startIndex, endIndex !== -1 ? endIndex + 1 : this.phaseQueue.length)
.sort(by);
this.phaseQueue.splice(startIndex, sortedSubset.length, ...sortedSubset);
this.pushPhase(new ActivatePriorityQueuePhase(type));
this.dynamicPhaseQueues[type].push(phase);
}
/**
* Unshifts the top phase from the corresponding dynamic queue onto {@linkcode phaseQueue}
* @param type {@linkcode DynamicPhaseType} The type of dynamic phase to start
*/
public startDynamicPhaseType(type: DynamicPhaseType): void {
const phase = this.dynamicPhaseQueues[type].pop();
if (phase) {
this.unshiftPhase(phase);
}
}
/**
* Unshifts an {@linkcode ActivatePriorityQueuePhase} for {@linkcode phase}, then pushes {@linkcode phase} to its dynamic queue
*
* This is the same as {@linkcode pushDynamicPhase}, except the activation phase is unshifted
*
* {@linkcode phase} is not guaranteed to be the next phase from the queue to run (if the queue is not empty)
* @param phase The phase to add
* @returns
*/
public startDynamicPhase(phase: Phase): void {
const type = this.getDynamicPhaseType(phase);
if (type === undefined) {
return;
}
this.unshiftPhase(new ActivatePriorityQueuePhase(type));
this.dynamicPhaseQueues[type].push(phase);
}
/**

View File

@ -0,0 +1,84 @@
import { globalScene } from "#app/global-scene";
import type { Phase } from "#app/phase";
import { ActivatePriorityQueuePhase } from "#app/phases/activate-priority-queue-phase";
import { type PostSummonPhase, PostSummonActivateAbilityPhase } from "#app/phases/post-summon-phase";
import { Stat } from "#enums/stat";
/**
* 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);
}
}
/**
* 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);
}
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 => {
this.queue.push(new PostSummonActivateAbilityPhase(phasePokemon.getBattlerIndex(), priority));
globalScene.appendToPhase(
new ActivatePriorityQueuePhase(DynamicPhaseType.POST_SUMMON),
ActivatePriorityQueuePhase,
(p: ActivatePriorityQueuePhase) => p.getType() === DynamicPhaseType.POST_SUMMON,
);
});
}
}
/**
* Enum representation of the phase types held by implementations of {@linkcode PhasePriorityQueue}
*/
export enum DynamicPhaseType {
POST_SUMMON,
}

View File

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

View File

@ -1,31 +0,0 @@
import type { BattlerIndex } from "#app/battle";
import { applyPostSummonAbAttrs, PostSummonAbAttr } from "#app/data/ability";
import { PokemonPhase } from "#app/phases/pokemon-phase";
/**
* Phase to apply (post-summon) ability attributes for abilities with nonzero priority
*
* Priority abilities activate before others and before hazards
*
* @see Example - {@link https://bulbapedia.bulbagarden.net/wiki/Neutralizing_Gas_(Ability) | Neutralizing Gas}
*/
export class PostSummonActivateAbilityPhase extends PokemonPhase {
private priority: number;
constructor(battlerIndex: BattlerIndex, priority: number) {
super(battlerIndex);
this.priority = priority;
}
start() {
super.start();
applyPostSummonAbAttrs(PostSummonAbAttr, this.getPokemon(), false, (p: number) => p === this.priority);
this.end();
}
public getPriority() {
return this.priority;
}
}

View File

@ -6,42 +6,12 @@ import { StatusEffect } from "#app/enums/status-effect";
import { PokemonPhase } from "./pokemon-phase";
import { MysteryEncounterPostSummonTag } from "#app/data/battler-tags";
import { BattlerTagType } from "#enums/battler-tag-type";
import { Stat } from "#enums/stat";
import { PostSummonActivateAbilityPhase } from "#app/phases/post-summon-activate-ability-phase";
export class PostSummonPhase extends PokemonPhase {
/** Represents whether or not this phase has already been placed in the correct (speed) order */
private ordered: boolean;
constructor(battlerIndex?: BattlerIndex, ordered = false) {
super(battlerIndex);
this.ordered = ordered;
}
start() {
super.start();
const pokemon = this.getPokemon();
let indexAfterPostSummon = globalScene.phaseQueue.findIndex(phase => !(phase instanceof PostSummonPhase));
indexAfterPostSummon = indexAfterPostSummon === -1 ? globalScene.phaseQueue.length : indexAfterPostSummon;
if (
!this.ordered &&
globalScene.findPhase(phase => phase instanceof PostSummonPhase && phase.getPokemon() !== pokemon)
) {
globalScene.phaseQueue.splice(indexAfterPostSummon++, 0, new PostSummonPhase(pokemon.getBattlerIndex(), true));
this.orderPostSummonPhases();
this.queueAbilityActivationPhases(indexAfterPostSummon);
this.end();
return;
}
if (!this.ordered) {
applyPostSummonAbAttrs(PostSummonAbAttr, pokemon, false, (p: number) => p > 0);
}
if (pokemon.status?.effect === StatusEffect.TOXIC) {
pokemon.status.toxicTurnCount = 0;
@ -56,10 +26,6 @@ export class PostSummonPhase extends PokemonPhase {
pokemon.lapseTag(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON);
}
if (!this.ordered) {
applyPostSummonAbAttrs(PostSummonAbAttr, pokemon, false, (p: number) => p <= 0);
}
const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField();
for (const p of field) {
applyAbAttrs(CommanderAbAttr, p, null, false);
@ -68,47 +34,33 @@ export class PostSummonPhase extends PokemonPhase {
this.end();
}
/**
* Sorts the {@linkcode PostSummonPhase}s in the queue by effective speed
*/
private orderPostSummonPhases() {
globalScene.sortPhaseType(
PostSummonPhase,
(phaseA: PostSummonPhase, phaseB: PostSummonPhase) =>
phaseB.getPokemon().getEffectiveStat(Stat.SPD) - phaseA.getPokemon().getEffectiveStat(Stat.SPD),
);
for (let i = 0; i < globalScene.phaseQueue.length && globalScene.phaseQueue[i] instanceof PostSummonPhase; i++) {
(globalScene.phaseQueue[i] as PostSummonPhase).ordered = true;
}
}
/**
* Adds {@linkcode PostSummonActivateAbilityPhase}s for all {@linkcode PostSummonPhase}s in the queue
* @param endIndex The index of the first non-{@linkcode PostSummonPhase} Phase in the queue, or the length if none exists
*/
private queueAbilityActivationPhases(endIndex: number) {
const abilityPhases: PostSummonActivateAbilityPhase[] = [];
globalScene.phaseQueue.slice(0, endIndex).forEach((phase: PostSummonPhase) => {
const phasePokemon = phase.getPokemon();
phasePokemon
.getAbilityPriorities()
.forEach(priority =>
abilityPhases.push(new PostSummonActivateAbilityPhase(phasePokemon.getBattlerIndex(), priority)),
);
});
abilityPhases.sort(
(phaseA: PostSummonActivateAbilityPhase, phaseB: PostSummonActivateAbilityPhase) =>
phaseB.getPriority() - phaseA.getPriority(),
);
let zeroIndex = abilityPhases.findIndex(phase => phase.getPriority() === 0);
zeroIndex = zeroIndex === -1 ? abilityPhases.length : zeroIndex;
globalScene.unshiftPhase(...abilityPhases.slice(0, zeroIndex));
globalScene.phaseQueue.splice(endIndex, 0, ...abilityPhases.slice(zeroIndex));
public getPriority() {
return 0;
}
}
/**
* Phase to apply (post-summon) ability attributes for abilities with nonzero priority
*
* Priority abilities activate before others and before hazards
*
* @see Example - {@link https://bulbapedia.bulbagarden.net/wiki/Neutralizing_Gas_(Ability) | Neutralizing Gas}
*/
export class PostSummonActivateAbilityPhase extends PostSummonPhase {
private priority: number;
constructor(battlerIndex: BattlerIndex, priority: number) {
super(battlerIndex);
this.priority = priority;
}
start() {
applyPostSummonAbAttrs(PostSummonAbAttr, this.getPokemon(), false, (p: number) => p === this.priority);
this.end();
}
public override getPriority() {
return this.priority;
}
}

View File

@ -246,6 +246,6 @@ export class SwitchSummonPhase extends SummonPhase {
}
queuePostSummon(): void {
globalScene.unshiftPhase(new PostSummonPhase(this.getPokemon().getBattlerIndex()));
globalScene.startDynamicPhase(new PostSummonPhase(this.getPokemon().getBattlerIndex()));
}
}