From 5f098545f5de6bc8aaf66509f1d4d620139742af Mon Sep 17 00:00:00 2001 From: Dean Date: Sat, 14 Jun 2025 17:39:52 -0700 Subject: [PATCH] Add new priority queues --- src/data/abilities/ability.ts | 50 +++++++--- src/data/battler-tags.ts | 19 ++++ src/data/moves/move.ts | 8 ++ src/data/phase-priority-queue.ts | 97 ------------------- src/enums/battler-tag-type.ts | 1 + src/enums/move-priority-modifier.ts | 5 + src/modifier/modifier.ts | 21 +--- src/queues/move-phase-priority-queue.ts | 28 ++++++ src/queues/phase-priority-queue.ts | 43 ++++++++ src/queues/pokemon-phase-priority-queue.ts | 10 ++ .../post-summon-phase-priority-queue.ts | 42 ++++++++ src/utils/speed-order.ts | 46 +++++++++ 12 files changed, 244 insertions(+), 126 deletions(-) delete mode 100644 src/data/phase-priority-queue.ts create mode 100644 src/enums/move-priority-modifier.ts create mode 100644 src/queues/move-phase-priority-queue.ts create mode 100644 src/queues/phase-priority-queue.ts create mode 100644 src/queues/pokemon-phase-priority-queue.ts create mode 100644 src/queues/post-summon-phase-priority-queue.ts create mode 100644 src/utils/speed-order.ts diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index ef4529c361e..77aa74b90e4 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -79,6 +79,7 @@ import type { ArenaTrapTag, SuppressAbilitiesTag } from "#app/data/arena-tag"; import { noAbilityTypeOverrideMoves } from "../moves/invalid-moves"; import type { Localizable } from "#app/@types/locales"; import { applyAbAttrs } from "./apply-ab-attrs"; +import { MovePriorityModifier } from "#enums/move-priority-modifier"; export class Ability implements Localizable { public id: AbilityId; @@ -4978,6 +4979,31 @@ export class ChangeMovePriorityAbAttr extends AbAttr { } } +export class ChangeMovePriorityModifierAbAttr extends AbAttr { + private newModifier: MovePriorityModifier; + private moveFunc: (pokemon: Pokemon, move: Move) => boolean; + + constructor(moveFunc: (pokemon: Pokemon, move: Move) => boolean, newModifier: MovePriorityModifier) { + super(false); + this.newModifier = newModifier; + this.moveFunc = moveFunc; + } + + override canApply(pokemon: Pokemon, _passive: boolean, _simulated: boolean, args: any[]): boolean { + return this.moveFunc(pokemon, args[0] as Move); + } + + override apply( + _pokemon: Pokemon, + _passive: boolean, + _simulated: boolean, + _cancelled: BooleanHolder | null, + args: any[], + ): void { + (args[1] as NumberHolder).value = this.newModifier; + } +} + export class IgnoreContactAbAttr extends AbAttr {} export class PreWeatherEffectAbAttr extends AbAttr { @@ -7219,7 +7245,12 @@ export class BypassSpeedChanceAbAttr extends AbAttr { const move = turnCommand?.move?.move ? allMoves[turnCommand.move.move] : null; const isDamageMove = move?.category === MoveCategory.PHYSICAL || move?.category === MoveCategory.SPECIAL; return ( - !simulated && !bypassSpeed.value && pokemon.randBattleSeedInt(100) < this.chance && isCommandFight && isDamageMove + !simulated && + !bypassSpeed.value && + pokemon.randBattleSeedInt(100) < this.chance && + isCommandFight && + isDamageMove && + pokemon.canAddTag(BattlerTagType.BYPASS_SPEED) ); } @@ -7228,17 +7259,16 @@ export class BypassSpeedChanceAbAttr extends AbAttr { * @param {Pokemon} _pokemon {@linkcode Pokemon} the Pokemon applying this ability * @param {boolean} _passive N/A * @param {BooleanHolder} _cancelled N/A - * @param {any[]} args [0] {@linkcode BooleanHolder} set to true when the ability activated + * @param {any[]} _args N/A */ override apply( - _pokemon: Pokemon, + pokemon: Pokemon, _passive: boolean, _simulated: boolean, _cancelled: BooleanHolder, - args: any[], + _args: any[], ): void { - const bypassSpeed = args[0] as BooleanHolder; - bypassSpeed.value = true; + pokemon.addTag(BattlerTagType.BYPASS_SPEED); } getTriggerMessage(pokemon: Pokemon, _abilityName: string, ..._args: any[]): string { @@ -7270,7 +7300,6 @@ export class PreventBypassSpeedChanceAbAttr extends AbAttr { /** * @argument {boolean} bypassSpeed - determines if a Pokemon is able to bypass speed at the moment - * @argument {boolean} canCheckHeldItems - determines if a Pokemon has access to Quick Claw's effects or not */ override apply( _pokemon: Pokemon, @@ -7280,9 +7309,7 @@ export class PreventBypassSpeedChanceAbAttr extends AbAttr { args: any[], ): void { const bypassSpeed = args[0] as BooleanHolder; - const canCheckHeldItems = args[1] as BooleanHolder; bypassSpeed.value = false; - canCheckHeldItems.value = false; } } @@ -7814,6 +7841,7 @@ const AbilityAttrs = Object.freeze({ BlockStatusDamageAbAttr, BlockOneHitKOAbAttr, ChangeMovePriorityAbAttr, + ChangeMovePriorityModifierAbAttr, IgnoreContactAbAttr, PreWeatherEffectAbAttr, PreWeatherDamageAbAttr, @@ -8230,7 +8258,7 @@ export function initAbilities() { .attr(AlwaysHitAbAttr) .attr(DoubleBattleChanceAbAttr), new Ability(AbilityId.STALL, 4) - .attr(ChangeMovePriorityAbAttr, (_pokemon, _move: Move) => true, -0.2), + .attr(ChangeMovePriorityModifierAbAttr, (_pokemon, _move: Move) => true, MovePriorityModifier.LAST_IN_BRACKET), new Ability(AbilityId.TECHNICIAN, 4) .attr(MovePowerBoostAbAttr, (user, target, move) => { const power = new NumberHolder(move.power); @@ -8957,7 +8985,7 @@ export function initAbilities() { .attr(TypeImmunityHealAbAttr, PokemonType.GROUND) .ignorable(), new Ability(AbilityId.MYCELIUM_MIGHT, 9) - .attr(ChangeMovePriorityAbAttr, (_pokemon, move) => move.category === MoveCategory.STATUS, -0.2) + .attr(ChangeMovePriorityModifierAbAttr, (_pokemon, move) => move.category === MoveCategory.STATUS, MovePriorityModifier.LAST_IN_BRACKET) .attr(PreventBypassSpeedChanceAbAttr, (_pokemon, move) => move.category === MoveCategory.STATUS) .attr(MoveAbilityBypassAbAttr, (_pokemon, move: Move) => move.category === MoveCategory.STATUS), new Ability(AbilityId.MINDS_EYE, 9) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 89d5a76159f..8d817fffff3 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -3421,6 +3421,23 @@ export class GrudgeTag extends BattlerTag { } } +/** + * Tag to allow the affected Pokemon's move to go first in its priority bracket. + * Used for {@link https://bulbapedia.bulbagarden.net/wiki/Quick_Draw_(Ability) Quick Draw} + * and {@link https://bulbapedia.bulbagarden.net/wiki/Quick_Claw Quick Claw}. + */ +export class BypassSpeedTag extends BattlerTag { + constructor() { + super(BattlerTagType.BYPASS_SPEED, BattlerTagLapseType.TURN_END, 1); + } + + override canAdd(pokemon: Pokemon): boolean { + const cancelled = new BooleanHolder(false); + applyAbAttrs("PreventBypassSpeedChanceAbAttr", pokemon, null, false, cancelled); + return !cancelled.value; + } +} + /** * Tag used to heal the user of Psycho Shift of its status effect if Psycho Shift succeeds in transferring its status effect to the target Pokemon */ @@ -3668,6 +3685,8 @@ export function getBattlerTag( return new PsychoShiftTag(); case BattlerTagType.MAGIC_COAT: return new MagicCoatTag(); + case BattlerTagType.BYPASS_SPEED: + return new BypassSpeedTag(); case BattlerTagType.NONE: default: return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId); diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index e713020cf9c..7c3bb26161b 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -100,6 +100,7 @@ import { SelectBiomePhase } from "#app/phases/select-biome-phase"; import { ChargingMove, MoveAttrMap, MoveAttrString, MoveKindString, MoveClassMap } from "#app/@types/move-types"; import { applyMoveAttrs } from "./apply-attrs"; import { frenzyMissFunc, getMoveTargets } from "./move-utils"; +import { MovePriorityModifier } from "#enums/move-priority-modifier"; type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean; export type UserMoveConditionFunc = (user: Pokemon, move: Move) => boolean; @@ -867,6 +868,13 @@ export default abstract class Move implements Localizable { return priority.value; } + getPriorityModifier(user: Pokemon, simulated = true): MovePriorityModifier { + const modifierHolder = new NumberHolder(MovePriorityModifier.NORMAL); + applyAbAttrs("ChangeMovePriorityModifierAbAttr", user, null, simulated, this, modifierHolder); + modifierHolder.value = user.getTag(BattlerTagType.BYPASS_SPEED) ? MovePriorityModifier.FIRST_IN_BRACKET : modifierHolder.value; + return modifierHolder.value; + } + /** * Calculate the [Expected Power](https://en.wikipedia.org/wiki/Expected_value) per turn * of this move, taking into account multi hit moves, accuracy, and the number of turns it diff --git a/src/data/phase-priority-queue.ts b/src/data/phase-priority-queue.ts deleted file mode 100644 index b815a6ac34f..00000000000 --- a/src/data/phase-priority-queue.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { globalScene } from "#app/global-scene"; -import type { Phase } from "#app/phase"; -import { ActivatePriorityQueuePhase } from "#app/phases/activate-priority-queue-phase"; -import type { PostSummonPhase } from "#app/phases/post-summon-phase"; -import { PostSummonActivateAbilityPhase } from "#app/phases/post-summon-activate-ability-phase"; -import { Stat } from "#enums/stat"; -import { BooleanHolder } from "#app/utils/common"; -import { TrickRoomTag } from "#app/data/arena-tag"; -import { DynamicPhaseType } from "#enums/dynamic-phase-type"; - -/** - * Stores a list of {@linkcode Phase}s - * - * Dynamically updates ordering to always pop the highest "priority", based on implementation of {@linkcode reorder} - */ -export abstract class PhasePriorityQueue { - protected abstract queue: Phase[]; - - /** - * Sorts the elements in the queue - */ - public abstract reorder(): void; - - /** - * Calls {@linkcode reorder} and shifts the queue - * @returns The front element of the queue after sorting - */ - public pop(): Phase | undefined { - this.reorder(); - return this.queue.shift(); - } - - /** - * Adds a phase to the queue - * @param phase The phase to add - */ - public push(phase: Phase): void { - this.queue.push(phase); - } - - /** - * Removes all phases from the queue - */ - public clear(): void { - this.queue.splice(0, this.queue.length); - } -} - -/** - * Priority Queue for {@linkcode PostSummonPhase} and {@linkcode PostSummonActivateAbilityPhase} - * - * Orders phases first by ability priority, then by the {@linkcode Pokemon}'s effective speed - */ -export class PostSummonPhasePriorityQueue extends PhasePriorityQueue { - protected override queue: PostSummonPhase[] = []; - - public override reorder(): void { - this.queue.sort((phaseA: PostSummonPhase, phaseB: PostSummonPhase) => { - if (phaseA.getPriority() === phaseB.getPriority()) { - return ( - (phaseB.getPokemon().getEffectiveStat(Stat.SPD) - phaseA.getPokemon().getEffectiveStat(Stat.SPD)) * - (isTrickRoom() ? -1 : 1) - ); - } - - return phaseB.getPriority() - phaseA.getPriority(); - }); - } - - public override push(phase: PostSummonPhase): void { - super.push(phase); - this.queueAbilityPhase(phase); - } - - /** - * Queues all necessary {@linkcode PostSummonActivateAbilityPhase}s for each pushed {@linkcode PostSummonPhase} - * @param phase The {@linkcode PostSummonPhase} that was pushed onto the queue - */ - private queueAbilityPhase(phase: PostSummonPhase): void { - const phasePokemon = phase.getPokemon(); - - phasePokemon.getAbilityPriorities().forEach((priority, idx) => { - this.queue.push(new PostSummonActivateAbilityPhase(phasePokemon.getBattlerIndex(), priority, !!idx)); - globalScene.phaseManager.appendToPhase( - new ActivatePriorityQueuePhase(DynamicPhaseType.POST_SUMMON), - "ActivatePriorityQueuePhase", - (p: ActivatePriorityQueuePhase) => p.getType() === DynamicPhaseType.POST_SUMMON, - ); - }); - } -} - -function isTrickRoom(): boolean { - const speedReversed = new BooleanHolder(false); - globalScene.arena.applyTags(TrickRoomTag, false, speedReversed); - return speedReversed.value; -} diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index 719b08c5b81..b7482bc9fb5 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -95,4 +95,5 @@ export enum BattlerTagType { ENDURE_TOKEN = "ENDURE_TOKEN", POWDER = "POWDER", MAGIC_COAT = "MAGIC_COAT", + BYPASS_SPEED = "BYPASS_SPEED" } diff --git a/src/enums/move-priority-modifier.ts b/src/enums/move-priority-modifier.ts new file mode 100644 index 00000000000..60d00ab2862 --- /dev/null +++ b/src/enums/move-priority-modifier.ts @@ -0,0 +1,5 @@ +export enum MovePriorityModifier { + LAST_IN_BRACKET = 0, + NORMAL, + FIRST_IN_BRACKET, +} \ No newline at end of file diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index e11f2c07ce8..687c789cad3 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -12,7 +12,6 @@ import { getPokemonNameWithAffix } from "#app/messages"; import Overrides from "#app/overrides"; import { LearnMoveType } from "#enums/learn-move-type"; import type { VoucherType } from "#app/system/voucher"; -import { Command } from "#enums/command"; import { addTextObject, TextStyle } from "#app/ui/text"; import { BooleanHolder, hslToHex, isNullOrUndefined, NumberHolder, randSeedFloat, toDmgValue } from "#app/utils/common"; import { BattlerTagType } from "#enums/battler-tag-type"; @@ -1588,30 +1587,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), diff --git a/src/queues/move-phase-priority-queue.ts b/src/queues/move-phase-priority-queue.ts new file mode 100644 index 00000000000..142271424b3 --- /dev/null +++ b/src/queues/move-phase-priority-queue.ts @@ -0,0 +1,28 @@ +import type { MovePhase } from "#app/phases/move-phase"; +import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-queue"; + +export class MovePhasePriorityQueue extends PokemonPhasePriorityQueue { + public override reorder(): void { + super.reorder(); + this.sortPostSpeed(); + } + + private sortPostSpeed(): void { + this.queue.sort((a: MovePhase, b: MovePhase) => { + const priority = [a, b].map(movePhase => { + const move = movePhase.move.getMove(); + return move.getPriority(movePhase.pokemon, true); + }); + + const priorityModifiers = [a, b].map(movePhase => + movePhase.move.getMove().getPriorityModifier(movePhase.pokemon), + ); + + if (priority[0] === priority[1] && priorityModifiers[0] !== priorityModifiers[1]) { + return priorityModifiers[0] - priorityModifiers[1]; + } + + return priority[0] - priority[1]; + }); + } +} diff --git a/src/queues/phase-priority-queue.ts b/src/queues/phase-priority-queue.ts new file mode 100644 index 00000000000..8ace2e6af59 --- /dev/null +++ b/src/queues/phase-priority-queue.ts @@ -0,0 +1,43 @@ +import type { Phase } from "#app/phase"; + +/** + * Stores a list of {@linkcode Phase}s + * + * Dynamically updates ordering to always pop the highest "priority", based on implementation of {@linkcode reorder} + */ +export abstract class PhasePriorityQueue { + protected queue: T[] = []; + + /** + * Sorts the elements in the queue + */ + public abstract reorder(): void; + + /** + * Calls {@linkcode reorder} and shifts the queue + * @returns The front element of the queue after sorting + */ + public pop(): T | undefined { + this.reorder(); + return this.queue.shift(); + } + + /** + * Adds a phase to the queue + * @param phase The phase to add + */ + public push(phase: T): void { + this.queue.push(phase); + } + + /** + * Removes all phases from the queue + */ + public clear(): void { + this.queue.splice(0, this.queue.length); + } + + public isEmpty(): boolean { + return !this.queue.length; + } +} diff --git a/src/queues/pokemon-phase-priority-queue.ts b/src/queues/pokemon-phase-priority-queue.ts new file mode 100644 index 00000000000..cabba068001 --- /dev/null +++ b/src/queues/pokemon-phase-priority-queue.ts @@ -0,0 +1,10 @@ +import type { PartyMemberPokemonPhase } from "#app/phases/party-member-pokemon-phase"; +import type { PokemonPhase } from "#app/phases/pokemon-phase"; +import { PhasePriorityQueue } from "#app/queues/phase-priority-queue"; +import { sortInSpeedOrder } from "#app/utils/speed-order"; + +export class PokemonPhasePriorityQueue extends PhasePriorityQueue { + public override reorder(): void { + this.queue = sortInSpeedOrder(this.queue); + } +} diff --git a/src/queues/post-summon-phase-priority-queue.ts b/src/queues/post-summon-phase-priority-queue.ts new file mode 100644 index 00000000000..8843e5514a7 --- /dev/null +++ b/src/queues/post-summon-phase-priority-queue.ts @@ -0,0 +1,42 @@ +import { globalScene } from "#app/global-scene"; +import { ActivatePriorityQueuePhase } from "#app/phases/activate-priority-queue-phase"; +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"; + +/** + * 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 { + public override reorder(): void { + super.reorder(); + this.queue.sort((phaseA: PostSummonPhase, phaseB: PostSummonPhase) => { + return phaseB.getPriority() - phaseA.getPriority(); + }); + } + + public override push(phase: PostSummonPhase): void { + super.push(phase); + this.queueAbilityPhase(phase); + } + + /** + * Queues all necessary {@linkcode PostSummonActivateAbilityPhase}s for each pushed {@linkcode PostSummonPhase} + * @param phase The {@linkcode PostSummonPhase} that was pushed onto the queue + */ + private queueAbilityPhase(phase: PostSummonPhase): void { + const phasePokemon = phase.getPokemon(); + + phasePokemon.getAbilityPriorities().forEach((priority, idx) => { + this.queue.push(new PostSummonActivateAbilityPhase(phasePokemon.getBattlerIndex(), priority, !!idx)); + globalScene.phaseManager.appendToPhase( + new ActivatePriorityQueuePhase("PostSummonPhase"), + "ActivatePriorityQueuePhase", + (p: ActivatePriorityQueuePhase) => p.getType() === "PostSummonPhase", + ); + }); + } +} diff --git a/src/utils/speed-order.ts b/src/utils/speed-order.ts new file mode 100644 index 00000000000..7916a471159 --- /dev/null +++ b/src/utils/speed-order.ts @@ -0,0 +1,46 @@ +import Pokemon from "#app/field/pokemon"; +import { globalScene } from "#app/global-scene"; +import { BooleanHolder, randSeedShuffle } from "#app/utils/common"; +import { ArenaTagType } from "#enums/arena-tag-type"; +import { Stat } from "#enums/stat"; + +export interface hasPokemon { + getPokemon(): Pokemon; +} + +export function sortInSpeedOrder(pokemonList: T[]): T[] { + pokemonList = shuffle(pokemonList); + sortBySpeed(pokemonList); + return pokemonList; +} + +/** Randomly shuffles the queue. */ +function shuffle(pokemonList: T[]): T[] { + // This is seeded with the current turn to prevent an inconsistency where it + // was varying based on how long since you last reloaded + globalScene.executeWithSeedOffset( + () => { + pokemonList = randSeedShuffle(pokemonList); + }, + globalScene.currentBattle.turn * 1000 + pokemonList.length, + globalScene.waveSeed, + ); + return pokemonList; +} + +function sortBySpeed(pokemonList: T[]): void { + pokemonList.sort((a, b) => { + const [aSpeed, bSpeed] = [a, b].map(pkmn => + pkmn instanceof Pokemon ? pkmn.getEffectiveStat(Stat.SPD) : pkmn.getPokemon().getEffectiveStat(Stat.SPD), + ); + return bSpeed - aSpeed; + }); + + /** 'true' if Trick Room is on the field. */ + const speedReversed = new BooleanHolder(false); + globalScene.arena.applyTags(ArenaTagType.TRICK_ROOM, false, speedReversed); + + if (speedReversed.value) { + pokemonList.reverse(); + } +}