diff --git a/src/data/ability.ts b/src/data/ability.ts index 25ffa797140..6f2ae37678e 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -58,14 +58,16 @@ export class Ability implements Localizable { public generation: number; public isBypassFaint: boolean; public isIgnorable: boolean; + public isPriority: boolean; public attrs: AbAttr[]; public conditions: AbAttrCondition[]; - constructor(id: Abilities, generation: number) { + constructor(id: Abilities, generation: number, isPriority: boolean = false) { this.id = id; this.nameAppend = ""; this.generation = generation; + this.isPriority = isPriority; this.attrs = []; this.conditions = []; @@ -5999,6 +6001,21 @@ export function applyOnGainAbAttrs(pokemon: Pokemon, passive = false, simulated export function applyOnLoseAbAttrs(pokemon: Pokemon, passive = false, simulated = false, ...args: any[]): void { applySingleAbAttrs(pokemon, passive, PreLeaveFieldAbAttr, (attr, passive) => attr.applyPreLeaveField(pokemon, passive, simulated, [ ...args, true ]), args, true, simulated); } + +/** + * Applies only abilities that are priority or are not, based on parameters + * + * @param priority If true, apply only priority abilities. If false, apply only non-priority abilities + */ +export function applyPriorityBasedAbAttrs(pokemon: Pokemon, priority: boolean, simulated: boolean = false, ...args: any[]) { + for (const passive of [ false, true ]) { + const ability: Ability = passive ? pokemon.getPassiveAbility() : pokemon.getAbility(); + if (ability.isPriority == priority) { + applySingleAbAttrs(pokemon, passive, PostSummonAbAttr, (attr, passive) => attr.applyPostSummon(pokemon, passive, simulated, args), args, false, simulated) + } + } +} + function queueShowAbility(pokemon: Pokemon, passive: boolean): void { globalScene.unshiftPhase(new ShowAbilityPhase(pokemon.id, passive)); globalScene.clearPhaseQueueSplice(); @@ -6887,7 +6904,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, true) .attr(PostSummonAddArenaTagAbAttr, ArenaTagType.NEUTRALIZING_GAS, 0) .attr(PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr) .attr(UncopiableAbilityAbAttr) @@ -6920,14 +6937,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, true) .attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonAsOneGlastrier", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })) .attr(PreventBerryUseAbAttr) .attr(PostVictoryStatStageChangeAbAttr, Stat.ATK, 1) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) .attr(UnsuppressableAbilityAbAttr), - new Ability(Abilities.AS_ONE_SPECTRIER, 8) + new Ability(Abilities.AS_ONE_SPECTRIER, 8, true) .attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonAsOneSpectrier", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })) .attr(PreventBerryUseAbAttr) .attr(PostVictoryStatStageChangeAbAttr, Stat.SPATK, 1) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 3cd25e4d10a..dd5fb26cbec 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2265,6 +2265,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return false; } + /** + * @returns If either of the Pokemon's abilities have priority activation + */ + public hasPriorityAbility() { + return [this.getAbility(), this.getPassiveAbility()].some(ability => ability.isPriority); + } + /** * Gets the weight of the Pokemon with subtractive modifiers (Autotomize) happening first * and then multiplicative modifiers happening after (Heavy Metal and Light Metal) diff --git a/src/phases/post-summon-phase.ts b/src/phases/post-summon-phase.ts index b31682dd250..42c7f097d15 100644 --- a/src/phases/post-summon-phase.ts +++ b/src/phases/post-summon-phase.ts @@ -1,31 +1,46 @@ import { globalScene } from "#app/global-scene"; import type { BattlerIndex } from "#app/battle"; -import { applyAbAttrs, applyPostSummonAbAttrs, CommanderAbAttr, PostSummonAbAttr } from "#app/data/ability"; +import { applyAbAttrs, applyPriorityBasedAbAttrs, CommanderAbAttr, PostSummonAbAttr } from "#app/data/ability"; import { ArenaTrapTag } from "#app/data/arena-tag"; 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 { PriorityAbilityActivationPhase } from "#app/phases/priority-ability-activation-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(); - globalScene.phaseQueue; - const fasterPhase = globalScene.findPhase( - phase => - phase instanceof PostSummonPhase && - phase.getPokemon().getEffectiveStat(Stat.SPD) > pokemon.getEffectiveStat(Stat.SPD), - ); - if (fasterPhase) { - globalScene.pushPhase(new PostSummonPhase(pokemon.getBattlerIndex())); + if ( + !this.ordered && + globalScene.findPhase(phase => phase instanceof PostSummonPhase && phase.getPokemon() !== pokemon) + ) { + globalScene.pushPhase(new PostSummonPhase(pokemon.getBattlerIndex(), true)); globalScene.phaseQueue.sort( (phaseA: PostSummonPhase, phaseB: PostSummonPhase) => phaseB.getPokemon().getEffectiveStat(Stat.SPD) - phaseA.getPokemon().getEffectiveStat(Stat.SPD), ); + globalScene.phaseQueue.forEach((phase: PostSummonPhase) => { + phase.ordered = true; + const phasePokemon = phase.getPokemon(); + + if (phasePokemon.hasPriorityAbility()) { + globalScene.unshiftPhase(new PriorityAbilityActivationPhase(this.getPokemon().getBattlerIndex())); + } + }); this.end(); return; } @@ -43,7 +58,7 @@ export class PostSummonPhase extends PokemonPhase { pokemon.lapseTag(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON); } - applyPostSummonAbAttrs(PostSummonAbAttr, pokemon); + applyPriorityBasedAbAttrs(pokemon, false); const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); for (const p of field) { applyAbAttrs(CommanderAbAttr, p, null, false); diff --git a/src/phases/priority-ability-activation-phase.ts b/src/phases/priority-ability-activation-phase.ts new file mode 100644 index 00000000000..2d1ca08ebbc --- /dev/null +++ b/src/phases/priority-ability-activation-phase.ts @@ -0,0 +1,19 @@ +import { applyPriorityBasedAbAttrs } from "#app/data/ability"; +import { PokemonPhase } from "#app/phases/pokemon-phase"; + +/** + * Phase to apply (post-summon) ability attributes for "priority" abilities + * + * Priority abilities activate before others and before hazards + * + * @see Example - {@link https://bulbapedia.bulbagarden.net/wiki/Neutralizing_Gas_(Ability) | Neutralizing Gas} + */ +export class PriorityAbilityActivationPhase extends PokemonPhase { + start() { + super.start(); + + applyPriorityBasedAbAttrs(this.getPokemon(), true); + + this.end(); + } +}