Merge 7c7531f778
into f9ff4abfb0
This commit is contained in:
commit
6fcdb8aa29
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
/**
|
||||
* Enum representation of the phase types held by implementations of {@linkcode PhasePriorityQueue}
|
||||
*/
|
||||
|
||||
export enum DynamicPhaseType {
|
||||
POST_SUMMON
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue