This commit is contained in:
Dean 2025-04-12 17:11:16 -05:00 committed by GitHub
commit 6fcdb8aa29
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 387 additions and 33 deletions

View File

@ -184,6 +184,10 @@ import { HideAbilityPhase } from "#app/phases/hide-ability-phase";
import { expSpriteKeys } from "./sprites/sprite-keys";
import { hasExpSprite } from "./sprites/sprite-utils";
import { timedEventManager } from "./global-event-manager";
import { type PhasePriorityQueue, PostSummonPhasePriorityQueue } from "#app/data/phase-priority-queue";
import type { DynamicPhaseType } from "#enums/dynamic-phase-type";
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";
@ -314,6 +318,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;
@ -409,6 +417,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);
@ -2635,12 +2645,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);
}
}
/**
@ -2669,6 +2683,7 @@ export default class BattleScene extends SceneBase {
for (const queue of [this.phaseQueue, this.phaseQueuePrepend, this.conditionalQueue, this.nextCommandPhaseQueue]) {
queue.splice(0, queue.length);
}
this.dynamicPhaseQueues.forEach(queue => queue.clear());
this.currentPhase = null;
this.standbyPhase = null;
this.clearPhaseQueueSplice();
@ -2719,8 +2734,9 @@ export default class BattleScene extends SceneBase {
this.currentPhase = this.phaseQueue.shift() ?? null;
const unactivatedConditionalPhases: [() => boolean, Phase][] = [];
// Check if there are any conditional phases queued
if (this.conditionalQueue?.length) {
while (this.conditionalQueue?.length) {
// Retrieve the first conditional phase from the queue
const conditionalPhase = this.conditionalQueue.shift();
// Evaluate the condition associated with the phase
@ -2729,11 +2745,12 @@ export default class BattleScene extends SceneBase {
this.pushPhase(conditionalPhase[1]);
} else if (conditionalPhase) {
// If the condition is not met, re-add the phase back to the front of the conditional queue
this.conditionalQueue.unshift(conditionalPhase);
unactivatedConditionalPhases.push(conditionalPhase);
} else {
console.warn("condition phase is undefined/null!", conditionalPhase);
}
}
this.conditionalQueue.push(...unactivatedConditionalPhases);
if (this.currentPhase) {
console.log(`%cStart Phase ${this.currentPhase.constructor.name}`, "color:green;");
@ -2819,13 +2836,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);
@ -2835,6 +2853,68 @@ export default class BattleScene extends SceneBase {
return false;
}
/**
* Checks a phase and returns the matching {@linkcode DynamicPhaseType}, or undefined if it does not match one
* @param phase The phase to check
* @returns The corresponding {@linkcode DynamicPhaseType} or `undefined`
*/
public getDynamicPhaseType(phase: Phase | null): DynamicPhaseType | undefined {
let phaseType: DynamicPhaseType | undefined;
this.dynamicPhaseTypes.forEach((cls, index) => {
if (phase instanceof cls) {
phaseType = index;
}
});
return phaseType;
}
/**
* Pushes a phase onto its corresponding dynamic queue and marks the activation point in {@linkcode phaseQueue}
*
* The {@linkcode ActivatePriorityQueuePhase} will run the top phase in the dynamic queue (not necessarily {@linkcode phase})
* @param phase The phase to push
*/
public pushDynamicPhase(phase: Phase): void {
const type = this.getDynamicPhaseType(phase);
if (type === undefined) {
return;
}
this.pushPhase(new ActivatePriorityQueuePhase(type));
this.dynamicPhaseQueues[type].push(phase);
}
/**
* Unshifts the top phase from the corresponding dynamic queue onto {@linkcode phaseQueue}
* @param type {@linkcode DynamicPhaseType} The type of dynamic phase to start
*/
public startDynamicPhaseType(type: DynamicPhaseType): void {
const phase = this.dynamicPhaseQueues[type].pop();
if (phase) {
this.unshiftPhase(phase);
}
}
/**
* Unshifts an {@linkcode ActivatePriorityQueuePhase} for {@linkcode phase}, then pushes {@linkcode phase} to its dynamic queue
*
* This is the same as {@linkcode pushDynamicPhase}, except the activation phase is unshifted
*
* {@linkcode phase} is not guaranteed to be the next phase from the queue to run (if the queue is not empty)
* @param phase The phase to add
* @returns
*/
public startDynamicPhase(phase: Phase): void {
const type = this.getDynamicPhaseType(phase);
if (type === undefined) {
return;
}
this.unshiftPhase(new ActivatePriorityQueuePhase(type));
this.dynamicPhaseQueues[type].push(phase);
}
/**
* Adds a MessagePhase, either to PhaseQueuePrepend or nextCommandPhaseQueue
* @param message string for MessagePhase

View File

@ -54,6 +54,7 @@ export class Ability implements Localizable {
public name: string;
public description: string;
public generation: number;
public postSummonPriority: number;
public isBypassFaint: boolean;
public isIgnorable: boolean;
public isSuppressable = true;
@ -62,11 +63,12 @@ export class Ability implements Localizable {
public attrs: AbAttr[];
public conditions: AbAttrCondition[];
constructor(id: Abilities, generation: number) {
constructor(id: Abilities, generation: number, postSummonPriority: number = 0) {
this.id = id;
this.nameAppend = "";
this.generation = generation;
this.postSummonPriority = postSummonPriority;
this.attrs = [];
this.conditions = [];
@ -5421,8 +5423,8 @@ function applySingleAbAttrs<TAttr extends AbAttr>(
applyFunc: AbAttrApplyFunc<TAttr>,
successFunc: AbAttrSuccessFunc<TAttr>,
args: any[],
gainedMidTurn: boolean = false,
simulated: boolean = false,
gainedMidTurn = false,
simulated = false,
messages: string[] = []
) {
if (!pokemon?.canApplyAbility(passive) || (passive && (pokemon.getPassiveAbility().id === pokemon.getAbility().id))) {
@ -5721,6 +5723,18 @@ export class PostDamageForceSwitchAbAttr extends PostDamageAbAttr {
this.helper.switchOutLogic(pokemon);
}
}
/**
* Main Function for handling ability application. Applies both the normal ability and passive of the Pokemon
* @param attrType The type of {@linkcode AbAttr} to apply
* @param pokemon The {@linkcode Pokemon} whose abilities should be applied
* @param applyFunc The {@linkcode AbAttrApplyFunc} corresponding to {@linkcode attrType}
* @param args Extra arguments, handled by individual {@linkcode AbAttr}s
* @param showAbilityInstant If `true`, show the ability bar instantly instead of queuing it
* @param simulated `true` if the call is simulated and the battle state should not be changes
* @param messages Array of messages which will be added to if the ability displays a message
* @param gainedMidTurn `true` if the ability is activating because it was gained during the battle
*/
function applyAbAttrsInternal<TAttr extends AbAttr>(
attrType: Constructor<TAttr>,
pokemon: Pokemon | null,
@ -5729,7 +5743,7 @@ function applyAbAttrsInternal<TAttr extends AbAttr>(
args: any[],
simulated: boolean = false,
messages: string[] = [],
gainedMidTurn = false
gainedMidTurn = false,
) {
for (const passive of [ false, true ]) {
if (pokemon) {
@ -6004,16 +6018,19 @@ export function applyPostVictoryAbAttrs(
export function applyPostSummonAbAttrs(
attrType: Constructor<PostSummonAbAttr>,
pokemon: Pokemon,
passive = false,
simulated = false,
...args: any[]
): void {
applyAbAttrsInternal<PostSummonAbAttr>(
attrType,
applySingleAbAttrs<PostSummonAbAttr>(
pokemon,
passive,
attrType,
(attr, passive) => attr.applyPostSummon(pokemon, passive, simulated, args),
(attr, passive) => attr.canApplyPostSummon(pokemon, passive, simulated, args),
args,
simulated,
false,
simulated
);
}
@ -6527,7 +6544,7 @@ export function initAbilities() {
.conditionalAttr(p => globalScene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => (p.getAlly()?.hasAbility(a) ?? false)), StatMultiplierAbAttr, Stat.SPATK, 1.5),
new Ability(Abilities.MINUS, 3)
.conditionalAttr(p => globalScene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => (p.getAlly()?.hasAbility(a) ?? false)), StatMultiplierAbAttr, Stat.SPATK, 1.5),
new Ability(Abilities.FORECAST, 3)
new Ability(Abilities.FORECAST, 3, -2)
.uncopiable()
.unreplaceable()
.attr(NoFusionAbilityAbAttr)
@ -6668,7 +6685,7 @@ export function initAbilities() {
.attr(StatusEffectImmunityAbAttr)
.condition(getWeatherCondition(WeatherType.SUNNY, WeatherType.HARSH_SUN))
.ignorable(),
new Ability(Abilities.KLUTZ, 4)
new Ability(Abilities.KLUTZ, 4, 1)
.unimplemented(),
new Ability(Abilities.MOLD_BREAKER, 4)
.attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonMoldBreaker", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }))
@ -6720,7 +6737,7 @@ export function initAbilities() {
.uncopiable()
.unsuppressable()
.unreplaceable(),
new Ability(Abilities.FLOWER_GIFT, 4)
new Ability(Abilities.FLOWER_GIFT, 4, -2)
.conditionalAttr(getWeatherCondition(WeatherType.SUNNY || WeatherType.HARSH_SUN), StatMultiplierAbAttr, Stat.ATK, 1.5)
.conditionalAttr(getWeatherCondition(WeatherType.SUNNY || WeatherType.HARSH_SUN), StatMultiplierAbAttr, Stat.SPDEF, 1.5)
.conditionalAttr(getWeatherCondition(WeatherType.SUNNY || WeatherType.HARSH_SUN), AllyStatMultiplierAbAttr, Stat.ATK, 1.5)
@ -6742,7 +6759,7 @@ export function initAbilities() {
new Ability(Abilities.CONTRARY, 5)
.attr(StatStageChangeMultiplierAbAttr, -1)
.ignorable(),
new Ability(Abilities.UNNERVE, 5)
new Ability(Abilities.UNNERVE, 5, 1)
.attr(PreventBerryUseAbAttr),
new Ability(Abilities.DEFIANT, 5)
.attr(PostStatStageChangeStatStageChangeAbAttr, (target, statsChanged, stages) => stages < 0, [ Stat.ATK ], 2),
@ -6976,7 +6993,7 @@ export function initAbilities() {
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => user.getMoveType(move) === PokemonType.WATER && move.category !== MoveCategory.STATUS, Stat.DEF, 2),
new Ability(Abilities.MERCILESS, 7)
.attr(ConditionalCritAbAttr, (user, target, move) => target?.status?.effect === StatusEffect.TOXIC || target?.status?.effect === StatusEffect.POISON),
new Ability(Abilities.SHIELDS_DOWN, 7)
new Ability(Abilities.SHIELDS_DOWN, 7, -1)
.attr(PostBattleInitFormChangeAbAttr, () => 0)
.attr(PostSummonFormChangeAbAttr, p => p.formIndex % 7 + (p.getHpRatio() <= 0.5 ? 7 : 0))
.attr(PostTurnFormChangeAbAttr, p => p.formIndex % 7 + (p.getHpRatio() <= 0.5 ? 7 : 0))
@ -7014,7 +7031,7 @@ export function initAbilities() {
.attr(MoveTypeChangeAbAttr, PokemonType.ELECTRIC, 1.2, (user, target, move) => move.type === PokemonType.NORMAL && !move.hasAttr(VariableMoveTypeAttr)),
new Ability(Abilities.SURGE_SURFER, 7)
.conditionalAttr(getTerrainCondition(TerrainType.ELECTRIC), StatMultiplierAbAttr, Stat.SPD, 2),
new Ability(Abilities.SCHOOLING, 7)
new Ability(Abilities.SCHOOLING, 7, -1)
.attr(PostBattleInitFormChangeAbAttr, () => 0)
.attr(PostSummonFormChangeAbAttr, p => p.level < 20 || p.getHpRatio() <= 0.25 ? 0 : 1)
.attr(PostTurnFormChangeAbAttr, p => p.level < 20 || p.getHpRatio() <= 0.25 ? 0 : 1)
@ -7183,7 +7200,7 @@ export function initAbilities() {
.ignorable(),
new Ability(Abilities.RIPEN, 8)
.attr(DoubleBerryEffectAbAttr),
new Ability(Abilities.ICE_FACE, 8)
new Ability(Abilities.ICE_FACE, 8, -2)
.attr(NoTransformAbilityAbAttr)
.attr(NoFusionAbilityAbAttr)
// Add BattlerTagType.ICE_FACE if the pokemon is in ice face form
@ -7203,7 +7220,7 @@ export function initAbilities() {
.ignorable(),
new Ability(Abilities.POWER_SPOT, 8)
.attr(AllyMoveCategoryPowerBoostAbAttr, [ MoveCategory.SPECIAL, MoveCategory.PHYSICAL ], 1.3),
new Ability(Abilities.MIMICRY, 8)
new Ability(Abilities.MIMICRY, 8, -1)
.attr(TerrainEventTypeChangeAbAttr),
new Ability(Abilities.SCREEN_CLEANER, 8)
.attr(PostSummonRemoveArenaTagAbAttr, [ ArenaTagType.AURORA_VEIL, ArenaTagType.LIGHT_SCREEN, ArenaTagType.REFLECT ]),
@ -7218,7 +7235,7 @@ export function initAbilities() {
.edgeCase(), // interacts incorrectly with rock head. It's meant to switch abilities before recoil would apply so that a pokemon with rock head would lose rock head first and still take the recoil
new Ability(Abilities.GORILLA_TACTICS, 8)
.attr(GorillaTacticsAbAttr),
new Ability(Abilities.NEUTRALIZING_GAS, 8)
new Ability(Abilities.NEUTRALIZING_GAS, 8, 2)
.attr(PostSummonAddArenaTagAbAttr, true, ArenaTagType.NEUTRALIZING_GAS, 0)
.attr(PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr)
.uncopiable()
@ -7250,14 +7267,14 @@ export function initAbilities() {
.attr(PostVictoryStatStageChangeAbAttr, Stat.ATK, 1),
new Ability(Abilities.GRIM_NEIGH, 8)
.attr(PostVictoryStatStageChangeAbAttr, Stat.SPATK, 1),
new Ability(Abilities.AS_ONE_GLASTRIER, 8)
new Ability(Abilities.AS_ONE_GLASTRIER, 8, 1)
.attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonAsOneGlastrier", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }))
.attr(PreventBerryUseAbAttr)
.attr(PostVictoryStatStageChangeAbAttr, Stat.ATK, 1)
.uncopiable()
.unreplaceable()
.unsuppressable(),
new Ability(Abilities.AS_ONE_SPECTRIER, 8)
new Ability(Abilities.AS_ONE_SPECTRIER, 8, 1)
.attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonAsOneSpectrier", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }))
.attr(PreventBerryUseAbAttr)
.attr(PostVictoryStatStageChangeAbAttr, Stat.SPATK, 1)
@ -7315,12 +7332,12 @@ export function initAbilities() {
.edgeCase(), // Encore, Frenzy, and other non-`TURN_END` tags don't lapse correctly on the commanding Pokemon.
new Ability(Abilities.ELECTROMORPHOSIS, 9)
.attr(PostDefendApplyBattlerTagAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, BattlerTagType.CHARGED),
new Ability(Abilities.PROTOSYNTHESIS, 9)
new Ability(Abilities.PROTOSYNTHESIS, 9, -2)
.conditionalAttr(getWeatherCondition(WeatherType.SUNNY, WeatherType.HARSH_SUN), PostSummonAddBattlerTagAbAttr, BattlerTagType.PROTOSYNTHESIS, 0, true)
.attr(PostWeatherChangeAddBattlerTagAttr, BattlerTagType.PROTOSYNTHESIS, 0, WeatherType.SUNNY, WeatherType.HARSH_SUN)
.uncopiable()
.attr(NoTransformAbilityAbAttr),
new Ability(Abilities.QUARK_DRIVE, 9)
new Ability(Abilities.QUARK_DRIVE, 9, -2)
.conditionalAttr(getTerrainCondition(TerrainType.ELECTRIC), PostSummonAddBattlerTagAbAttr, BattlerTagType.QUARK_DRIVE, 0, true)
.attr(PostTerrainChangeAddBattlerTagAttr, BattlerTagType.QUARK_DRIVE, 0, TerrainType.ELECTRIC)
.uncopiable()
@ -7359,7 +7376,7 @@ export function initAbilities() {
new Ability(Abilities.SUPREME_OVERLORD, 9)
.attr(VariableMovePowerBoostAbAttr, (user, target, move) => 1 + 0.1 * Math.min(user.isPlayer() ? globalScene.arena.playerFaints : globalScene.currentBattle.enemyFaints, 5))
.partial(), // Should only boost once, on summon
new Ability(Abilities.COSTAR, 9)
new Ability(Abilities.COSTAR, 9, -2)
.attr(PostSummonCopyAllyStatsAbAttr),
new Ability(Abilities.TOXIC_DEBRIS, 9)
.attr(PostDefendApplyArenaTrapTagAbAttr, (target, user, move) => move.category === MoveCategory.PHYSICAL, ArenaTagType.TOXIC_SPIKES)
@ -7381,7 +7398,7 @@ export function initAbilities() {
.ignorable(),
new Ability(Abilities.SUPERSWEET_SYRUP, 9)
.attr(PostSummonStatStageChangeAbAttr, [ Stat.EVA ], -1),
new Ability(Abilities.HOSPITALITY, 9)
new Ability(Abilities.HOSPITALITY, 9, -2)
.attr(PostSummonAllyHealAbAttr, 4, true),
new Ability(Abilities.TOXIC_CHAIN, 9)
.attr(PostAttackApplyStatusEffectAbAttr, false, 30, StatusEffect.TOXIC),
@ -7405,7 +7422,7 @@ export function initAbilities() {
.uncopiable()
.unreplaceable()
.attr(NoTransformAbilityAbAttr),
new Ability(Abilities.TERA_SHIFT, 9)
new Ability(Abilities.TERA_SHIFT, 9, 2)
.attr(PostSummonFormChangeAbAttr, p => p.getFormKey() ? 0 : 1)
.uncopiable()
.unreplaceable()

View File

@ -0,0 +1,97 @@
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";
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.appendToPhase(
new ActivatePriorityQueuePhase(DynamicPhaseType.POST_SUMMON),
ActivatePriorityQueuePhase,
(p: ActivatePriorityQueuePhase) => p.getType() === DynamicPhaseType.POST_SUMMON,
);
});
}
}
function isTrickRoom(): boolean {
const speedReversed = new BooleanHolder(false);
globalScene.arena.applyTags(TrickRoomTag, false, speedReversed);
return speedReversed.value;
}

View File

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

View File

@ -2224,6 +2224,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return false;
}
public getAbilityPriorities(): [number, number] {
return [this.getAbility().postSummonPriority, this.getPassiveAbility().postSummonPriority];
}
/**
* Gets the weight of the Pokemon with subtractive modifiers (Autotomize) happening first
* and then multiplicative modifiers happening after (Heavy Metal and Light Metal)

View File

@ -0,0 +1,22 @@
import type { DynamicPhaseType } from "#enums/dynamic-phase-type";
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

@ -0,0 +1,28 @@
import type { BattlerIndex } from "#app/battle";
import { applyPostSummonAbAttrs, PostSummonAbAttr } from "#app/data/ability";
import { PostSummonPhase } from "#app/phases/post-summon-phase";
/**
* Helper to {@linkcode PostSummonPhase} which applies abilities
*/
export class PostSummonActivateAbilityPhase extends PostSummonPhase {
private priority: number;
private passive: boolean;
constructor(battlerIndex: BattlerIndex, priority: number, passive: boolean) {
super(battlerIndex);
this.priority = priority;
this.passive = passive;
}
start() {
applyPostSummonAbAttrs(PostSummonAbAttr, this.getPokemon(), this.passive, false);
this.end();
}
public override getPriority() {
return this.priority;
}
}

View File

@ -1,5 +1,5 @@
import { globalScene } from "#app/global-scene";
import { applyAbAttrs, applyPostSummonAbAttrs, CommanderAbAttr, PostSummonAbAttr } from "#app/data/ability";
import { applyAbAttrs, CommanderAbAttr } from "#app/data/ability";
import { ArenaTrapTag } from "#app/data/arena-tag";
import { StatusEffect } from "#app/enums/status-effect";
import { PokemonPhase } from "./pokemon-phase";
@ -25,7 +25,6 @@ export class PostSummonPhase extends PokemonPhase {
pokemon.lapseTag(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON);
}
applyPostSummonAbAttrs(PostSummonAbAttr, pokemon);
const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField();
for (const p of field) {
applyAbAttrs(CommanderAbAttr, p, null, false);
@ -33,4 +32,8 @@ export class PostSummonPhase extends PokemonPhase {
this.end();
}
public getPriority() {
return 0;
}
}

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()));
}
}

View File

@ -0,0 +1,96 @@
import { PreventBerryUseAbAttr, TerrainEventTypeChangeAbAttr } from "#app/data/ability";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { Stat } from "#enums/stat";
import { WeatherType } from "#enums/weather-type";
import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
describe("Ability Activation Order", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([Moves.SPLASH])
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("should activate the ability of the faster Pokemon first", async () => {
game.override.enemyLevel(100).ability(Abilities.DRIZZLE).enemyAbility(Abilities.DROUGHT);
await game.classicMode.startBattle([Species.SLOWPOKE]);
// Enemy's ability should activate first, so sun ends up replaced with rain
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.RAIN);
});
it("should consider base stat boosting items in determining order", async () => {
game.override
.startingLevel(25)
.enemyLevel(50)
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.DROUGHT)
.ability(Abilities.DRIZZLE)
.startingHeldItems([{ name: "BASE_STAT_BOOSTER", type: Stat.SPD, count: 100 }]);
await game.classicMode.startBattle([Species.MAGIKARP]);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SUNNY);
});
it("should consider stat boosting items in determining order", async () => {
game.override
.startingLevel(35)
.enemyLevel(50)
.enemySpecies(Species.DITTO)
.enemyAbility(Abilities.DROUGHT)
.ability(Abilities.DRIZZLE)
.startingHeldItems([{ name: "SPECIES_STAT_BOOSTER", type: "QUICK_POWDER" }]);
await game.classicMode.startBattle([Species.DITTO]);
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SUNNY);
});
it("should activate priority abilities first", async () => {
game.override
.startingLevel(1)
.enemyLevel(100)
.enemySpecies(Species.ACCELGOR)
.enemyAbility(Abilities.DROUGHT)
.ability(Abilities.NEUTRALIZING_GAS);
await game.classicMode.startBattle([Species.SLOWPOKE]);
expect(game.scene.arena.weather).toBeUndefined();
});
it("should update dynamically based on speed order", async () => {
game.override
.startingLevel(35)
.enemyLevel(50)
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.SLOW_START)
.enemyPassiveAbility(Abilities.DROUGHT)
.ability(Abilities.DRIZZLE);
await game.classicMode.startBattle([Species.MAGIKARP]);
// Slow start activates and makes enemy slower, so drought activates after drizzle
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SUNNY);
});
});