mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-07-03 23:12:20 +02:00
Merge 1dd94c45a0
into 9926a6e799
This commit is contained in:
commit
5d80d74fc1
3
src/@types/phase-condition.ts
Normal file
3
src/@types/phase-condition.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import type { Phase } from "#app/phase";
|
||||||
|
|
||||||
|
export type PhaseConditionFunc = (phase: Phase) => boolean;
|
@ -1,3 +1,5 @@
|
|||||||
|
import type Pokemon from "#app/field/pokemon";
|
||||||
|
import type { Phase } from "#app/phase";
|
||||||
import type { PhaseConstructorMap } from "#app/phase-manager";
|
import type { PhaseConstructorMap } from "#app/phase-manager";
|
||||||
|
|
||||||
// Intentionally export the types of everything in phase-manager, as this file is meant to be
|
// Intentionally export the types of everything in phase-manager, as this file is meant to be
|
||||||
@ -23,3 +25,11 @@ export type PhaseClass = PhaseConstructorMap[keyof PhaseConstructorMap];
|
|||||||
* Union type of all phase names as strings.
|
* Union type of all phase names as strings.
|
||||||
*/
|
*/
|
||||||
export type PhaseString = keyof PhaseMap;
|
export type PhaseString = keyof PhaseMap;
|
||||||
|
|
||||||
|
export type DynamicPhaseString = "PostSummonPhase" | "SwitchSummonPhase" | "MovePhase" | "MoveHeaderPhase";
|
||||||
|
|
||||||
|
export type StaticPhaseString = Exclude<PhaseString, DynamicPhaseString>;
|
||||||
|
|
||||||
|
export interface DynamicPhase extends Phase {
|
||||||
|
getPokemon(): Pokemon;
|
||||||
|
}
|
||||||
|
@ -861,8 +861,8 @@ export default class BattleScene extends SceneBase {
|
|||||||
let targetingMovePhase: MovePhase;
|
let targetingMovePhase: MovePhase;
|
||||||
do {
|
do {
|
||||||
targetingMovePhase = this.phaseManager.findPhase(
|
targetingMovePhase = this.phaseManager.findPhase(
|
||||||
|
"MovePhase",
|
||||||
mp =>
|
mp =>
|
||||||
mp.is("MovePhase") &&
|
|
||||||
mp.targets.length === 1 &&
|
mp.targets.length === 1 &&
|
||||||
mp.targets[0] === removedPokemon.getBattlerIndex() &&
|
mp.targets[0] === removedPokemon.getBattlerIndex() &&
|
||||||
mp.pokemon.isPlayer() !== allyPokemon.isPlayer(),
|
mp.pokemon.isPlayer() !== allyPokemon.isPlayer(),
|
||||||
|
@ -82,6 +82,9 @@ import type { Constructor } from "#app/utils/common";
|
|||||||
import type { Localizable } from "#app/@types/locales";
|
import type { Localizable } from "#app/@types/locales";
|
||||||
import { applyAbAttrs } from "./apply-ab-attrs";
|
import { applyAbAttrs } from "./apply-ab-attrs";
|
||||||
import type { Closed, Exact } from "#app/@types/type-helpers";
|
import type { Closed, Exact } from "#app/@types/type-helpers";
|
||||||
|
import { MovePriorityModifier } from "#enums/move-priority-modifier";
|
||||||
|
import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
|
||||||
|
import type { MovePhase } from "#app/phases/move-phase";
|
||||||
|
|
||||||
// biome-ignore-start lint/correctness/noUnusedImports: Used in TSDoc
|
// biome-ignore-start lint/correctness/noUnusedImports: Used in TSDoc
|
||||||
import type BattleScene from "#app/battle-scene";
|
import type BattleScene from "#app/battle-scene";
|
||||||
@ -3181,6 +3184,7 @@ export class CommanderAbAttr extends AbAttr {
|
|||||||
return (
|
return (
|
||||||
globalScene.currentBattle?.double &&
|
globalScene.currentBattle?.double &&
|
||||||
!isNullOrUndefined(ally) &&
|
!isNullOrUndefined(ally) &&
|
||||||
|
ally.isActive(true) &&
|
||||||
ally.species.speciesId === SpeciesId.DONDOZO &&
|
ally.species.speciesId === SpeciesId.DONDOZO &&
|
||||||
!(ally.isFainted() || ally.getTag(BattlerTagType.COMMANDED))
|
!(ally.isFainted() || ally.getTag(BattlerTagType.COMMANDED))
|
||||||
);
|
);
|
||||||
@ -4014,6 +4018,25 @@ export class ChangeMovePriorityAbAttr extends AbAttr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ChangeMovePriorityModifierAbAttr extends AbAttr {
|
||||||
|
private newModifier: MovePriorityModifier;
|
||||||
|
private moveFunc: (pokemon: Pokemon, move: Move) => boolean;
|
||||||
|
|
||||||
|
constructor(moveFunc: (pokemon: Pokemon, move: Move) => boolean, newModifier: MovePriorityModifier) {
|
||||||
|
super(false);
|
||||||
|
this.newModifier = newModifier;
|
||||||
|
this.moveFunc = moveFunc;
|
||||||
|
}
|
||||||
|
|
||||||
|
override canApply({ pokemon, move }: ChangeMovePriorityAbAttrParams): boolean {
|
||||||
|
return this.moveFunc(pokemon, move);
|
||||||
|
}
|
||||||
|
|
||||||
|
override apply({ priority }: ChangeMovePriorityAbAttrParams): void {
|
||||||
|
priority.value = this.newModifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class IgnoreContactAbAttr extends AbAttr {
|
export class IgnoreContactAbAttr extends AbAttr {
|
||||||
private declare readonly _: never;
|
private declare readonly _: never;
|
||||||
}
|
}
|
||||||
@ -4955,15 +4978,23 @@ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr {
|
|||||||
// If the move is an AttackMove or a StatusMove the Dancer must replicate the move on the source of the Dance
|
// If the move is an AttackMove or a StatusMove the Dancer must replicate the move on the source of the Dance
|
||||||
if (move.getMove().is("AttackMove") || move.getMove().is("StatusMove")) {
|
if (move.getMove().is("AttackMove") || move.getMove().is("StatusMove")) {
|
||||||
const target = this.getTarget(pokemon, source, targets);
|
const target = this.getTarget(pokemon, source, targets);
|
||||||
globalScene.phaseManager.unshiftNew("MovePhase", pokemon, target, move, MoveUseMode.INDIRECT);
|
globalScene.phaseManager.pushNew(
|
||||||
|
"MovePhase",
|
||||||
|
pokemon,
|
||||||
|
target,
|
||||||
|
move,
|
||||||
|
MoveUseMode.INDIRECT,
|
||||||
|
MovePhaseTimingModifier.FIRST,
|
||||||
|
);
|
||||||
} else if (move.getMove().is("SelfStatusMove")) {
|
} else if (move.getMove().is("SelfStatusMove")) {
|
||||||
// If the move is a SelfStatusMove (ie. Swords Dance) the Dancer should replicate it on itself
|
// If the move is a SelfStatusMove (ie. Swords Dance) the Dancer should replicate it on itself
|
||||||
globalScene.phaseManager.unshiftNew(
|
globalScene.phaseManager.pushNew(
|
||||||
"MovePhase",
|
"MovePhase",
|
||||||
pokemon,
|
pokemon,
|
||||||
[pokemon.getBattlerIndex()],
|
[pokemon.getBattlerIndex()],
|
||||||
move,
|
move,
|
||||||
MoveUseMode.INDIRECT,
|
MoveUseMode.INDIRECT,
|
||||||
|
MovePhaseTimingModifier.FIRST,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -5952,11 +5983,6 @@ export class IllusionPostBattleAbAttr extends PostBattleAbAttr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BypassSpeedChanceAbAttrParams extends AbAttrBaseParams {
|
|
||||||
/** Holds whether the speed check is bypassed after ability application */
|
|
||||||
bypass: BooleanHolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If a Pokémon with this Ability selects a damaging move, it has a 30% chance of going first in its priority bracket. If the Ability activates, this is announced at the start of the turn (after move selection).
|
* If a Pokémon with this Ability selects a damaging move, it has a 30% chance of going first in its priority bracket. If the Ability activates, this is announced at the start of the turn (after move selection).
|
||||||
* @sealed
|
* @sealed
|
||||||
@ -5972,26 +5998,28 @@ export class BypassSpeedChanceAbAttr extends AbAttr {
|
|||||||
this.chance = chance;
|
this.chance = chance;
|
||||||
}
|
}
|
||||||
|
|
||||||
override canApply({ bypass, simulated, pokemon }: BypassSpeedChanceAbAttrParams): boolean {
|
override canApply({ simulated, pokemon }: AbAttrBaseParams): boolean {
|
||||||
// TODO: Consider whether we can move the simulated check to the `apply` method
|
// TODO: Consider whether we can move the simulated check to the `apply` method
|
||||||
// May be difficult as we likely do not want to modify the randBattleSeed
|
// May be difficult as we likely do not want to modify the randBattleSeed
|
||||||
const turnCommand = globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()];
|
const turnCommand = globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()];
|
||||||
const isCommandFight = turnCommand?.command === Command.FIGHT;
|
|
||||||
const move = turnCommand?.move?.move ? allMoves[turnCommand.move.move] : null;
|
const move = turnCommand?.move?.move ? allMoves[turnCommand.move.move] : null;
|
||||||
const isDamageMove = move?.category === MoveCategory.PHYSICAL || move?.category === MoveCategory.SPECIAL;
|
const isDamageMove = move?.category === MoveCategory.PHYSICAL || move?.category === MoveCategory.SPECIAL;
|
||||||
return (
|
return (
|
||||||
!simulated && !bypass.value && pokemon.randBattleSeedInt(100) < this.chance && isCommandFight && isDamageMove
|
!simulated &&
|
||||||
|
pokemon.randBattleSeedInt(100) < this.chance &&
|
||||||
|
isDamageMove &&
|
||||||
|
pokemon.canAddTag(BattlerTagType.BYPASS_SPEED)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* bypass move order in their priority bracket when pokemon choose damaging move
|
* bypass move order in their priority bracket when pokemon choose damaging move
|
||||||
*/
|
*/
|
||||||
override apply({ bypass }: BypassSpeedChanceAbAttrParams): void {
|
override apply({ pokemon }: AbAttrBaseParams): void {
|
||||||
bypass.value = true;
|
pokemon.addTag(BattlerTagType.BYPASS_SPEED);
|
||||||
}
|
}
|
||||||
|
|
||||||
override getTriggerMessage({ pokemon }: BypassSpeedChanceAbAttrParams, _abilityName: string): string {
|
override getTriggerMessage({ pokemon }: AbAttrBaseParams, _abilityName: string): string {
|
||||||
return i18next.t("abilityTriggers:quickDraw", { pokemonName: getPokemonNameWithAffix(pokemon) });
|
return i18next.t("abilityTriggers:quickDraw", { pokemonName: getPokemonNameWithAffix(pokemon) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -6027,9 +6055,8 @@ export class PreventBypassSpeedChanceAbAttr extends AbAttr {
|
|||||||
return isCommandFight && this.condition(pokemon, move!);
|
return isCommandFight && this.condition(pokemon, move!);
|
||||||
}
|
}
|
||||||
|
|
||||||
override apply({ bypass, canCheckHeldItems }: PreventBypassSpeedChanceAbAttrParams): void {
|
override apply({ bypass }: PreventBypassSpeedChanceAbAttrParams): void {
|
||||||
bypass.value = false;
|
bypass.value = false;
|
||||||
canCheckHeldItems.value = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -6150,7 +6177,7 @@ class ForceSwitchOutHelper {
|
|||||||
: 0;
|
: 0;
|
||||||
globalScene.phaseManager.prependNewToPhase(
|
globalScene.phaseManager.prependNewToPhase(
|
||||||
"MoveEndPhase",
|
"MoveEndPhase",
|
||||||
"SwitchSummonPhase",
|
"StaticSwitchSummonPhase",
|
||||||
this.switchType,
|
this.switchType,
|
||||||
switchOutTarget.getFieldIndex(),
|
switchOutTarget.getFieldIndex(),
|
||||||
summonIndex,
|
summonIndex,
|
||||||
@ -6518,6 +6545,7 @@ const AbilityAttrs = Object.freeze({
|
|||||||
BlockStatusDamageAbAttr,
|
BlockStatusDamageAbAttr,
|
||||||
BlockOneHitKOAbAttr,
|
BlockOneHitKOAbAttr,
|
||||||
ChangeMovePriorityAbAttr,
|
ChangeMovePriorityAbAttr,
|
||||||
|
ChangeMovePriorityModifierAbAttr,
|
||||||
IgnoreContactAbAttr,
|
IgnoreContactAbAttr,
|
||||||
PreWeatherEffectAbAttr,
|
PreWeatherEffectAbAttr,
|
||||||
PreWeatherDamageAbAttr,
|
PreWeatherDamageAbAttr,
|
||||||
@ -6935,7 +6963,7 @@ export function initAbilities() {
|
|||||||
.attr(AlwaysHitAbAttr)
|
.attr(AlwaysHitAbAttr)
|
||||||
.attr(DoubleBattleChanceAbAttr),
|
.attr(DoubleBattleChanceAbAttr),
|
||||||
new Ability(AbilityId.STALL, 4)
|
new Ability(AbilityId.STALL, 4)
|
||||||
.attr(ChangeMovePriorityAbAttr, (_pokemon, _move: Move) => true, -0.2),
|
.attr(ChangeMovePriorityModifierAbAttr, (_pokemon, _move: Move) => true, MovePriorityModifier.LAST_IN_BRACKET),
|
||||||
new Ability(AbilityId.TECHNICIAN, 4)
|
new Ability(AbilityId.TECHNICIAN, 4)
|
||||||
.attr(MovePowerBoostAbAttr, (user, target, move) => {
|
.attr(MovePowerBoostAbAttr, (user, target, move) => {
|
||||||
const power = new NumberHolder(move.power);
|
const power = new NumberHolder(move.power);
|
||||||
@ -7085,7 +7113,7 @@ export function initAbilities() {
|
|||||||
new Ability(AbilityId.ANALYTIC, 5)
|
new Ability(AbilityId.ANALYTIC, 5)
|
||||||
.attr(MovePowerBoostAbAttr, (user) =>
|
.attr(MovePowerBoostAbAttr, (user) =>
|
||||||
// Boost power if all other Pokemon have already moved (no other moves are slated to execute)
|
// Boost power if all other Pokemon have already moved (no other moves are slated to execute)
|
||||||
!globalScene.phaseManager.findPhase((phase) => phase.is("MovePhase") && phase.pokemon.id !== user?.id),
|
!globalScene.phaseManager.hasPhaseOfType("MovePhase", (phase: MovePhase) => phase.pokemon.id !== user?.id),
|
||||||
1.3),
|
1.3),
|
||||||
new Ability(AbilityId.ILLUSION, 5)
|
new Ability(AbilityId.ILLUSION, 5)
|
||||||
// The Pokemon generate an illusion if it's available
|
// The Pokemon generate an illusion if it's available
|
||||||
@ -7673,7 +7701,7 @@ export function initAbilities() {
|
|||||||
.attr(TypeImmunityHealAbAttr, PokemonType.GROUND)
|
.attr(TypeImmunityHealAbAttr, PokemonType.GROUND)
|
||||||
.ignorable(),
|
.ignorable(),
|
||||||
new Ability(AbilityId.MYCELIUM_MIGHT, 9)
|
new Ability(AbilityId.MYCELIUM_MIGHT, 9)
|
||||||
.attr(ChangeMovePriorityAbAttr, (_pokemon, move) => move.category === MoveCategory.STATUS, -0.2)
|
.attr(ChangeMovePriorityModifierAbAttr, (_pokemon, move) => move.category === MoveCategory.STATUS, MovePriorityModifier.LAST_IN_BRACKET)
|
||||||
.attr(PreventBypassSpeedChanceAbAttr, (_pokemon, move) => move.category === MoveCategory.STATUS)
|
.attr(PreventBypassSpeedChanceAbAttr, (_pokemon, move) => move.category === MoveCategory.STATUS)
|
||||||
.attr(MoveAbilityBypassAbAttr, (_pokemon, move: Move) => move.category === MoveCategory.STATUS),
|
.attr(MoveAbilityBypassAbAttr, (_pokemon, move: Move) => move.category === MoveCategory.STATUS),
|
||||||
new Ability(AbilityId.MINDS_EYE, 9)
|
new Ability(AbilityId.MINDS_EYE, 9)
|
||||||
|
@ -507,17 +507,7 @@ export class ShellTrapTag extends BattlerTag {
|
|||||||
|
|
||||||
// Trap should only be triggered by opponent's Physical moves
|
// Trap should only be triggered by opponent's Physical moves
|
||||||
if (phaseData?.move.category === MoveCategory.PHYSICAL && pokemon.isOpponent(phaseData.attacker)) {
|
if (phaseData?.move.category === MoveCategory.PHYSICAL && pokemon.isOpponent(phaseData.attacker)) {
|
||||||
const shellTrapPhaseIndex = globalScene.phaseManager.phaseQueue.findIndex(
|
globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === pokemon);
|
||||||
phase => phase.is("MovePhase") && phase.pokemon === pokemon,
|
|
||||||
);
|
|
||||||
const firstMovePhaseIndex = globalScene.phaseManager.phaseQueue.findIndex(phase => phase.is("MovePhase"));
|
|
||||||
|
|
||||||
// Only shift MovePhase timing if it's not already next up
|
|
||||||
if (shellTrapPhaseIndex !== -1 && shellTrapPhaseIndex !== firstMovePhaseIndex) {
|
|
||||||
const shellTrapMovePhase = globalScene.phaseManager.phaseQueue.splice(shellTrapPhaseIndex, 1)[0];
|
|
||||||
globalScene.phaseManager.prependToPhase(shellTrapMovePhase, "MovePhase");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.activated = true;
|
this.activated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1165,22 +1155,9 @@ export class EncoreTag extends MoveRestrictionBattlerTag {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const movePhase = globalScene.phaseManager.findPhase(m => m.is("MovePhase") && m.pokemon === pokemon);
|
const movesetMove = pokemon.getMoveset().find(m => m.moveId === this.moveId);
|
||||||
if (movePhase) {
|
if (movesetMove) {
|
||||||
const movesetMove = pokemon.getMoveset().find(m => m.moveId === this.moveId);
|
globalScene.phaseManager.changePhaseMove((phase: MovePhase) => phase.pokemon === pokemon, movesetMove);
|
||||||
if (movesetMove) {
|
|
||||||
const lastMove = pokemon.getLastXMoves(1)[0];
|
|
||||||
globalScene.phaseManager.tryReplacePhase(
|
|
||||||
m => m.is("MovePhase") && m.pokemon === pokemon,
|
|
||||||
globalScene.phaseManager.create(
|
|
||||||
"MovePhase",
|
|
||||||
pokemon,
|
|
||||||
lastMove.targets ?? [],
|
|
||||||
movesetMove,
|
|
||||||
MoveUseMode.NORMAL,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3481,6 +3458,23 @@ export class GrudgeTag extends BattlerTag {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag to allow the affected Pokemon's move to go first in its priority bracket.
|
||||||
|
* Used for {@link https://bulbapedia.bulbagarden.net/wiki/Quick_Draw_(Ability) Quick Draw}
|
||||||
|
* and {@link https://bulbapedia.bulbagarden.net/wiki/Quick_Claw Quick Claw}.
|
||||||
|
*/
|
||||||
|
export class BypassSpeedTag extends BattlerTag {
|
||||||
|
constructor() {
|
||||||
|
super(BattlerTagType.BYPASS_SPEED, BattlerTagLapseType.TURN_END, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
override canAdd(pokemon: Pokemon): boolean {
|
||||||
|
const cancelled = new BooleanHolder(false);
|
||||||
|
applyAbAttrs("PreventBypassSpeedChanceAbAttr", pokemon, null, false, cancelled);
|
||||||
|
return !cancelled.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tag used to heal the user of Psycho Shift of its status effect if Psycho Shift succeeds in transferring its status effect to the target Pokemon
|
* Tag used to heal the user of Psycho Shift of its status effect if Psycho Shift succeeds in transferring its status effect to the target Pokemon
|
||||||
*/
|
*/
|
||||||
@ -3728,6 +3722,8 @@ export function getBattlerTag(
|
|||||||
return new PsychoShiftTag();
|
return new PsychoShiftTag();
|
||||||
case BattlerTagType.MAGIC_COAT:
|
case BattlerTagType.MAGIC_COAT:
|
||||||
return new MagicCoatTag();
|
return new MagicCoatTag();
|
||||||
|
case BattlerTagType.BYPASS_SPEED:
|
||||||
|
return new BypassSpeedTag();
|
||||||
case BattlerTagType.NONE:
|
case BattlerTagType.NONE:
|
||||||
default:
|
default:
|
||||||
return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId);
|
return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId);
|
||||||
|
@ -90,6 +90,8 @@ import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveKindString, MoveCla
|
|||||||
import { applyMoveAttrs } from "./apply-attrs";
|
import { applyMoveAttrs } from "./apply-attrs";
|
||||||
import { frenzyMissFunc, getMoveTargets } from "./move-utils";
|
import { frenzyMissFunc, getMoveTargets } from "./move-utils";
|
||||||
import { AbAttrBaseParams, AbAttrParamsWithCancel, PreAttackModifyPowerAbAttrParams } from "../abilities/ability";
|
import { AbAttrBaseParams, AbAttrParamsWithCancel, PreAttackModifyPowerAbAttrParams } from "../abilities/ability";
|
||||||
|
import { MovePriorityModifier } from "#enums/move-priority-modifier";
|
||||||
|
import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A function used to conditionally determine execution of a given {@linkcode MoveAttr}.
|
* A function used to conditionally determine execution of a given {@linkcode MoveAttr}.
|
||||||
@ -870,6 +872,13 @@ export default abstract class Move implements Localizable {
|
|||||||
return priority.value;
|
return priority.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPriorityModifier(user: Pokemon, simulated = true): MovePriorityModifier {
|
||||||
|
const modifierHolder = new NumberHolder(MovePriorityModifier.NORMAL);
|
||||||
|
applyAbAttrs("ChangeMovePriorityModifierAbAttr", {pokemon: user, simulated: simulated, move: this, priority: modifierHolder});
|
||||||
|
modifierHolder.value = user.getTag(BattlerTagType.BYPASS_SPEED) ? MovePriorityModifier.FIRST_IN_BRACKET : modifierHolder.value;
|
||||||
|
return modifierHolder.value;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate the [Expected Power](https://en.wikipedia.org/wiki/Expected_value) per turn
|
* Calculate the [Expected Power](https://en.wikipedia.org/wiki/Expected_value) per turn
|
||||||
* of this move, taking into account multi hit moves, accuracy, and the number of turns it
|
* of this move, taking into account multi hit moves, accuracy, and the number of turns it
|
||||||
@ -3182,7 +3191,7 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr {
|
|||||||
|
|
||||||
const overridden = args[0] as BooleanHolder;
|
const overridden = args[0] as BooleanHolder;
|
||||||
|
|
||||||
const allyMovePhase = globalScene.phaseManager.findPhase<MovePhase>((phase) => phase.is("MovePhase") && phase.pokemon.isPlayer() === user.isPlayer());
|
const allyMovePhase = globalScene.phaseManager.findPhase("MovePhase", (phase) => phase.pokemon.isPlayer() === user.isPlayer());
|
||||||
if (allyMovePhase) {
|
if (allyMovePhase) {
|
||||||
const allyMove = allyMovePhase.move.getMove();
|
const allyMove = allyMovePhase.move.getMove();
|
||||||
if (allyMove !== move && allyMove.hasAttr("AwaitCombinedPledgeAttr")) {
|
if (allyMove !== move && allyMove.hasAttr("AwaitCombinedPledgeAttr")) {
|
||||||
@ -3195,11 +3204,7 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
// Move the ally's MovePhase (if needed) so that the ally moves next
|
// Move the ally's MovePhase (if needed) so that the ally moves next
|
||||||
const allyMovePhaseIndex = globalScene.phaseManager.phaseQueue.indexOf(allyMovePhase);
|
globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === user.getAlly());
|
||||||
const firstMovePhaseIndex = globalScene.phaseManager.phaseQueue.findIndex((phase) => phase.is("MovePhase"));
|
|
||||||
if (allyMovePhaseIndex !== firstMovePhaseIndex) {
|
|
||||||
globalScene.phaseManager.prependToPhase(globalScene.phaseManager.phaseQueue.splice(allyMovePhaseIndex, 1)[0], "MovePhase");
|
|
||||||
}
|
|
||||||
|
|
||||||
overridden.value = true;
|
overridden.value = true;
|
||||||
return true;
|
return true;
|
||||||
@ -4455,28 +4460,7 @@ export class LastMoveDoublePowerAttr extends VariablePowerAttr {
|
|||||||
*/
|
*/
|
||||||
apply(user: Pokemon, _target: Pokemon, _move: Move, args: any[]): boolean {
|
apply(user: Pokemon, _target: Pokemon, _move: Move, args: any[]): boolean {
|
||||||
const power = args[0] as NumberHolder;
|
const power = args[0] as NumberHolder;
|
||||||
const enemy = user.getOpponent(0);
|
for (const p of globalScene.phaseManager.dynamicQueueManager.getLastTurnOrder().slice(0, -1).reverse()) {
|
||||||
const pokemonActed: Pokemon[] = [];
|
|
||||||
|
|
||||||
if (enemy?.turnData.acted) {
|
|
||||||
pokemonActed.push(enemy);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (globalScene.currentBattle.double) {
|
|
||||||
const userAlly = user.getAlly();
|
|
||||||
const enemyAlly = enemy?.getAlly();
|
|
||||||
|
|
||||||
if (userAlly?.turnData.acted) {
|
|
||||||
pokemonActed.push(userAlly);
|
|
||||||
}
|
|
||||||
if (enemyAlly?.turnData.acted) {
|
|
||||||
pokemonActed.push(enemyAlly);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pokemonActed.sort((a, b) => b.turnData.order - a.turnData.order);
|
|
||||||
|
|
||||||
for (const p of pokemonActed) {
|
|
||||||
const [ lastMove ] = p.getLastXMoves(1);
|
const [ lastMove ] = p.getLastXMoves(1);
|
||||||
if (lastMove.result !== MoveResult.FAIL) {
|
if (lastMove.result !== MoveResult.FAIL) {
|
||||||
if ((lastMove.result === MoveResult.SUCCESS) && (lastMove.move === this.move)) {
|
if ((lastMove.result === MoveResult.SUCCESS) && (lastMove.move === this.move)) {
|
||||||
@ -4561,20 +4545,14 @@ export class CueNextRoundAttr extends MoveEffectAttr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean {
|
override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean {
|
||||||
const nextRoundPhase = globalScene.phaseManager.findPhase<MovePhase>(phase =>
|
const nextRoundPhase = globalScene.phaseManager.findPhase("MovePhase", phase => phase.move.moveId === MoveId.ROUND
|
||||||
phase.is("MovePhase") && phase.move.moveId === MoveId.ROUND
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!nextRoundPhase) {
|
if (!nextRoundPhase) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the phase queue so that the next Pokemon using Round moves next
|
globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.is("MovePhase") && phase.move.moveId === MoveId.ROUND);
|
||||||
const nextRoundIndex = globalScene.phaseManager.phaseQueue.indexOf(nextRoundPhase);
|
|
||||||
const nextMoveIndex = globalScene.phaseManager.phaseQueue.findIndex(phase => phase.is("MovePhase"));
|
|
||||||
if (nextRoundIndex !== nextMoveIndex) {
|
|
||||||
globalScene.phaseManager.prependToPhase(globalScene.phaseManager.phaseQueue.splice(nextRoundIndex, 1)[0], "MovePhase");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark the corresponding Pokemon as having "joined the Round" (for doubling power later)
|
// Mark the corresponding Pokemon as having "joined the Round" (for doubling power later)
|
||||||
nextRoundPhase.pokemon.turnData.joinedRound = true;
|
nextRoundPhase.pokemon.turnData.joinedRound = true;
|
||||||
@ -6253,15 +6231,15 @@ export class RevivalBlessingAttr extends MoveEffectAttr {
|
|||||||
// Handle cases where revived pokemon needs to get switched in on same turn
|
// Handle cases where revived pokemon needs to get switched in on same turn
|
||||||
if (allyPokemon.isFainted() || allyPokemon === pokemon) {
|
if (allyPokemon.isFainted() || allyPokemon === pokemon) {
|
||||||
// Enemy switch phase should be removed and replaced with the revived pkmn switching in
|
// Enemy switch phase should be removed and replaced with the revived pkmn switching in
|
||||||
globalScene.phaseManager.tryRemovePhase((phase: SwitchSummonPhase) => phase.is("SwitchSummonPhase") && phase.getPokemon() === pokemon);
|
globalScene.phaseManager.tryRemovePhase((phase: SwitchSummonPhase) => phase.is("StaticSwitchSummonPhase") && phase.getPokemon() === pokemon);
|
||||||
// If the pokemon being revived was alive earlier in the turn, cancel its move
|
// If the pokemon being revived was alive earlier in the turn, cancel its move
|
||||||
// (revived pokemon can't move in the turn they're brought back)
|
// (revived pokemon can't move in the turn they're brought back)
|
||||||
// TODO: might make sense to move this to `FaintPhase` after checking for Rev Seed (rather than handling it in the move)
|
// TODO: might make sense to move this to `FaintPhase` after checking for Rev Seed (rather than handling it in the move)
|
||||||
globalScene.phaseManager.findPhase((phase: MovePhase) => phase.pokemon === pokemon)?.cancel();
|
globalScene.phaseManager.findPhase("MovePhase", (phase: MovePhase) => phase.pokemon === pokemon)?.cancel();
|
||||||
if (user.fieldPosition === FieldPosition.CENTER) {
|
if (user.fieldPosition === FieldPosition.CENTER) {
|
||||||
user.setFieldPosition(FieldPosition.LEFT);
|
user.setFieldPosition(FieldPosition.LEFT);
|
||||||
}
|
}
|
||||||
globalScene.phaseManager.unshiftNew("SwitchSummonPhase", SwitchType.SWITCH, allyPokemon.getFieldIndex(), slotIndex, false, false);
|
globalScene.phaseManager.unshiftNew("StaticSwitchSummonPhase", SwitchType.SWITCH, allyPokemon.getFieldIndex(), slotIndex, false, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@ -6341,7 +6319,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
|||||||
const slotIndex = eligibleNewIndices[user.randBattleSeedInt(eligibleNewIndices.length)];
|
const slotIndex = eligibleNewIndices[user.randBattleSeedInt(eligibleNewIndices.length)];
|
||||||
globalScene.phaseManager.prependNewToPhase(
|
globalScene.phaseManager.prependNewToPhase(
|
||||||
"MoveEndPhase",
|
"MoveEndPhase",
|
||||||
"SwitchSummonPhase",
|
"StaticSwitchSummonPhase",
|
||||||
this.switchType,
|
this.switchType,
|
||||||
switchOutTarget.getFieldIndex(),
|
switchOutTarget.getFieldIndex(),
|
||||||
slotIndex,
|
slotIndex,
|
||||||
@ -6380,7 +6358,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
|||||||
switchOutTarget.leaveField(true);
|
switchOutTarget.leaveField(true);
|
||||||
const slotIndex = eligibleNewIndices[user.randBattleSeedInt(eligibleNewIndices.length)];
|
const slotIndex = eligibleNewIndices[user.randBattleSeedInt(eligibleNewIndices.length)];
|
||||||
globalScene.phaseManager.prependNewToPhase("MoveEndPhase",
|
globalScene.phaseManager.prependNewToPhase("MoveEndPhase",
|
||||||
"SwitchSummonPhase",
|
"StaticSwitchSummonPhase",
|
||||||
this.switchType,
|
this.switchType,
|
||||||
switchOutTarget.getFieldIndex(),
|
switchOutTarget.getFieldIndex(),
|
||||||
slotIndex,
|
slotIndex,
|
||||||
@ -6390,7 +6368,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
|||||||
} else {
|
} else {
|
||||||
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
|
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
|
||||||
globalScene.phaseManager.prependNewToPhase("MoveEndPhase",
|
globalScene.phaseManager.prependNewToPhase("MoveEndPhase",
|
||||||
"SwitchSummonPhase",
|
"StaticSwitchSummonPhase",
|
||||||
this.switchType,
|
this.switchType,
|
||||||
switchOutTarget.getFieldIndex(),
|
switchOutTarget.getFieldIndex(),
|
||||||
(globalScene.currentBattle.trainer ? globalScene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0),
|
(globalScene.currentBattle.trainer ? globalScene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0),
|
||||||
@ -6817,7 +6795,7 @@ class CallMoveAttr extends OverrideMoveEffectAttr {
|
|||||||
: moveTargets.targets[user.randBattleSeedInt(moveTargets.targets.length)]];
|
: moveTargets.targets[user.randBattleSeedInt(moveTargets.targets.length)]];
|
||||||
|
|
||||||
globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", move.id);
|
globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", move.id);
|
||||||
globalScene.phaseManager.unshiftNew("MovePhase", user, targets, new PokemonMove(move.id), MoveUseMode.FOLLOW_UP);
|
globalScene.phaseManager.pushNew("MovePhase", user, targets, new PokemonMove(move.id), MoveUseMode.FOLLOW_UP, MovePhaseTimingModifier.FIRST);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -7047,7 +7025,7 @@ export class NaturePowerAttr extends OverrideMoveEffectAttr {
|
|||||||
|
|
||||||
// Load the move's animation if we didn't already and unshift a new usage phase
|
// Load the move's animation if we didn't already and unshift a new usage phase
|
||||||
globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", moveId);
|
globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", moveId);
|
||||||
globalScene.phaseManager.unshiftNew("MovePhase", user, [ target.getBattlerIndex() ], new PokemonMove(moveId), MoveUseMode.FOLLOW_UP);
|
globalScene.phaseManager.pushNew("MovePhase", user, [ target.getBattlerIndex() ], new PokemonMove(moveId), MoveUseMode.FOLLOW_UP, MovePhaseTimingModifier.FIRST);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -7132,7 +7110,7 @@ export class RepeatMoveAttr extends MoveEffectAttr {
|
|||||||
targetPokemonName: getPokemonNameWithAffix(target)
|
targetPokemonName: getPokemonNameWithAffix(target)
|
||||||
}));
|
}));
|
||||||
target.turnData.extraTurns++;
|
target.turnData.extraTurns++;
|
||||||
globalScene.phaseManager.appendNewToPhase("MoveEndPhase", "MovePhase", target, moveTargets, movesetMove, MoveUseMode.NORMAL);
|
globalScene.phaseManager.pushNew("MovePhase", target, moveTargets, movesetMove, MoveUseMode.NORMAL, MovePhaseTimingModifier.FIRST);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -7920,12 +7898,7 @@ export class AfterYouAttr extends MoveEffectAttr {
|
|||||||
*/
|
*/
|
||||||
override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
|
override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
|
||||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:afterYou", { targetName: getPokemonNameWithAffix(target) }));
|
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:afterYou", { targetName: getPokemonNameWithAffix(target) }));
|
||||||
|
globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === target);
|
||||||
// Will find next acting phase of the targeted pokémon, delete it and queue it right after us.
|
|
||||||
const targetNextPhase = globalScene.phaseManager.findPhase<MovePhase>(phase => phase.pokemon === target);
|
|
||||||
if (targetNextPhase && globalScene.phaseManager.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) {
|
|
||||||
globalScene.phaseManager.prependToPhase(targetNextPhase, "MovePhase");
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -7949,45 +7922,11 @@ export class ForceLastAttr extends MoveEffectAttr {
|
|||||||
override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
|
override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
|
||||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:forceLast", { targetPokemonName: getPokemonNameWithAffix(target) }));
|
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:forceLast", { targetPokemonName: getPokemonNameWithAffix(target) }));
|
||||||
|
|
||||||
// TODO: Refactor this to be more readable and less janky
|
globalScene.phaseManager.forceMoveLast((phase: MovePhase) => phase.pokemon === target);
|
||||||
const targetMovePhase = globalScene.phaseManager.findPhase<MovePhase>((phase) => phase.pokemon === target);
|
|
||||||
if (targetMovePhase && !targetMovePhase.isForcedLast() && globalScene.phaseManager.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) {
|
|
||||||
// Finding the phase to insert the move in front of -
|
|
||||||
// Either the end of the turn or in front of another, slower move which has also been forced last
|
|
||||||
const prependPhase = globalScene.phaseManager.findPhase((phase) =>
|
|
||||||
[ MovePhase, MoveEndPhase ].every(cls => !(phase instanceof cls))
|
|
||||||
|| (phase.is("MovePhase")) && phaseForcedSlower(phase, target, !!globalScene.arena.getTag(ArenaTagType.TRICK_ROOM))
|
|
||||||
);
|
|
||||||
if (prependPhase) {
|
|
||||||
globalScene.phaseManager.phaseQueue.splice(
|
|
||||||
globalScene.phaseManager.phaseQueue.indexOf(prependPhase),
|
|
||||||
0,
|
|
||||||
globalScene.phaseManager.create("MovePhase", target, [ ...targetMovePhase.targets ], targetMovePhase.move, targetMovePhase.useMode, true)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether a {@linkcode MovePhase} has been forced last and the corresponding pokemon is slower than {@linkcode target}.
|
|
||||||
|
|
||||||
* TODO:
|
|
||||||
- Make this a class method
|
|
||||||
- Make this look at speed order from TurnStartPhase
|
|
||||||
*/
|
|
||||||
const phaseForcedSlower = (phase: MovePhase, target: Pokemon, trickRoom: boolean): boolean => {
|
|
||||||
let slower: boolean;
|
|
||||||
// quashed pokemon still have speed ties
|
|
||||||
if (phase.pokemon.getEffectiveStat(Stat.SPD) === target.getEffectiveStat(Stat.SPD)) {
|
|
||||||
slower = !!target.randBattleSeedInt(2);
|
|
||||||
} else {
|
|
||||||
slower = !trickRoom ? phase.pokemon.getEffectiveStat(Stat.SPD) < target.getEffectiveStat(Stat.SPD) : phase.pokemon.getEffectiveStat(Stat.SPD) > target.getEffectiveStat(Stat.SPD);
|
|
||||||
}
|
|
||||||
return phase.isForcedLast() && slower;
|
|
||||||
};
|
|
||||||
|
|
||||||
const failOnGravityCondition: MoveConditionFunc = (user, target, move) => !globalScene.arena.getTag(ArenaTagType.GRAVITY);
|
const failOnGravityCondition: MoveConditionFunc = (user, target, move) => !globalScene.arena.getTag(ArenaTagType.GRAVITY);
|
||||||
|
|
||||||
const failOnBossCondition: MoveConditionFunc = (user, target, move) => !target.isBossImmune();
|
const failOnBossCondition: MoveConditionFunc = (user, target, move) => !target.isBossImmune();
|
||||||
@ -8008,7 +7947,7 @@ const userSleptOrComatoseCondition: MoveConditionFunc = (user: Pokemon, target:
|
|||||||
|
|
||||||
const targetSleptOrComatoseCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => target.status?.effect === StatusEffect.SLEEP || target.hasAbility(AbilityId.COMATOSE);
|
const targetSleptOrComatoseCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => target.status?.effect === StatusEffect.SLEEP || target.hasAbility(AbilityId.COMATOSE);
|
||||||
|
|
||||||
const failIfLastCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => globalScene.phaseManager.phaseQueue.find(phase => phase.is("MovePhase")) !== undefined;
|
const failIfLastCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => globalScene.phaseManager.hasPhaseOfType("MovePhase");
|
||||||
|
|
||||||
const failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => {
|
const failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => {
|
||||||
const party: Pokemon[] = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty();
|
const party: Pokemon[] = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty();
|
||||||
|
@ -1,97 +0,0 @@
|
|||||||
import { globalScene } from "#app/global-scene";
|
|
||||||
import type { Phase } from "#app/phase";
|
|
||||||
import { ActivatePriorityQueuePhase } from "#app/phases/activate-priority-queue-phase";
|
|
||||||
import type { PostSummonPhase } from "#app/phases/post-summon-phase";
|
|
||||||
import { PostSummonActivateAbilityPhase } from "#app/phases/post-summon-activate-ability-phase";
|
|
||||||
import { Stat } from "#enums/stat";
|
|
||||||
import { BooleanHolder } from "#app/utils/common";
|
|
||||||
import { TrickRoomTag } from "#app/data/arena-tag";
|
|
||||||
import { DynamicPhaseType } from "#enums/dynamic-phase-type";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stores a list of {@linkcode Phase}s
|
|
||||||
*
|
|
||||||
* Dynamically updates ordering to always pop the highest "priority", based on implementation of {@linkcode reorder}
|
|
||||||
*/
|
|
||||||
export abstract class PhasePriorityQueue {
|
|
||||||
protected abstract queue: Phase[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sorts the elements in the queue
|
|
||||||
*/
|
|
||||||
public abstract reorder(): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calls {@linkcode reorder} and shifts the queue
|
|
||||||
* @returns The front element of the queue after sorting
|
|
||||||
*/
|
|
||||||
public pop(): Phase | undefined {
|
|
||||||
this.reorder();
|
|
||||||
return this.queue.shift();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a phase to the queue
|
|
||||||
* @param phase The phase to add
|
|
||||||
*/
|
|
||||||
public push(phase: Phase): void {
|
|
||||||
this.queue.push(phase);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes all phases from the queue
|
|
||||||
*/
|
|
||||||
public clear(): void {
|
|
||||||
this.queue.splice(0, this.queue.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Priority Queue for {@linkcode PostSummonPhase} and {@linkcode PostSummonActivateAbilityPhase}
|
|
||||||
*
|
|
||||||
* Orders phases first by ability priority, then by the {@linkcode Pokemon}'s effective speed
|
|
||||||
*/
|
|
||||||
export class PostSummonPhasePriorityQueue extends PhasePriorityQueue {
|
|
||||||
protected override queue: PostSummonPhase[] = [];
|
|
||||||
|
|
||||||
public override reorder(): void {
|
|
||||||
this.queue.sort((phaseA: PostSummonPhase, phaseB: PostSummonPhase) => {
|
|
||||||
if (phaseA.getPriority() === phaseB.getPriority()) {
|
|
||||||
return (
|
|
||||||
(phaseB.getPokemon().getEffectiveStat(Stat.SPD) - phaseA.getPokemon().getEffectiveStat(Stat.SPD)) *
|
|
||||||
(isTrickRoom() ? -1 : 1)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return phaseB.getPriority() - phaseA.getPriority();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public override push(phase: PostSummonPhase): void {
|
|
||||||
super.push(phase);
|
|
||||||
this.queueAbilityPhase(phase);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Queues all necessary {@linkcode PostSummonActivateAbilityPhase}s for each pushed {@linkcode PostSummonPhase}
|
|
||||||
* @param phase The {@linkcode PostSummonPhase} that was pushed onto the queue
|
|
||||||
*/
|
|
||||||
private queueAbilityPhase(phase: PostSummonPhase): void {
|
|
||||||
const phasePokemon = phase.getPokemon();
|
|
||||||
|
|
||||||
phasePokemon.getAbilityPriorities().forEach((priority, idx) => {
|
|
||||||
this.queue.push(new PostSummonActivateAbilityPhase(phasePokemon.getBattlerIndex(), priority, !!idx));
|
|
||||||
globalScene.phaseManager.appendToPhase(
|
|
||||||
new ActivatePriorityQueuePhase(DynamicPhaseType.POST_SUMMON),
|
|
||||||
"ActivatePriorityQueuePhase",
|
|
||||||
(p: ActivatePriorityQueuePhase) => p.getType() === DynamicPhaseType.POST_SUMMON,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isTrickRoom(): boolean {
|
|
||||||
const speedReversed = new BooleanHolder(false);
|
|
||||||
globalScene.arena.applyTags(TrickRoomTag, false, speedReversed);
|
|
||||||
return speedReversed.value;
|
|
||||||
}
|
|
@ -95,4 +95,5 @@ export enum BattlerTagType {
|
|||||||
ENDURE_TOKEN = "ENDURE_TOKEN",
|
ENDURE_TOKEN = "ENDURE_TOKEN",
|
||||||
POWDER = "POWDER",
|
POWDER = "POWDER",
|
||||||
MAGIC_COAT = "MAGIC_COAT",
|
MAGIC_COAT = "MAGIC_COAT",
|
||||||
|
BYPASS_SPEED = "BYPASS_SPEED"
|
||||||
}
|
}
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
/**
|
|
||||||
* Enum representation of the phase types held by implementations of {@linkcode PhasePriorityQueue}
|
|
||||||
*/
|
|
||||||
export enum DynamicPhaseType {
|
|
||||||
POST_SUMMON
|
|
||||||
}
|
|
5
src/enums/move-phase-timing-modifier.ts
Normal file
5
src/enums/move-phase-timing-modifier.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export enum MovePhaseTimingModifier {
|
||||||
|
LAST = 0,
|
||||||
|
NORMAL,
|
||||||
|
FIRST
|
||||||
|
}
|
5
src/enums/move-priority-modifier.ts
Normal file
5
src/enums/move-priority-modifier.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export enum MovePriorityModifier {
|
||||||
|
LAST_IN_BRACKET = 0,
|
||||||
|
NORMAL,
|
||||||
|
FIRST_IN_BRACKET,
|
||||||
|
}
|
@ -374,9 +374,15 @@ export class Arena {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Function to trigger all weather based form changes
|
* Function to trigger all weather based form changes
|
||||||
|
* @param source - The Pokemon causing the changes by removing itself from the field
|
||||||
*/
|
*/
|
||||||
triggerWeatherBasedFormChanges(): void {
|
triggerWeatherBasedFormChanges(source?: Pokemon): void {
|
||||||
globalScene.getField(true).forEach(p => {
|
globalScene.getField(true).forEach(p => {
|
||||||
|
// TODO - This is a bandaid. Abilities leaving the field needs a better approach than
|
||||||
|
// calling this method for every switch out that happens
|
||||||
|
if (p === source) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const isCastformWithForecast = p.hasAbility(AbilityId.FORECAST) && p.species.speciesId === SpeciesId.CASTFORM;
|
const isCastformWithForecast = p.hasAbility(AbilityId.FORECAST) && p.species.speciesId === SpeciesId.CASTFORM;
|
||||||
const isCherrimWithFlowerGift = p.hasAbility(AbilityId.FLOWER_GIFT) && p.species.speciesId === SpeciesId.CHERRIM;
|
const isCherrimWithFlowerGift = p.hasAbility(AbilityId.FLOWER_GIFT) && p.species.speciesId === SpeciesId.CHERRIM;
|
||||||
|
|
||||||
|
@ -182,6 +182,7 @@ import type { IllusionData } from "#app/@types/illusion-data";
|
|||||||
import type { TurnMove } from "#app/@types/turn-move";
|
import type { TurnMove } from "#app/@types/turn-move";
|
||||||
import type { DamageCalculationResult, DamageResult } from "#app/@types/damage-result";
|
import type { DamageCalculationResult, DamageResult } from "#app/@types/damage-result";
|
||||||
import type { AbAttrMap, AbAttrString, TypeMultiplierAbAttrParams } from "#app/@types/ability-types";
|
import type { AbAttrMap, AbAttrString, TypeMultiplierAbAttrParams } from "#app/@types/ability-types";
|
||||||
|
import type { TurnCommand } from "#app/battle";
|
||||||
import { getTerrainBlockMessage } from "#app/data/terrain";
|
import { getTerrainBlockMessage } from "#app/data/terrain";
|
||||||
import { LearnMoveSituation } from "#enums/learn-move-situation";
|
import { LearnMoveSituation } from "#enums/learn-move-situation";
|
||||||
|
|
||||||
@ -5731,7 +5732,7 @@ export class PlayerPokemon extends Pokemon {
|
|||||||
if (slotIndex >= globalScene.currentBattle.getBattlerCount() && slotIndex < 6) {
|
if (slotIndex >= globalScene.currentBattle.getBattlerCount() && slotIndex < 6) {
|
||||||
globalScene.phaseManager.prependNewToPhase(
|
globalScene.phaseManager.prependNewToPhase(
|
||||||
"MoveEndPhase",
|
"MoveEndPhase",
|
||||||
"SwitchSummonPhase",
|
"StaticSwitchSummonPhase",
|
||||||
switchType,
|
switchType,
|
||||||
this.getFieldIndex(),
|
this.getFieldIndex(),
|
||||||
slotIndex,
|
slotIndex,
|
||||||
|
@ -12,7 +12,6 @@ import { getPokemonNameWithAffix } from "#app/messages";
|
|||||||
import Overrides from "#app/overrides";
|
import Overrides from "#app/overrides";
|
||||||
import { LearnMoveType } from "#enums/learn-move-type";
|
import { LearnMoveType } from "#enums/learn-move-type";
|
||||||
import type { VoucherType } from "#app/system/voucher";
|
import type { VoucherType } from "#app/system/voucher";
|
||||||
import { Command } from "#enums/command";
|
|
||||||
import { addTextObject, TextStyle } from "#app/ui/text";
|
import { addTextObject, TextStyle } from "#app/ui/text";
|
||||||
import { BooleanHolder, hslToHex, isNullOrUndefined, NumberHolder, randSeedFloat, toDmgValue } from "#app/utils/common";
|
import { BooleanHolder, hslToHex, isNullOrUndefined, NumberHolder, randSeedFloat, toDmgValue } from "#app/utils/common";
|
||||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
@ -1574,30 +1573,16 @@ export class BypassSpeedChanceModifier extends PokemonHeldItemModifier {
|
|||||||
return new BypassSpeedChanceModifier(this.type, this.pokemonId, this.stackCount);
|
return new BypassSpeedChanceModifier(this.type, this.pokemonId, this.stackCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if {@linkcode BypassSpeedChanceModifier} should be applied
|
|
||||||
* @param pokemon the {@linkcode Pokemon} that holds the item
|
|
||||||
* @param doBypassSpeed {@linkcode BooleanHolder} that is `true` if speed should be bypassed
|
|
||||||
* @returns `true` if {@linkcode BypassSpeedChanceModifier} should be applied
|
|
||||||
*/
|
|
||||||
override shouldApply(pokemon?: Pokemon, doBypassSpeed?: BooleanHolder): boolean {
|
|
||||||
return super.shouldApply(pokemon, doBypassSpeed) && !!doBypassSpeed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies {@linkcode BypassSpeedChanceModifier}
|
* Applies {@linkcode BypassSpeedChanceModifier}
|
||||||
* @param pokemon the {@linkcode Pokemon} that holds the item
|
* @param pokemon the {@linkcode Pokemon} that holds the item
|
||||||
* @param doBypassSpeed {@linkcode BooleanHolder} that is `true` if speed should be bypassed
|
|
||||||
* @returns `true` if {@linkcode BypassSpeedChanceModifier} has been applied
|
* @returns `true` if {@linkcode BypassSpeedChanceModifier} has been applied
|
||||||
*/
|
*/
|
||||||
override apply(pokemon: Pokemon, doBypassSpeed: BooleanHolder): boolean {
|
override apply(pokemon: Pokemon): boolean {
|
||||||
if (!doBypassSpeed.value && pokemon.randBattleSeedInt(10) < this.getStackCount()) {
|
if (pokemon.randBattleSeedInt(10) < this.getStackCount() && pokemon.addTag(BattlerTagType.BYPASS_SPEED)) {
|
||||||
doBypassSpeed.value = true;
|
|
||||||
const isCommandFight =
|
|
||||||
globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()]?.command === Command.FIGHT;
|
|
||||||
const hasQuickClaw = this.type.is("PokemonHeldItemModifierType") && this.type.id === "QUICK_CLAW";
|
const hasQuickClaw = this.type.is("PokemonHeldItemModifierType") && this.type.id === "QUICK_CLAW";
|
||||||
|
|
||||||
if (isCommandFight && hasQuickClaw) {
|
if (hasQuickClaw) {
|
||||||
globalScene.phaseManager.queueMessage(
|
globalScene.phaseManager.queueMessage(
|
||||||
i18next.t("modifier:bypassSpeedChanceApply", {
|
i18next.t("modifier:bypassSpeedChanceApply", {
|
||||||
pokemonName: getPokemonNameWithAffix(pokemon),
|
pokemonName: getPokemonNameWithAffix(pokemon),
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import type { Phase } from "#app/phase";
|
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, DynamicPhase, StaticPhaseString } 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";
|
||||||
@ -12,9 +11,7 @@ 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 { coerceArray, 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";
|
||||||
@ -58,7 +55,6 @@ 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";
|
||||||
@ -99,6 +95,11 @@ import { UnavailablePhase } from "#app/phases/unavailable-phase";
|
|||||||
import { UnlockPhase } from "#app/phases/unlock-phase";
|
import { UnlockPhase } from "#app/phases/unlock-phase";
|
||||||
import { VictoryPhase } from "#app/phases/victory-phase";
|
import { VictoryPhase } from "#app/phases/victory-phase";
|
||||||
import { WeatherEffectPhase } from "#app/phases/weather-effect-phase";
|
import { WeatherEffectPhase } from "#app/phases/weather-effect-phase";
|
||||||
|
import { DynamicQueueManager } from "#app/queues/dynamic-queue-manager";
|
||||||
|
import type { PhaseConditionFunc } from "#app/@types/phase-condition";
|
||||||
|
import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
|
||||||
|
import type { PokemonMove } from "#app/data/moves/pokemon-move";
|
||||||
|
import { StaticSwitchSummonPhase } from "#app/phases/static-switch-summon-phase";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Manager for phases used by battle scene.
|
* Manager for phases used by battle scene.
|
||||||
@ -115,7 +116,6 @@ 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,
|
||||||
@ -190,6 +190,7 @@ const PHASES = Object.freeze({
|
|||||||
ShowAbilityPhase,
|
ShowAbilityPhase,
|
||||||
ShowPartyExpBarPhase,
|
ShowPartyExpBarPhase,
|
||||||
ShowTrainerPhase,
|
ShowTrainerPhase,
|
||||||
|
StaticSwitchSummonPhase,
|
||||||
StatStageChangePhase,
|
StatStageChangePhase,
|
||||||
SummonMissingPhase,
|
SummonMissingPhase,
|
||||||
SummonPhase,
|
SummonPhase,
|
||||||
@ -213,13 +214,18 @@ const PHASES = Object.freeze({
|
|||||||
/** Maps Phase strings to their constructors */
|
/** Maps Phase strings to their constructors */
|
||||||
export type PhaseConstructorMap = typeof PHASES;
|
export type PhaseConstructorMap = typeof PHASES;
|
||||||
|
|
||||||
|
const turnEndPhases: PhaseString[] = ["WeatherEffectPhase", "BerryPhase", "CheckStatusEffectPhase", "TurnEndPhase"];
|
||||||
|
|
||||||
|
const ignorablePhases: PhaseString[] = ["ShowAbilityPhase", "HideAbilityPhase"];
|
||||||
|
// TODO might be easier to define which phases should be dynamic instead
|
||||||
|
const nonDynamicPokemonPhases: PhaseString[] = ["SummonPhase", "CommandPhase"];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PhaseManager is responsible for managing the phases in the battle scene
|
* PhaseManager is responsible for managing the phases in the battle scene
|
||||||
*/
|
*/
|
||||||
export class PhaseManager {
|
export class PhaseManager {
|
||||||
/** PhaseQueue: dequeue/remove the first element to get the next phase */
|
/** PhaseQueue: dequeue/remove the first element to get the next phase */
|
||||||
public phaseQueue: Phase[] = [];
|
private phaseQueue: Phase[] = [];
|
||||||
public conditionalQueue: Array<[() => boolean, Phase]> = [];
|
|
||||||
/** PhaseQueuePrepend: is a temp storage of what will be added to PhaseQueue */
|
/** PhaseQueuePrepend: is a temp storage of what will be added to PhaseQueue */
|
||||||
private phaseQueuePrepend: Phase[] = [];
|
private phaseQueuePrepend: Phase[] = [];
|
||||||
|
|
||||||
@ -227,18 +233,12 @@ 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 */
|
public dynamicQueueManager = new DynamicQueueManager();
|
||||||
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() {
|
public turnEnded = false;
|
||||||
this.dynamicPhaseQueues = [new PostSummonPhasePriorityQueue()];
|
|
||||||
this.dynamicPhaseTypes = [PostSummonPhase];
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Phase Functions */
|
/* Phase Functions */
|
||||||
getCurrentPhase(): Phase | null {
|
getCurrentPhase(): Phase | null {
|
||||||
@ -249,31 +249,17 @@ export class PhaseManager {
|
|||||||
return this.standbyPhase;
|
return this.standbyPhase;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a phase to the conditional queue and ensures it is executed only when the specified condition is met.
|
|
||||||
*
|
|
||||||
* This method allows deferring the execution of a phase until certain conditions are met, which is useful for handling
|
|
||||||
* situations like abilities and entry hazards that depend on specific game states.
|
|
||||||
*
|
|
||||||
* @param phase - The phase to be added to the conditional queue.
|
|
||||||
* @param condition - A function that returns a boolean indicating whether the phase should be executed.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
pushConditionalPhase(phase: Phase, condition: () => boolean): void {
|
|
||||||
this.conditionalQueue.push([condition, phase]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a phase to nextCommandPhaseQueue, as long as boolean passed in is false
|
* Adds a phase to nextCommandPhaseQueue, as long as boolean passed in is false
|
||||||
* @param phase {@linkcode Phase} the phase to add
|
* @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 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) {
|
if (this.isDynamicPhase(phase) && this.dynamicQueueManager.activeQueueExists(phase.phaseName)) {
|
||||||
this.pushDynamicPhase(phase);
|
this.dynamicQueueManager.queueDynamicPhase(phase);
|
||||||
} else {
|
return;
|
||||||
(!defer ? this.phaseQueue : this.nextCommandPhaseQueue).push(phase);
|
|
||||||
}
|
}
|
||||||
|
(!defer ? this.phaseQueue : this.nextCommandPhaseQueue).push(phase);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -281,6 +267,10 @@ export class PhaseManager {
|
|||||||
* @param phases {@linkcode Phase} the phase(s) to add
|
* @param phases {@linkcode Phase} the phase(s) to add
|
||||||
*/
|
*/
|
||||||
unshiftPhase(...phases: Phase[]): void {
|
unshiftPhase(...phases: Phase[]): void {
|
||||||
|
if (this.isDynamicPhase(phases[0]) && this.dynamicQueueManager.activeQueueExists(phases[0].phaseName)) {
|
||||||
|
phases.forEach((p: DynamicPhase) => this.dynamicQueueManager.queueDynamicPhase(p));
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.phaseQueuePrependSpliceIndex === -1) {
|
if (this.phaseQueuePrependSpliceIndex === -1) {
|
||||||
this.phaseQueuePrepend.push(...phases);
|
this.phaseQueuePrepend.push(...phases);
|
||||||
} else {
|
} else {
|
||||||
@ -299,12 +289,13 @@ export class PhaseManager {
|
|||||||
* Clears all phase-related stuff, including all phase queues, the current and standby phases, and a splice index
|
* Clears all phase-related stuff, including all phase queues, the current and standby phases, and a splice index
|
||||||
*/
|
*/
|
||||||
clearAllPhases(): void {
|
clearAllPhases(): void {
|
||||||
for (const queue of [this.phaseQueue, this.phaseQueuePrepend, this.conditionalQueue, this.nextCommandPhaseQueue]) {
|
for (const queue of [this.phaseQueue, this.phaseQueuePrepend, this.nextCommandPhaseQueue]) {
|
||||||
queue.splice(0, queue.length);
|
queue.splice(0, queue.length);
|
||||||
}
|
}
|
||||||
this.dynamicPhaseQueues.forEach(queue => queue.clear());
|
this.dynamicQueueManager.clearQueues();
|
||||||
this.currentPhase = null;
|
this.currentPhase = null;
|
||||||
this.standbyPhase = null;
|
this.standbyPhase = null;
|
||||||
|
this.turnEnded = false;
|
||||||
this.clearPhaseQueueSplice();
|
this.clearPhaseQueueSplice();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -345,32 +336,21 @@ export class PhaseManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!this.phaseQueue.length) {
|
|
||||||
this.populatePhaseQueue();
|
this.queueDynamicPhasesAtFront();
|
||||||
// Clear the conditionalQueue if there are no phases left in the phaseQueue
|
|
||||||
this.conditionalQueue = [];
|
if (this.phaseQueue.every(p => ignorablePhases.includes(p.phaseName))) {
|
||||||
|
this.startNextDynamicPhase();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.turnEnded && (!this.phaseQueue.length || this.phaseQueue[0].is("BattleEndPhase"))) {
|
||||||
|
if (!this.startNextDynamicPhase()) {
|
||||||
|
this.turnEndSequence();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.currentPhase = this.phaseQueue.shift() ?? null;
|
this.currentPhase = this.phaseQueue.shift() ?? null;
|
||||||
|
|
||||||
const unactivatedConditionalPhases: [() => boolean, Phase][] = [];
|
|
||||||
// Check if there are any conditional phases queued
|
|
||||||
while (this.conditionalQueue?.length) {
|
|
||||||
// Retrieve the first conditional phase from the queue
|
|
||||||
const conditionalPhase = this.conditionalQueue.shift();
|
|
||||||
// Evaluate the condition associated with the phase
|
|
||||||
if (conditionalPhase?.[0]()) {
|
|
||||||
// If the condition is met, add the phase to the phase queue
|
|
||||||
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
|
|
||||||
unactivatedConditionalPhases.push(conditionalPhase);
|
|
||||||
} else {
|
|
||||||
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;");
|
||||||
this.currentPhase.start();
|
this.currentPhase.start();
|
||||||
@ -390,17 +370,32 @@ export class PhaseManager {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public hasPhaseOfType(type: PhaseString, condition?: PhaseConditionFunc): boolean {
|
||||||
|
if (this.dynamicQueueManager.exists(type, condition)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return [this.phaseQueue, this.phaseQueuePrepend].some((queue: Phase[]) =>
|
||||||
|
queue.find(phase => phase.is(type) && (!condition || condition(phase))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find a specific {@linkcode Phase} in the phase queue.
|
* Find a specific {@linkcode Phase} in the phase queue.
|
||||||
*
|
* @param phaseType - A {@linkcode PhaseString} representing which type to search for
|
||||||
* @param phaseFilter filter function to use to find the wanted phase
|
* @param phaseFilter filter function to use to find the wanted phase
|
||||||
* @returns the found phase or undefined if none found
|
* @returns the found phase or undefined if none found
|
||||||
*/
|
*/
|
||||||
findPhase<P extends Phase = Phase>(phaseFilter: (phase: P) => boolean): P | undefined {
|
findPhase<P extends PhaseString>(
|
||||||
return this.phaseQueue.find(phaseFilter) as P | undefined;
|
phaseType: P,
|
||||||
|
phaseFilter?: (phase: PhaseMap[P]) => boolean,
|
||||||
|
): PhaseMap[P] | undefined {
|
||||||
|
if (this.dynamicQueueManager.exists(phaseType, phaseFilter)) {
|
||||||
|
return this.dynamicQueueManager.findPhaseOfType(phaseType, phaseFilter) as PhaseMap[P];
|
||||||
|
}
|
||||||
|
return this.phaseQueue.find(phase => phase.is(phaseType) && (!phaseFilter || phaseFilter(phase))) as PhaseMap[P];
|
||||||
}
|
}
|
||||||
|
|
||||||
tryReplacePhase(phaseFilter: (phase: Phase) => boolean, phase: Phase): boolean {
|
tryReplacePhase(phaseFilter: PhaseConditionFunc, phase: Phase): boolean {
|
||||||
const phaseIndex = this.phaseQueue.findIndex(phaseFilter);
|
const phaseIndex = this.phaseQueue.findIndex(phaseFilter);
|
||||||
if (phaseIndex > -1) {
|
if (phaseIndex > -1) {
|
||||||
this.phaseQueue[phaseIndex] = phase;
|
this.phaseQueue[phaseIndex] = phase;
|
||||||
@ -409,20 +404,23 @@ export class PhaseManager {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
tryRemovePhase(phaseFilter: (phase: Phase) => boolean): boolean {
|
tryRemovePhase(phaseFilter: PhaseConditionFunc): boolean {
|
||||||
|
if (this.dynamicQueueManager.removePhase(phaseFilter)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const phaseIndex = this.phaseQueue.findIndex(phaseFilter);
|
const phaseIndex = this.phaseQueue.findIndex(phaseFilter);
|
||||||
if (phaseIndex > -1) {
|
if (phaseIndex > -1) {
|
||||||
this.phaseQueue.splice(phaseIndex, 1);
|
this.phaseQueue.splice(phaseIndex, 1);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return this.dynamicQueueManager.removePhase(phaseFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Will search for a specific phase in {@linkcode phaseQueuePrepend} via filter, and remove the first result if a match is found.
|
* Will search for a specific phase in {@linkcode phaseQueuePrepend} via filter, and remove the first result if a match is found.
|
||||||
* @param phaseFilter filter function
|
* @param phaseFilter filter function
|
||||||
*/
|
*/
|
||||||
tryRemoveUnshiftedPhase(phaseFilter: (phase: Phase) => boolean): boolean {
|
tryRemoveUnshiftedPhase(phaseFilter: PhaseConditionFunc): boolean {
|
||||||
const phaseIndex = this.phaseQueuePrepend.findIndex(phaseFilter);
|
const phaseIndex = this.phaseQueuePrepend.findIndex(phaseFilter);
|
||||||
if (phaseIndex > -1) {
|
if (phaseIndex > -1) {
|
||||||
this.phaseQueuePrepend.splice(phaseIndex, 1);
|
this.phaseQueuePrepend.splice(phaseIndex, 1);
|
||||||
@ -431,22 +429,27 @@ export class PhaseManager {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public removeAllPhasesOfType(type: PhaseString): void {
|
||||||
|
this.phaseQueue = this.phaseQueue.filter(phase => !phase.is(type));
|
||||||
|
this.phaseQueuePrepend = this.phaseQueuePrepend.filter(phase => !phase.is(type));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tries to add the input phase to index before target phase in the phaseQueue, else simply calls unshiftPhase()
|
* Tries to add the input phase to index before target phase in the phaseQueue, else simply calls unshiftPhase()
|
||||||
* @param phase - The phase to be added
|
* @param phase - The phase to be added
|
||||||
* @param targetPhase - The phase to search for in phaseQueue
|
* @param targetPhase - The phase to search for in phaseQueue
|
||||||
* @returns boolean if a targetPhase was found and added
|
* @returns boolean if a targetPhase was found and added
|
||||||
*/
|
*/
|
||||||
prependToPhase(phase: Phase | Phase[], targetPhase: PhaseString): boolean {
|
prependToPhase(phase: Phase, targetPhase: StaticPhaseString): boolean {
|
||||||
phase = coerceArray(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);
|
||||||
|
|
||||||
if (targetIndex !== -1) {
|
if (targetIndex !== -1) {
|
||||||
this.phaseQueue.splice(targetIndex, 0, ...phase);
|
this.phaseQueue.splice(targetIndex, 0, phase);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
this.unshiftPhase(...phase);
|
this.unshiftPhase(phase);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -457,81 +460,18 @@ export class PhaseManager {
|
|||||||
* @param condition Condition the target phase must meet to be appended to
|
* @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, condition?: (p: Phase) => boolean): boolean {
|
appendToPhase(phase: Phase, targetPhase: StaticPhaseString, condition?: PhaseConditionFunc): boolean {
|
||||||
phase = coerceArray(phase);
|
|
||||||
const target = PHASES[targetPhase];
|
const target = PHASES[targetPhase];
|
||||||
const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof target && (!condition || condition(ph)));
|
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);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
this.unshiftPhase(...phase);
|
this.unshiftPhase(phase);
|
||||||
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
|
||||||
@ -582,7 +522,10 @@ export class PhaseManager {
|
|||||||
/**
|
/**
|
||||||
* Moves everything from nextCommandPhaseQueue to phaseQueue (keeping order)
|
* Moves everything from nextCommandPhaseQueue to phaseQueue (keeping order)
|
||||||
*/
|
*/
|
||||||
private populatePhaseQueue(): void {
|
private turnEndSequence(): void {
|
||||||
|
this.turnEnded = true;
|
||||||
|
this.dynamicQueueManager.clearQueues();
|
||||||
|
this.queueTurnEndPhases();
|
||||||
if (this.nextCommandPhaseQueue.length) {
|
if (this.nextCommandPhaseQueue.length) {
|
||||||
this.phaseQueue.push(...this.nextCommandPhaseQueue);
|
this.phaseQueue.push(...this.nextCommandPhaseQueue);
|
||||||
this.nextCommandPhaseQueue.splice(0, this.nextCommandPhaseQueue.length);
|
this.nextCommandPhaseQueue.splice(0, this.nextCommandPhaseQueue.length);
|
||||||
@ -637,7 +580,7 @@ export class PhaseManager {
|
|||||||
* @returns `true` if a `targetPhase` was found to prepend to
|
* @returns `true` if a `targetPhase` was found to prepend to
|
||||||
*/
|
*/
|
||||||
public prependNewToPhase<T extends PhaseString>(
|
public prependNewToPhase<T extends PhaseString>(
|
||||||
targetPhase: PhaseString,
|
targetPhase: StaticPhaseString,
|
||||||
phase: T,
|
phase: T,
|
||||||
...args: ConstructorParameters<PhaseConstructorMap[T]>
|
...args: ConstructorParameters<PhaseConstructorMap[T]>
|
||||||
): boolean {
|
): boolean {
|
||||||
@ -653,17 +596,62 @@ export class PhaseManager {
|
|||||||
* @returns `true` if a `targetPhase` was found to append to
|
* @returns `true` if a `targetPhase` was found to append to
|
||||||
*/
|
*/
|
||||||
public appendNewToPhase<T extends PhaseString>(
|
public appendNewToPhase<T extends PhaseString>(
|
||||||
targetPhase: PhaseString,
|
targetPhase: StaticPhaseString,
|
||||||
phase: T,
|
phase: T,
|
||||||
...args: ConstructorParameters<PhaseConstructorMap[T]>
|
...args: ConstructorParameters<PhaseConstructorMap[T]>
|
||||||
): boolean {
|
): boolean {
|
||||||
return this.appendToPhase(this.create(phase, ...args), targetPhase);
|
return this.appendToPhase(this.create(phase, ...args), targetPhase);
|
||||||
}
|
}
|
||||||
|
|
||||||
public startNewDynamicPhase<T extends PhaseString>(
|
public forceMoveNext(phaseCondition: PhaseConditionFunc) {
|
||||||
phase: T,
|
this.dynamicQueueManager.setMoveTimingModifier(phaseCondition, MovePhaseTimingModifier.FIRST);
|
||||||
...args: ConstructorParameters<PhaseConstructorMap[T]>
|
}
|
||||||
): void {
|
|
||||||
this.startDynamicPhase(this.create(phase, ...args));
|
public forceMoveLast(phaseCondition: PhaseConditionFunc) {
|
||||||
|
this.dynamicQueueManager.setMoveTimingModifier(phaseCondition, MovePhaseTimingModifier.LAST);
|
||||||
|
}
|
||||||
|
|
||||||
|
public changePhaseMove(phaseCondition: PhaseConditionFunc, move: PokemonMove) {
|
||||||
|
this.dynamicQueueManager.setMoveForPhase(phaseCondition, move);
|
||||||
|
}
|
||||||
|
|
||||||
|
public queueTurnEndPhases(): void {
|
||||||
|
turnEndPhases
|
||||||
|
.slice()
|
||||||
|
.reverse()
|
||||||
|
.forEach(p => this.phaseQueue.unshift(this.create(p)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private consecutivePokemonPhases(): DynamicPhase[] | undefined {
|
||||||
|
if (this.phaseQueue.length < 1 || !this.isDynamicPhase(this.phaseQueue[0])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let spliceLength = this.phaseQueue.findIndex(p => !p.is(this.phaseQueue[0].phaseName));
|
||||||
|
spliceLength = spliceLength !== -1 ? spliceLength : this.phaseQueue.length;
|
||||||
|
if (spliceLength > 1) {
|
||||||
|
return this.phaseQueue.splice(0, spliceLength) as DynamicPhase[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private queueDynamicPhasesAtFront(): void {
|
||||||
|
const dynamicPhases = this.consecutivePokemonPhases();
|
||||||
|
if (dynamicPhases) {
|
||||||
|
dynamicPhases.forEach((p: DynamicPhase) => {
|
||||||
|
globalScene.phaseManager.dynamicQueueManager.queueDynamicPhase(p);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public startNextDynamicPhase(): boolean {
|
||||||
|
const dynamicPhase = this.dynamicQueueManager.popNextPhase();
|
||||||
|
if (dynamicPhase) {
|
||||||
|
this.phaseQueue.unshift(dynamicPhase);
|
||||||
|
}
|
||||||
|
return !!dynamicPhase;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isDynamicPhase(phase: Phase): phase is DynamicPhase {
|
||||||
|
return typeof (phase as any).getPokemon === "function" && !nonDynamicPokemonPhases.includes(phase.phaseName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -18,23 +18,11 @@ export class BattleEndPhase extends BattlePhase {
|
|||||||
super.start();
|
super.start();
|
||||||
|
|
||||||
// cull any extra `BattleEnd` phases from the queue.
|
// cull any extra `BattleEnd` phases from the queue.
|
||||||
globalScene.phaseManager.phaseQueue = globalScene.phaseManager.phaseQueue.filter(phase => {
|
this.isVictory ||= globalScene.phaseManager.hasPhaseOfType(
|
||||||
if (phase.is("BattleEndPhase")) {
|
"BattleEndPhase",
|
||||||
this.isVictory ||= phase.isVictory;
|
(phase: BattleEndPhase) => phase.isVictory,
|
||||||
return false;
|
);
|
||||||
}
|
globalScene.phaseManager.removeAllPhasesOfType("BattleEndPhase");
|
||||||
return true;
|
|
||||||
});
|
|
||||||
// `phaseQueuePrepend` is private, so we have to use this inefficient loop.
|
|
||||||
while (
|
|
||||||
globalScene.phaseManager.tryRemoveUnshiftedPhase(phase => {
|
|
||||||
if (phase.is("BattleEndPhase")) {
|
|
||||||
this.isVictory ||= phase.isVictory;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
})
|
|
||||||
) {}
|
|
||||||
|
|
||||||
globalScene.gameData.gameStats.battles++;
|
globalScene.gameData.gameStats.battles++;
|
||||||
if (
|
if (
|
||||||
|
@ -1,20 +1,14 @@
|
|||||||
import { Phase } from "#app/phase";
|
import { Phase } from "#app/phase";
|
||||||
import type { BattlerIndex } from "#enums/battler-index";
|
|
||||||
import { globalScene } from "#app/global-scene";
|
import { globalScene } from "#app/global-scene";
|
||||||
|
|
||||||
export class CheckStatusEffectPhase extends Phase {
|
export class CheckStatusEffectPhase extends Phase {
|
||||||
public readonly phaseName = "CheckStatusEffectPhase";
|
public readonly phaseName = "CheckStatusEffectPhase";
|
||||||
private order: BattlerIndex[];
|
|
||||||
constructor(order: BattlerIndex[]) {
|
|
||||||
super();
|
|
||||||
this.order = order;
|
|
||||||
}
|
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
const field = globalScene.getField();
|
const field = globalScene.getField();
|
||||||
for (const o of this.order) {
|
for (const p of field) {
|
||||||
if (field[o].status?.isPostTurn()) {
|
if (p?.status?.isPostTurn()) {
|
||||||
globalScene.phaseManager.unshiftNew("PostTurnStatusEffectPhase", o);
|
globalScene.phaseManager.unshiftNew("PostTurnStatusEffectPhase", p.getBattlerIndex());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.end();
|
this.end();
|
||||||
|
@ -225,7 +225,7 @@ export class EggHatchPhase extends Phase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
end() {
|
end() {
|
||||||
if (globalScene.phaseManager.findPhase(p => p.is("EggHatchPhase"))) {
|
if (globalScene.phaseManager.findPhase("EggHatchPhase")) {
|
||||||
this.eggHatchHandler.clear();
|
this.eggHatchHandler.clear();
|
||||||
} else {
|
} else {
|
||||||
globalScene.time.delayedCall(250, () => globalScene.setModifiersVisible(true));
|
globalScene.time.delayedCall(250, () => globalScene.setModifiersVisible(true));
|
||||||
|
@ -562,29 +562,6 @@ export class EncounterPhase extends BattlePhase {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (![BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(globalScene.currentBattle.battleType)) {
|
if (![BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(globalScene.currentBattle.battleType)) {
|
||||||
enemyField.map(p =>
|
|
||||||
globalScene.phaseManager.pushConditionalPhase(
|
|
||||||
globalScene.phaseManager.create("PostSummonPhase", p.getBattlerIndex()),
|
|
||||||
() => {
|
|
||||||
// if there is not a player party, we can't continue
|
|
||||||
if (!globalScene.getPlayerParty().length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// how many player pokemon are on the field ?
|
|
||||||
const pokemonsOnFieldCount = globalScene.getPlayerParty().filter(p => p.isOnField()).length;
|
|
||||||
// if it's a 2vs1, there will never be a 2nd pokemon on our field even
|
|
||||||
const requiredPokemonsOnField = Math.min(
|
|
||||||
globalScene.getPlayerParty().filter(p => !p.isFainted()).length,
|
|
||||||
2,
|
|
||||||
);
|
|
||||||
// if it's a double, there should be 2, otherwise 1
|
|
||||||
if (globalScene.currentBattle.double) {
|
|
||||||
return pokemonsOnFieldCount === requiredPokemonsOnField;
|
|
||||||
}
|
|
||||||
return pokemonsOnFieldCount === 1;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const ivScannerModifier = globalScene.findModifier(m => m instanceof IvScannerModifier);
|
const ivScannerModifier = globalScene.findModifier(m => m instanceof IvScannerModifier);
|
||||||
if (ivScannerModifier) {
|
if (ivScannerModifier) {
|
||||||
enemyField.map(p => globalScene.phaseManager.pushNew("ScanIvsPhase", p.getBattlerIndex()));
|
enemyField.map(p => globalScene.phaseManager.pushNew("ScanIvsPhase", p.getBattlerIndex()));
|
||||||
@ -625,6 +602,9 @@ export class EncounterPhase extends BattlePhase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (![BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(globalScene.currentBattle.battleType)) {
|
||||||
|
enemyField.map(p => globalScene.phaseManager.pushNew("PostSummonPhase", p.getBattlerIndex()));
|
||||||
|
}
|
||||||
handleTutorial(Tutorial.Access_Menu).then(() => super.end());
|
handleTutorial(Tutorial.Access_Menu).then(() => super.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,7 +181,14 @@ export class FaintPhase extends PokemonPhase {
|
|||||||
.filter(p => p.isActive() && !p.isOnField() && p.trainerSlot === (pokemon as EnemyPokemon).trainerSlot)
|
.filter(p => p.isActive() && !p.isOnField() && p.trainerSlot === (pokemon as EnemyPokemon).trainerSlot)
|
||||||
.length;
|
.length;
|
||||||
if (hasReservePartyMember) {
|
if (hasReservePartyMember) {
|
||||||
globalScene.phaseManager.pushNew("SwitchSummonPhase", SwitchType.SWITCH, this.fieldIndex, -1, false, false);
|
globalScene.phaseManager.pushNew(
|
||||||
|
"StaticSwitchSummonPhase",
|
||||||
|
SwitchType.SWITCH,
|
||||||
|
this.fieldIndex,
|
||||||
|
-1,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,6 +50,7 @@ import type Move from "#app/data/moves/move";
|
|||||||
import { isFieldTargeted } from "#app/data/moves/move-utils";
|
import { isFieldTargeted } from "#app/data/moves/move-utils";
|
||||||
import { DamageAchv } from "#app/system/achv";
|
import { DamageAchv } from "#app/system/achv";
|
||||||
import { isVirtual, isReflected, MoveUseMode } from "#enums/move-use-mode";
|
import { isVirtual, isReflected, MoveUseMode } from "#enums/move-use-mode";
|
||||||
|
import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
|
||||||
|
|
||||||
export type HitCheckEntry = [HitCheckResult, TypeDamageMultiplier];
|
export type HitCheckEntry = [HitCheckResult, TypeDamageMultiplier];
|
||||||
|
|
||||||
@ -156,7 +157,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Queue the phaes that should occur when the target reflects the move back to the user
|
* Queue the phases that should occur when the target reflects the move back to the user
|
||||||
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
* @param user - The {@linkcode Pokemon} using this phase's invoked move
|
||||||
* @param target - The {@linkcode Pokemon} that is reflecting the move
|
* @param target - The {@linkcode Pokemon} that is reflecting the move
|
||||||
* TODO: Rework this to use `onApply` of Magic Coat
|
* TODO: Rework this to use `onApply` of Magic Coat
|
||||||
@ -167,24 +168,21 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
: [user.getBattlerIndex()];
|
: [user.getBattlerIndex()];
|
||||||
// TODO: ability displays should be handled by the ability
|
// TODO: ability displays should be handled by the ability
|
||||||
if (!target.getTag(BattlerTagType.MAGIC_COAT)) {
|
if (!target.getTag(BattlerTagType.MAGIC_COAT)) {
|
||||||
this.queuedPhases.push(
|
globalScene.phaseManager.unshiftNew(
|
||||||
globalScene.phaseManager.create(
|
"ShowAbilityPhase",
|
||||||
"ShowAbilityPhase",
|
target.getBattlerIndex(),
|
||||||
target.getBattlerIndex(),
|
target.getPassiveAbility().hasAttr("ReflectStatusMoveAbAttr"),
|
||||||
target.getPassiveAbility().hasAttr("ReflectStatusMoveAbAttr"),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
this.queuedPhases.push(globalScene.phaseManager.create("HideAbilityPhase"));
|
globalScene.phaseManager.unshiftNew("HideAbilityPhase");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.queuedPhases.push(
|
globalScene.phaseManager.pushNew(
|
||||||
globalScene.phaseManager.create(
|
"MovePhase",
|
||||||
"MovePhase",
|
target,
|
||||||
target,
|
newTargets,
|
||||||
newTargets,
|
new PokemonMove(this.move.id),
|
||||||
new PokemonMove(this.move.id),
|
MoveUseMode.REFLECTED,
|
||||||
MoveUseMode.REFLECTED,
|
MovePhaseTimingModifier.FIRST,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -376,9 +374,6 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.queuedPhases.length) {
|
|
||||||
globalScene.phaseManager.appendToPhase(this.queuedPhases, "MoveEndPhase");
|
|
||||||
}
|
|
||||||
const moveType = user.getMoveType(this.move, true);
|
const moveType = user.getMoveType(this.move, true);
|
||||||
if (this.move.category !== MoveCategory.STATUS && !user.stellarTypesBoosted.includes(moveType)) {
|
if (this.move.category !== MoveCategory.STATUS && !user.stellarTypesBoosted.includes(moveType)) {
|
||||||
user.stellarTypesBoosted.push(moveType);
|
user.stellarTypesBoosted.push(moveType);
|
||||||
|
@ -1,29 +1,27 @@
|
|||||||
import { applyMoveAttrs } from "#app/data/moves/apply-attrs";
|
import { applyMoveAttrs } from "#app/data/moves/apply-attrs";
|
||||||
import type { PokemonMove } from "#app/data/moves/pokemon-move";
|
import type { PokemonMove } from "#app/data/moves/pokemon-move";
|
||||||
import type Pokemon from "#app/field/pokemon";
|
import { PokemonPhase } from "#app/phases/pokemon-phase";
|
||||||
import { BattlePhase } from "./battle-phase";
|
import type { BattlerIndex } from "#enums/battler-index";
|
||||||
|
|
||||||
export class MoveHeaderPhase extends BattlePhase {
|
export class MoveHeaderPhase extends PokemonPhase {
|
||||||
public readonly phaseName = "MoveHeaderPhase";
|
public readonly phaseName = "MoveHeaderPhase";
|
||||||
public pokemon: Pokemon;
|
|
||||||
public move: PokemonMove;
|
public move: PokemonMove;
|
||||||
|
|
||||||
constructor(pokemon: Pokemon, move: PokemonMove) {
|
constructor(battlerIndex: BattlerIndex, move: PokemonMove) {
|
||||||
super();
|
super(battlerIndex);
|
||||||
|
|
||||||
this.pokemon = pokemon;
|
|
||||||
this.move = move;
|
this.move = move;
|
||||||
}
|
}
|
||||||
|
|
||||||
canMove(): boolean {
|
canMove(): boolean {
|
||||||
return this.pokemon.isActive(true) && this.move.isUsable(this.pokemon);
|
return this.getPokemon().isActive(true) && this.move.isUsable(this.getPokemon());
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
super.start();
|
super.start();
|
||||||
|
|
||||||
if (this.canMove()) {
|
if (this.canMove()) {
|
||||||
applyMoveAttrs("MoveHeaderAttr", this.pokemon, null, this.move.getMove());
|
applyMoveAttrs("MoveHeaderAttr", this.getPokemon(), null, this.move.getMove());
|
||||||
}
|
}
|
||||||
this.end();
|
this.end();
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,6 @@ import type Pokemon from "#app/field/pokemon";
|
|||||||
import { MoveResult } from "#enums/move-result";
|
import { MoveResult } from "#enums/move-result";
|
||||||
import { getPokemonNameWithAffix } from "#app/messages";
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
import Overrides from "#app/overrides";
|
import Overrides from "#app/overrides";
|
||||||
import { BattlePhase } from "#app/phases/battle-phase";
|
|
||||||
import { enumValueToKey, NumberHolder } from "#app/utils/common";
|
import { enumValueToKey, NumberHolder } from "#app/utils/common";
|
||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||||
@ -29,14 +28,16 @@ import i18next from "i18next";
|
|||||||
import { getTerrainBlockMessage } from "#app/data/terrain";
|
import { getTerrainBlockMessage } from "#app/data/terrain";
|
||||||
import { isVirtual, isIgnorePP, isReflected, MoveUseMode, isIgnoreStatus } from "#enums/move-use-mode";
|
import { isVirtual, isIgnorePP, isReflected, MoveUseMode, isIgnoreStatus } from "#enums/move-use-mode";
|
||||||
import { frenzyMissFunc } from "#app/data/moves/move-utils";
|
import { frenzyMissFunc } from "#app/data/moves/move-utils";
|
||||||
|
import { PokemonPhase } from "#app/phases/pokemon-phase";
|
||||||
|
import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
|
||||||
|
|
||||||
export class MovePhase extends BattlePhase {
|
export class MovePhase extends PokemonPhase {
|
||||||
public readonly phaseName = "MovePhase";
|
public readonly phaseName = "MovePhase";
|
||||||
protected _pokemon: Pokemon;
|
protected _pokemon: Pokemon;
|
||||||
protected _move: PokemonMove;
|
protected _move: PokemonMove;
|
||||||
protected _targets: BattlerIndex[];
|
protected _targets: BattlerIndex[];
|
||||||
public readonly useMode: MoveUseMode; // Made public for quash
|
public readonly useMode: MoveUseMode; // Made public for quash
|
||||||
protected forcedLast: boolean;
|
protected _timingModifier: MovePhaseTimingModifier;
|
||||||
|
|
||||||
/** Whether the current move should fail but still use PP */
|
/** Whether the current move should fail but still use PP */
|
||||||
protected failed = false;
|
protected failed = false;
|
||||||
@ -56,7 +57,7 @@ export class MovePhase extends BattlePhase {
|
|||||||
return this._move;
|
return this._move;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected set move(move: PokemonMove) {
|
public set move(move: PokemonMove) {
|
||||||
this._move = move;
|
this._move = move;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,6 +69,14 @@ export class MovePhase extends BattlePhase {
|
|||||||
this._targets = targets;
|
this._targets = targets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get timingModifier(): MovePhaseTimingModifier {
|
||||||
|
return this._timingModifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set timingModifier(modifier: MovePhaseTimingModifier) {
|
||||||
|
this._timingModifier = modifier;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new MovePhase for using moves.
|
* Create a new MovePhase for using moves.
|
||||||
* @param pokemon - The {@linkcode Pokemon} using the move
|
* @param pokemon - The {@linkcode Pokemon} using the move
|
||||||
@ -76,14 +85,14 @@ export class MovePhase extends BattlePhase {
|
|||||||
* Not marked optional to ensure callers correctly pass on `useModes`.
|
* Not marked optional to ensure callers correctly pass on `useModes`.
|
||||||
* @param forcedLast - Whether to force this phase to occur last in order (for {@linkcode MoveId.QUASH}); default `false`
|
* @param forcedLast - Whether to force this phase to occur last in order (for {@linkcode MoveId.QUASH}); default `false`
|
||||||
*/
|
*/
|
||||||
constructor(pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, useMode: MoveUseMode, forcedLast = false) {
|
constructor(pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, useMode: MoveUseMode, timingModifier = MovePhaseTimingModifier.NORMAL) {
|
||||||
super();
|
super(pokemon.getBattlerIndex());
|
||||||
|
|
||||||
this.pokemon = pokemon;
|
this.pokemon = pokemon;
|
||||||
this.targets = targets;
|
this.targets = targets;
|
||||||
this.move = move;
|
this.move = move;
|
||||||
this.useMode = useMode;
|
this.useMode = useMode;
|
||||||
this.forcedLast = forcedLast;
|
this.timingModifier = timingModifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -109,14 +118,6 @@ export class MovePhase extends BattlePhase {
|
|||||||
this.cancelled = true;
|
this.cancelled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows whether the current move has been forced to the end of the turn
|
|
||||||
* Needed for speed order, see {@linkcode MoveId.QUASH}
|
|
||||||
*/
|
|
||||||
public isForcedLast(): boolean {
|
|
||||||
return this.forcedLast;
|
|
||||||
}
|
|
||||||
|
|
||||||
public start(): void {
|
public start(): void {
|
||||||
super.start();
|
super.start();
|
||||||
|
|
||||||
|
@ -237,7 +237,7 @@ export class MysteryEncounterBattleStartCleanupPhase extends Phase {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Remove any status tick phases
|
// Remove any status tick phases
|
||||||
while (globalScene.phaseManager.findPhase(p => p.is("PostTurnStatusEffectPhase"))) {
|
while (globalScene.phaseManager.findPhase("PostTurnStatusEffectPhase")) {
|
||||||
globalScene.phaseManager.tryRemovePhase(p => p.is("PostTurnStatusEffectPhase"));
|
globalScene.phaseManager.tryRemovePhase(p => p.is("PostTurnStatusEffectPhase"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,12 +6,7 @@ export class NewBattlePhase extends BattlePhase {
|
|||||||
start() {
|
start() {
|
||||||
super.start();
|
super.start();
|
||||||
|
|
||||||
// cull any extra `NewBattle` phases from the queue.
|
globalScene.phaseManager.removeAllPhasesOfType("NewBattlePhase");
|
||||||
globalScene.phaseManager.phaseQueue = globalScene.phaseManager.phaseQueue.filter(
|
|
||||||
phase => !phase.is("NewBattlePhase"),
|
|
||||||
);
|
|
||||||
// `phaseQueuePrepend` is private, so we have to use this inefficient loop.
|
|
||||||
while (globalScene.phaseManager.tryRemoveUnshiftedPhase(phase => phase.is("NewBattlePhase"))) {}
|
|
||||||
|
|
||||||
globalScene.newBattle();
|
globalScene.newBattle();
|
||||||
|
|
||||||
|
@ -22,4 +22,8 @@ export abstract class PartyMemberPokemonPhase extends FieldPhase {
|
|||||||
getPokemon(): Pokemon {
|
getPokemon(): Pokemon {
|
||||||
return this.getParty()[this.partyMemberIndex];
|
return this.getParty()[this.partyMemberIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isPlayer(): boolean {
|
||||||
|
return this.player;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,9 @@ export class PostSummonPhase extends PokemonPhase {
|
|||||||
|
|
||||||
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", { pokemon: p });
|
if (p.isActive(true)) {
|
||||||
|
applyAbAttrs("CommanderAbAttr", { pokemon: p });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.end();
|
this.end();
|
||||||
|
@ -11,7 +11,6 @@ import { BattlerTagType } from "#app/enums/battler-tag-type";
|
|||||||
import type Pokemon from "#app/field/pokemon";
|
import type Pokemon from "#app/field/pokemon";
|
||||||
import { getPokemonNameWithAffix } from "#app/messages";
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
import { BattlePhase } from "./battle-phase";
|
import { BattlePhase } from "./battle-phase";
|
||||||
import type { MovePhase } from "./move-phase";
|
|
||||||
import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
|
import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
|
||||||
|
|
||||||
export class QuietFormChangePhase extends BattlePhase {
|
export class QuietFormChangePhase extends BattlePhase {
|
||||||
@ -173,9 +172,7 @@ export class QuietFormChangePhase extends BattlePhase {
|
|||||||
this.pokemon.initBattleInfo();
|
this.pokemon.initBattleInfo();
|
||||||
this.pokemon.cry();
|
this.pokemon.cry();
|
||||||
|
|
||||||
const movePhase = globalScene.phaseManager.findPhase(
|
const movePhase = globalScene.phaseManager.findPhase("MovePhase", p => p.pokemon === this.pokemon);
|
||||||
p => p.is("MovePhase") && p.pokemon === this.pokemon,
|
|
||||||
) as MovePhase;
|
|
||||||
if (movePhase) {
|
if (movePhase) {
|
||||||
movePhase.cancel();
|
movePhase.cancel();
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ export class RevivalBlessingPhase extends BattlePhase {
|
|||||||
if (slotIndex <= 1) {
|
if (slotIndex <= 1) {
|
||||||
// Revived ally pokemon
|
// Revived ally pokemon
|
||||||
globalScene.phaseManager.unshiftNew(
|
globalScene.phaseManager.unshiftNew(
|
||||||
"SwitchSummonPhase",
|
"StaticSwitchSummonPhase",
|
||||||
SwitchType.SWITCH,
|
SwitchType.SWITCH,
|
||||||
pokemon.getFieldIndex(),
|
pokemon.getFieldIndex(),
|
||||||
slotIndex,
|
slotIndex,
|
||||||
@ -61,7 +61,7 @@ export class RevivalBlessingPhase extends BattlePhase {
|
|||||||
} else if (allyPokemon.isFainted()) {
|
} else if (allyPokemon.isFainted()) {
|
||||||
// Revived party pokemon, and ally pokemon is fainted
|
// Revived party pokemon, and ally pokemon is fainted
|
||||||
globalScene.phaseManager.unshiftNew(
|
globalScene.phaseManager.unshiftNew(
|
||||||
"SwitchSummonPhase",
|
"StaticSwitchSummonPhase",
|
||||||
SwitchType.SWITCH,
|
SwitchType.SWITCH,
|
||||||
allyPokemon.getFieldIndex(),
|
allyPokemon.getFieldIndex(),
|
||||||
slotIndex,
|
slotIndex,
|
||||||
|
@ -231,7 +231,8 @@ export class StatStageChangePhase extends PokemonPhase {
|
|||||||
|
|
||||||
// Look for any other stat change phases; if this is the last one, do White Herb check
|
// Look for any other stat change phases; if this is the last one, do White Herb check
|
||||||
const existingPhase = globalScene.phaseManager.findPhase(
|
const existingPhase = globalScene.phaseManager.findPhase(
|
||||||
p => p.is("StatStageChangePhase") && p.battlerIndex === this.battlerIndex,
|
"StatStageChangePhase",
|
||||||
|
p => p.battlerIndex === this.battlerIndex,
|
||||||
);
|
);
|
||||||
if (!existingPhase?.is("StatStageChangePhase")) {
|
if (!existingPhase?.is("StatStageChangePhase")) {
|
||||||
// Apply White Herb if needed
|
// Apply White Herb if needed
|
||||||
@ -311,8 +312,8 @@ export class StatStageChangePhase extends PokemonPhase {
|
|||||||
if (this.stats.length === 1) {
|
if (this.stats.length === 1) {
|
||||||
while (
|
while (
|
||||||
(existingPhase = globalScene.phaseManager.findPhase(
|
(existingPhase = globalScene.phaseManager.findPhase(
|
||||||
|
"StatStageChangePhase",
|
||||||
p =>
|
p =>
|
||||||
p.is("StatStageChangePhase") &&
|
|
||||||
p.battlerIndex === this.battlerIndex &&
|
p.battlerIndex === this.battlerIndex &&
|
||||||
p.stats.length === 1 &&
|
p.stats.length === 1 &&
|
||||||
p.stats[0] === this.stats[0] &&
|
p.stats[0] === this.stats[0] &&
|
||||||
@ -330,8 +331,8 @@ export class StatStageChangePhase extends PokemonPhase {
|
|||||||
}
|
}
|
||||||
while (
|
while (
|
||||||
(existingPhase = globalScene.phaseManager.findPhase(
|
(existingPhase = globalScene.phaseManager.findPhase(
|
||||||
|
"StatStageChangePhase",
|
||||||
p =>
|
p =>
|
||||||
p.is("StatStageChangePhase") &&
|
|
||||||
p.battlerIndex === this.battlerIndex &&
|
p.battlerIndex === this.battlerIndex &&
|
||||||
p.selfTarget === this.selfTarget &&
|
p.selfTarget === this.selfTarget &&
|
||||||
accEva.some(s => p.stats.includes(s)) === isAccEva &&
|
accEva.some(s => p.stats.includes(s)) === isAccEva &&
|
||||||
|
5
src/phases/static-switch-summon-phase.ts
Normal file
5
src/phases/static-switch-summon-phase.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
|
||||||
|
|
||||||
|
export class StaticSwitchSummonPhase extends SwitchSummonPhase {
|
||||||
|
public readonly phaseName = "StaticSwitchSummonPhase";
|
||||||
|
}
|
@ -15,7 +15,12 @@ import { globalScene } from "#app/global-scene";
|
|||||||
|
|
||||||
export class SummonPhase extends PartyMemberPokemonPhase {
|
export class SummonPhase extends PartyMemberPokemonPhase {
|
||||||
// The union type is needed to keep typescript happy as these phases extend from SummonPhase
|
// The union type is needed to keep typescript happy as these phases extend from SummonPhase
|
||||||
public readonly phaseName: "SummonPhase" | "SummonMissingPhase" | "SwitchSummonPhase" | "ReturnPhase" = "SummonPhase";
|
public readonly phaseName:
|
||||||
|
| "SummonPhase"
|
||||||
|
| "SummonMissingPhase"
|
||||||
|
| "SwitchSummonPhase"
|
||||||
|
| "ReturnPhase"
|
||||||
|
| "StaticSwitchSummonPhase" = "SummonPhase";
|
||||||
private loaded: boolean;
|
private loaded: boolean;
|
||||||
|
|
||||||
constructor(fieldIndex: number, player = true, loaded = false) {
|
constructor(fieldIndex: number, player = true, loaded = false) {
|
||||||
@ -296,4 +301,8 @@ export class SummonPhase extends PartyMemberPokemonPhase {
|
|||||||
|
|
||||||
super.end();
|
super.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getFieldIndex(): number {
|
||||||
|
return this.fieldIndex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,7 +79,13 @@ export class SwitchPhase extends BattlePhase {
|
|||||||
p => p.is("PostSummonPhase") && p.player && p.fieldIndex === this.fieldIndex,
|
p => p.is("PostSummonPhase") && p.player && p.fieldIndex === this.fieldIndex,
|
||||||
);
|
);
|
||||||
const switchType = option === PartyOption.PASS_BATON ? SwitchType.BATON_PASS : this.switchType;
|
const switchType = option === PartyOption.PASS_BATON ? SwitchType.BATON_PASS : this.switchType;
|
||||||
globalScene.phaseManager.unshiftNew("SwitchSummonPhase", switchType, fieldIndex, slotIndex, this.doReturn);
|
globalScene.phaseManager.unshiftNew(
|
||||||
|
"StaticSwitchSummonPhase",
|
||||||
|
switchType,
|
||||||
|
fieldIndex,
|
||||||
|
slotIndex,
|
||||||
|
this.doReturn,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
globalScene.ui.setMode(UiMode.MESSAGE).then(() => super.end());
|
globalScene.ui.setMode(UiMode.MESSAGE).then(() => super.end());
|
||||||
},
|
},
|
||||||
|
@ -14,7 +14,7 @@ import { SubstituteTag } from "#app/data/battler-tags";
|
|||||||
import { SwitchType } from "#enums/switch-type";
|
import { SwitchType } from "#enums/switch-type";
|
||||||
|
|
||||||
export class SwitchSummonPhase extends SummonPhase {
|
export class SwitchSummonPhase extends SummonPhase {
|
||||||
public readonly phaseName: "SwitchSummonPhase" | "ReturnPhase" = "SwitchSummonPhase";
|
public readonly phaseName: "SwitchSummonPhase" | "ReturnPhase" | "StaticSwitchSummonPhase" = "SwitchSummonPhase";
|
||||||
private readonly switchType: SwitchType;
|
private readonly switchType: SwitchType;
|
||||||
private readonly slotIndex: number;
|
private readonly slotIndex: number;
|
||||||
private readonly doReturn: boolean;
|
private readonly doReturn: boolean;
|
||||||
@ -241,11 +241,11 @@ export class SwitchSummonPhase extends SummonPhase {
|
|||||||
|
|
||||||
globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true);
|
globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true);
|
||||||
// Reverts to weather-based forms when weather suppressors (Cloud Nine/Air Lock) are switched out
|
// Reverts to weather-based forms when weather suppressors (Cloud Nine/Air Lock) are switched out
|
||||||
globalScene.arena.triggerWeatherBasedFormChanges();
|
globalScene.arena.triggerWeatherBasedFormChanges(pokemon);
|
||||||
}
|
}
|
||||||
|
|
||||||
queuePostSummon(): void {
|
queuePostSummon(): void {
|
||||||
globalScene.phaseManager.startNewDynamicPhase("PostSummonPhase", this.getPokemon().getBattlerIndex());
|
globalScene.phaseManager.pushNew("PostSummonPhase", this.getPokemon().getBattlerIndex());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -23,6 +23,7 @@ export class TurnEndPhase extends FieldPhase {
|
|||||||
|
|
||||||
globalScene.currentBattle.incrementTurn();
|
globalScene.currentBattle.incrementTurn();
|
||||||
globalScene.eventTarget.dispatchEvent(new TurnEndEvent(globalScene.currentBattle.turn));
|
globalScene.eventTarget.dispatchEvent(new TurnEndEvent(globalScene.currentBattle.turn));
|
||||||
|
globalScene.phaseManager.dynamicQueueManager.clearLastTurnOrder();
|
||||||
|
|
||||||
globalScene.phaseManager.hideAbilityBar();
|
globalScene.phaseManager.hideAbilityBar();
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ export class TurnInitPhase extends FieldPhase {
|
|||||||
start() {
|
start() {
|
||||||
super.start();
|
super.start();
|
||||||
|
|
||||||
|
globalScene.phaseManager.turnEnded = false;
|
||||||
globalScene.getPlayerField().forEach(p => {
|
globalScene.getPlayerField().forEach(p => {
|
||||||
// If this pokemon is in play and evolved into something illegal under the current challenge, force a switch
|
// If this pokemon is in play and evolved into something illegal under the current challenge, force a switch
|
||||||
if (p.isOnField() && !p.isAllowedInBattle()) {
|
if (p.isOnField() && !p.isAllowedInBattle()) {
|
||||||
|
@ -1,86 +1,34 @@
|
|||||||
import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
|
|
||||||
import { allMoves } from "#app/data/data-lists";
|
|
||||||
import { Stat } from "#app/enums/stat";
|
|
||||||
import type Pokemon from "#app/field/pokemon";
|
|
||||||
import { PokemonMove } from "#app/data/moves/pokemon-move";
|
import { PokemonMove } from "#app/data/moves/pokemon-move";
|
||||||
import { BypassSpeedChanceModifier } from "#app/modifier/modifier";
|
|
||||||
import { Command } from "#enums/command";
|
import { Command } from "#enums/command";
|
||||||
import { randSeedShuffle, BooleanHolder } from "#app/utils/common";
|
|
||||||
import { FieldPhase } from "./field-phase";
|
import { FieldPhase } from "./field-phase";
|
||||||
import { BattlerIndex } from "#enums/battler-index";
|
import type { BattlerIndex } from "#enums/battler-index";
|
||||||
import { TrickRoomTag } from "#app/data/arena-tag";
|
|
||||||
import { SwitchType } from "#enums/switch-type";
|
import { SwitchType } from "#enums/switch-type";
|
||||||
import { globalScene } from "#app/global-scene";
|
import { globalScene } from "#app/global-scene";
|
||||||
|
import { applyInSpeedOrder } from "#app/utils/speed-order";
|
||||||
|
import type Pokemon from "#app/field/pokemon";
|
||||||
|
import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
|
||||||
|
import { BypassSpeedChanceModifier } from "#app/modifier/modifier";
|
||||||
|
|
||||||
export class TurnStartPhase extends FieldPhase {
|
export class TurnStartPhase extends FieldPhase {
|
||||||
public readonly phaseName = "TurnStartPhase";
|
public readonly phaseName = "TurnStartPhase";
|
||||||
/**
|
/**
|
||||||
* This orders the active Pokemon on the field by speed into an BattlerIndex array and returns that array.
|
* Returns an ordering of the current field based on command priority
|
||||||
* It also checks for Trick Room and reverses the array if it is present.
|
* @returns {@linkcode BattlerIndex[]} the sequence of commands for this turn
|
||||||
* @returns {@linkcode BattlerIndex[]} the battle indices of all pokemon on the field ordered by speed
|
|
||||||
*/
|
|
||||||
getSpeedOrder(): BattlerIndex[] {
|
|
||||||
const playerField = globalScene.getPlayerField().filter(p => p.isActive()) as Pokemon[];
|
|
||||||
const enemyField = globalScene.getEnemyField().filter(p => p.isActive()) as Pokemon[];
|
|
||||||
|
|
||||||
// We shuffle the list before sorting so speed ties produce random results
|
|
||||||
let orderedTargets: Pokemon[] = playerField.concat(enemyField);
|
|
||||||
// We seed it with the current turn to prevent an inconsistency where it
|
|
||||||
// was varying based on how long since you last reloaded
|
|
||||||
globalScene.executeWithSeedOffset(
|
|
||||||
() => {
|
|
||||||
orderedTargets = randSeedShuffle(orderedTargets);
|
|
||||||
},
|
|
||||||
globalScene.currentBattle.turn,
|
|
||||||
globalScene.waveSeed,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Next, a check for Trick Room is applied to determine sort order.
|
|
||||||
const speedReversed = new BooleanHolder(false);
|
|
||||||
globalScene.arena.applyTags(TrickRoomTag, false, speedReversed);
|
|
||||||
|
|
||||||
// Adjust the sort function based on whether Trick Room is active.
|
|
||||||
orderedTargets.sort((a: Pokemon, b: Pokemon) => {
|
|
||||||
const aSpeed = a?.getEffectiveStat(Stat.SPD) ?? 0;
|
|
||||||
const bSpeed = b?.getEffectiveStat(Stat.SPD) ?? 0;
|
|
||||||
|
|
||||||
return speedReversed.value ? aSpeed - bSpeed : bSpeed - aSpeed;
|
|
||||||
});
|
|
||||||
|
|
||||||
return orderedTargets.map(t => t.getFieldIndex() + (!t.isPlayer() ? BattlerIndex.ENEMY : BattlerIndex.PLAYER));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This takes the result of getSpeedOrder and applies priority / bypass speed attributes to it.
|
|
||||||
* This also considers the priority levels of various commands and changes the result of getSpeedOrder based on such.
|
|
||||||
* @returns {@linkcode BattlerIndex[]} the final sequence of commands for this turn
|
|
||||||
*/
|
*/
|
||||||
getCommandOrder(): BattlerIndex[] {
|
getCommandOrder(): BattlerIndex[] {
|
||||||
let moveOrder = this.getSpeedOrder();
|
const playerField = globalScene
|
||||||
// The creation of the battlerBypassSpeed object contains checks for the ability Quick Draw and the held item Quick Claw
|
.getPlayerField()
|
||||||
// The ability Mycelium Might disables Quick Claw's activation when using a status move
|
.filter(p => p.isActive())
|
||||||
// This occurs before the main loop because of battles with more than two Pokemon
|
.map(p => p.getBattlerIndex());
|
||||||
const battlerBypassSpeed = {};
|
const enemyField = globalScene
|
||||||
|
.getEnemyField()
|
||||||
globalScene.getField(true).forEach(p => {
|
.filter(p => p.isActive())
|
||||||
const bypassSpeed = new BooleanHolder(false);
|
.map(p => p.getBattlerIndex());
|
||||||
const canCheckHeldItems = new BooleanHolder(true);
|
const orderedTargets: BattlerIndex[] = playerField.concat(enemyField);
|
||||||
applyAbAttrs("BypassSpeedChanceAbAttr", { pokemon: p, bypass: bypassSpeed });
|
|
||||||
applyAbAttrs("PreventBypassSpeedChanceAbAttr", {
|
|
||||||
pokemon: p,
|
|
||||||
bypass: bypassSpeed,
|
|
||||||
canCheckHeldItems: canCheckHeldItems,
|
|
||||||
});
|
|
||||||
if (canCheckHeldItems.value) {
|
|
||||||
globalScene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p, bypassSpeed);
|
|
||||||
}
|
|
||||||
battlerBypassSpeed[p.getBattlerIndex()] = bypassSpeed;
|
|
||||||
});
|
|
||||||
|
|
||||||
// The function begins sorting orderedTargets based on command priority, move priority, and possible speed bypasses.
|
// The function begins sorting orderedTargets based on command priority, move priority, and possible speed bypasses.
|
||||||
// Non-FIGHT commands (SWITCH, BALL, RUN) have a higher command priority and will always occur before any FIGHT commands.
|
// Non-FIGHT commands (SWITCH, BALL, RUN) have a higher command priority and will always occur before any FIGHT commands.
|
||||||
moveOrder = moveOrder.slice(0);
|
orderedTargets.sort((a, b) => {
|
||||||
moveOrder.sort((a, b) => {
|
|
||||||
const aCommand = globalScene.currentBattle.turnCommands[a];
|
const aCommand = globalScene.currentBattle.turnCommands[a];
|
||||||
const bCommand = globalScene.currentBattle.turnCommands[b];
|
const bCommand = globalScene.currentBattle.turnCommands[b];
|
||||||
|
|
||||||
@ -91,40 +39,14 @@ export class TurnStartPhase extends FieldPhase {
|
|||||||
if (bCommand?.command === Command.FIGHT) {
|
if (bCommand?.command === Command.FIGHT) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
} else if (aCommand?.command === Command.FIGHT) {
|
|
||||||
const aMove = allMoves[aCommand.move!.move];
|
|
||||||
const bMove = allMoves[bCommand!.move!.move];
|
|
||||||
|
|
||||||
const aUser = globalScene.getField(true).find(p => p.getBattlerIndex() === a)!;
|
|
||||||
const bUser = globalScene.getField(true).find(p => p.getBattlerIndex() === b)!;
|
|
||||||
|
|
||||||
const aPriority = aMove.getPriority(aUser, false);
|
|
||||||
const bPriority = bMove.getPriority(bUser, false);
|
|
||||||
|
|
||||||
// The game now checks for differences in priority levels.
|
|
||||||
// If the moves share the same original priority bracket, it can check for differences in battlerBypassSpeed and return the result.
|
|
||||||
// This conditional is used to ensure that Quick Claw can still activate with abilities like Stall and Mycelium Might (attack moves only)
|
|
||||||
// Otherwise, the game returns the user of the move with the highest priority.
|
|
||||||
const isSameBracket = Math.ceil(aPriority) - Math.ceil(bPriority) === 0;
|
|
||||||
if (aPriority !== bPriority) {
|
|
||||||
if (isSameBracket && battlerBypassSpeed[a].value !== battlerBypassSpeed[b].value) {
|
|
||||||
return battlerBypassSpeed[a].value ? -1 : 1;
|
|
||||||
}
|
|
||||||
return aPriority < bPriority ? 1 : -1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is no difference between the move's calculated priorities, the game checks for differences in battlerBypassSpeed and returns the result.
|
const aIndex = orderedTargets.indexOf(a);
|
||||||
if (battlerBypassSpeed[a].value !== battlerBypassSpeed[b].value) {
|
const bIndex = orderedTargets.indexOf(b);
|
||||||
return battlerBypassSpeed[a].value ? -1 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const aIndex = moveOrder.indexOf(a);
|
|
||||||
const bIndex = moveOrder.indexOf(b);
|
|
||||||
|
|
||||||
return aIndex < bIndex ? -1 : aIndex > bIndex ? 1 : 0;
|
return aIndex < bIndex ? -1 : aIndex > bIndex ? 1 : 0;
|
||||||
});
|
});
|
||||||
return moveOrder;
|
return orderedTargets;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Refactor this alongside `CommandPhase.handleCommand` to use SEPARATE METHODS
|
// TODO: Refactor this alongside `CommandPhase.handleCommand` to use SEPARATE METHODS
|
||||||
@ -133,25 +55,27 @@ export class TurnStartPhase extends FieldPhase {
|
|||||||
super.start();
|
super.start();
|
||||||
|
|
||||||
const field = globalScene.getField();
|
const field = globalScene.getField();
|
||||||
|
const activeField = globalScene.getField(true);
|
||||||
const moveOrder = this.getCommandOrder();
|
const moveOrder = this.getCommandOrder();
|
||||||
|
|
||||||
let orderIndex = 0;
|
applyInSpeedOrder(activeField, (p: Pokemon) => {
|
||||||
|
const preTurnCommand = globalScene.currentBattle.preTurnCommands[p.getBattlerIndex()];
|
||||||
for (const o of this.getSpeedOrder()) {
|
|
||||||
const pokemon = field[o];
|
|
||||||
const preTurnCommand = globalScene.currentBattle.preTurnCommands[o];
|
|
||||||
|
|
||||||
if (preTurnCommand?.skip) {
|
if (preTurnCommand?.skip) {
|
||||||
continue;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (preTurnCommand?.command) {
|
switch (preTurnCommand?.command) {
|
||||||
case Command.TERA:
|
case Command.TERA:
|
||||||
globalScene.phaseManager.pushNew("TeraPhase", pokemon);
|
globalScene.phaseManager.pushNew("TeraPhase", p);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
const phaseManager = globalScene.phaseManager;
|
const phaseManager = globalScene.phaseManager;
|
||||||
|
applyInSpeedOrder(activeField, (p: Pokemon) => {
|
||||||
|
applyAbAttrs("BypassSpeedChanceAbAttr", { pokemon: p });
|
||||||
|
globalScene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p);
|
||||||
|
});
|
||||||
|
|
||||||
for (const o of moveOrder) {
|
for (const o of moveOrder) {
|
||||||
const pokemon = field[o];
|
const pokemon = field[o];
|
||||||
@ -164,7 +88,6 @@ export class TurnStartPhase extends FieldPhase {
|
|||||||
switch (turnCommand?.command) {
|
switch (turnCommand?.command) {
|
||||||
case Command.FIGHT: {
|
case Command.FIGHT: {
|
||||||
const queuedMove = turnCommand.move;
|
const queuedMove = turnCommand.move;
|
||||||
pokemon.turnData.order = orderIndex++;
|
|
||||||
if (!queuedMove) {
|
if (!queuedMove) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -172,7 +95,7 @@ export class TurnStartPhase extends FieldPhase {
|
|||||||
pokemon.getMoveset().find(m => m.moveId === queuedMove.move && m.ppUsed < m.getMovePp()) ??
|
pokemon.getMoveset().find(m => m.moveId === queuedMove.move && m.ppUsed < m.getMovePp()) ??
|
||||||
new PokemonMove(queuedMove.move);
|
new PokemonMove(queuedMove.move);
|
||||||
if (move.getMove().hasAttr("MoveHeaderAttr")) {
|
if (move.getMove().hasAttr("MoveHeaderAttr")) {
|
||||||
phaseManager.unshiftNew("MoveHeaderPhase", pokemon, move);
|
phaseManager.pushNew("MoveHeaderPhase", pokemon.getBattlerIndex(), move);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pokemon.isPlayer() && turnCommand.cursor === -1) {
|
if (pokemon.isPlayer() && turnCommand.cursor === -1) {
|
||||||
@ -200,7 +123,7 @@ export class TurnStartPhase extends FieldPhase {
|
|||||||
case Command.POKEMON:
|
case Command.POKEMON:
|
||||||
{
|
{
|
||||||
const switchType = turnCommand.args?.[0] ? SwitchType.BATON_PASS : SwitchType.SWITCH;
|
const switchType = turnCommand.args?.[0] ? SwitchType.BATON_PASS : SwitchType.SWITCH;
|
||||||
phaseManager.unshiftNew(
|
phaseManager.pushNew(
|
||||||
"SwitchSummonPhase",
|
"SwitchSummonPhase",
|
||||||
switchType,
|
switchType,
|
||||||
pokemon.getFieldIndex(),
|
pokemon.getFieldIndex(),
|
||||||
@ -219,14 +142,6 @@ export class TurnStartPhase extends FieldPhase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
phaseManager.pushNew("WeatherEffectPhase");
|
|
||||||
phaseManager.pushNew("BerryPhase");
|
|
||||||
|
|
||||||
/** Add a new phase to check who should be taking status damage */
|
|
||||||
phaseManager.pushNew("CheckStatusEffectPhase", moveOrder);
|
|
||||||
|
|
||||||
phaseManager.pushNew("TurnEndPhase");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* this.end() will call shiftPhase(), which dumps everything from PrependQueue (aka everything that is unshifted()) to the front
|
* this.end() will call shiftPhase(), which dumps everything from PrependQueue (aka everything that is unshifted()) to the front
|
||||||
* of the queue and dequeues to start the next phase
|
* of the queue and dequeues to start the next phase
|
||||||
|
103
src/queues/dynamic-queue-manager.ts
Normal file
103
src/queues/dynamic-queue-manager.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import type { PhaseConditionFunc } from "#app/@types/phase-condition";
|
||||||
|
import type { PhaseString, DynamicPhase } from "#app/@types/phase-types";
|
||||||
|
import type { PokemonMove } from "#app/data/moves/pokemon-move";
|
||||||
|
import type Pokemon from "#app/field/pokemon";
|
||||||
|
import type { Phase } from "#app/phase";
|
||||||
|
import { MovePhasePriorityQueue } from "#app/queues/move-phase-priority-queue";
|
||||||
|
import type { PhasePriorityQueue } from "#app/queues/phase-priority-queue";
|
||||||
|
import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-queue";
|
||||||
|
import { PostSummonPhasePriorityQueue } from "#app/queues/post-summon-phase-priority-queue";
|
||||||
|
import { SwitchSummonPhasePriorityQueue } from "#app/queues/switch-summon-phase-priority-queue";
|
||||||
|
import type { BattlerIndex } from "#enums/battler-index";
|
||||||
|
import type { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
|
||||||
|
|
||||||
|
export class DynamicQueueManager {
|
||||||
|
private dynamicPhaseMap: Map<PhaseString, PhasePriorityQueue<Phase>>;
|
||||||
|
private alwaysDynamic: PhaseString[] = ["SwitchSummonPhase", "PostSummonPhase", "MovePhase"];
|
||||||
|
private popOrder: PhaseString[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.dynamicPhaseMap = new Map();
|
||||||
|
this.dynamicPhaseMap.set("SwitchSummonPhase", new SwitchSummonPhasePriorityQueue());
|
||||||
|
this.dynamicPhaseMap.set("PostSummonPhase", new PostSummonPhasePriorityQueue());
|
||||||
|
this.dynamicPhaseMap.set("MovePhase", new MovePhasePriorityQueue());
|
||||||
|
}
|
||||||
|
|
||||||
|
public clearQueues(): void {
|
||||||
|
for (const queue of this.dynamicPhaseMap.values()) {
|
||||||
|
queue.clear();
|
||||||
|
}
|
||||||
|
this.popOrder.splice(0, this.popOrder.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public queueDynamicPhase<T extends DynamicPhase>(phase: T): void {
|
||||||
|
if (!this.dynamicPhaseMap.has(phase.phaseName)) {
|
||||||
|
this.dynamicPhaseMap.set(phase.phaseName, new PokemonPhasePriorityQueue<T>());
|
||||||
|
}
|
||||||
|
this.dynamicPhaseMap.get(phase.phaseName)?.push(phase);
|
||||||
|
this.popOrder.push(phase.phaseName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public popNextPhase(): Phase | undefined {
|
||||||
|
const type = this.popOrder.pop();
|
||||||
|
if (!type) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.alwaysDynamic.includes(type)) {
|
||||||
|
return this.dynamicPhaseMap.get(type)?.pop();
|
||||||
|
}
|
||||||
|
return this.alwaysDynamic
|
||||||
|
.map((p: PhaseString) => this.dynamicPhaseMap.get(p))
|
||||||
|
.find(q => q && !q.isEmpty())
|
||||||
|
?.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public findPhaseOfType(type: PhaseString, condition?: PhaseConditionFunc): Phase | undefined {
|
||||||
|
return this.dynamicPhaseMap.get(type)?.findPhase(condition);
|
||||||
|
}
|
||||||
|
|
||||||
|
public activeQueueExists(type: PhaseString) {
|
||||||
|
return this.alwaysDynamic.includes(type) || this.dynamicPhaseMap.get(type)?.isEmpty() === false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public exists(type: PhaseString, condition?: PhaseConditionFunc): boolean {
|
||||||
|
return !!this.dynamicPhaseMap.get(type)?.hasPhaseWithCondition(condition);
|
||||||
|
}
|
||||||
|
|
||||||
|
public removePhase(condition: PhaseConditionFunc) {
|
||||||
|
for (const queue of this.dynamicPhaseMap.values()) {
|
||||||
|
if (queue.remove(condition)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setMoveTimingModifier(condition: PhaseConditionFunc, modifier: MovePhaseTimingModifier) {
|
||||||
|
this.getMovePhaseQueue().setTimingModifier(condition, modifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setMoveForPhase(condition: PhaseConditionFunc, move: PokemonMove) {
|
||||||
|
this.getMovePhaseQueue().setMoveForPhase(condition, move);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setMoveOrder(order: BattlerIndex[]) {
|
||||||
|
this.getMovePhaseQueue().setMoveOrder(order);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getLastTurnOrder(): Pokemon[] {
|
||||||
|
return this.getMovePhaseQueue().getTurnOrder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public clearLastTurnOrder(): void {
|
||||||
|
this.getMovePhaseQueue().clearTurnOrder();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getMovePhaseQueue(): MovePhasePriorityQueue {
|
||||||
|
return this.dynamicPhaseMap.get("MovePhase") as MovePhasePriorityQueue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public addPopType(type: PhaseString): void {
|
||||||
|
this.popOrder.push(type);
|
||||||
|
}
|
||||||
|
}
|
84
src/queues/move-phase-priority-queue.ts
Normal file
84
src/queues/move-phase-priority-queue.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import type { PhaseConditionFunc } from "#app/@types/phase-condition";
|
||||||
|
import type { PokemonMove } from "#app/data/moves/pokemon-move";
|
||||||
|
import type Pokemon from "#app/field/pokemon";
|
||||||
|
import type { MovePhase } from "#app/phases/move-phase";
|
||||||
|
import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-queue";
|
||||||
|
import { isNullOrUndefined } from "#app/utils/common";
|
||||||
|
import type { BattlerIndex } from "#enums/battler-index";
|
||||||
|
import type { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
|
||||||
|
|
||||||
|
export class MovePhasePriorityQueue extends PokemonPhasePriorityQueue<MovePhase> {
|
||||||
|
private lastTurnOrder: Pokemon[] = [];
|
||||||
|
|
||||||
|
public override reorder(): void {
|
||||||
|
super.reorder();
|
||||||
|
this.sortPostSpeed();
|
||||||
|
console.log(this.queue.map(p => p.getPokemon().name));
|
||||||
|
}
|
||||||
|
|
||||||
|
public setTimingModifier(condition: PhaseConditionFunc, modifier: MovePhaseTimingModifier): void {
|
||||||
|
const phase = this.queue.find(phase => condition(phase));
|
||||||
|
if (!isNullOrUndefined(phase)) {
|
||||||
|
phase.timingModifier = modifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public setMoveForPhase(condition: PhaseConditionFunc, move: PokemonMove) {
|
||||||
|
const phase = this.queue.find(phase => condition(phase));
|
||||||
|
if (!isNullOrUndefined(phase)) {
|
||||||
|
phase.move = move;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public setMoveOrder(order: BattlerIndex[]) {
|
||||||
|
this.setOrder = order;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override pop(): MovePhase | undefined {
|
||||||
|
this.reorder();
|
||||||
|
const phase = this.queue.shift();
|
||||||
|
if (phase) {
|
||||||
|
this.lastTurnOrder.push(phase.pokemon);
|
||||||
|
}
|
||||||
|
return phase;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTurnOrder(): Pokemon[] {
|
||||||
|
return this.lastTurnOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public clearTurnOrder(): void {
|
||||||
|
this.lastTurnOrder = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public override clear(): void {
|
||||||
|
this.setOrder = undefined;
|
||||||
|
this.lastTurnOrder = [];
|
||||||
|
super.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sortPostSpeed(): void {
|
||||||
|
this.queue.sort((a: MovePhase, b: MovePhase) => {
|
||||||
|
const priority = [a, b].map(movePhase => {
|
||||||
|
const move = movePhase.move.getMove();
|
||||||
|
return move.getPriority(movePhase.pokemon, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
const priorityModifiers = [a, b].map(movePhase =>
|
||||||
|
movePhase.move.getMove().getPriorityModifier(movePhase.pokemon),
|
||||||
|
);
|
||||||
|
|
||||||
|
const timingModifiers = [a, b].map(movePhase => movePhase.timingModifier);
|
||||||
|
|
||||||
|
if (timingModifiers[0] !== timingModifiers[1]) {
|
||||||
|
return timingModifiers[1] - timingModifiers[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (priority[0] === priority[1] && priorityModifiers[0] !== priorityModifiers[1]) {
|
||||||
|
return priorityModifiers[1] - priorityModifiers[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return priority[1] - priority[0];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
61
src/queues/phase-priority-queue.ts
Normal file
61
src/queues/phase-priority-queue.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import type { PhaseConditionFunc } from "#app/@types/phase-condition";
|
||||||
|
import type { Phase } from "#app/phase";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores a list of {@linkcode Phase}s
|
||||||
|
*
|
||||||
|
* Dynamically updates ordering to always pop the highest "priority", based on implementation of {@linkcode reorder}
|
||||||
|
*/
|
||||||
|
export abstract class PhasePriorityQueue<T extends Phase> {
|
||||||
|
protected queue: T[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sorts the elements in the queue
|
||||||
|
*/
|
||||||
|
public abstract reorder(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls {@linkcode reorder} and shifts the queue
|
||||||
|
* @returns The front element of the queue after sorting
|
||||||
|
*/
|
||||||
|
public pop(): T | undefined {
|
||||||
|
this.reorder();
|
||||||
|
return this.queue.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a phase to the queue
|
||||||
|
* @param phase The phase to add
|
||||||
|
*/
|
||||||
|
public push(phase: T): void {
|
||||||
|
this.queue.push(phase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all phases from the queue
|
||||||
|
*/
|
||||||
|
public clear(): void {
|
||||||
|
this.queue.splice(0, this.queue.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isEmpty(): boolean {
|
||||||
|
return !this.queue.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public remove(condition: PhaseConditionFunc): boolean {
|
||||||
|
const phaseIndex = this.queue.findIndex(condition);
|
||||||
|
if (phaseIndex > -1) {
|
||||||
|
this.queue.splice(phaseIndex, 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public findPhase(condition?: PhaseConditionFunc): Phase | undefined {
|
||||||
|
return this.queue.find(phase => !condition || condition(phase));
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasPhaseWithCondition(condition?: PhaseConditionFunc): boolean {
|
||||||
|
return this.queue.find(phase => !condition || condition(phase)) !== undefined;
|
||||||
|
}
|
||||||
|
}
|
20
src/queues/pokemon-phase-priority-queue.ts
Normal file
20
src/queues/pokemon-phase-priority-queue.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import type { DynamicPhase } from "#app/@types/phase-types";
|
||||||
|
import { PhasePriorityQueue } from "#app/queues/phase-priority-queue";
|
||||||
|
import { sortInSpeedOrder } from "#app/utils/speed-order";
|
||||||
|
import type { BattlerIndex } from "#enums/battler-index";
|
||||||
|
|
||||||
|
export class PokemonPhasePriorityQueue<T extends DynamicPhase> extends PhasePriorityQueue<T> {
|
||||||
|
protected setOrder: BattlerIndex[] | undefined;
|
||||||
|
public override reorder(): void {
|
||||||
|
this.queue = this.queue.filter(phase => phase.getPokemon()?.isActive(true));
|
||||||
|
if (this.setOrder) {
|
||||||
|
this.queue.sort(
|
||||||
|
(a, b) =>
|
||||||
|
this.setOrder!.indexOf(a.getPokemon().getBattlerIndex()) -
|
||||||
|
this.setOrder!.indexOf(b.getPokemon().getBattlerIndex()),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.queue = sortInSpeedOrder(this.queue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
38
src/queues/post-summon-phase-priority-queue.ts
Normal file
38
src/queues/post-summon-phase-priority-queue.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { globalScene } from "#app/global-scene";
|
||||||
|
import { PostSummonActivateAbilityPhase } from "#app/phases/post-summon-activate-ability-phase";
|
||||||
|
import type { PostSummonPhase } from "#app/phases/post-summon-phase";
|
||||||
|
import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-queue";
|
||||||
|
import { sortInSpeedOrder } from "#app/utils/speed-order";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Priority Queue for {@linkcode PostSummonPhase} and {@linkcode PostSummonActivateAbilityPhase}
|
||||||
|
*
|
||||||
|
* Orders phases first by ability priority, then by the {@linkcode Pokemon}'s effective speed
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class PostSummonPhasePriorityQueue extends PokemonPhasePriorityQueue<PostSummonPhase> {
|
||||||
|
public override reorder(): void {
|
||||||
|
this.queue = sortInSpeedOrder(this.queue, false);
|
||||||
|
this.queue.sort((phaseA: PostSummonPhase, phaseB: PostSummonPhase) => {
|
||||||
|
return phaseB.getPriority() - phaseA.getPriority();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public override push(phase: PostSummonPhase): void {
|
||||||
|
super.push(phase);
|
||||||
|
this.queueAbilityPhase(phase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queues all necessary {@linkcode PostSummonActivateAbilityPhase}s for each pushed {@linkcode PostSummonPhase}
|
||||||
|
* @param phase The {@linkcode PostSummonPhase} that was pushed onto the queue
|
||||||
|
*/
|
||||||
|
private queueAbilityPhase(phase: PostSummonPhase): void {
|
||||||
|
const phasePokemon = phase.getPokemon();
|
||||||
|
|
||||||
|
phasePokemon.getAbilityPriorities().forEach((priority, idx) => {
|
||||||
|
this.queue.push(new PostSummonActivateAbilityPhase(phasePokemon.getBattlerIndex(), priority, !!idx));
|
||||||
|
globalScene.phaseManager.dynamicQueueManager.addPopType("PostSummonPhase");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
14
src/queues/switch-summon-phase-priority-queue.ts
Normal file
14
src/queues/switch-summon-phase-priority-queue.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import type { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
|
||||||
|
import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-queue";
|
||||||
|
|
||||||
|
export class SwitchSummonPhasePriorityQueue extends PokemonPhasePriorityQueue<SwitchSummonPhase> {
|
||||||
|
public override push(phase: SwitchSummonPhase): void {
|
||||||
|
// The same pokemon or slot cannot be switched into at the same time
|
||||||
|
this.queue = this.queue.filter(
|
||||||
|
old =>
|
||||||
|
old.getPokemon() !== phase.getPokemon() &&
|
||||||
|
!(old.isPlayer() === phase.isPlayer() && old.getFieldIndex() === phase.getFieldIndex()),
|
||||||
|
);
|
||||||
|
super.push(phase);
|
||||||
|
}
|
||||||
|
}
|
50
src/utils/speed-order.ts
Normal file
50
src/utils/speed-order.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import Pokemon from "#app/field/pokemon";
|
||||||
|
import { globalScene } from "#app/global-scene";
|
||||||
|
import { BooleanHolder, randSeedShuffle } from "#app/utils/common";
|
||||||
|
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||||
|
import { Stat } from "#enums/stat";
|
||||||
|
|
||||||
|
export interface hasPokemon {
|
||||||
|
getPokemon(): Pokemon;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyInSpeedOrder<T extends Pokemon>(pokemonList: T[], callback: (pokemon: Pokemon) => void): void {
|
||||||
|
sortInSpeedOrder(pokemonList).forEach(pokemon => callback(pokemon));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sortInSpeedOrder<T extends Pokemon | hasPokemon>(pokemonList: T[], shuffleFirst = true): T[] {
|
||||||
|
pokemonList = shuffleFirst ? shuffle(pokemonList) : pokemonList;
|
||||||
|
sortBySpeed(pokemonList);
|
||||||
|
return pokemonList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Randomly shuffles the queue. */
|
||||||
|
function shuffle<T extends Pokemon | hasPokemon>(pokemonList: T[]): T[] {
|
||||||
|
// This is seeded with the current turn to prevent an inconsistency where it
|
||||||
|
// was varying based on how long since you last reloaded
|
||||||
|
globalScene.executeWithSeedOffset(
|
||||||
|
() => {
|
||||||
|
pokemonList = randSeedShuffle(pokemonList);
|
||||||
|
},
|
||||||
|
globalScene.currentBattle.turn * 1000 + pokemonList.length,
|
||||||
|
globalScene.waveSeed,
|
||||||
|
);
|
||||||
|
return pokemonList;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortBySpeed<T extends Pokemon | hasPokemon>(pokemonList: T[]): void {
|
||||||
|
pokemonList.sort((a, b) => {
|
||||||
|
const [aSpeed, bSpeed] = [a, b].map(pkmn =>
|
||||||
|
pkmn instanceof Pokemon ? pkmn.getEffectiveStat(Stat.SPD) : pkmn.getPokemon().getEffectiveStat(Stat.SPD),
|
||||||
|
);
|
||||||
|
return bSpeed - aSpeed;
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 'true' if Trick Room is on the field. */
|
||||||
|
const speedReversed = new BooleanHolder(false);
|
||||||
|
globalScene.arena.applyTags(ArenaTagType.TRICK_ROOM, false, speedReversed);
|
||||||
|
|
||||||
|
if (speedReversed.value) {
|
||||||
|
pokemonList.reverse();
|
||||||
|
}
|
||||||
|
}
|
@ -35,6 +35,7 @@ describe("Abilities - Dancer", () => {
|
|||||||
await game.classicMode.startBattle([SpeciesId.ORICORIO, SpeciesId.FEEBAS]);
|
await game.classicMode.startBattle([SpeciesId.ORICORIO, SpeciesId.FEEBAS]);
|
||||||
|
|
||||||
const [oricorio, feebas] = game.scene.getPlayerField();
|
const [oricorio, feebas] = game.scene.getPlayerField();
|
||||||
|
const [magikarp1] = game.scene.getEnemyField();
|
||||||
game.move.changeMoveset(oricorio, [MoveId.SWORDS_DANCE, MoveId.VICTORY_DANCE, MoveId.SPLASH]);
|
game.move.changeMoveset(oricorio, [MoveId.SWORDS_DANCE, MoveId.VICTORY_DANCE, MoveId.SPLASH]);
|
||||||
game.move.changeMoveset(feebas, [MoveId.SWORDS_DANCE, MoveId.SPLASH]);
|
game.move.changeMoveset(feebas, [MoveId.SWORDS_DANCE, MoveId.SPLASH]);
|
||||||
|
|
||||||
@ -44,8 +45,9 @@ describe("Abilities - Dancer", () => {
|
|||||||
await game.phaseInterceptor.to("MovePhase"); // feebas uses swords dance
|
await game.phaseInterceptor.to("MovePhase"); // feebas uses swords dance
|
||||||
await game.phaseInterceptor.to("MovePhase", false); // oricorio copies swords dance
|
await game.phaseInterceptor.to("MovePhase", false); // oricorio copies swords dance
|
||||||
|
|
||||||
|
// Dancer order will be Magikarp, Oricorio, Magikarp based on set turn order
|
||||||
let currentPhase = game.scene.phaseManager.getCurrentPhase() as MovePhase;
|
let currentPhase = game.scene.phaseManager.getCurrentPhase() as MovePhase;
|
||||||
expect(currentPhase.pokemon).toBe(oricorio);
|
expect(currentPhase.pokemon).toBe(magikarp1);
|
||||||
expect(currentPhase.move.moveId).toBe(MoveId.SWORDS_DANCE);
|
expect(currentPhase.move.moveId).toBe(MoveId.SWORDS_DANCE);
|
||||||
|
|
||||||
await game.phaseInterceptor.to("MoveEndPhase"); // end oricorio's move
|
await game.phaseInterceptor.to("MoveEndPhase"); // end oricorio's move
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import { TurnEndPhase } from "#app/phases/turn-end-phase";
|
|
||||||
import { TurnStartPhase } from "#app/phases/turn-start-phase";
|
|
||||||
import GameManager from "#test/testUtils/gameManager";
|
import GameManager from "#test/testUtils/gameManager";
|
||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
import { Stat } from "#enums/stat";
|
import { Stat } from "#enums/stat";
|
||||||
@ -45,65 +43,50 @@ describe("Abilities - Mycelium Might", () => {
|
|||||||
it("will move last in its priority bracket and ignore protective abilities", async () => {
|
it("will move last in its priority bracket and ignore protective abilities", async () => {
|
||||||
await game.classicMode.startBattle([SpeciesId.REGIELEKI]);
|
await game.classicMode.startBattle([SpeciesId.REGIELEKI]);
|
||||||
|
|
||||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
const enemy = game.scene.getEnemyPokemon()!;
|
||||||
const playerIndex = game.scene.getPlayerPokemon()?.getBattlerIndex();
|
const player = game.scene.getPlayerPokemon()!;
|
||||||
const enemyIndex = enemyPokemon?.getBattlerIndex();
|
|
||||||
|
|
||||||
game.move.select(MoveId.BABY_DOLL_EYES);
|
game.move.select(MoveId.BABY_DOLL_EYES);
|
||||||
|
|
||||||
await game.phaseInterceptor.to(TurnStartPhase, false);
|
await game.phaseInterceptor.to("MoveEndPhase", false);
|
||||||
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
|
|
||||||
const speedOrder = phase.getSpeedOrder();
|
|
||||||
const commandOrder = phase.getCommandOrder();
|
|
||||||
// The opponent Pokemon (without Mycelium Might) goes first despite having lower speed than the player Pokemon.
|
// The opponent Pokemon (without Mycelium Might) goes first despite having lower speed than the player Pokemon.
|
||||||
// The player Pokemon (with Mycelium Might) goes last despite having higher speed than the opponent.
|
// The player Pokemon (with Mycelium Might) goes last despite having higher speed than the opponent.
|
||||||
expect(speedOrder).toEqual([playerIndex, enemyIndex]);
|
expect(player.hp).not.toEqual(player.getMaxHp());
|
||||||
expect(commandOrder).toEqual([enemyIndex, playerIndex]);
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
|
|
||||||
// Despite the opponent's ability (Clear Body), its ATK stat stage is still reduced.
|
// Despite the opponent's ability (Clear Body), its ATK stat stage is still reduced.
|
||||||
expect(enemyPokemon?.getStatStage(Stat.ATK)).toBe(-1);
|
expect(enemy.getStatStage(Stat.ATK)).toBe(-1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("will still go first if a status move that is in a higher priority bracket than the opponent's move is used", async () => {
|
it("will still go first if a status move that is in a higher priority bracket than the opponent's move is used", async () => {
|
||||||
game.override.enemyMoveset(MoveId.TACKLE);
|
game.override.enemyMoveset(MoveId.TACKLE);
|
||||||
await game.classicMode.startBattle([SpeciesId.REGIELEKI]);
|
await game.classicMode.startBattle([SpeciesId.REGIELEKI]);
|
||||||
|
|
||||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
const enemy = game.scene.getEnemyPokemon()!;
|
||||||
const playerIndex = game.scene.getPlayerPokemon()?.getBattlerIndex();
|
const player = game.scene.getPlayerPokemon()!;
|
||||||
const enemyIndex = enemyPokemon?.getBattlerIndex();
|
|
||||||
|
|
||||||
game.move.select(MoveId.BABY_DOLL_EYES);
|
game.move.select(MoveId.BABY_DOLL_EYES);
|
||||||
|
|
||||||
await game.phaseInterceptor.to(TurnStartPhase, false);
|
await game.phaseInterceptor.to("MoveEndPhase", false);
|
||||||
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
|
|
||||||
const speedOrder = phase.getSpeedOrder();
|
|
||||||
const commandOrder = phase.getCommandOrder();
|
|
||||||
// The player Pokemon (with M.M.) goes first because its move is still within a higher priority bracket than its opponent.
|
// The player Pokemon (with M.M.) goes first because its move is still within a higher priority bracket than its opponent.
|
||||||
// The enemy Pokemon goes second because its move is in a lower priority bracket.
|
// The enemy Pokemon goes second because its move is in a lower priority bracket.
|
||||||
expect(speedOrder).toEqual([playerIndex, enemyIndex]);
|
expect(player.hp).toEqual(player.getMaxHp());
|
||||||
expect(commandOrder).toEqual([playerIndex, enemyIndex]);
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
await game.phaseInterceptor.to(TurnEndPhase);
|
|
||||||
// Despite the opponent's ability (Clear Body), its ATK stat stage is still reduced.
|
// Despite the opponent's ability (Clear Body), its ATK stat stage is still reduced.
|
||||||
expect(enemyPokemon?.getStatStage(Stat.ATK)).toBe(-1);
|
expect(enemy.getStatStage(Stat.ATK)).toBe(-1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("will not affect non-status moves", async () => {
|
it("will not affect non-status moves", async () => {
|
||||||
await game.classicMode.startBattle([SpeciesId.REGIELEKI]);
|
await game.classicMode.startBattle([SpeciesId.REGIELEKI]);
|
||||||
|
|
||||||
const playerIndex = game.scene.getPlayerPokemon()!.getBattlerIndex();
|
const player = game.scene.getPlayerPokemon()!;
|
||||||
const enemyIndex = game.scene.getEnemyPokemon()!.getBattlerIndex();
|
|
||||||
|
|
||||||
game.move.select(MoveId.QUICK_ATTACK);
|
game.move.select(MoveId.QUICK_ATTACK);
|
||||||
|
|
||||||
await game.phaseInterceptor.to(TurnStartPhase, false);
|
await game.phaseInterceptor.to("MoveEndPhase", false);
|
||||||
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
|
|
||||||
const speedOrder = phase.getSpeedOrder();
|
|
||||||
const commandOrder = phase.getCommandOrder();
|
|
||||||
// The player Pokemon (with M.M.) goes first because it has a higher speed and did not use a status move.
|
// The player Pokemon (with M.M.) goes first because it has a higher speed and did not use a status move.
|
||||||
// The enemy Pokemon (without M.M.) goes second because its speed is lower.
|
// The enemy Pokemon (without M.M.) goes second because its speed is lower.
|
||||||
// This means that the commandOrder should be identical to the speedOrder
|
// This means that the commandOrder should be identical to the speedOrder
|
||||||
expect(speedOrder).toEqual([playerIndex, enemyIndex]);
|
expect(player.hp).toEqual(player.getMaxHp());
|
||||||
expect(commandOrder).toEqual([playerIndex, enemyIndex]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -59,7 +59,7 @@ describe("Abilities - Neutralizing Gas", () => {
|
|||||||
expect(game.scene.getPlayerPokemon()?.getStatStage(Stat.ATK)).toBe(1);
|
expect(game.scene.getPlayerPokemon()?.getStatStage(Stat.ATK)).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.todo("should activate before other abilities", async () => {
|
it("should activate before other abilities", async () => {
|
||||||
game.override.enemySpecies(SpeciesId.ACCELGOR).enemyLevel(100).enemyAbility(AbilityId.INTIMIDATE);
|
game.override.enemySpecies(SpeciesId.ACCELGOR).enemyLevel(100).enemyAbility(AbilityId.INTIMIDATE);
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||||
|
@ -5,7 +5,7 @@ import { MoveId } from "#enums/move-id";
|
|||||||
import { SpeciesId } from "#enums/species-id";
|
import { SpeciesId } from "#enums/species-id";
|
||||||
import GameManager from "#test/testUtils/gameManager";
|
import GameManager from "#test/testUtils/gameManager";
|
||||||
import Phaser from "phaser";
|
import Phaser from "phaser";
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
describe("Abilities - Quick Draw", () => {
|
describe("Abilities - Quick Draw", () => {
|
||||||
let phaserGame: Phaser.Game;
|
let phaserGame: Phaser.Game;
|
||||||
@ -25,7 +25,6 @@ describe("Abilities - Quick Draw", () => {
|
|||||||
game = new GameManager(phaserGame);
|
game = new GameManager(phaserGame);
|
||||||
game.override
|
game.override
|
||||||
.battleStyle("single")
|
.battleStyle("single")
|
||||||
.starterSpecies(SpeciesId.MAGIKARP)
|
|
||||||
.ability(AbilityId.QUICK_DRAW)
|
.ability(AbilityId.QUICK_DRAW)
|
||||||
.moveset([MoveId.TACKLE, MoveId.TAIL_WHIP])
|
.moveset([MoveId.TACKLE, MoveId.TAIL_WHIP])
|
||||||
.enemyLevel(100)
|
.enemyLevel(100)
|
||||||
@ -40,8 +39,8 @@ describe("Abilities - Quick Draw", () => {
|
|||||||
).mockReturnValue(100);
|
).mockReturnValue(100);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("makes pokemon going first in its priority bracket", async () => {
|
it("makes pokemon go first in its priority bracket", async () => {
|
||||||
await game.classicMode.startBattle();
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
const pokemon = game.scene.getPlayerPokemon()!;
|
const pokemon = game.scene.getPlayerPokemon()!;
|
||||||
const enemy = game.scene.getEnemyPokemon()!;
|
const enemy = game.scene.getEnemyPokemon()!;
|
||||||
@ -57,33 +56,27 @@ describe("Abilities - Quick Draw", () => {
|
|||||||
expect(pokemon.waveData.abilitiesApplied).toContain(AbilityId.QUICK_DRAW);
|
expect(pokemon.waveData.abilitiesApplied).toContain(AbilityId.QUICK_DRAW);
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
it("is not triggered by non damaging moves", async () => {
|
||||||
"does not triggered by non damage moves",
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||||
{
|
|
||||||
retry: 5,
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
await game.classicMode.startBattle();
|
|
||||||
|
|
||||||
const pokemon = game.scene.getPlayerPokemon()!;
|
const pokemon = game.scene.getPlayerPokemon()!;
|
||||||
const enemy = game.scene.getEnemyPokemon()!;
|
const enemy = game.scene.getEnemyPokemon()!;
|
||||||
|
|
||||||
pokemon.hp = 1;
|
pokemon.hp = 1;
|
||||||
enemy.hp = 1;
|
enemy.hp = 1;
|
||||||
|
|
||||||
game.move.select(MoveId.TAIL_WHIP);
|
game.move.select(MoveId.TAIL_WHIP);
|
||||||
await game.phaseInterceptor.to(FaintPhase, false);
|
await game.phaseInterceptor.to(FaintPhase, false);
|
||||||
|
|
||||||
expect(pokemon.isFainted()).toBe(true);
|
expect(pokemon.isFainted()).toBe(true);
|
||||||
expect(enemy.isFainted()).toBe(false);
|
expect(enemy.isFainted()).toBe(false);
|
||||||
expect(pokemon.waveData.abilitiesApplied).not.contain(AbilityId.QUICK_DRAW);
|
expect(pokemon.waveData.abilitiesApplied).not.contain(AbilityId.QUICK_DRAW);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
test("does not increase priority", async () => {
|
it("does not increase priority", async () => {
|
||||||
game.override.enemyMoveset([MoveId.EXTREME_SPEED]);
|
game.override.enemyMoveset([MoveId.EXTREME_SPEED]);
|
||||||
|
|
||||||
await game.classicMode.startBattle();
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||||
|
|
||||||
const pokemon = game.scene.getPlayerPokemon()!;
|
const pokemon = game.scene.getPlayerPokemon()!;
|
||||||
const enemy = game.scene.getEnemyPokemon()!;
|
const enemy = game.scene.getEnemyPokemon()!;
|
||||||
|
@ -4,7 +4,6 @@ import { SpeciesId } from "#enums/species-id";
|
|||||||
import GameManager from "#test/testUtils/gameManager";
|
import GameManager from "#test/testUtils/gameManager";
|
||||||
import Phaser from "phaser";
|
import Phaser from "phaser";
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||||
import { TurnStartPhase } from "#app/phases/turn-start-phase";
|
|
||||||
|
|
||||||
describe("Abilities - Stall", () => {
|
describe("Abilities - Stall", () => {
|
||||||
let phaserGame: Phaser.Game;
|
let phaserGame: Phaser.Game;
|
||||||
@ -40,56 +39,41 @@ describe("Abilities - Stall", () => {
|
|||||||
it("Pokemon with Stall should move last in its priority bracket regardless of speed", async () => {
|
it("Pokemon with Stall should move last in its priority bracket regardless of speed", async () => {
|
||||||
await game.classicMode.startBattle([SpeciesId.SHUCKLE]);
|
await game.classicMode.startBattle([SpeciesId.SHUCKLE]);
|
||||||
|
|
||||||
const playerIndex = game.scene.getPlayerPokemon()!.getBattlerIndex();
|
const player = game.scene.getPlayerPokemon()!;
|
||||||
const enemyIndex = game.scene.getEnemyPokemon()!.getBattlerIndex();
|
|
||||||
|
|
||||||
game.move.select(MoveId.QUICK_ATTACK);
|
game.move.select(MoveId.QUICK_ATTACK);
|
||||||
|
|
||||||
await game.phaseInterceptor.to(TurnStartPhase, false);
|
await game.phaseInterceptor.to("MoveEndPhase", false);
|
||||||
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
|
|
||||||
const speedOrder = phase.getSpeedOrder();
|
|
||||||
const commandOrder = phase.getCommandOrder();
|
|
||||||
// The player Pokemon (without Stall) goes first despite having lower speed than the opponent.
|
// The player Pokemon (without Stall) goes first despite having lower speed than the opponent.
|
||||||
// The opponent Pokemon (with Stall) goes last despite having higher speed than the player Pokemon.
|
// The opponent Pokemon (with Stall) goes last despite having higher speed than the player Pokemon.
|
||||||
expect(speedOrder).toEqual([enemyIndex, playerIndex]);
|
expect(player.hp).toEqual(player.getMaxHp());
|
||||||
expect(commandOrder).toEqual([playerIndex, enemyIndex]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Pokemon with Stall will go first if a move that is in a higher priority bracket than the opponent's move is used", async () => {
|
it("Pokemon with Stall will go first if a move that is in a higher priority bracket than the opponent's move is used", async () => {
|
||||||
await game.classicMode.startBattle([SpeciesId.SHUCKLE]);
|
await game.classicMode.startBattle([SpeciesId.SHUCKLE]);
|
||||||
|
|
||||||
const playerIndex = game.scene.getPlayerPokemon()!.getBattlerIndex();
|
const player = game.scene.getPlayerPokemon()!;
|
||||||
const enemyIndex = game.scene.getEnemyPokemon()!.getBattlerIndex();
|
|
||||||
|
|
||||||
game.move.select(MoveId.TACKLE);
|
game.move.select(MoveId.TACKLE);
|
||||||
|
|
||||||
await game.phaseInterceptor.to(TurnStartPhase, false);
|
await game.phaseInterceptor.to("MoveEndPhase", false);
|
||||||
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
|
|
||||||
const speedOrder = phase.getSpeedOrder();
|
|
||||||
const commandOrder = phase.getCommandOrder();
|
|
||||||
// The opponent Pokemon (with Stall) goes first because its move is still within a higher priority bracket than its opponent.
|
// The opponent Pokemon (with Stall) goes first because its move is still within a higher priority bracket than its opponent.
|
||||||
// The player Pokemon goes second because its move is in a lower priority bracket.
|
// The player Pokemon goes second because its move is in a lower priority bracket.
|
||||||
expect(speedOrder).toEqual([enemyIndex, playerIndex]);
|
expect(player.hp).not.toEqual(player.getMaxHp());
|
||||||
expect(commandOrder).toEqual([enemyIndex, playerIndex]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("If both Pokemon have stall and use the same move, speed is used to determine who goes first.", async () => {
|
it("If both Pokemon have stall and use the same move, speed is used to determine who goes first.", async () => {
|
||||||
game.override.ability(AbilityId.STALL);
|
game.override.ability(AbilityId.STALL);
|
||||||
await game.classicMode.startBattle([SpeciesId.SHUCKLE]);
|
await game.classicMode.startBattle([SpeciesId.SHUCKLE]);
|
||||||
|
|
||||||
const playerIndex = game.scene.getPlayerPokemon()!.getBattlerIndex();
|
const player = game.scene.getPlayerPokemon()!;
|
||||||
const enemyIndex = game.scene.getEnemyPokemon()!.getBattlerIndex();
|
|
||||||
|
|
||||||
game.move.select(MoveId.TACKLE);
|
game.move.select(MoveId.TACKLE);
|
||||||
|
|
||||||
await game.phaseInterceptor.to(TurnStartPhase, false);
|
await game.phaseInterceptor.to("MoveEndPhase", false);
|
||||||
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
|
|
||||||
const speedOrder = phase.getSpeedOrder();
|
|
||||||
const commandOrder = phase.getCommandOrder();
|
|
||||||
|
|
||||||
// The opponent Pokemon (with Stall) goes first because it has a higher speed.
|
// The opponent Pokemon (with Stall) goes first because it has a higher speed.
|
||||||
// The player Pokemon (with Stall) goes second because its speed is lower.
|
// The player Pokemon (with Stall) goes second because its speed is lower.
|
||||||
expect(speedOrder).toEqual([enemyIndex, playerIndex]);
|
expect(player.hp).not.toEqual(player.getMaxHp());
|
||||||
expect(commandOrder).toEqual([enemyIndex, playerIndex]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { EnemyCommandPhase } from "#app/phases/enemy-command-phase";
|
import type { MovePhase } from "#app/phases/move-phase";
|
||||||
import { SelectTargetPhase } from "#app/phases/select-target-phase";
|
|
||||||
import { TurnStartPhase } from "#app/phases/turn-start-phase";
|
|
||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
|
import { BattlerIndex } from "#enums/battler-index";
|
||||||
import { MoveId } from "#enums/move-id";
|
import { MoveId } from "#enums/move-id";
|
||||||
import { SpeciesId } from "#enums/species-id";
|
import { SpeciesId } from "#enums/species-id";
|
||||||
import GameManager from "#test/testUtils/gameManager";
|
import GameManager from "#test/testUtils/gameManager";
|
||||||
@ -36,38 +35,34 @@ describe("Battle order", () => {
|
|||||||
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
|
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
|
||||||
|
|
||||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||||
|
const playerStartHp = playerPokemon.hp;
|
||||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||||
|
const enemyStartHp = enemyPokemon.hp;
|
||||||
|
|
||||||
vi.spyOn(playerPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 50]); // set playerPokemon's speed to 50
|
vi.spyOn(playerPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 50]); // set playerPokemon's speed to 50
|
||||||
vi.spyOn(enemyPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set enemyPokemon's speed to 150
|
vi.spyOn(enemyPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set enemyPokemon's speed to 150
|
||||||
|
|
||||||
game.move.select(MoveId.TACKLE);
|
game.move.select(MoveId.TACKLE);
|
||||||
await game.phaseInterceptor.run(EnemyCommandPhase);
|
|
||||||
|
|
||||||
const playerPokemonIndex = playerPokemon.getBattlerIndex();
|
await game.phaseInterceptor.to("MoveEndPhase", false);
|
||||||
const enemyPokemonIndex = enemyPokemon.getBattlerIndex();
|
expect(playerPokemon.hp).not.toEqual(playerStartHp);
|
||||||
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
|
expect(enemyPokemon.hp).toEqual(enemyStartHp);
|
||||||
const order = phase.getCommandOrder();
|
|
||||||
expect(order[0]).toBe(enemyPokemonIndex);
|
|
||||||
expect(order[1]).toBe(playerPokemonIndex);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Player faster than opponent 150 vs 50", async () => {
|
it("Player faster than opponent 150 vs 50", async () => {
|
||||||
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
|
await game.classicMode.startBattle([SpeciesId.BULBASAUR]);
|
||||||
|
|
||||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||||
|
const playerStartHp = playerPokemon.hp;
|
||||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||||
|
const enemyStartHp = enemyPokemon.hp;
|
||||||
vi.spyOn(playerPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set playerPokemon's speed to 150
|
vi.spyOn(playerPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set playerPokemon's speed to 150
|
||||||
vi.spyOn(enemyPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 50]); // set enemyPokemon's speed to 50
|
vi.spyOn(enemyPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 50]); // set enemyPokemon's speed to 50
|
||||||
|
|
||||||
game.move.select(MoveId.TACKLE);
|
game.move.select(MoveId.TACKLE);
|
||||||
await game.phaseInterceptor.run(EnemyCommandPhase);
|
|
||||||
|
|
||||||
const playerPokemonIndex = playerPokemon.getBattlerIndex();
|
await game.phaseInterceptor.to("MoveEndPhase", false);
|
||||||
const enemyPokemonIndex = enemyPokemon.getBattlerIndex();
|
expect(playerPokemon.hp).toEqual(playerStartHp);
|
||||||
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
|
expect(enemyPokemon.hp).not.toEqual(enemyStartHp);
|
||||||
const order = phase.getCommandOrder();
|
|
||||||
expect(order[0]).toBe(playerPokemonIndex);
|
|
||||||
expect(order[1]).toBe(enemyPokemonIndex);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("double - both opponents faster than player 50/50 vs 150/150", async () => {
|
it("double - both opponents faster than player 50/50 vs 150/150", async () => {
|
||||||
@ -75,23 +70,24 @@ describe("Battle order", () => {
|
|||||||
await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.BLASTOISE]);
|
await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.BLASTOISE]);
|
||||||
|
|
||||||
const playerPokemon = game.scene.getPlayerField();
|
const playerPokemon = game.scene.getPlayerField();
|
||||||
|
const playerHps = playerPokemon.map(p => p.hp);
|
||||||
const enemyPokemon = game.scene.getEnemyField();
|
const enemyPokemon = game.scene.getEnemyField();
|
||||||
|
const enemyHps = enemyPokemon.map(p => p.hp);
|
||||||
|
|
||||||
playerPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 50])); // set both playerPokemons' speed to 50
|
playerPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 50])); // set both playerPokemons' speed to 50
|
||||||
enemyPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150])); // set both enemyPokemons' speed to 150
|
enemyPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150])); // set both enemyPokemons' speed to 150
|
||||||
const playerIndices = playerPokemon.map(p => p?.getBattlerIndex());
|
|
||||||
const enemyIndices = enemyPokemon.map(p => p?.getBattlerIndex());
|
|
||||||
|
|
||||||
game.move.select(MoveId.TACKLE);
|
game.move.select(MoveId.TACKLE);
|
||||||
game.move.select(MoveId.TACKLE, 1);
|
game.move.select(MoveId.TACKLE, 1);
|
||||||
await game.phaseInterceptor.runFrom(SelectTargetPhase).to(TurnStartPhase, false);
|
await game.move.selectEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER);
|
||||||
|
await game.move.selectEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER_2);
|
||||||
|
|
||||||
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
|
await game.phaseInterceptor.to("MoveEndPhase", true);
|
||||||
const order = phase.getCommandOrder();
|
await game.phaseInterceptor.to("MoveEndPhase", false);
|
||||||
expect(order.slice(0, 2).includes(enemyIndices[0])).toBe(true);
|
for (let i = 0; i < 2; i++) {
|
||||||
expect(order.slice(0, 2).includes(enemyIndices[1])).toBe(true);
|
expect(playerPokemon[i].hp).not.toEqual(playerHps[i]);
|
||||||
expect(order.slice(2, 4).includes(playerIndices[0])).toBe(true);
|
expect(enemyPokemon[i].hp).toEqual(enemyHps[i]);
|
||||||
expect(order.slice(2, 4).includes(playerIndices[1])).toBe(true);
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("double - speed tie except 1 - 100/100 vs 100/150", async () => {
|
it("double - speed tie except 1 - 100/100 vs 100/150", async () => {
|
||||||
@ -103,18 +99,13 @@ describe("Battle order", () => {
|
|||||||
playerPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 100])); //set both playerPokemons' speed to 100
|
playerPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 100])); //set both playerPokemons' speed to 100
|
||||||
vi.spyOn(enemyPokemon[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 100]); // set enemyPokemon's speed to 100
|
vi.spyOn(enemyPokemon[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 100]); // set enemyPokemon's speed to 100
|
||||||
vi.spyOn(enemyPokemon[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set enemyPokemon's speed to 150
|
vi.spyOn(enemyPokemon[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set enemyPokemon's speed to 150
|
||||||
const playerIndices = playerPokemon.map(p => p?.getBattlerIndex());
|
|
||||||
const enemyIndices = enemyPokemon.map(p => p?.getBattlerIndex());
|
|
||||||
|
|
||||||
game.move.select(MoveId.TACKLE);
|
game.move.select(MoveId.TACKLE);
|
||||||
game.move.select(MoveId.TACKLE, 1);
|
game.move.select(MoveId.TACKLE, 1);
|
||||||
await game.phaseInterceptor.runFrom(SelectTargetPhase).to(TurnStartPhase, false);
|
await game.phaseInterceptor.to("MovePhase", false);
|
||||||
|
|
||||||
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
|
const phase = game.scene.phaseManager.getCurrentPhase() as MovePhase;
|
||||||
const order = phase.getCommandOrder();
|
expect(phase.pokemon).toEqual(enemyPokemon[1]);
|
||||||
// enemy 2 should be first, followed by some other assortment of the other 3 pokemon
|
|
||||||
expect(order[0]).toBe(enemyIndices[1]);
|
|
||||||
expect(order.slice(1, 4)).toEqual(expect.arrayContaining([enemyIndices[0], ...playerIndices]));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("double - speed tie 100/150 vs 100/150", async () => {
|
it("double - speed tie 100/150 vs 100/150", async () => {
|
||||||
@ -127,17 +118,13 @@ describe("Battle order", () => {
|
|||||||
vi.spyOn(playerPokemon[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set other playerPokemon's speed to 150
|
vi.spyOn(playerPokemon[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set other playerPokemon's speed to 150
|
||||||
vi.spyOn(enemyPokemon[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 100]); // set one enemyPokemon's speed to 100
|
vi.spyOn(enemyPokemon[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 100]); // set one enemyPokemon's speed to 100
|
||||||
vi.spyOn(enemyPokemon[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set other enemyPokemon's speed to 150
|
vi.spyOn(enemyPokemon[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set other enemyPokemon's speed to 150
|
||||||
const playerIndices = playerPokemon.map(p => p?.getBattlerIndex());
|
|
||||||
const enemyIndices = enemyPokemon.map(p => p?.getBattlerIndex());
|
|
||||||
|
|
||||||
game.move.select(MoveId.TACKLE);
|
game.move.select(MoveId.TACKLE);
|
||||||
game.move.select(MoveId.TACKLE, 1);
|
game.move.select(MoveId.TACKLE, 1);
|
||||||
await game.phaseInterceptor.runFrom(SelectTargetPhase).to(TurnStartPhase, false);
|
|
||||||
|
|
||||||
const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase;
|
await game.phaseInterceptor.to("MovePhase", false);
|
||||||
const order = phase.getCommandOrder();
|
|
||||||
// P2/E2 should be randomly first/second, then P1/E1 randomly 3rd/4th
|
const phase = game.scene.phaseManager.getCurrentPhase() as MovePhase;
|
||||||
expect(order.slice(0, 2)).toStrictEqual(expect.arrayContaining([playerIndices[1], enemyIndices[1]]));
|
expect(enemyPokemon[1] === phase.pokemon || playerPokemon[1] === phase.pokemon);
|
||||||
expect(order.slice(2, 4)).toStrictEqual(expect.arrayContaining([playerIndices[0], enemyIndices[0]]));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -76,12 +76,7 @@ describe("Moves - Baton Pass", () => {
|
|||||||
expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.SPATK)).toEqual(2);
|
expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.SPATK)).toEqual(2);
|
||||||
// confirm that a switch actually happened. can't use species because I
|
// confirm that a switch actually happened. can't use species because I
|
||||||
// can't find a way to override trainer parties with more than 1 pokemon species
|
// can't find a way to override trainer parties with more than 1 pokemon species
|
||||||
expect(game.phaseInterceptor.log.slice(-4)).toEqual([
|
expect(game.scene.getEnemyPokemon()?.summonData.moveHistory.length).toEqual(0);
|
||||||
"MoveEffectPhase",
|
|
||||||
"SwitchSummonPhase",
|
|
||||||
"SummonPhase",
|
|
||||||
"PostSummonPhase",
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("doesn't transfer effects that aren't transferrable", async () => {
|
it("doesn't transfer effects that aren't transferrable", async () => {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { BerryPhase } from "#app/phases/berry-phase";
|
import { BerryPhase } from "#app/phases/berry-phase";
|
||||||
import { MessagePhase } from "#app/phases/message-phase";
|
import { MessagePhase } from "#app/phases/message-phase";
|
||||||
import { MoveHeaderPhase } from "#app/phases/move-header-phase";
|
|
||||||
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
|
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase";
|
||||||
import { TurnStartPhase } from "#app/phases/turn-start-phase";
|
import { TurnStartPhase } from "#app/phases/turn-start-phase";
|
||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
@ -116,7 +115,7 @@ describe("Moves - Focus Punch", () => {
|
|||||||
await game.phaseInterceptor.to(TurnStartPhase);
|
await game.phaseInterceptor.to(TurnStartPhase);
|
||||||
|
|
||||||
expect(game.scene.phaseManager.getCurrentPhase() instanceof SwitchSummonPhase).toBeTruthy();
|
expect(game.scene.phaseManager.getCurrentPhase() instanceof SwitchSummonPhase).toBeTruthy();
|
||||||
expect(game.scene.phaseManager.phaseQueue.find(phase => phase instanceof MoveHeaderPhase)).toBeDefined();
|
expect(game.scene.phaseManager.hasPhaseOfType("MoveHeaderPhase")).toBeTruthy();
|
||||||
});
|
});
|
||||||
it("should replace the 'but it failed' text when the user gets hit", async () => {
|
it("should replace the 'but it failed' text when the user gets hit", async () => {
|
||||||
game.override.enemyMoveset([MoveId.TACKLE]);
|
game.override.enemyMoveset([MoveId.TACKLE]);
|
||||||
|
@ -166,7 +166,7 @@ describe("Moves - Rage Fist", () => {
|
|||||||
|
|
||||||
// Charizard hit
|
// Charizard hit
|
||||||
game.move.select(MoveId.SPLASH);
|
game.move.select(MoveId.SPLASH);
|
||||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
expect(getPartyHitCount()).toEqual([1, 0]);
|
expect(getPartyHitCount()).toEqual([1, 0]);
|
||||||
|
|
||||||
|
@ -523,7 +523,7 @@ export default class GameManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Intercepts `TurnStartPhase` and mocks {@linkcode TurnStartPhase.getSpeedOrder}'s return value.
|
* Modifies the queue manager to return move phases in a particular order
|
||||||
* Used to manually modify Pokemon turn order.
|
* Used to manually modify Pokemon turn order.
|
||||||
* Note: This *DOES NOT* account for priority.
|
* Note: This *DOES NOT* account for priority.
|
||||||
* @param order - The turn order to set as an array of {@linkcode BattlerIndex}es.
|
* @param order - The turn order to set as an array of {@linkcode BattlerIndex}es.
|
||||||
@ -535,7 +535,7 @@ export default class GameManager {
|
|||||||
async setTurnOrder(order: BattlerIndex[]): Promise<void> {
|
async setTurnOrder(order: BattlerIndex[]): Promise<void> {
|
||||||
await this.phaseInterceptor.to(TurnStartPhase, false);
|
await this.phaseInterceptor.to(TurnStartPhase, false);
|
||||||
|
|
||||||
vi.spyOn(this.scene.phaseManager.getCurrentPhase() as TurnStartPhase, "getSpeedOrder").mockReturnValue(order);
|
this.scene.phaseManager.dynamicQueueManager.setMoveOrder(order);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user