mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-06-21 17:12:44 +02:00
[Bug] Activate PostSummon
Abilities in Speed and Priority Order
https://github.com/pagefaultgames/pokerogue/pull/5513
* Add prependToPhaseWithCondition and use it in SummonPhase to determine speed order
* Move logic to PostSummonPhase
* Add test base
* Pivot to using sort strategy instead
* Add and update tests
* Support priority ability activations
* Ensure priority abilities are still activated on switch in
* Add test for priority
* Update to use priority numbers instead of a boolean
* Add ability priorities to constructors
* Move sorting to BattleScene
* Rename phase file
* Update import
* Move application to applyPostSummonAbAttrs and stop assuming no other phases in queue
* Ensure all PostSummonPhases from encounters are added at the same time
* Switch to priority queue approach
* Ensure that zero/negative priority activations happen after postsummonphase
* Revert 07646fe
(not needed due to stable sort)
* Always create separate ability phases for passive and use boolean instead of priority number when applying
* Add test for dynamic updates
* Add BattlerIndex import
* Clear queues for testing
* Benjie suggestion
* Split files
* Update import in battlescene
* Remove extra spaces added by VSCode
* Fix other conflicts
* Update PhaseManager
* Update to use PhaseManager
* Immediately start postsummons
* Fix test
* Fix BattlerIndex import
* Remove unused imports
* Fix postsummon application
* Make priority readonly
This commit is contained in:
parent
1029afcdbf
commit
ff9aefb0e5
@ -86,6 +86,7 @@ export class Ability implements Localizable {
|
|||||||
public name: string;
|
public name: string;
|
||||||
public description: string;
|
public description: string;
|
||||||
public generation: number;
|
public generation: number;
|
||||||
|
public readonly postSummonPriority: number;
|
||||||
public isBypassFaint: boolean;
|
public isBypassFaint: boolean;
|
||||||
public isIgnorable: boolean;
|
public isIgnorable: boolean;
|
||||||
public isSuppressable = true;
|
public isSuppressable = true;
|
||||||
@ -94,11 +95,12 @@ export class Ability implements Localizable {
|
|||||||
public attrs: AbAttr[];
|
public attrs: AbAttr[];
|
||||||
public conditions: AbAttrCondition[];
|
public conditions: AbAttrCondition[];
|
||||||
|
|
||||||
constructor(id: AbilityId, generation: number) {
|
constructor(id: AbilityId, generation: number, postSummonPriority = 0) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
|
|
||||||
this.nameAppend = "";
|
this.nameAppend = "";
|
||||||
this.generation = generation;
|
this.generation = generation;
|
||||||
|
this.postSummonPriority = postSummonPriority;
|
||||||
this.attrs = [];
|
this.attrs = [];
|
||||||
this.conditions = [];
|
this.conditions = [];
|
||||||
|
|
||||||
@ -8104,7 +8106,7 @@ export function initAbilities() {
|
|||||||
.conditionalAttr(p => globalScene.currentBattle.double && [ AbilityId.PLUS, AbilityId.MINUS ].some(a => (p.getAlly()?.hasAbility(a) ?? false)), StatMultiplierAbAttr, Stat.SPATK, 1.5),
|
.conditionalAttr(p => globalScene.currentBattle.double && [ AbilityId.PLUS, AbilityId.MINUS ].some(a => (p.getAlly()?.hasAbility(a) ?? false)), StatMultiplierAbAttr, Stat.SPATK, 1.5),
|
||||||
new Ability(AbilityId.MINUS, 3)
|
new Ability(AbilityId.MINUS, 3)
|
||||||
.conditionalAttr(p => globalScene.currentBattle.double && [ AbilityId.PLUS, AbilityId.MINUS ].some(a => (p.getAlly()?.hasAbility(a) ?? false)), StatMultiplierAbAttr, Stat.SPATK, 1.5),
|
.conditionalAttr(p => globalScene.currentBattle.double && [ AbilityId.PLUS, AbilityId.MINUS ].some(a => (p.getAlly()?.hasAbility(a) ?? false)), StatMultiplierAbAttr, Stat.SPATK, 1.5),
|
||||||
new Ability(AbilityId.FORECAST, 3)
|
new Ability(AbilityId.FORECAST, 3, -2)
|
||||||
.uncopiable()
|
.uncopiable()
|
||||||
.unreplaceable()
|
.unreplaceable()
|
||||||
.attr(NoFusionAbilityAbAttr)
|
.attr(NoFusionAbilityAbAttr)
|
||||||
@ -8238,7 +8240,7 @@ export function initAbilities() {
|
|||||||
.attr(StatusEffectImmunityAbAttr)
|
.attr(StatusEffectImmunityAbAttr)
|
||||||
.condition(getWeatherCondition(WeatherType.SUNNY, WeatherType.HARSH_SUN))
|
.condition(getWeatherCondition(WeatherType.SUNNY, WeatherType.HARSH_SUN))
|
||||||
.ignorable(),
|
.ignorable(),
|
||||||
new Ability(AbilityId.KLUTZ, 4)
|
new Ability(AbilityId.KLUTZ, 4, 1)
|
||||||
.unimplemented(),
|
.unimplemented(),
|
||||||
new Ability(AbilityId.MOLD_BREAKER, 4)
|
new Ability(AbilityId.MOLD_BREAKER, 4)
|
||||||
.attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonMoldBreaker", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }))
|
.attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonMoldBreaker", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }))
|
||||||
@ -8290,7 +8292,7 @@ export function initAbilities() {
|
|||||||
.uncopiable()
|
.uncopiable()
|
||||||
.unsuppressable()
|
.unsuppressable()
|
||||||
.unreplaceable(),
|
.unreplaceable(),
|
||||||
new Ability(AbilityId.FLOWER_GIFT, 4)
|
new Ability(AbilityId.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.ATK, 1.5)
|
||||||
.conditionalAttr(getWeatherCondition(WeatherType.SUNNY || WeatherType.HARSH_SUN), StatMultiplierAbAttr, Stat.SPDEF, 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)
|
.conditionalAttr(getWeatherCondition(WeatherType.SUNNY || WeatherType.HARSH_SUN), AllyStatMultiplierAbAttr, Stat.ATK, 1.5)
|
||||||
@ -8312,7 +8314,7 @@ export function initAbilities() {
|
|||||||
new Ability(AbilityId.CONTRARY, 5)
|
new Ability(AbilityId.CONTRARY, 5)
|
||||||
.attr(StatStageChangeMultiplierAbAttr, -1)
|
.attr(StatStageChangeMultiplierAbAttr, -1)
|
||||||
.ignorable(),
|
.ignorable(),
|
||||||
new Ability(AbilityId.UNNERVE, 5)
|
new Ability(AbilityId.UNNERVE, 5, 1)
|
||||||
.attr(PreventBerryUseAbAttr),
|
.attr(PreventBerryUseAbAttr),
|
||||||
new Ability(AbilityId.DEFIANT, 5)
|
new Ability(AbilityId.DEFIANT, 5)
|
||||||
.attr(PostStatStageChangeStatStageChangeAbAttr, (_target, _statsChanged, stages) => stages < 0, [ Stat.ATK ], 2),
|
.attr(PostStatStageChangeStatStageChangeAbAttr, (_target, _statsChanged, stages) => stages < 0, [ Stat.ATK ], 2),
|
||||||
@ -8554,7 +8556,7 @@ export function initAbilities() {
|
|||||||
.attr(PostDefendStatStageChangeAbAttr, (_target, user, move) => user.getMoveType(move) === PokemonType.WATER && move.category !== MoveCategory.STATUS, Stat.DEF, 2),
|
.attr(PostDefendStatStageChangeAbAttr, (_target, user, move) => user.getMoveType(move) === PokemonType.WATER && move.category !== MoveCategory.STATUS, Stat.DEF, 2),
|
||||||
new Ability(AbilityId.MERCILESS, 7)
|
new Ability(AbilityId.MERCILESS, 7)
|
||||||
.attr(ConditionalCritAbAttr, (_user, target, _move) => target?.status?.effect === StatusEffect.TOXIC || target?.status?.effect === StatusEffect.POISON),
|
.attr(ConditionalCritAbAttr, (_user, target, _move) => target?.status?.effect === StatusEffect.TOXIC || target?.status?.effect === StatusEffect.POISON),
|
||||||
new Ability(AbilityId.SHIELDS_DOWN, 7)
|
new Ability(AbilityId.SHIELDS_DOWN, 7, -1)
|
||||||
.attr(PostBattleInitFormChangeAbAttr, () => 0)
|
.attr(PostBattleInitFormChangeAbAttr, () => 0)
|
||||||
.attr(PostSummonFormChangeAbAttr, p => p.formIndex % 7 + (p.getHpRatio() <= 0.5 ? 7 : 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))
|
.attr(PostTurnFormChangeAbAttr, p => p.formIndex % 7 + (p.getHpRatio() <= 0.5 ? 7 : 0))
|
||||||
@ -8592,7 +8594,7 @@ export function initAbilities() {
|
|||||||
.attr(MoveTypeChangeAbAttr, PokemonType.ELECTRIC, 1.2, (_user, _target, move) => move.type === PokemonType.NORMAL),
|
.attr(MoveTypeChangeAbAttr, PokemonType.ELECTRIC, 1.2, (_user, _target, move) => move.type === PokemonType.NORMAL),
|
||||||
new Ability(AbilityId.SURGE_SURFER, 7)
|
new Ability(AbilityId.SURGE_SURFER, 7)
|
||||||
.conditionalAttr(getTerrainCondition(TerrainType.ELECTRIC), StatMultiplierAbAttr, Stat.SPD, 2),
|
.conditionalAttr(getTerrainCondition(TerrainType.ELECTRIC), StatMultiplierAbAttr, Stat.SPD, 2),
|
||||||
new Ability(AbilityId.SCHOOLING, 7)
|
new Ability(AbilityId.SCHOOLING, 7, -1)
|
||||||
.attr(PostBattleInitFormChangeAbAttr, () => 0)
|
.attr(PostBattleInitFormChangeAbAttr, () => 0)
|
||||||
.attr(PostSummonFormChangeAbAttr, p => p.level < 20 || p.getHpRatio() <= 0.25 ? 0 : 1)
|
.attr(PostSummonFormChangeAbAttr, p => p.level < 20 || p.getHpRatio() <= 0.25 ? 0 : 1)
|
||||||
.attr(PostTurnFormChangeAbAttr, p => p.level < 20 || p.getHpRatio() <= 0.25 ? 0 : 1)
|
.attr(PostTurnFormChangeAbAttr, p => p.level < 20 || p.getHpRatio() <= 0.25 ? 0 : 1)
|
||||||
@ -8761,7 +8763,7 @@ export function initAbilities() {
|
|||||||
.ignorable(),
|
.ignorable(),
|
||||||
new Ability(AbilityId.RIPEN, 8)
|
new Ability(AbilityId.RIPEN, 8)
|
||||||
.attr(DoubleBerryEffectAbAttr),
|
.attr(DoubleBerryEffectAbAttr),
|
||||||
new Ability(AbilityId.ICE_FACE, 8)
|
new Ability(AbilityId.ICE_FACE, 8, -2)
|
||||||
.attr(NoTransformAbilityAbAttr)
|
.attr(NoTransformAbilityAbAttr)
|
||||||
.attr(NoFusionAbilityAbAttr)
|
.attr(NoFusionAbilityAbAttr)
|
||||||
// Add BattlerTagType.ICE_FACE if the pokemon is in ice face form
|
// Add BattlerTagType.ICE_FACE if the pokemon is in ice face form
|
||||||
@ -8781,7 +8783,7 @@ export function initAbilities() {
|
|||||||
.ignorable(),
|
.ignorable(),
|
||||||
new Ability(AbilityId.POWER_SPOT, 8)
|
new Ability(AbilityId.POWER_SPOT, 8)
|
||||||
.attr(AllyMoveCategoryPowerBoostAbAttr, [ MoveCategory.SPECIAL, MoveCategory.PHYSICAL ], 1.3),
|
.attr(AllyMoveCategoryPowerBoostAbAttr, [ MoveCategory.SPECIAL, MoveCategory.PHYSICAL ], 1.3),
|
||||||
new Ability(AbilityId.MIMICRY, 8)
|
new Ability(AbilityId.MIMICRY, 8, -1)
|
||||||
.attr(TerrainEventTypeChangeAbAttr),
|
.attr(TerrainEventTypeChangeAbAttr),
|
||||||
new Ability(AbilityId.SCREEN_CLEANER, 8)
|
new Ability(AbilityId.SCREEN_CLEANER, 8)
|
||||||
.attr(PostSummonRemoveArenaTagAbAttr, [ ArenaTagType.AURORA_VEIL, ArenaTagType.LIGHT_SCREEN, ArenaTagType.REFLECT ]),
|
.attr(PostSummonRemoveArenaTagAbAttr, [ ArenaTagType.AURORA_VEIL, ArenaTagType.LIGHT_SCREEN, ArenaTagType.REFLECT ]),
|
||||||
@ -8796,7 +8798,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
|
.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(AbilityId.GORILLA_TACTICS, 8)
|
new Ability(AbilityId.GORILLA_TACTICS, 8)
|
||||||
.attr(GorillaTacticsAbAttr),
|
.attr(GorillaTacticsAbAttr),
|
||||||
new Ability(AbilityId.NEUTRALIZING_GAS, 8)
|
new Ability(AbilityId.NEUTRALIZING_GAS, 8, 2)
|
||||||
.attr(PostSummonAddArenaTagAbAttr, true, ArenaTagType.NEUTRALIZING_GAS, 0)
|
.attr(PostSummonAddArenaTagAbAttr, true, ArenaTagType.NEUTRALIZING_GAS, 0)
|
||||||
.attr(PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr)
|
.attr(PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr)
|
||||||
.uncopiable()
|
.uncopiable()
|
||||||
@ -8828,14 +8830,14 @@ export function initAbilities() {
|
|||||||
.attr(PostVictoryStatStageChangeAbAttr, Stat.ATK, 1),
|
.attr(PostVictoryStatStageChangeAbAttr, Stat.ATK, 1),
|
||||||
new Ability(AbilityId.GRIM_NEIGH, 8)
|
new Ability(AbilityId.GRIM_NEIGH, 8)
|
||||||
.attr(PostVictoryStatStageChangeAbAttr, Stat.SPATK, 1),
|
.attr(PostVictoryStatStageChangeAbAttr, Stat.SPATK, 1),
|
||||||
new Ability(AbilityId.AS_ONE_GLASTRIER, 8)
|
new Ability(AbilityId.AS_ONE_GLASTRIER, 8, 1)
|
||||||
.attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonAsOneGlastrier", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }))
|
.attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonAsOneGlastrier", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }))
|
||||||
.attr(PreventBerryUseAbAttr)
|
.attr(PreventBerryUseAbAttr)
|
||||||
.attr(PostVictoryStatStageChangeAbAttr, Stat.ATK, 1)
|
.attr(PostVictoryStatStageChangeAbAttr, Stat.ATK, 1)
|
||||||
.uncopiable()
|
.uncopiable()
|
||||||
.unreplaceable()
|
.unreplaceable()
|
||||||
.unsuppressable(),
|
.unsuppressable(),
|
||||||
new Ability(AbilityId.AS_ONE_SPECTRIER, 8)
|
new Ability(AbilityId.AS_ONE_SPECTRIER, 8, 1)
|
||||||
.attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonAsOneSpectrier", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }))
|
.attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonAsOneSpectrier", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }))
|
||||||
.attr(PreventBerryUseAbAttr)
|
.attr(PreventBerryUseAbAttr)
|
||||||
.attr(PostVictoryStatStageChangeAbAttr, Stat.SPATK, 1)
|
.attr(PostVictoryStatStageChangeAbAttr, Stat.SPATK, 1)
|
||||||
@ -8893,12 +8895,12 @@ export function initAbilities() {
|
|||||||
.edgeCase(), // Encore, Frenzy, and other non-`TURN_END` tags don't lapse correctly on the commanding Pokemon.
|
.edgeCase(), // Encore, Frenzy, and other non-`TURN_END` tags don't lapse correctly on the commanding Pokemon.
|
||||||
new Ability(AbilityId.ELECTROMORPHOSIS, 9)
|
new Ability(AbilityId.ELECTROMORPHOSIS, 9)
|
||||||
.attr(PostDefendApplyBattlerTagAbAttr, (_target, _user, move) => move.category !== MoveCategory.STATUS, BattlerTagType.CHARGED),
|
.attr(PostDefendApplyBattlerTagAbAttr, (_target, _user, move) => move.category !== MoveCategory.STATUS, BattlerTagType.CHARGED),
|
||||||
new Ability(AbilityId.PROTOSYNTHESIS, 9)
|
new Ability(AbilityId.PROTOSYNTHESIS, 9, -2)
|
||||||
.conditionalAttr(getWeatherCondition(WeatherType.SUNNY, WeatherType.HARSH_SUN), PostSummonAddBattlerTagAbAttr, BattlerTagType.PROTOSYNTHESIS, 0, true)
|
.conditionalAttr(getWeatherCondition(WeatherType.SUNNY, WeatherType.HARSH_SUN), PostSummonAddBattlerTagAbAttr, BattlerTagType.PROTOSYNTHESIS, 0, true)
|
||||||
.attr(PostWeatherChangeAddBattlerTagAttr, BattlerTagType.PROTOSYNTHESIS, 0, WeatherType.SUNNY, WeatherType.HARSH_SUN)
|
.attr(PostWeatherChangeAddBattlerTagAttr, BattlerTagType.PROTOSYNTHESIS, 0, WeatherType.SUNNY, WeatherType.HARSH_SUN)
|
||||||
.uncopiable()
|
.uncopiable()
|
||||||
.attr(NoTransformAbilityAbAttr),
|
.attr(NoTransformAbilityAbAttr),
|
||||||
new Ability(AbilityId.QUARK_DRIVE, 9)
|
new Ability(AbilityId.QUARK_DRIVE, 9, -2)
|
||||||
.conditionalAttr(getTerrainCondition(TerrainType.ELECTRIC), PostSummonAddBattlerTagAbAttr, BattlerTagType.QUARK_DRIVE, 0, true)
|
.conditionalAttr(getTerrainCondition(TerrainType.ELECTRIC), PostSummonAddBattlerTagAbAttr, BattlerTagType.QUARK_DRIVE, 0, true)
|
||||||
.attr(PostTerrainChangeAddBattlerTagAttr, BattlerTagType.QUARK_DRIVE, 0, TerrainType.ELECTRIC)
|
.attr(PostTerrainChangeAddBattlerTagAttr, BattlerTagType.QUARK_DRIVE, 0, TerrainType.ELECTRIC)
|
||||||
.uncopiable()
|
.uncopiable()
|
||||||
@ -8942,7 +8944,7 @@ export function initAbilities() {
|
|||||||
new Ability(AbilityId.SUPREME_OVERLORD, 9)
|
new Ability(AbilityId.SUPREME_OVERLORD, 9)
|
||||||
.attr(VariableMovePowerBoostAbAttr, (user, _target, _move) => 1 + 0.1 * Math.min(user.isPlayer() ? globalScene.arena.playerFaints : globalScene.currentBattle.enemyFaints, 5))
|
.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
|
.partial(), // Should only boost once, on summon
|
||||||
new Ability(AbilityId.COSTAR, 9)
|
new Ability(AbilityId.COSTAR, 9, -2)
|
||||||
.attr(PostSummonCopyAllyStatsAbAttr),
|
.attr(PostSummonCopyAllyStatsAbAttr),
|
||||||
new Ability(AbilityId.TOXIC_DEBRIS, 9)
|
new Ability(AbilityId.TOXIC_DEBRIS, 9)
|
||||||
.attr(PostDefendApplyArenaTrapTagAbAttr, (_target, _user, move) => move.category === MoveCategory.PHYSICAL, ArenaTagType.TOXIC_SPIKES)
|
.attr(PostDefendApplyArenaTrapTagAbAttr, (_target, _user, move) => move.category === MoveCategory.PHYSICAL, ArenaTagType.TOXIC_SPIKES)
|
||||||
@ -8964,7 +8966,7 @@ export function initAbilities() {
|
|||||||
.ignorable(),
|
.ignorable(),
|
||||||
new Ability(AbilityId.SUPERSWEET_SYRUP, 9)
|
new Ability(AbilityId.SUPERSWEET_SYRUP, 9)
|
||||||
.attr(PostSummonStatStageChangeAbAttr, [ Stat.EVA ], -1),
|
.attr(PostSummonStatStageChangeAbAttr, [ Stat.EVA ], -1),
|
||||||
new Ability(AbilityId.HOSPITALITY, 9)
|
new Ability(AbilityId.HOSPITALITY, 9, -2)
|
||||||
.attr(PostSummonAllyHealAbAttr, 4, true),
|
.attr(PostSummonAllyHealAbAttr, 4, true),
|
||||||
new Ability(AbilityId.TOXIC_CHAIN, 9)
|
new Ability(AbilityId.TOXIC_CHAIN, 9)
|
||||||
.attr(PostAttackApplyStatusEffectAbAttr, false, 30, StatusEffect.TOXIC),
|
.attr(PostAttackApplyStatusEffectAbAttr, false, 30, StatusEffect.TOXIC),
|
||||||
@ -8988,7 +8990,7 @@ export function initAbilities() {
|
|||||||
.uncopiable()
|
.uncopiable()
|
||||||
.unreplaceable()
|
.unreplaceable()
|
||||||
.attr(NoTransformAbilityAbAttr),
|
.attr(NoTransformAbilityAbAttr),
|
||||||
new Ability(AbilityId.TERA_SHIFT, 9)
|
new Ability(AbilityId.TERA_SHIFT, 9, 2)
|
||||||
.attr(PostSummonFormChangeAbAttr, p => p.getFormKey() ? 0 : 1)
|
.attr(PostSummonFormChangeAbAttr, p => p.getFormKey() ? 0 : 1)
|
||||||
.uncopiable()
|
.uncopiable()
|
||||||
.unreplaceable()
|
.unreplaceable()
|
||||||
|
@ -470,15 +470,18 @@ export function applyPostVictoryAbAttrs<K extends AbAttrString>(
|
|||||||
export function applyPostSummonAbAttrs<K extends AbAttrString>(
|
export function applyPostSummonAbAttrs<K extends AbAttrString>(
|
||||||
attrType: AbAttrMap[K] extends PostSummonAbAttr ? K : never,
|
attrType: AbAttrMap[K] extends PostSummonAbAttr ? K : never,
|
||||||
pokemon: Pokemon,
|
pokemon: Pokemon,
|
||||||
|
passive = false,
|
||||||
simulated = false,
|
simulated = false,
|
||||||
...args: any[]
|
...args: any[]
|
||||||
): void {
|
): void {
|
||||||
applyAbAttrsInternal(
|
applySingleAbAttrs(
|
||||||
attrType,
|
|
||||||
pokemon,
|
pokemon,
|
||||||
|
passive,
|
||||||
|
attrType,
|
||||||
(attr, passive) => (attr as PostSummonAbAttr).applyPostSummon(pokemon, passive, simulated, args),
|
(attr, passive) => (attr as PostSummonAbAttr).applyPostSummon(pokemon, passive, simulated, args),
|
||||||
(attr, passive) => (attr as PostSummonAbAttr).canApplyPostSummon(pokemon, passive, simulated, args),
|
(attr, passive) => (attr as PostSummonAbAttr).canApplyPostSummon(pokemon, passive, simulated, args),
|
||||||
args,
|
args,
|
||||||
|
false,
|
||||||
simulated,
|
simulated,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
97
src/data/phase-priority-queue.ts
Normal file
97
src/data/phase-priority-queue.ts
Normal 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/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;
|
||||||
|
}
|
6
src/enums/dynamic-phase-type.ts
Normal file
6
src/enums/dynamic-phase-type.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* Enum representation of the phase types held by implementations of {@linkcode PhasePriorityQueue}
|
||||||
|
*/
|
||||||
|
export enum DynamicPhaseType {
|
||||||
|
POST_SUMMON
|
||||||
|
}
|
@ -2181,6 +2181,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
return this.hasPassive() && (!canApply || this.canApplyAbility(true)) && this.getPassiveAbility().hasAttr(attrType);
|
return this.hasPassive() && (!canApply || this.canApplyAbility(true)) && this.getPassiveAbility().hasAttr(attrType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getAbilityPriorities(): [number, number] {
|
||||||
|
return [this.getAbility().postSummonPriority, this.getPassiveAbility().postSummonPriority];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the weight of the Pokemon with subtractive modifiers (Autotomize) happening first
|
* Gets the weight of the Pokemon with subtractive modifiers (Autotomize) happening first
|
||||||
* and then multiplicative modifiers happening after (Heavy Metal and Light Metal)
|
* and then multiplicative modifiers happening after (Heavy Metal and Light Metal)
|
||||||
|
@ -2,6 +2,7 @@ import type { Phase } from "#app/phase";
|
|||||||
import type { default as Pokemon } from "#app/field/pokemon";
|
import type { default as Pokemon } from "#app/field/pokemon";
|
||||||
import type { PhaseMap, PhaseString } from "./@types/phase-types";
|
import type { PhaseMap, PhaseString } from "./@types/phase-types";
|
||||||
import { globalScene } from "#app/global-scene";
|
import { globalScene } from "#app/global-scene";
|
||||||
|
import { ActivatePriorityQueuePhase } from "#app/phases/activate-priority-queue-phase";
|
||||||
import { AddEnemyBuffModifierPhase } from "#app/phases/add-enemy-buff-modifier-phase";
|
import { AddEnemyBuffModifierPhase } from "#app/phases/add-enemy-buff-modifier-phase";
|
||||||
import { AttemptCapturePhase } from "#app/phases/attempt-capture-phase";
|
import { AttemptCapturePhase } from "#app/phases/attempt-capture-phase";
|
||||||
import { AttemptRunPhase } from "#app/phases/attempt-run-phase";
|
import { AttemptRunPhase } from "#app/phases/attempt-run-phase";
|
||||||
@ -11,7 +12,9 @@ import { CheckStatusEffectPhase } from "#app/phases/check-status-effect-phase";
|
|||||||
import { CheckSwitchPhase } from "#app/phases/check-switch-phase";
|
import { CheckSwitchPhase } from "#app/phases/check-switch-phase";
|
||||||
import { CommandPhase } from "#app/phases/command-phase";
|
import { CommandPhase } from "#app/phases/command-phase";
|
||||||
import { CommonAnimPhase } from "#app/phases/common-anim-phase";
|
import { CommonAnimPhase } from "#app/phases/common-anim-phase";
|
||||||
|
import type { Constructor } from "#app/utils/common";
|
||||||
import { DamageAnimPhase } from "#app/phases/damage-anim-phase";
|
import { DamageAnimPhase } from "#app/phases/damage-anim-phase";
|
||||||
|
import type { DynamicPhaseType } from "#enums/dynamic-phase-type";
|
||||||
import { EggHatchPhase } from "#app/phases/egg-hatch-phase";
|
import { EggHatchPhase } from "#app/phases/egg-hatch-phase";
|
||||||
import { EggLapsePhase } from "#app/phases/egg-lapse-phase";
|
import { EggLapsePhase } from "#app/phases/egg-lapse-phase";
|
||||||
import { EggSummaryPhase } from "#app/phases/egg-summary-phase";
|
import { EggSummaryPhase } from "#app/phases/egg-summary-phase";
|
||||||
@ -55,6 +58,7 @@ import { NextEncounterPhase } from "#app/phases/next-encounter-phase";
|
|||||||
import { ObtainStatusEffectPhase } from "#app/phases/obtain-status-effect-phase";
|
import { ObtainStatusEffectPhase } from "#app/phases/obtain-status-effect-phase";
|
||||||
import { PartyExpPhase } from "#app/phases/party-exp-phase";
|
import { PartyExpPhase } from "#app/phases/party-exp-phase";
|
||||||
import { PartyHealPhase } from "#app/phases/party-heal-phase";
|
import { PartyHealPhase } from "#app/phases/party-heal-phase";
|
||||||
|
import { type PhasePriorityQueue, PostSummonPhasePriorityQueue } from "#app/data/phase-priority-queue";
|
||||||
import { PokemonAnimPhase } from "#app/phases/pokemon-anim-phase";
|
import { PokemonAnimPhase } from "#app/phases/pokemon-anim-phase";
|
||||||
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
|
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase";
|
||||||
import { PokemonTransformPhase } from "#app/phases/pokemon-transform-phase";
|
import { PokemonTransformPhase } from "#app/phases/pokemon-transform-phase";
|
||||||
@ -111,6 +115,7 @@ import { WeatherEffectPhase } from "#app/phases/weather-effect-phase";
|
|||||||
* This allows for easy creation of new phases without needing to import each phase individually.
|
* This allows for easy creation of new phases without needing to import each phase individually.
|
||||||
*/
|
*/
|
||||||
const PHASES = Object.freeze({
|
const PHASES = Object.freeze({
|
||||||
|
ActivatePriorityQueuePhase,
|
||||||
AddEnemyBuffModifierPhase,
|
AddEnemyBuffModifierPhase,
|
||||||
AttemptCapturePhase,
|
AttemptCapturePhase,
|
||||||
AttemptRunPhase,
|
AttemptRunPhase,
|
||||||
@ -222,9 +227,19 @@ export class PhaseManager {
|
|||||||
private phaseQueuePrependSpliceIndex = -1;
|
private phaseQueuePrependSpliceIndex = -1;
|
||||||
private nextCommandPhaseQueue: Phase[] = [];
|
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 = null;
|
private currentPhase: Phase | null = null;
|
||||||
private standbyPhase: Phase | null = null;
|
private standbyPhase: Phase | null = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.dynamicPhaseQueues = [new PostSummonPhasePriorityQueue()];
|
||||||
|
this.dynamicPhaseTypes = [PostSummonPhase];
|
||||||
|
}
|
||||||
|
|
||||||
/* Phase Functions */
|
/* Phase Functions */
|
||||||
getCurrentPhase(): Phase | null {
|
getCurrentPhase(): Phase | null {
|
||||||
return this.currentPhase;
|
return this.currentPhase;
|
||||||
@ -254,8 +269,12 @@ export class PhaseManager {
|
|||||||
* @param defer boolean on which queue to add to, defaults to false, and adds to phaseQueue
|
* @param defer boolean on which queue to add to, defaults to false, and adds to phaseQueue
|
||||||
*/
|
*/
|
||||||
pushPhase(phase: Phase, defer = false): void {
|
pushPhase(phase: Phase, defer = false): void {
|
||||||
|
if (this.getDynamicPhaseType(phase) !== undefined) {
|
||||||
|
this.pushDynamicPhase(phase);
|
||||||
|
} else {
|
||||||
(!defer ? this.phaseQueue : this.nextCommandPhaseQueue).push(phase);
|
(!defer ? this.phaseQueue : this.nextCommandPhaseQueue).push(phase);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds Phase(s) to the end of phaseQueuePrepend, or at phaseQueuePrependSpliceIndex
|
* Adds Phase(s) to the end of phaseQueuePrepend, or at phaseQueuePrependSpliceIndex
|
||||||
@ -283,6 +302,7 @@ export class PhaseManager {
|
|||||||
for (const queue of [this.phaseQueue, this.phaseQueuePrepend, this.conditionalQueue, this.nextCommandPhaseQueue]) {
|
for (const queue of [this.phaseQueue, this.phaseQueuePrepend, this.conditionalQueue, this.nextCommandPhaseQueue]) {
|
||||||
queue.splice(0, queue.length);
|
queue.splice(0, queue.length);
|
||||||
}
|
}
|
||||||
|
this.dynamicPhaseQueues.forEach(queue => queue.clear());
|
||||||
this.currentPhase = null;
|
this.currentPhase = null;
|
||||||
this.standbyPhase = null;
|
this.standbyPhase = null;
|
||||||
this.clearPhaseQueueSplice();
|
this.clearPhaseQueueSplice();
|
||||||
@ -333,8 +353,9 @@ export class PhaseManager {
|
|||||||
|
|
||||||
this.currentPhase = this.phaseQueue.shift() ?? null;
|
this.currentPhase = this.phaseQueue.shift() ?? null;
|
||||||
|
|
||||||
|
const unactivatedConditionalPhases: [() => boolean, Phase][] = [];
|
||||||
// Check if there are any conditional phases queued
|
// Check if there are any conditional phases queued
|
||||||
if (this.conditionalQueue?.length) {
|
while (this.conditionalQueue?.length) {
|
||||||
// Retrieve the first conditional phase from the queue
|
// Retrieve the first conditional phase from the queue
|
||||||
const conditionalPhase = this.conditionalQueue.shift();
|
const conditionalPhase = this.conditionalQueue.shift();
|
||||||
// Evaluate the condition associated with the phase
|
// Evaluate the condition associated with the phase
|
||||||
@ -343,11 +364,12 @@ export class PhaseManager {
|
|||||||
this.pushPhase(conditionalPhase[1]);
|
this.pushPhase(conditionalPhase[1]);
|
||||||
} else if (conditionalPhase) {
|
} else if (conditionalPhase) {
|
||||||
// If the condition is not met, re-add the phase back to the front of the conditional queue
|
// 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 {
|
} else {
|
||||||
console.warn("condition phase is undefined/null!", conditionalPhase);
|
console.warn("condition phase is undefined/null!", conditionalPhase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.conditionalQueue.push(...unactivatedConditionalPhases);
|
||||||
|
|
||||||
if (this.currentPhase) {
|
if (this.currentPhase) {
|
||||||
console.log(`%cStart Phase ${this.currentPhase.constructor.name}`, "color:green;");
|
console.log(`%cStart Phase ${this.currentPhase.constructor.name}`, "color:green;");
|
||||||
@ -431,17 +453,18 @@ export class PhaseManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt to add the input phase(s) to index after target phase in the {@linkcode phaseQueue}, else simply calls {@linkcode unshiftPhase()}
|
* Tries to add the input phase(s) to index after target phase in the {@linkcode phaseQueue}, else simply calls {@linkcode unshiftPhase()}
|
||||||
* @param phase - The phase(s) to be added
|
* @param phase {@linkcode Phase} the phase(s) to be added
|
||||||
* @param targetPhase - The phase to search for in phaseQueue
|
* @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
|
* @returns `true` if a `targetPhase` was found to append to
|
||||||
*/
|
*/
|
||||||
appendToPhase(phase: Phase | Phase[], targetPhase: PhaseString): boolean {
|
appendToPhase(phase: Phase | Phase[], targetPhase: PhaseString, condition?: (p: Phase) => boolean): boolean {
|
||||||
if (!Array.isArray(phase)) {
|
if (!Array.isArray(phase)) {
|
||||||
phase = [phase];
|
phase = [phase];
|
||||||
}
|
}
|
||||||
const target = PHASES[targetPhase];
|
const target = PHASES[targetPhase];
|
||||||
const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof target);
|
const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof target && (!condition || condition(ph)));
|
||||||
|
|
||||||
if (targetIndex !== -1 && this.phaseQueue.length > targetIndex) {
|
if (targetIndex !== -1 && this.phaseQueue.length > targetIndex) {
|
||||||
this.phaseQueue.splice(targetIndex + 1, 0, ...phase);
|
this.phaseQueue.splice(targetIndex + 1, 0, ...phase);
|
||||||
@ -451,6 +474,68 @@ export class PhaseManager {
|
|||||||
return false;
|
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
|
* Adds a MessagePhase, either to PhaseQueuePrepend or nextCommandPhaseQueue
|
||||||
* @param message - string for MessagePhase
|
* @param message - string for MessagePhase
|
||||||
@ -578,4 +663,11 @@ export class PhaseManager {
|
|||||||
): boolean {
|
): boolean {
|
||||||
return this.appendToPhase(this.create(phase, ...args), targetPhase);
|
return this.appendToPhase(this.create(phase, ...args), targetPhase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public startNewDynamicPhase<T extends PhaseString>(
|
||||||
|
phase: T,
|
||||||
|
...args: ConstructorParameters<PhaseConstructorMap[T]>
|
||||||
|
): void {
|
||||||
|
this.startDynamicPhase(this.create(phase, ...args));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
23
src/phases/activate-priority-queue-phase.ts
Normal file
23
src/phases/activate-priority-queue-phase.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import type { DynamicPhaseType } from "#enums/dynamic-phase-type";
|
||||||
|
import { globalScene } from "#app/global-scene";
|
||||||
|
import { Phase } from "#app/phase";
|
||||||
|
|
||||||
|
export class ActivatePriorityQueuePhase extends Phase {
|
||||||
|
public readonly phaseName = "ActivatePriorityQueuePhase";
|
||||||
|
private type: DynamicPhaseType;
|
||||||
|
|
||||||
|
constructor(type: DynamicPhaseType) {
|
||||||
|
super();
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
override start() {
|
||||||
|
super.start();
|
||||||
|
globalScene.phaseManager.startDynamicPhaseType(this.type);
|
||||||
|
this.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getType(): DynamicPhaseType {
|
||||||
|
return this.type;
|
||||||
|
}
|
||||||
|
}
|
27
src/phases/post-summon-activate-ability-phase.ts
Normal file
27
src/phases/post-summon-activate-ability-phase.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { applyPostSummonAbAttrs } from "#app/data/abilities/apply-ab-attrs";
|
||||||
|
import { PostSummonPhase } from "#app/phases/post-summon-phase";
|
||||||
|
import type { BattlerIndex } from "#enums/battler-index";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,10 +1,10 @@
|
|||||||
import { globalScene } from "#app/global-scene";
|
import { globalScene } from "#app/global-scene";
|
||||||
import { applyAbAttrs, applyPostSummonAbAttrs } from "#app/data/abilities/apply-ab-attrs";
|
|
||||||
import { ArenaTrapTag } from "#app/data/arena-tag";
|
import { ArenaTrapTag } from "#app/data/arena-tag";
|
||||||
import { StatusEffect } from "#app/enums/status-effect";
|
import { StatusEffect } from "#app/enums/status-effect";
|
||||||
import { PokemonPhase } from "./pokemon-phase";
|
import { PokemonPhase } from "./pokemon-phase";
|
||||||
import { MysteryEncounterPostSummonTag } from "#app/data/battler-tags";
|
import { MysteryEncounterPostSummonTag } from "#app/data/battler-tags";
|
||||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
|
import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
|
||||||
|
|
||||||
export class PostSummonPhase extends PokemonPhase {
|
export class PostSummonPhase extends PokemonPhase {
|
||||||
public readonly phaseName = "PostSummonPhase";
|
public readonly phaseName = "PostSummonPhase";
|
||||||
@ -26,7 +26,6 @@ export class PostSummonPhase extends PokemonPhase {
|
|||||||
pokemon.lapseTag(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON);
|
pokemon.lapseTag(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON);
|
||||||
}
|
}
|
||||||
|
|
||||||
applyPostSummonAbAttrs("PostSummonAbAttr", pokemon);
|
|
||||||
const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField();
|
const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField();
|
||||||
for (const p of field) {
|
for (const p of field) {
|
||||||
applyAbAttrs("CommanderAbAttr", p, null, false);
|
applyAbAttrs("CommanderAbAttr", p, null, false);
|
||||||
@ -34,4 +33,8 @@ export class PostSummonPhase extends PokemonPhase {
|
|||||||
|
|
||||||
this.end();
|
this.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getPriority() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -245,7 +245,7 @@ export class SwitchSummonPhase extends SummonPhase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
queuePostSummon(): void {
|
queuePostSummon(): void {
|
||||||
globalScene.phaseManager.unshiftNew("PostSummonPhase", this.getPokemon().getBattlerIndex());
|
globalScene.phaseManager.startNewDynamicPhase("PostSummonPhase", this.getPokemon().getBattlerIndex());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
95
test/abilities/ability_activation_order.test.ts
Normal file
95
test/abilities/ability_activation_order.test.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { AbilityId } from "#enums/ability-id";
|
||||||
|
import { MoveId } from "#enums/move-id";
|
||||||
|
import { SpeciesId } from "#enums/species-id";
|
||||||
|
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 } 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([MoveId.SPLASH])
|
||||||
|
.ability(AbilityId.BALL_FETCH)
|
||||||
|
.battleStyle("single")
|
||||||
|
.disableCrits()
|
||||||
|
.enemySpecies(SpeciesId.MAGIKARP)
|
||||||
|
.enemyAbility(AbilityId.BALL_FETCH)
|
||||||
|
.enemyMoveset(MoveId.SPLASH);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should activate the ability of the faster Pokemon first", async () => {
|
||||||
|
game.override.enemyLevel(100).ability(AbilityId.DRIZZLE).enemyAbility(AbilityId.DROUGHT);
|
||||||
|
await game.classicMode.startBattle([SpeciesId.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(SpeciesId.MAGIKARP)
|
||||||
|
.enemyAbility(AbilityId.DROUGHT)
|
||||||
|
.ability(AbilityId.DRIZZLE)
|
||||||
|
.startingHeldItems([{ name: "BASE_STAT_BOOSTER", type: Stat.SPD, count: 100 }]);
|
||||||
|
|
||||||
|
await game.classicMode.startBattle([SpeciesId.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(SpeciesId.DITTO)
|
||||||
|
.enemyAbility(AbilityId.DROUGHT)
|
||||||
|
.ability(AbilityId.DRIZZLE)
|
||||||
|
.startingHeldItems([{ name: "SPECIES_STAT_BOOSTER", type: "QUICK_POWDER" }]);
|
||||||
|
|
||||||
|
await game.classicMode.startBattle([SpeciesId.DITTO]);
|
||||||
|
expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SUNNY);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should activate priority abilities first", async () => {
|
||||||
|
game.override
|
||||||
|
.startingLevel(1)
|
||||||
|
.enemyLevel(100)
|
||||||
|
.enemySpecies(SpeciesId.ACCELGOR)
|
||||||
|
.enemyAbility(AbilityId.DROUGHT)
|
||||||
|
.ability(AbilityId.NEUTRALIZING_GAS);
|
||||||
|
|
||||||
|
await game.classicMode.startBattle([SpeciesId.SLOWPOKE]);
|
||||||
|
expect(game.scene.arena.weather).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update dynamically based on speed order", async () => {
|
||||||
|
game.override
|
||||||
|
.startingLevel(35)
|
||||||
|
.enemyLevel(50)
|
||||||
|
.enemySpecies(SpeciesId.MAGIKARP)
|
||||||
|
.enemyAbility(AbilityId.SLOW_START)
|
||||||
|
.enemyPassiveAbility(AbilityId.DROUGHT)
|
||||||
|
.ability(AbilityId.DRIZZLE);
|
||||||
|
|
||||||
|
await game.classicMode.startBattle([SpeciesId.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
Block a user