Merge remote-tracking branch 'upstream/beta' into arena-tag-cleanup
@ -48,7 +48,7 @@
|
||||
"lefthook": "^1.12.2",
|
||||
"msw": "^2.10.4",
|
||||
"phaser3spectorjs": "^0.0.8",
|
||||
"typedoc": "0.28.7",
|
||||
"typedoc": "^0.28.13",
|
||||
"typedoc-github-theme": "^0.3.1",
|
||||
"typedoc-plugin-coverage": "^4.0.1",
|
||||
"typedoc-plugin-mdn-links": "^5.0.9",
|
||||
@ -74,5 +74,5 @@
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.16.1"
|
||||
"packageManager": "pnpm@10.17.0"
|
||||
}
|
||||
|
@ -88,17 +88,17 @@ importers:
|
||||
specifier: ^0.0.8
|
||||
version: 0.0.8
|
||||
typedoc:
|
||||
specifier: 0.28.7
|
||||
version: 0.28.7(typescript@5.9.2)
|
||||
specifier: ^0.28.13
|
||||
version: 0.28.13(typescript@5.9.2)
|
||||
typedoc-github-theme:
|
||||
specifier: ^0.3.1
|
||||
version: 0.3.1(typedoc@0.28.7(typescript@5.9.2))
|
||||
version: 0.3.1(typedoc@0.28.13(typescript@5.9.2))
|
||||
typedoc-plugin-coverage:
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1(typedoc@0.28.7(typescript@5.9.2))
|
||||
version: 4.0.1(typedoc@0.28.13(typescript@5.9.2))
|
||||
typedoc-plugin-mdn-links:
|
||||
specifier: ^5.0.9
|
||||
version: 5.0.9(typedoc@0.28.7(typescript@5.9.2))
|
||||
version: 5.0.9(typedoc@0.28.13(typescript@5.9.2))
|
||||
typescript:
|
||||
specifier: ^5.9.2
|
||||
version: 5.9.2
|
||||
@ -717,17 +717,17 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@shikijs/engine-oniguruma@3.12.2':
|
||||
resolution: {integrity: sha512-hozwnFHsLvujK4/CPVHNo3Bcg2EsnG8krI/ZQ2FlBlCRpPZW4XAEQmEwqegJsypsTAN9ehu2tEYe30lYKSZW/w==}
|
||||
'@shikijs/engine-oniguruma@3.13.0':
|
||||
resolution: {integrity: sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg==}
|
||||
|
||||
'@shikijs/langs@3.12.2':
|
||||
resolution: {integrity: sha512-bVx5PfuZHDSHoBal+KzJZGheFuyH4qwwcwG/n+MsWno5cTlKmaNtTsGzJpHYQ8YPbB5BdEdKU1rga5/6JGY8ww==}
|
||||
'@shikijs/langs@3.13.0':
|
||||
resolution: {integrity: sha512-672c3WAETDYHwrRP0yLy3W1QYB89Hbpj+pO4KhxK6FzIrDI2FoEXNiNCut6BQmEApYLfuYfpgOZaqbY+E9b8wQ==}
|
||||
|
||||
'@shikijs/themes@3.12.2':
|
||||
resolution: {integrity: sha512-fTR3QAgnwYpfGczpIbzPjlRnxyONJOerguQv1iwpyQZ9QXX4qy/XFQqXlf17XTsorxnHoJGbH/LXBvwtqDsF5A==}
|
||||
'@shikijs/themes@3.13.0':
|
||||
resolution: {integrity: sha512-Vxw1Nm1/Od8jyA7QuAenaV78BG2nSr3/gCGdBkLpfLscddCkzkL36Q5b67SrLLfvAJTOUzW39x4FHVCFriPVgg==}
|
||||
|
||||
'@shikijs/types@3.12.2':
|
||||
resolution: {integrity: sha512-K5UIBzxCyv0YoxN3LMrKB9zuhp1bV+LgewxuVwHdl4Gz5oePoUFrr9EfgJlGlDeXCU1b/yhdnXeuRvAnz8HN8Q==}
|
||||
'@shikijs/types@3.13.0':
|
||||
resolution: {integrity: sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw==}
|
||||
|
||||
'@shikijs/vscode-textmate@10.0.2':
|
||||
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
|
||||
@ -1828,12 +1828,12 @@ packages:
|
||||
peerDependencies:
|
||||
typedoc: 0.27.x || 0.28.x
|
||||
|
||||
typedoc@0.28.7:
|
||||
resolution: {integrity: sha512-lpz0Oxl6aidFkmS90VQDQjk/Qf2iw0IUvFqirdONBdj7jPSN9mGXhy66BcGNDxx5ZMyKKiBVAREvPEzT6Uxipw==}
|
||||
typedoc@0.28.13:
|
||||
resolution: {integrity: sha512-dNWY8msnYB2a+7Audha+aTF1Pu3euiE7ySp53w8kEsXoYw7dMouV5A1UsTUY345aB152RHnmRMDiovuBi7BD+w==}
|
||||
engines: {node: '>= 18', pnpm: '>= 10'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x
|
||||
typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x
|
||||
|
||||
typescript@5.9.2:
|
||||
resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==}
|
||||
@ -2312,10 +2312,10 @@ snapshots:
|
||||
|
||||
'@gerrit0/mini-shiki@3.12.2':
|
||||
dependencies:
|
||||
'@shikijs/engine-oniguruma': 3.12.2
|
||||
'@shikijs/langs': 3.12.2
|
||||
'@shikijs/themes': 3.12.2
|
||||
'@shikijs/types': 3.12.2
|
||||
'@shikijs/engine-oniguruma': 3.13.0
|
||||
'@shikijs/langs': 3.13.0
|
||||
'@shikijs/themes': 3.13.0
|
||||
'@shikijs/types': 3.13.0
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
|
||||
'@inquirer/checkbox@4.2.0(@types/node@22.16.5)':
|
||||
@ -2547,20 +2547,20 @@ snapshots:
|
||||
'@rollup/rollup-win32-x64-msvc@4.50.1':
|
||||
optional: true
|
||||
|
||||
'@shikijs/engine-oniguruma@3.12.2':
|
||||
'@shikijs/engine-oniguruma@3.13.0':
|
||||
dependencies:
|
||||
'@shikijs/types': 3.12.2
|
||||
'@shikijs/types': 3.13.0
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
|
||||
'@shikijs/langs@3.12.2':
|
||||
'@shikijs/langs@3.13.0':
|
||||
dependencies:
|
||||
'@shikijs/types': 3.12.2
|
||||
'@shikijs/types': 3.13.0
|
||||
|
||||
'@shikijs/themes@3.12.2':
|
||||
'@shikijs/themes@3.13.0':
|
||||
dependencies:
|
||||
'@shikijs/types': 3.12.2
|
||||
'@shikijs/types': 3.13.0
|
||||
|
||||
'@shikijs/types@3.12.2':
|
||||
'@shikijs/types@3.13.0':
|
||||
dependencies:
|
||||
'@shikijs/vscode-textmate': 10.0.2
|
||||
'@types/hast': 3.0.4
|
||||
@ -3682,19 +3682,19 @@ snapshots:
|
||||
|
||||
type-fest@4.41.0: {}
|
||||
|
||||
typedoc-github-theme@0.3.1(typedoc@0.28.7(typescript@5.9.2)):
|
||||
typedoc-github-theme@0.3.1(typedoc@0.28.13(typescript@5.9.2)):
|
||||
dependencies:
|
||||
typedoc: 0.28.7(typescript@5.9.2)
|
||||
typedoc: 0.28.13(typescript@5.9.2)
|
||||
|
||||
typedoc-plugin-coverage@4.0.1(typedoc@0.28.7(typescript@5.9.2)):
|
||||
typedoc-plugin-coverage@4.0.1(typedoc@0.28.13(typescript@5.9.2)):
|
||||
dependencies:
|
||||
typedoc: 0.28.7(typescript@5.9.2)
|
||||
typedoc: 0.28.13(typescript@5.9.2)
|
||||
|
||||
typedoc-plugin-mdn-links@5.0.9(typedoc@0.28.7(typescript@5.9.2)):
|
||||
typedoc-plugin-mdn-links@5.0.9(typedoc@0.28.13(typescript@5.9.2)):
|
||||
dependencies:
|
||||
typedoc: 0.28.7(typescript@5.9.2)
|
||||
typedoc: 0.28.13(typescript@5.9.2)
|
||||
|
||||
typedoc@0.28.7(typescript@5.9.2):
|
||||
typedoc@0.28.13(typescript@5.9.2):
|
||||
dependencies:
|
||||
'@gerrit0/mini-shiki': 3.12.2
|
||||
lunr: 2.3.9
|
||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 131 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 202 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 119 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 116 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 124 B |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 235 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 186 B |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 180 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 181 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 242 B |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 198 B |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 196 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 119 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 183 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 169 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 131 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 202 B |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 249 B |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 190 B |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 244 B |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 195 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 235 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 193 B |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 180 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 119 B |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 197 B |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 307 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 131 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 202 B |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 249 B |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 190 B |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 244 B |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 195 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 235 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 193 B |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 180 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 119 B |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 197 B |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 307 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 116 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 116 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 119 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 116 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 119 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 202 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 119 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 116 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 119 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 116 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 119 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 119 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 116 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 124 B |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 235 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 187 B |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 180 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 181 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 242 B |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 198 B |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 196 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 119 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 183 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 169 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 116 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 124 B |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 235 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 187 B |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 180 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 181 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 242 B |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 198 B |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 196 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 119 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 183 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 169 B |
@ -89,7 +89,8 @@ export type AbilityBattlerTagType =
|
||||
| BattlerTagType.QUARK_DRIVE
|
||||
| BattlerTagType.UNBURDEN
|
||||
| BattlerTagType.SLOW_START
|
||||
| BattlerTagType.TRUANT;
|
||||
| BattlerTagType.TRUANT
|
||||
| BattlerTagType.SUPREME_OVERLORD;
|
||||
|
||||
/** Subset of {@linkcode BattlerTagType}s that provide type boosts */
|
||||
export type TypeBoostTagType = BattlerTagType.FIRE_BOOST | BattlerTagType.CHARGED;
|
||||
|
44
src/@types/damage-params.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import type { MoveCategory } from "#enums/move-category";
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
import type { Move } from "#types/move-types";
|
||||
|
||||
/**
|
||||
* Collection of types for methods like {@linkcode Pokemon#getBaseDamage} and {@linkcode Pokemon#getAttackDamage}.
|
||||
* @module
|
||||
*/
|
||||
|
||||
/** Base type for damage parameter methods, used for DRY */
|
||||
export interface damageParams {
|
||||
/** The attacking {@linkcode Pokemon} */
|
||||
source: Pokemon;
|
||||
/** The move used in the attack */
|
||||
move: Move;
|
||||
/** The move's {@linkcode MoveCategory} after variable-category effects are applied */
|
||||
moveCategory: MoveCategory;
|
||||
/** If `true`, ignores this Pokemon's defensive ability effects */
|
||||
ignoreAbility?: boolean;
|
||||
/** If `true`, ignores the attacking Pokemon's ability effects */
|
||||
ignoreSourceAbility?: boolean;
|
||||
/** If `true`, ignores the ally Pokemon's ability effects */
|
||||
ignoreAllyAbility?: boolean;
|
||||
/** If `true`, ignores the ability effects of the attacking pokemon's ally */
|
||||
ignoreSourceAllyAbility?: boolean;
|
||||
/** If `true`, calculates damage for a critical hit */
|
||||
isCritical?: boolean;
|
||||
/** If `true`, suppresses changes to game state during the calculation */
|
||||
simulated?: boolean;
|
||||
/** If defined, used in place of calculated effectiveness values */
|
||||
effectiveness?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type for the parameters of {@linkcode Pokemon#getBaseDamage | getBaseDamage}
|
||||
* @interface
|
||||
*/
|
||||
export type getBaseDamageParams = Omit<damageParams, "effectiveness">;
|
||||
|
||||
/**
|
||||
* Type for the parameters of {@linkcode Pokemon#getAttackDamage | getAttackDamage}
|
||||
* @interface
|
||||
*/
|
||||
export type getAttackDamageParams = Omit<damageParams, "moveCategory">;
|
@ -1,26 +1,27 @@
|
||||
import type { Pokemon } from "#app/field/pokemon";
|
||||
import type { Phase } from "#app/phase";
|
||||
import type { PhaseConstructorMap } from "#app/phase-manager";
|
||||
import type { ObjectValues } from "#types/type-helpers";
|
||||
|
||||
// Intentionally export the types of everything in phase-manager, as this file is meant to be
|
||||
// Intentionally [re-]export the types of everything in phase-manager, as this file is meant to be
|
||||
// the centralized place for type definitions for the phase system.
|
||||
export type * from "#app/phase-manager";
|
||||
|
||||
// This file includes helpful types for the phase system.
|
||||
// It intentionally imports the phase constructor map from the phase manager (and re-exports it)
|
||||
|
||||
/**
|
||||
* Map of phase names to constructors for said phase
|
||||
*/
|
||||
/** Map of phase names to constructors for said phase */
|
||||
export type PhaseMap = {
|
||||
[K in keyof PhaseConstructorMap]: InstanceType<PhaseConstructorMap[K]>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Union type of all phase constructors.
|
||||
*/
|
||||
/** Union type of all phase constructors. */
|
||||
export type PhaseClass = ObjectValues<PhaseConstructorMap>;
|
||||
|
||||
/**
|
||||
* Union type of all phase names as strings.
|
||||
*/
|
||||
/** Union type of all phase names as strings. */
|
||||
export type PhaseString = keyof PhaseMap;
|
||||
|
||||
/** Type for predicate functions operating on a specific type of {@linkcode Phase}. */
|
||||
export type PhaseConditionFunc<T extends PhaseString> = (phase: PhaseMap[T]) => boolean;
|
||||
|
||||
/** Interface type representing the assumption that all phases with pokemon associated are dynamic */
|
||||
export interface DynamicPhase extends Phase {
|
||||
getPokemon(): Pokemon;
|
||||
}
|
||||
|
@ -104,7 +104,6 @@ import {
|
||||
import { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
|
||||
import { MysteryEncounterSaveData } from "#mystery-encounters/mystery-encounter-save-data";
|
||||
import { allMysteryEncounters, mysteryEncountersByBiome } from "#mystery-encounters/mystery-encounters";
|
||||
import type { MovePhase } from "#phases/move-phase";
|
||||
import { expSpriteKeys } from "#sprites/sprite-keys";
|
||||
import { hasExpSprite } from "#sprites/sprite-utils";
|
||||
import type { Variant } from "#sprites/variant";
|
||||
@ -787,12 +786,14 @@ export class BattleScene extends SceneBase {
|
||||
|
||||
/**
|
||||
* Returns an array of EnemyPokemon of length 1 or 2 depending on if in a double battle or not.
|
||||
* Does not actually check if the pokemon are on the field or not.
|
||||
* @param active - (Default `false`) Whether to consider only {@linkcode Pokemon.isActive | active} on-field pokemon
|
||||
* @returns array of {@linkcode EnemyPokemon}
|
||||
*/
|
||||
public getEnemyField(): EnemyPokemon[] {
|
||||
public getEnemyField(active = false): EnemyPokemon[] {
|
||||
const party = this.getEnemyParty();
|
||||
return party.slice(0, Math.min(party.length, this.currentBattle?.double ? 2 : 1));
|
||||
return party
|
||||
.slice(0, Math.min(party.length, this.currentBattle?.double ? 2 : 1))
|
||||
.filter(p => !active || p.isActive());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -817,25 +818,7 @@ export class BattleScene extends SceneBase {
|
||||
* @param allyPokemon - The {@linkcode Pokemon} allied with the removed Pokemon; will have moves redirected to it
|
||||
*/
|
||||
redirectPokemonMoves(removedPokemon: Pokemon, allyPokemon: Pokemon): void {
|
||||
// failsafe: if not a double battle just return
|
||||
if (this.currentBattle.double === false) {
|
||||
return;
|
||||
}
|
||||
if (allyPokemon?.isActive(true)) {
|
||||
let targetingMovePhase: MovePhase;
|
||||
do {
|
||||
targetingMovePhase = this.phaseManager.findPhase(
|
||||
mp =>
|
||||
mp.is("MovePhase")
|
||||
&& mp.targets.length === 1
|
||||
&& mp.targets[0] === removedPokemon.getBattlerIndex()
|
||||
&& mp.pokemon.isPlayer() !== allyPokemon.isPlayer(),
|
||||
) as MovePhase;
|
||||
if (targetingMovePhase && targetingMovePhase.targets[0] !== allyPokemon.getBattlerIndex()) {
|
||||
targetingMovePhase.targets[0] = allyPokemon.getBattlerIndex();
|
||||
}
|
||||
} while (targetingMovePhase);
|
||||
}
|
||||
this.phaseManager.redirectMoves(removedPokemon, allyPokemon);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1433,7 +1416,7 @@ export class BattleScene extends SceneBase {
|
||||
}
|
||||
|
||||
if (lastBattle?.double && !newDouble) {
|
||||
this.phaseManager.tryRemovePhase((p: Phase) => p.is("SwitchPhase"));
|
||||
this.phaseManager.tryRemovePhase("SwitchPhase");
|
||||
for (const p of this.getPlayerField()) {
|
||||
p.lapseTag(BattlerTagType.COMMANDED);
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ import { CommonAnim } from "#enums/move-anims-common";
|
||||
import { MoveCategory } from "#enums/move-category";
|
||||
import { MoveFlags } from "#enums/move-flags";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { MoveTarget } from "#enums/move-target";
|
||||
import { MoveUseMode } from "#enums/move-use-mode";
|
||||
@ -2552,7 +2553,7 @@ export class PostIntimidateStatStageChangeAbAttr extends AbAttr {
|
||||
|
||||
override apply({ pokemon, simulated, cancelled }: AbAttrParamsWithCancel): void {
|
||||
if (!simulated) {
|
||||
globalScene.phaseManager.pushNew(
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"StatStageChangePhase",
|
||||
pokemon.getBattlerIndex(),
|
||||
false,
|
||||
@ -3237,6 +3238,7 @@ export class CommanderAbAttr extends AbAttr {
|
||||
return (
|
||||
globalScene.currentBattle?.double
|
||||
&& ally != null
|
||||
&& ally.isActive(true)
|
||||
&& ally.species.speciesId === SpeciesId.DONDOZO
|
||||
&& !(ally.isFainted() || ally.getTag(BattlerTagType.COMMANDED))
|
||||
);
|
||||
@ -3251,7 +3253,7 @@ export class CommanderAbAttr extends AbAttr {
|
||||
// Apply boosts from this effect to the ally Dondozo
|
||||
pokemon.getAlly()?.addTag(BattlerTagType.COMMANDED, 0, MoveId.NONE, pokemon.id);
|
||||
// Cancel the source Pokemon's next move (if a move is queued)
|
||||
globalScene.phaseManager.tryRemovePhase(phase => phase.is("MovePhase") && phase.pokemon === pokemon);
|
||||
globalScene.phaseManager.tryRemovePhase("MovePhase", phase => phase.pokemon === pokemon);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5001,7 +5003,14 @@ 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 (move.getMove().is("AttackMove") || move.getMove().is("StatusMove")) {
|
||||
const target = this.getTarget(pokemon, source, targets);
|
||||
globalScene.phaseManager.unshiftNew("MovePhase", pokemon, target, move, MoveUseMode.INDIRECT);
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"MovePhase",
|
||||
pokemon,
|
||||
target,
|
||||
move,
|
||||
MoveUseMode.INDIRECT,
|
||||
MovePhaseTimingModifier.FIRST,
|
||||
);
|
||||
} else if (move.getMove().is("SelfStatusMove")) {
|
||||
// If the move is a SelfStatusMove (ie. Swords Dance) the Dancer should replicate it on itself
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
@ -5010,6 +5019,7 @@ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr {
|
||||
[pokemon.getBattlerIndex()],
|
||||
move,
|
||||
MoveUseMode.INDIRECT,
|
||||
MovePhaseTimingModifier.FIRST,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -6025,11 +6035,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).
|
||||
* @sealed
|
||||
@ -6045,26 +6050,28 @@ export class BypassSpeedChanceAbAttr extends AbAttr {
|
||||
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
|
||||
// May be difficult as we likely do not want to modify the randBattleSeed
|
||||
const turnCommand = globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()];
|
||||
const isCommandFight = turnCommand?.command === Command.FIGHT;
|
||||
const move = turnCommand?.move?.move ? allMoves[turnCommand.move.move] : null;
|
||||
const isDamageMove = move?.category === MoveCategory.PHYSICAL || move?.category === MoveCategory.SPECIAL;
|
||||
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
|
||||
*/
|
||||
override apply({ bypass }: BypassSpeedChanceAbAttrParams): void {
|
||||
bypass.value = true;
|
||||
override apply({ pokemon }: AbAttrBaseParams): void {
|
||||
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) });
|
||||
}
|
||||
}
|
||||
@ -6072,8 +6079,6 @@ export class BypassSpeedChanceAbAttr extends AbAttr {
|
||||
export interface PreventBypassSpeedChanceAbAttrParams extends AbAttrBaseParams {
|
||||
/** Holds whether the speed check is bypassed after ability application */
|
||||
bypass: BooleanHolder;
|
||||
/** Holds whether the Pokemon can check held items for Quick Claw's effects */
|
||||
canCheckHeldItems: BooleanHolder;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -6100,9 +6105,8 @@ export class PreventBypassSpeedChanceAbAttr extends AbAttr {
|
||||
return isCommandFight && this.condition(pokemon, move!);
|
||||
}
|
||||
|
||||
override apply({ bypass, canCheckHeldItems }: PreventBypassSpeedChanceAbAttrParams): void {
|
||||
override apply({ bypass }: PreventBypassSpeedChanceAbAttrParams): void {
|
||||
bypass.value = false;
|
||||
canCheckHeldItems.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -6200,8 +6204,7 @@ class ForceSwitchOutHelper {
|
||||
|
||||
if (switchOutTarget.hp > 0) {
|
||||
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
|
||||
globalScene.phaseManager.prependNewToPhase(
|
||||
"MoveEndPhase",
|
||||
globalScene.phaseManager.queueDeferred(
|
||||
"SwitchPhase",
|
||||
this.switchType,
|
||||
switchOutTarget.getFieldIndex(),
|
||||
@ -6223,8 +6226,7 @@ class ForceSwitchOutHelper {
|
||||
const summonIndex = globalScene.currentBattle.trainer
|
||||
? globalScene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot)
|
||||
: 0;
|
||||
globalScene.phaseManager.prependNewToPhase(
|
||||
"MoveEndPhase",
|
||||
globalScene.phaseManager.queueDeferred(
|
||||
"SwitchSummonPhase",
|
||||
this.switchType,
|
||||
switchOutTarget.getFieldIndex(),
|
||||
@ -6946,7 +6948,7 @@ export function initAbilities() {
|
||||
.attr(TypeImmunityStatStageChangeAbAttr, PokemonType.ELECTRIC, Stat.SPD, 1)
|
||||
.ignorable(),
|
||||
new Ability(AbilityId.RIVALRY, 4)
|
||||
.attr(MovePowerBoostAbAttr, (user, target, _move) => user?.gender !== Gender.GENDERLESS && target?.gender !== Gender.GENDERLESS && user?.gender === target?.gender, 1.25, true)
|
||||
.attr(MovePowerBoostAbAttr, (user, target, _move) => user?.gender !== Gender.GENDERLESS && target?.gender !== Gender.GENDERLESS && user?.gender === target?.gender, 1.25)
|
||||
.attr(MovePowerBoostAbAttr, (user, target, _move) => user?.gender !== Gender.GENDERLESS && target?.gender !== Gender.GENDERLESS && user?.gender !== target?.gender, 0.75),
|
||||
new Ability(AbilityId.STEADFAST, 4)
|
||||
.attr(FlinchStatStageChangeAbAttr, [ Stat.SPD ], 1),
|
||||
@ -7158,7 +7160,7 @@ export function initAbilities() {
|
||||
new Ability(AbilityId.ANALYTIC, 5)
|
||||
.attr(MovePowerBoostAbAttr, (user) =>
|
||||
// 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 => phase.pokemon.id !== user?.id),
|
||||
1.3),
|
||||
new Ability(AbilityId.ILLUSION, 5)
|
||||
// The Pokemon generate an illusion if it's available
|
||||
@ -7739,8 +7741,8 @@ export function initAbilities() {
|
||||
new Ability(AbilityId.SHARPNESS, 9)
|
||||
.attr(MovePowerBoostAbAttr, (_user, _target, move) => move.hasFlag(MoveFlags.SLICING_MOVE), 1.5),
|
||||
new Ability(AbilityId.SUPREME_OVERLORD, 9)
|
||||
.attr(VariableMovePowerBoostAbAttr, (user, _target, _move) => 1 + 0.1 * Math.min(user.isPlayer() ? globalScene.arena.playerFaints : globalScene.currentBattle.enemyFaints, 5))
|
||||
.partial(), // Should only boost once, on summon
|
||||
.conditionalAttr((p) => (p.isPlayer() ? globalScene.arena.playerFaints : globalScene.currentBattle.enemyFaints) > 0, PostSummonAddBattlerTagAbAttr, BattlerTagType.SUPREME_OVERLORD, 0, true)
|
||||
.edgeCase(), // Tag is not tied to ability, so suppression/removal etc will not function until a structure to allow this is implemented
|
||||
new Ability(AbilityId.COSTAR, 9, -2)
|
||||
.attr(PostSummonCopyAllyStatsAbAttr),
|
||||
new Ability(AbilityId.TOXIC_DEBRIS, 9)
|
||||
|
@ -74,7 +74,6 @@ function applyAbAttrsInternal<T extends CallableAbAttrString>(
|
||||
for (const passive of [false, true]) {
|
||||
params.passive = passive;
|
||||
applySingleAbAttrs(attrType, params, gainedMidTurn, messages);
|
||||
globalScene.phaseManager.clearPhaseQueueSplice();
|
||||
}
|
||||
// We need to restore passive to its original state in the case that it was undefined on entry
|
||||
// this is necessary in case this method is called with an object that is reused.
|
||||
|
@ -56,6 +56,7 @@ import { allMoves } from "#data/data-lists";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||
import type { BattlerIndex } from "#enums/battler-index";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { HitResult } from "#enums/hit-result";
|
||||
import { CommonAnim } from "#enums/move-anims-common";
|
||||
@ -1549,6 +1550,145 @@ export class SuppressAbilitiesTag extends SerializableArenaTag {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface containing data related to a queued healing effect from
|
||||
* {@link https://bulbapedia.bulbagarden.net/wiki/Healing_Wish_(move) | Healing Wish}
|
||||
* or {@link https://bulbapedia.bulbagarden.net/wiki/Lunar_Dance_(move) | Lunar Dance}.
|
||||
*/
|
||||
interface PendingHealEffect {
|
||||
/** The {@linkcode Pokemon.id | PID} of the {@linkcode Pokemon} that created the effect. */
|
||||
readonly sourceId: number;
|
||||
/** The {@linkcode MoveId} of the move that created the effect. */
|
||||
readonly moveId: MoveId;
|
||||
/** If `true`, also restores the target's PP when the effect activates. */
|
||||
readonly restorePP: boolean;
|
||||
/** The message to display when the effect activates */
|
||||
readonly healMessage: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Arena tag to contain stored healing effects, namely from
|
||||
* {@link https://bulbapedia.bulbagarden.net/wiki/Healing_Wish_(move) | Healing Wish}
|
||||
* and {@link https://bulbapedia.bulbagarden.net/wiki/Lunar_Dance_(move) | Lunar Dance}.
|
||||
* When a damaged Pokemon first enters the effect's {@linkcode BattlerIndex | field position},
|
||||
* their HP is fully restored, and they are cured of any non-volatile status condition.
|
||||
* If the effect is from Lunar Dance, their PP is also restored.
|
||||
*/
|
||||
export class PendingHealTag extends SerializableArenaTag {
|
||||
public readonly tagType = ArenaTagType.PENDING_HEAL;
|
||||
/** All pending healing effects, organized by {@linkcode BattlerIndex} */
|
||||
public readonly pendingHeals: Partial<Record<BattlerIndex, PendingHealEffect[]>> = {};
|
||||
|
||||
constructor() {
|
||||
super(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a pending healing effect to the field. Effects under the same move *and*
|
||||
* target index as an existing effect are ignored.
|
||||
* @param targetIndex - The {@linkcode BattlerIndex} under which the effect applies
|
||||
* @param healEffect - The {@linkcode PendingHealEffect | data} for the pending heal effect
|
||||
*/
|
||||
public queueHeal(targetIndex: BattlerIndex, healEffect: PendingHealEffect): void {
|
||||
const existingHealEffects = this.pendingHeals[targetIndex];
|
||||
if (existingHealEffects) {
|
||||
if (!existingHealEffects.some(he => he.moveId === healEffect.moveId)) {
|
||||
existingHealEffects.push(healEffect);
|
||||
}
|
||||
} else {
|
||||
this.pendingHeals[targetIndex] = [healEffect];
|
||||
}
|
||||
}
|
||||
|
||||
/** Removes default on-remove message */
|
||||
override onRemove(_arena: Arena): void {}
|
||||
|
||||
/** This arena tag is removed at the end of the turn if no pending healing effects are on the field */
|
||||
override lapse(_arena: Arena): boolean {
|
||||
for (const key in this.pendingHeals) {
|
||||
if (this.pendingHeals[key].length > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a pending healing effect on the given target index. If an effect is found for
|
||||
* the index, the Pokemon at that index is healed to full HP, is cured of any non-volatile status,
|
||||
* and has its PP fully restored (if the effect is from Lunar Dance).
|
||||
* @param arena - The {@linkcode Arena} containing this tag
|
||||
* @param simulated - If `true`, suppresses changes to game state
|
||||
* @param pokemon - The {@linkcode Pokemon} receiving the healing effect
|
||||
* @returns `true` if the target Pokemon was healed by this effect
|
||||
* @todo This should also be called when a Pokemon moves into a new position via Ally Switch
|
||||
*/
|
||||
override apply(arena: Arena, simulated: boolean, pokemon: Pokemon): boolean {
|
||||
const targetIndex = pokemon.getBattlerIndex();
|
||||
const targetEffects = this.pendingHeals[targetIndex];
|
||||
|
||||
if (targetEffects == null || targetEffects.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const healEffect = targetEffects.find(effect => this.canApply(effect, pokemon));
|
||||
|
||||
if (healEffect == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (simulated) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { sourceId, moveId, restorePP, healMessage } = healEffect;
|
||||
const sourcePokemon = globalScene.getPokemonById(sourceId);
|
||||
if (!sourcePokemon) {
|
||||
console.warn(`Source of pending ${allMoves[moveId].name} effect is undefined!`);
|
||||
targetEffects.splice(targetEffects.indexOf(healEffect), 1);
|
||||
// Re-evaluate after the invalid heal effect is removed
|
||||
return this.apply(arena, simulated, pokemon);
|
||||
}
|
||||
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"PokemonHealPhase",
|
||||
targetIndex,
|
||||
pokemon.getMaxHp(),
|
||||
healMessage,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
restorePP,
|
||||
);
|
||||
|
||||
targetEffects.splice(targetEffects.indexOf(healEffect), 1);
|
||||
|
||||
return healEffect != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the given {@linkcode PendingHealEffect} can immediately heal
|
||||
* the given target {@linkcode Pokemon}.
|
||||
* @param healEffect - The {@linkcode PendingHealEffect} to evaluate
|
||||
* @param pokemon - The {@linkcode Pokemon} to evaluate against
|
||||
* @returns `true` if the Pokemon can be healed by the effect
|
||||
*/
|
||||
private canApply(healEffect: PendingHealEffect, pokemon: Pokemon): boolean {
|
||||
return (
|
||||
!pokemon.isFullHp()
|
||||
|| pokemon.status != null
|
||||
|| (healEffect.restorePP && pokemon.getMoveset().some(mv => mv.ppUsed > 0))
|
||||
);
|
||||
}
|
||||
|
||||
override loadTag(source: BaseArenaTag & Pick<PendingHealTag, "tagType" | "pendingHeals">): void {
|
||||
super.loadTag(source);
|
||||
(this as Mutable<this>).pendingHeals = source.pendingHeals;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: swap `sourceMove` and `sourceId` and make `sourceMove` an optional parameter
|
||||
export function getArenaTag(
|
||||
tagType: ArenaTagType,
|
||||
@ -1612,6 +1752,8 @@ export function getArenaTag(
|
||||
return new FairyLockTag(turnCount, sourceId);
|
||||
case ArenaTagType.NEUTRALIZING_GAS:
|
||||
return new SuppressAbilitiesTag(sourceId);
|
||||
case ArenaTagType.PENDING_HEAL:
|
||||
return new PendingHealTag();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@ -1660,5 +1802,6 @@ export type ArenaTagTypeMap = {
|
||||
[ArenaTagType.GRASS_WATER_PLEDGE]: GrassWaterPledgeTag;
|
||||
[ArenaTagType.FAIRY_LOCK]: FairyLockTag;
|
||||
[ArenaTagType.NEUTRALIZING_GAS]: SuppressAbilitiesTag;
|
||||
[ArenaTagType.PENDING_HEAL]: PendingHealTag;
|
||||
[ArenaTagType.NONE]: NoneTag;
|
||||
};
|
||||
|
@ -606,17 +606,7 @@ export class ShellTrapTag extends BattlerTag {
|
||||
|
||||
// Trap should only be triggered by opponent's Physical moves
|
||||
if (phaseData?.move.category === MoveCategory.PHYSICAL && pokemon.isOpponent(phaseData.attacker)) {
|
||||
const shellTrapPhaseIndex = globalScene.phaseManager.phaseQueue.findIndex(
|
||||
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");
|
||||
}
|
||||
|
||||
globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === pokemon);
|
||||
this.activated = true;
|
||||
}
|
||||
|
||||
@ -1279,22 +1269,9 @@ export class EncoreTag extends MoveRestrictionBattlerTag {
|
||||
}),
|
||||
);
|
||||
|
||||
const movePhase = globalScene.phaseManager.findPhase(m => m.is("MovePhase") && m.pokemon === pokemon);
|
||||
if (movePhase) {
|
||||
const movesetMove = pokemon.getMoveset().find(m => m.moveId === this.moveId);
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
const movesetMove = pokemon.getMoveset().find(m => m.moveId === this.moveId);
|
||||
if (movesetMove) {
|
||||
globalScene.phaseManager.changePhaseMove((phase: MovePhase) => phase.pokemon === pokemon, movesetMove);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3578,6 +3555,25 @@ export class GrudgeTag extends SerializableBattlerTag {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
public override readonly tagType = BattlerTagType.BYPASS_SPEED;
|
||||
|
||||
constructor() {
|
||||
super(BattlerTagType.BYPASS_SPEED, BattlerTagLapseType.TURN_END, 1);
|
||||
}
|
||||
|
||||
override canAdd(pokemon: Pokemon): boolean {
|
||||
const bypass = new BooleanHolder(true);
|
||||
applyAbAttrs("PreventBypassSpeedChanceAbAttr", { pokemon, bypass });
|
||||
return bypass.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
|
||||
*/
|
||||
@ -3626,6 +3622,41 @@ export class MagicCoatTag extends BattlerTag {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag associated with {@linkcode AbilityId.SUPREME_OVERLORD}
|
||||
*/
|
||||
export class SupremeOverlordTag extends AbilityBattlerTag {
|
||||
public override readonly tagType = BattlerTagType.SUPREME_OVERLORD;
|
||||
/** The number of faints at the time the user was sent out */
|
||||
public readonly faintCount: number;
|
||||
constructor() {
|
||||
super(BattlerTagType.SUPREME_OVERLORD, AbilityId.SUPREME_OVERLORD, BattlerTagLapseType.FAINT, 0);
|
||||
}
|
||||
|
||||
public override onAdd(pokemon: Pokemon): boolean {
|
||||
(this as Mutable<this>).faintCount = Math.min(
|
||||
pokemon.isPlayer() ? globalScene.arena.playerFaints : globalScene.currentBattle.enemyFaints,
|
||||
5,
|
||||
);
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("battlerTags:supremeOverlordOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The damage multiplier for Supreme Overlord
|
||||
*/
|
||||
public getBoost(): number {
|
||||
return 1 + 0.1 * this.faintCount;
|
||||
}
|
||||
|
||||
public override loadTag(source: BaseBattlerTag & Pick<SupremeOverlordTag, "tagType" | "faintCount">): void {
|
||||
super.loadTag(source);
|
||||
(this as Mutable<this>).faintCount = source.faintCount;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a {@linkcode BattlerTag} based on the provided tag type, turn count, source move, and source ID.
|
||||
* @param sourceId - The ID of the pokemon adding the tag
|
||||
@ -3826,6 +3857,10 @@ export function getBattlerTag(
|
||||
return new PsychoShiftTag();
|
||||
case BattlerTagType.MAGIC_COAT:
|
||||
return new MagicCoatTag();
|
||||
case BattlerTagType.SUPREME_OVERLORD:
|
||||
return new SupremeOverlordTag();
|
||||
case BattlerTagType.BYPASS_SPEED:
|
||||
return new BypassSpeedTag();
|
||||
}
|
||||
}
|
||||
|
||||
@ -3960,4 +3995,6 @@ export type BattlerTagTypeMap = {
|
||||
[BattlerTagType.GRUDGE]: GrudgeTag;
|
||||
[BattlerTagType.PSYCHO_SHIFT]: PsychoShiftTag;
|
||||
[BattlerTagType.MAGIC_COAT]: MagicCoatTag;
|
||||
[BattlerTagType.SUPREME_OVERLORD]: SupremeOverlordTag;
|
||||
[BattlerTagType.BYPASS_SPEED]: BypassSpeedTag;
|
||||
};
|
||||
|
@ -6,7 +6,7 @@ import { loggedInUser } from "#app/account";
|
||||
import type { GameMode } from "#app/game-mode";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import type { EntryHazardTag } from "#data/arena-tag";
|
||||
import type { EntryHazardTag, PendingHealTag } from "#data/arena-tag";
|
||||
import { WeakenMoveTypeTag } from "#data/arena-tag";
|
||||
import { MoveChargeAnim } from "#data/battle-anims";
|
||||
import {
|
||||
@ -18,6 +18,7 @@ import {
|
||||
ShellTrapTag,
|
||||
StockpilingTag,
|
||||
SubstituteTag,
|
||||
SupremeOverlordTag,
|
||||
TrappedTag,
|
||||
TypeBoostTag,
|
||||
} from "#data/battler-tags";
|
||||
@ -80,10 +81,8 @@ import { applyMoveAttrs } from "#moves/apply-attrs";
|
||||
import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSketchMoves, invalidSleepTalkMoves } from "#moves/invalid-moves";
|
||||
import { frenzyMissFunc, getMoveTargets } from "#moves/move-utils";
|
||||
import { PokemonMove } from "#moves/pokemon-move";
|
||||
import { MoveEndPhase } from "#phases/move-end-phase";
|
||||
import { MovePhase } from "#phases/move-phase";
|
||||
import { PokemonHealPhase } from "#phases/pokemon-heal-phase";
|
||||
import { SwitchSummonPhase } from "#phases/switch-summon-phase";
|
||||
import type { AttackMoveResult } from "#types/attack-move-result";
|
||||
import type { Localizable } from "#types/locales";
|
||||
import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString, MoveMessageFunc } from "#types/move-types";
|
||||
@ -93,6 +92,7 @@ import { getEnumValues } from "#utils/enums";
|
||||
import { toCamelCase, toTitleCase } from "#utils/strings";
|
||||
import i18next from "i18next";
|
||||
import { applyChallenges } from "#utils/challenge-utils";
|
||||
import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
|
||||
import type { AbstractConstructor } from "#types/type-helpers";
|
||||
|
||||
/**
|
||||
@ -879,6 +879,8 @@ export abstract class Move implements Localizable {
|
||||
power.value *= 1.5;
|
||||
}
|
||||
|
||||
power.value *= (source.getTag(BattlerTagType.SUPREME_OVERLORD) as SupremeOverlordTag | undefined)?.getBoost() ?? 1;
|
||||
|
||||
return power.value;
|
||||
}
|
||||
|
||||
@ -888,6 +890,10 @@ export abstract class Move implements Localizable {
|
||||
applyMoveAttrs("IncrementMovePriorityAttr", user, null, this, priority);
|
||||
applyAbAttrs("ChangeMovePriorityAbAttr", {pokemon: user, simulated, move: this, priority});
|
||||
|
||||
if (user.getTag(BattlerTagType.BYPASS_SPEED)) {
|
||||
priority.value += 0.2;
|
||||
}
|
||||
|
||||
return priority.value;
|
||||
}
|
||||
|
||||
@ -2147,24 +2153,15 @@ export class SacrificialFullRestoreAttr extends SacrificialAttr {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We don't know which party member will be chosen, so pick the highest max HP in the party
|
||||
const party = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty();
|
||||
const maxPartyMemberHp = party.map(p => p.getMaxHp()).reduce((maxHp: number, hp: number) => Math.max(hp, maxHp), 0);
|
||||
|
||||
const pm = globalScene.phaseManager;
|
||||
|
||||
pm.pushPhase(
|
||||
pm.create("PokemonHealPhase",
|
||||
user.getBattlerIndex(),
|
||||
maxPartyMemberHp,
|
||||
i18next.t(this.moveMessage, { pokemonName: getPokemonNameWithAffix(user) }),
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
this.restorePP),
|
||||
true);
|
||||
// Add a tag to the field if it doesn't already exist, then queue a delayed healing effect in the user's current slot.
|
||||
globalScene.arena.addTag(ArenaTagType.PENDING_HEAL, 0, move.id, user.id); // Arguments after first go completely unused
|
||||
const tag = globalScene.arena.getTag(ArenaTagType.PENDING_HEAL) as PendingHealTag;
|
||||
tag.queueHeal(user.getBattlerIndex(), {
|
||||
sourceId: user.id,
|
||||
moveId: move.id,
|
||||
restorePP: this.restorePP,
|
||||
healMessage: i18next.t(this.moveMessage, { pokemonName: getPokemonNameWithAffix(user) }),
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -3304,7 +3301,7 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr {
|
||||
|
||||
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.getMovePhase((phase) => phase.pokemon.isPlayer() === user.isPlayer());
|
||||
if (allyMovePhase) {
|
||||
const allyMove = allyMovePhase.move.getMove();
|
||||
if (allyMove !== move && allyMove.hasAttr("AwaitCombinedPledgeAttr")) {
|
||||
@ -3317,11 +3314,7 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr {
|
||||
}));
|
||||
|
||||
// Move the ally's MovePhase (if needed) so that the ally moves next
|
||||
const allyMovePhaseIndex = globalScene.phaseManager.phaseQueue.indexOf(allyMovePhase);
|
||||
const firstMovePhaseIndex = globalScene.phaseManager.phaseQueue.findIndex((phase) => phase.is("MovePhase"));
|
||||
if (allyMovePhaseIndex !== firstMovePhaseIndex) {
|
||||
globalScene.phaseManager.prependToPhase(globalScene.phaseManager.phaseQueue.splice(allyMovePhaseIndex, 1)[0], "MovePhase");
|
||||
}
|
||||
globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === user.getAlly());
|
||||
|
||||
overridden.value = true;
|
||||
return true;
|
||||
@ -4556,28 +4549,7 @@ export class LastMoveDoublePowerAttr extends VariablePowerAttr {
|
||||
*/
|
||||
apply(user: Pokemon, _target: Pokemon, _move: Move, args: any[]): boolean {
|
||||
const power = args[0] as NumberHolder;
|
||||
const enemy = user.getOpponent(0);
|
||||
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) {
|
||||
for (const p of globalScene.phaseManager.dynamicQueueManager.getLastTurnOrder().slice(0, -1).reverse()) {
|
||||
const [ lastMove ] = p.getLastXMoves(1);
|
||||
if (lastMove.result !== MoveResult.FAIL) {
|
||||
if ((lastMove.result === MoveResult.SUCCESS) && (lastMove.move === this.move)) {
|
||||
@ -4659,20 +4631,13 @@ export class CueNextRoundAttr extends MoveEffectAttr {
|
||||
}
|
||||
|
||||
override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean {
|
||||
const nextRoundPhase = globalScene.phaseManager.findPhase<MovePhase>(phase =>
|
||||
phase.is("MovePhase") && phase.move.moveId === MoveId.ROUND
|
||||
);
|
||||
const nextRoundPhase = globalScene.phaseManager.getMovePhase(phase => phase.move.moveId === MoveId.ROUND);
|
||||
|
||||
if (!nextRoundPhase) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update the phase queue so that the next Pokemon using Round moves next
|
||||
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");
|
||||
}
|
||||
globalScene.phaseManager.forceMoveNext(phase => phase.move.moveId === MoveId.ROUND);
|
||||
|
||||
// Mark the corresponding Pokemon as having "joined the Round" (for doubling power later)
|
||||
nextRoundPhase.pokemon.turnData.joinedRound = true;
|
||||
@ -6297,11 +6262,11 @@ export class RevivalBlessingAttr extends MoveEffectAttr {
|
||||
// Handle cases where revived pokemon needs to get switched in on same turn
|
||||
if (allyPokemon.isFainted() || allyPokemon === pokemon) {
|
||||
// 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("SwitchSummonPhase", phase => phase.getFieldIndex() === slotIndex);
|
||||
// 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)
|
||||
// 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.getMovePhase((phase: MovePhase) => phase.pokemon === pokemon)?.cancel();
|
||||
if (user.fieldPosition === FieldPosition.CENTER) {
|
||||
user.setFieldPosition(FieldPosition.LEFT);
|
||||
}
|
||||
@ -6382,8 +6347,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
||||
if (this.switchType === SwitchType.FORCE_SWITCH) {
|
||||
switchOutTarget.leaveField(true);
|
||||
const slotIndex = eligibleNewIndices[user.randBattleSeedInt(eligibleNewIndices.length)];
|
||||
globalScene.phaseManager.prependNewToPhase(
|
||||
"MoveEndPhase",
|
||||
globalScene.phaseManager.queueDeferred(
|
||||
"SwitchSummonPhase",
|
||||
this.switchType,
|
||||
switchOutTarget.getFieldIndex(),
|
||||
@ -6393,7 +6357,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
||||
);
|
||||
} else {
|
||||
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
|
||||
globalScene.phaseManager.prependNewToPhase("MoveEndPhase",
|
||||
globalScene.phaseManager.queueDeferred(
|
||||
"SwitchPhase",
|
||||
this.switchType,
|
||||
switchOutTarget.getFieldIndex(),
|
||||
@ -6422,7 +6386,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
||||
if (this.switchType === SwitchType.FORCE_SWITCH) {
|
||||
switchOutTarget.leaveField(true);
|
||||
const slotIndex = eligibleNewIndices[user.randBattleSeedInt(eligibleNewIndices.length)];
|
||||
globalScene.phaseManager.prependNewToPhase("MoveEndPhase",
|
||||
globalScene.phaseManager.queueDeferred(
|
||||
"SwitchSummonPhase",
|
||||
this.switchType,
|
||||
switchOutTarget.getFieldIndex(),
|
||||
@ -6432,7 +6396,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
||||
);
|
||||
} else {
|
||||
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
|
||||
globalScene.phaseManager.prependNewToPhase("MoveEndPhase",
|
||||
globalScene.phaseManager.queueDeferred(
|
||||
"SwitchSummonPhase",
|
||||
this.switchType,
|
||||
switchOutTarget.getFieldIndex(),
|
||||
@ -6863,7 +6827,7 @@ class CallMoveAttr extends OverrideMoveEffectAttr {
|
||||
: moveTargets.targets[user.randBattleSeedInt(moveTargets.targets.length)]];
|
||||
|
||||
globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", move.id);
|
||||
globalScene.phaseManager.unshiftNew("MovePhase", user, targets, new PokemonMove(move.id), MoveUseMode.FOLLOW_UP);
|
||||
globalScene.phaseManager.unshiftNew("MovePhase", user, targets, new PokemonMove(move.id), MoveUseMode.FOLLOW_UP, MovePhaseTimingModifier.FIRST);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -7095,7 +7059,7 @@ export class NaturePowerAttr extends OverrideMoveEffectAttr {
|
||||
|
||||
// Load the move's animation if we didn't already and unshift a new usage phase
|
||||
globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", moveId);
|
||||
globalScene.phaseManager.unshiftNew("MovePhase", user, [ target.getBattlerIndex() ], new PokemonMove(moveId), MoveUseMode.FOLLOW_UP);
|
||||
globalScene.phaseManager.unshiftNew("MovePhase", user, [ target.getBattlerIndex() ], new PokemonMove(moveId), MoveUseMode.FOLLOW_UP, MovePhaseTimingModifier.FIRST);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -7179,7 +7143,7 @@ export class RepeatMoveAttr extends MoveEffectAttr {
|
||||
targetPokemonName: getPokemonNameWithAffix(target)
|
||||
}));
|
||||
target.turnData.extraTurns++;
|
||||
globalScene.phaseManager.appendNewToPhase("MoveEndPhase", "MovePhase", target, moveTargets, movesetMove, MoveUseMode.NORMAL);
|
||||
globalScene.phaseManager.unshiftNew("MovePhase", target, moveTargets, movesetMove, MoveUseMode.NORMAL, MovePhaseTimingModifier.FIRST);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -7952,12 +7916,7 @@ export class AfterYouAttr extends MoveEffectAttr {
|
||||
*/
|
||||
override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
|
||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:afterYou", { targetName: getPokemonNameWithAffix(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");
|
||||
}
|
||||
globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === target);
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -7980,45 +7939,11 @@ export class ForceLastAttr extends MoveEffectAttr {
|
||||
override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
|
||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:forceLast", { targetPokemonName: getPokemonNameWithAffix(target) }));
|
||||
|
||||
// TODO: Refactor this to be more readable and less janky
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
globalScene.phaseManager.forceMoveLast((phase: MovePhase) => phase.pokemon === target);
|
||||
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 failOnBossCondition: MoveConditionFunc = (user, target, move) => !target.isBossImmune();
|
||||
@ -8042,7 +7967,7 @@ const userSleptOrComatoseCondition: MoveConditionFunc = (user) => user.status?.e
|
||||
|
||||
const targetSleptOrComatoseCondition: MoveConditionFunc = (_user: Pokemon, target: Pokemon, _move: Move) => target.status?.effect === StatusEffect.SLEEP || target.hasAbility(AbilityId.COMATOSE);
|
||||
|
||||
const failIfLastCondition: MoveConditionFunc = () => globalScene.phaseManager.findPhase(phase => phase.is("MovePhase")) !== undefined;
|
||||
const failIfLastCondition: MoveConditionFunc = () => globalScene.phaseManager.hasPhaseOfType("MovePhase");
|
||||
|
||||
const failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => {
|
||||
const party: Pokemon[] = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty();
|
||||
|
@ -414,7 +414,7 @@ function summonPlayerPokemonAnimation(pokemon: PlayerPokemon): Promise<void> {
|
||||
pokemon.resetTurnData();
|
||||
|
||||
globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true);
|
||||
globalScene.phaseManager.pushNew("PostSummonPhase", pokemon.getBattlerIndex());
|
||||
globalScene.phaseManager.unshiftNew("PostSummonPhase", pokemon.getBattlerIndex());
|
||||
resolve();
|
||||
});
|
||||
},
|
||||
|
@ -669,7 +669,6 @@ function onGameOver() {
|
||||
|
||||
// Clear any leftover battle phases
|
||||
globalScene.phaseManager.clearPhaseQueue();
|
||||
globalScene.phaseManager.clearPhaseQueueSplice();
|
||||
|
||||
// Return enemy Pokemon
|
||||
const pokemon = globalScene.getEnemyPokemon();
|
||||
|
@ -738,7 +738,7 @@ export function setEncounterRewards(
|
||||
if (customShopRewards) {
|
||||
globalScene.phaseManager.unshiftNew("SelectModifierPhase", 0, undefined, customShopRewards);
|
||||
} else {
|
||||
globalScene.phaseManager.tryRemovePhase(p => p.is("MysteryEncounterRewardsPhase"));
|
||||
globalScene.phaseManager.removeAllPhasesOfType("MysteryEncounterRewardsPhase");
|
||||
}
|
||||
|
||||
if (eggRewards) {
|
||||
@ -812,8 +812,7 @@ export function leaveEncounterWithoutBattle(
|
||||
encounterMode: MysteryEncounterMode = MysteryEncounterMode.NO_BATTLE,
|
||||
) {
|
||||
globalScene.currentBattle.mysteryEncounter!.encounterMode = encounterMode;
|
||||
globalScene.phaseManager.clearPhaseQueue();
|
||||
globalScene.phaseManager.clearPhaseQueueSplice();
|
||||
globalScene.phaseManager.clearPhaseQueue(true);
|
||||
handleMysteryEncounterVictory(addHealPhase);
|
||||
}
|
||||
|
||||
@ -826,7 +825,7 @@ export function handleMysteryEncounterVictory(addHealPhase = false, doNotContinu
|
||||
const allowedPkm = globalScene.getPlayerParty().filter(pkm => pkm.isAllowedInBattle());
|
||||
|
||||
if (allowedPkm.length === 0) {
|
||||
globalScene.phaseManager.clearPhaseQueue();
|
||||
globalScene.phaseManager.clearPhaseQueue(true);
|
||||
globalScene.phaseManager.unshiftNew("GameOverPhase");
|
||||
return;
|
||||
}
|
||||
@ -869,7 +868,7 @@ export function handleMysteryEncounterBattleFailed(addHealPhase = false, doNotCo
|
||||
const allowedPkm = globalScene.getPlayerParty().filter(pkm => pkm.isAllowedInBattle());
|
||||
|
||||
if (allowedPkm.length === 0) {
|
||||
globalScene.phaseManager.clearPhaseQueue();
|
||||
globalScene.phaseManager.clearPhaseQueue(true);
|
||||
globalScene.phaseManager.unshiftNew("GameOverPhase");
|
||||
return;
|
||||
}
|
||||
|
@ -1,125 +0,0 @@
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import type { Phase } from "#app/phase";
|
||||
import { TrickRoomTag } from "#data/arena-tag";
|
||||
import { DynamicPhaseType } from "#enums/dynamic-phase-type";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { ActivatePriorityQueuePhase } from "#phases/activate-priority-queue-phase";
|
||||
import { PostSummonActivateAbilityPhase } from "#phases/post-summon-activate-ability-phase";
|
||||
import type { PostSummonPhase } from "#phases/post-summon-phase";
|
||||
import { BooleanHolder } from "#utils/common";
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to remove one or more Phases from the current queue.
|
||||
* @param phaseFilter - The function to select phases for removal
|
||||
* @param removeCount - The maximum number of phases to remove, or `all` to remove all matching phases;
|
||||
* default `1`
|
||||
* @returns The number of successfully removed phases
|
||||
* @todo Remove this eventually once the patchwork bug this is used for is fixed
|
||||
*/
|
||||
public tryRemovePhase(phaseFilter: (phase: Phase) => boolean, removeCount: number | "all" = 1): number {
|
||||
if (removeCount === "all") {
|
||||
removeCount = this.queue.length;
|
||||
} else if (removeCount < 1) {
|
||||
return 0;
|
||||
}
|
||||
let numRemoved = 0;
|
||||
|
||||
do {
|
||||
const phaseIndex = this.queue.findIndex(phaseFilter);
|
||||
if (phaseIndex === -1) {
|
||||
break;
|
||||
}
|
||||
this.queue.splice(phaseIndex, 1);
|
||||
numRemoved++;
|
||||
} while (numRemoved < removeCount && this.queue.length > 0);
|
||||
|
||||
return numRemoved;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, speedReversed);
|
||||
return speedReversed.value;
|
||||
}
|
182
src/dynamic-queue-manager.ts
Normal file
@ -0,0 +1,182 @@
|
||||
import type { DynamicPhase, PhaseConditionFunc, PhaseString } 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 type { MovePhase } from "#app/phases/move-phase";
|
||||
import { MovePhasePriorityQueue } from "#app/queues/move-phase-priority-queue";
|
||||
import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-queue";
|
||||
import { PostSummonPhasePriorityQueue } from "#app/queues/post-summon-phase-priority-queue";
|
||||
import type { PriorityQueue } from "#app/queues/priority-queue";
|
||||
import type { BattlerIndex } from "#enums/battler-index";
|
||||
import type { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
|
||||
|
||||
// TODO: might be easier to define which phases should be dynamic instead
|
||||
/** All phases which have defined a `getPokemon` method but should not be sorted dynamically */
|
||||
const nonDynamicPokemonPhases: readonly PhaseString[] = [
|
||||
"SummonPhase",
|
||||
"CommandPhase",
|
||||
"LearnMovePhase",
|
||||
"MoveEffectPhase",
|
||||
"MoveEndPhase",
|
||||
"FaintPhase",
|
||||
"DamageAnimPhase",
|
||||
"VictoryPhase",
|
||||
"PokemonHealPhase",
|
||||
"WeatherEffectPhase",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* The dynamic queue manager holds priority queues for phases which are queued as dynamic.
|
||||
*
|
||||
* Dynamic phases are generally those which hold a pokemon and are unshifted, not pushed. \
|
||||
* Queues work by sorting their entries in speed order (and possibly with more complex ordering) before each time a phase is popped.
|
||||
*
|
||||
* As the holder, this structure is also used to access and modify queued phases.
|
||||
* This is mostly used in redirection, cancellation, etc. of {@linkcode MovePhase}s.
|
||||
*/
|
||||
export class DynamicQueueManager {
|
||||
/** Maps phase types to their corresponding queues */
|
||||
private readonly dynamicPhaseMap: Map<PhaseString, PriorityQueue<Phase>>;
|
||||
|
||||
constructor() {
|
||||
this.dynamicPhaseMap = new Map();
|
||||
// PostSummon and Move phases have specialized queues
|
||||
this.dynamicPhaseMap.set("PostSummonPhase", new PostSummonPhasePriorityQueue());
|
||||
this.dynamicPhaseMap.set("MovePhase", new MovePhasePriorityQueue());
|
||||
}
|
||||
|
||||
/** Removes all phases from the manager */
|
||||
public clearQueues(): void {
|
||||
for (const queue of this.dynamicPhaseMap.values()) {
|
||||
queue.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new phase to the manager and creates the priority queue for it if one does not exist.
|
||||
* @param phase - The {@linkcode Phase} to add
|
||||
* @returns `true` if the phase was added, or `false` if it is not dynamic
|
||||
*/
|
||||
public queueDynamicPhase<T extends Phase>(phase: T): boolean {
|
||||
if (!this.isDynamicPhase(phase)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.dynamicPhaseMap.has(phase.phaseName)) {
|
||||
// TS can't figure out that T is dynamic at this point, but it does know that `typeof phase` is
|
||||
this.dynamicPhaseMap.set(phase.phaseName, new PokemonPhasePriorityQueue<typeof phase>());
|
||||
}
|
||||
this.dynamicPhaseMap.get(phase.phaseName)?.push(phase);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the highest-priority (generally by speed) {@linkcode Phase} of the specified type
|
||||
* @param type - The {@linkcode PhaseString | type} to pop
|
||||
* @returns The popped {@linkcode Phase}, or `undefined` if none of the specified type exist
|
||||
*/
|
||||
public popNextPhase(type: PhaseString): Phase | undefined {
|
||||
return this.dynamicPhaseMap.get(type)?.pop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if there is a queued dynamic {@linkcode Phase} meeting the conditions
|
||||
* @param type - The {@linkcode PhaseString | type} of phase to search for
|
||||
* @param condition - An optional {@linkcode PhaseConditionFunc} to add conditions to the search
|
||||
* @returns Whether a matching phase exists
|
||||
*/
|
||||
public exists<T extends PhaseString>(type: T, condition?: PhaseConditionFunc<T>): boolean {
|
||||
return !!this.dynamicPhaseMap.get(type)?.has(condition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and removes a single queued {@linkcode Phase}
|
||||
* @param type - The {@linkcode PhaseString | type} of phase to search for
|
||||
* @param phaseFilter - A {@linkcode PhaseConditionFunc} to specify conditions for the phase
|
||||
* @returns Whether a removal occurred
|
||||
*/
|
||||
public removePhase<T extends PhaseString>(type: T, condition?: PhaseConditionFunc<T>): boolean {
|
||||
return !!this.dynamicPhaseMap.get(type)?.remove(condition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the timing modifier of a move (i.e. to force it first or last)
|
||||
* @param condition - A {@linkcode PhaseConditionFunc} to specify conditions for the move
|
||||
* @param modifier - The {@linkcode MovePhaseTimingModifier} to switch the move to
|
||||
*/
|
||||
public setMoveTimingModifier(condition: PhaseConditionFunc<"MovePhase">, modifier: MovePhaseTimingModifier): void {
|
||||
this.getMovePhaseQueue().setTimingModifier(condition, modifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the {@linkcode MovePhase} meeting the condition and changes its move
|
||||
* @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function
|
||||
* @param move - The {@linkcode PokemonMove | move} to use in replacement
|
||||
*/
|
||||
public setMoveForPhase(condition: PhaseConditionFunc<"MovePhase">, move: PokemonMove): void {
|
||||
this.getMovePhaseQueue().setMoveForPhase(condition, move);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects moves which were targeted at a {@linkcode Pokemon} that has been removed
|
||||
* @param removedPokemon - The removed {@linkcode Pokemon}
|
||||
* @param allyPokemon - The ally of the removed pokemon
|
||||
*/
|
||||
public redirectMoves(removedPokemon: Pokemon, allyPokemon: Pokemon): void {
|
||||
this.getMovePhaseQueue().redirectMoves(removedPokemon, allyPokemon);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a {@linkcode MovePhase} meeting the condition
|
||||
* @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function
|
||||
* @returns The MovePhase, or `undefined` if it does not exist
|
||||
*/
|
||||
public getMovePhase(condition: PhaseConditionFunc<"MovePhase">): MovePhase | undefined {
|
||||
return this.getMovePhaseQueue().find(condition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and cancels a {@linkcode MovePhase} meeting the condition
|
||||
* @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function
|
||||
*/
|
||||
public cancelMovePhase(condition: PhaseConditionFunc<"MovePhase">): void {
|
||||
this.getMovePhaseQueue().cancelMove(condition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the move order to a static array rather than a dynamic queue
|
||||
* @param order - The order of {@linkcode BattlerIndex}s
|
||||
*/
|
||||
public setMoveOrder(order: BattlerIndex[]): void {
|
||||
this.getMovePhaseQueue().setMoveOrder(order);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns An in-order array of {@linkcode Pokemon}, representing the turn order as played out in the most recent turn
|
||||
*/
|
||||
public getLastTurnOrder(): Pokemon[] {
|
||||
return this.getMovePhaseQueue().getTurnOrder();
|
||||
}
|
||||
|
||||
/** Clears the stored `Move` turn order */
|
||||
public clearLastTurnOrder(): void {
|
||||
this.getMovePhaseQueue().clearTurnOrder();
|
||||
}
|
||||
|
||||
/** Internal helper to get the {@linkcode MovePhasePriorityQueue} */
|
||||
private getMovePhaseQueue(): MovePhasePriorityQueue {
|
||||
return this.dynamicPhaseMap.get("MovePhase") as MovePhasePriorityQueue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal helper to determine if a phase is dynamic.
|
||||
* @param phase - The {@linkcode Phase} to check
|
||||
* @returns Whether `phase` is dynamic
|
||||
* @privateRemarks
|
||||
* Currently, this checks that `phase` has a `getPokemon` method
|
||||
* and is not blacklisted in `nonDynamicPokemonPhases`.
|
||||
*/
|
||||
private isDynamicPhase(phase: Phase): phase is DynamicPhase {
|
||||
return typeof (phase as any).getPokemon === "function" && !nonDynamicPokemonPhases.includes(phase.phaseName);
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
// TODO: rename to something else (this isn't used only for arena tags)
|
||||
export enum ArenaTagSide {
|
||||
BOTH,
|
||||
PLAYER,
|
||||
|
@ -37,4 +37,5 @@ export enum ArenaTagType {
|
||||
GRASS_WATER_PLEDGE = "GRASS_WATER_PLEDGE",
|
||||
FAIRY_LOCK = "FAIRY_LOCK",
|
||||
NEUTRALIZING_GAS = "NEUTRALIZING_GAS",
|
||||
PENDING_HEAL = "PENDING_HEAL",
|
||||
}
|
||||
|
@ -94,4 +94,6 @@ export enum BattlerTagType {
|
||||
ENDURE_TOKEN = "ENDURE_TOKEN",
|
||||
POWDER = "POWDER",
|
||||
MAGIC_COAT = "MAGIC_COAT",
|
||||
SUPREME_OVERLORD = "SUPREME_OVERLORD",
|
||||
BYPASS_SPEED = "BYPASS_SPEED",
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
/**
|
||||
* Enum representation of the phase types held by implementations of {@linkcode PhasePriorityQueue}.
|
||||
*/
|
||||
// TODO: We currently assume these are in order
|
||||
export enum DynamicPhaseType {
|
||||
POST_SUMMON,
|
||||
}
|
16
src/enums/move-phase-timing-modifier.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import type { ObjectValues } from "#types/type-helpers";
|
||||
|
||||
/**
|
||||
* Enum representing modifiers for the timing of MovePhases.
|
||||
*
|
||||
* @remarks
|
||||
* This system is entirely independent of and takes precedence over move priority
|
||||
*/
|
||||
export const MovePhaseTimingModifier = Object.freeze({
|
||||
/** Used when moves go last regardless of speed and priority (i.e. Quash) */
|
||||
LAST: 0,
|
||||
NORMAL: 1,
|
||||
/** Used to trigger moves immediately (i.e. ones that were called through Instruct). */
|
||||
FIRST: 2,
|
||||
});
|
||||
export type MovePhaseTimingModifier = ObjectValues<typeof MovePhaseTimingModifier>;
|
@ -371,9 +371,15 @@ export class Arena {
|
||||
|
||||
/**
|
||||
* 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 => {
|
||||
// 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 isCherrimWithFlowerGift = p.hasAbility(AbilityId.FLOWER_GIFT) && p.species.speciesId === SpeciesId.CHERRIM;
|
||||
|
||||
|
@ -13,7 +13,6 @@ import { getStatusEffectHealText } from "#data/status-effect";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { BerryType } from "#enums/berry-type";
|
||||
import { Color, ShadowColor } from "#enums/color";
|
||||
import { Command } from "#enums/command";
|
||||
import type { FormChangeItem } from "#enums/form-change-item";
|
||||
import { LearnMoveType } from "#enums/learn-move-type";
|
||||
import type { MoveId } from "#enums/move-id";
|
||||
@ -1542,30 +1541,16 @@ export class BypassSpeedChanceModifier extends PokemonHeldItemModifier {
|
||||
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}
|
||||
* @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
|
||||
*/
|
||||
override apply(pokemon: Pokemon, doBypassSpeed: BooleanHolder): boolean {
|
||||
if (!doBypassSpeed.value && pokemon.randBattleSeedInt(10) < this.getStackCount()) {
|
||||
doBypassSpeed.value = true;
|
||||
const isCommandFight =
|
||||
globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()]?.command === Command.FIGHT;
|
||||
override apply(pokemon: Pokemon): boolean {
|
||||
if (pokemon.randBattleSeedInt(10) < this.getStackCount() && pokemon.addTag(BattlerTagType.BYPASS_SPEED)) {
|
||||
const hasQuickClaw = this.type.is("PokemonHeldItemModifierType") && this.type.id === "QUICK_CLAW";
|
||||
|
||||
if (isCommandFight && hasQuickClaw) {
|
||||
if (hasQuickClaw) {
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("modifier:bypassSpeedChanceApply", {
|
||||
pokemonName: getPokemonNameWithAffix(pokemon),
|
||||
|
@ -8,12 +8,14 @@
|
||||
*/
|
||||
|
||||
import { PHASE_START_COLOR } from "#app/constants/colors";
|
||||
import { DynamicQueueManager } from "#app/dynamic-queue-manager";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import type { Phase } from "#app/phase";
|
||||
import { type PhasePriorityQueue, PostSummonPhasePriorityQueue } from "#data/phase-priority-queue";
|
||||
import type { DynamicPhaseType } from "#enums/dynamic-phase-type";
|
||||
import { PhaseTree } from "#app/phase-tree";
|
||||
import { BattleType } from "#enums/battle-type";
|
||||
import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
import { ActivatePriorityQueuePhase } from "#phases/activate-priority-queue-phase";
|
||||
import type { PokemonMove } from "#moves/pokemon-move";
|
||||
import { AddEnemyBuffModifierPhase } from "#phases/add-enemy-buff-modifier-phase";
|
||||
import { AttemptCapturePhase } from "#phases/attempt-capture-phase";
|
||||
import { AttemptRunPhase } from "#phases/attempt-run-phase";
|
||||
@ -25,6 +27,7 @@ import { CheckSwitchPhase } from "#phases/check-switch-phase";
|
||||
import { CommandPhase } from "#phases/command-phase";
|
||||
import { CommonAnimPhase } from "#phases/common-anim-phase";
|
||||
import { DamageAnimPhase } from "#phases/damage-anim-phase";
|
||||
import { DynamicPhaseMarker } from "#phases/dynamic-phase-marker";
|
||||
import { EggHatchPhase } from "#phases/egg-hatch-phase";
|
||||
import { EggLapsePhase } from "#phases/egg-lapse-phase";
|
||||
import { EggSummaryPhase } from "#phases/egg-summary-phase";
|
||||
@ -109,8 +112,7 @@ import { UnavailablePhase } from "#phases/unavailable-phase";
|
||||
import { UnlockPhase } from "#phases/unlock-phase";
|
||||
import { VictoryPhase } from "#phases/victory-phase";
|
||||
import { WeatherEffectPhase } from "#phases/weather-effect-phase";
|
||||
import type { PhaseMap, PhaseString } from "#types/phase-types";
|
||||
import { type Constructor, coerceArray } from "#utils/common";
|
||||
import type { PhaseConditionFunc, PhaseMap, PhaseString } from "#types/phase-types";
|
||||
|
||||
/**
|
||||
* Object that holds all of the phase constructors.
|
||||
@ -121,7 +123,6 @@ import { type Constructor, coerceArray } from "#utils/common";
|
||||
* This allows for easy creation of new phases without needing to import each phase individually.
|
||||
*/
|
||||
const PHASES = Object.freeze({
|
||||
ActivatePriorityQueuePhase,
|
||||
AddEnemyBuffModifierPhase,
|
||||
AttemptCapturePhase,
|
||||
AttemptRunPhase,
|
||||
@ -133,6 +134,7 @@ const PHASES = Object.freeze({
|
||||
CommandPhase,
|
||||
CommonAnimPhase,
|
||||
DamageAnimPhase,
|
||||
DynamicPhaseMarker,
|
||||
EggHatchPhase,
|
||||
EggLapsePhase,
|
||||
EggSummaryPhase,
|
||||
@ -221,33 +223,30 @@ const PHASES = Object.freeze({
|
||||
/** Maps Phase strings to their constructors */
|
||||
export type PhaseConstructorMap = typeof PHASES;
|
||||
|
||||
/** Phases pushed at the end of each {@linkcode TurnStartPhase} */
|
||||
const turnEndPhases: readonly PhaseString[] = [
|
||||
"WeatherEffectPhase",
|
||||
"PositionalTagPhase",
|
||||
"BerryPhase",
|
||||
"CheckStatusEffectPhase",
|
||||
"TurnEndPhase",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* PhaseManager is responsible for managing the phases in the battle scene
|
||||
*/
|
||||
export class PhaseManager {
|
||||
/** PhaseQueue: dequeue/remove the first element to get the next phase */
|
||||
public phaseQueue: Phase[] = [];
|
||||
public conditionalQueue: Array<[() => boolean, Phase]> = [];
|
||||
/** PhaseQueuePrepend: is a temp storage of what will be added to PhaseQueue */
|
||||
private phaseQueuePrepend: Phase[] = [];
|
||||
private readonly phaseQueue: PhaseTree = new PhaseTree();
|
||||
|
||||
/** overrides default of inserting phases to end of phaseQueuePrepend array. Useful for inserting Phases "out of order" */
|
||||
private phaseQueuePrependSpliceIndex = -1;
|
||||
private nextCommandPhaseQueue: Phase[] = [];
|
||||
|
||||
/** Storage for {@linkcode PhasePriorityQueue}s which hold phases whose order dynamically changes */
|
||||
private dynamicPhaseQueues: PhasePriorityQueue[];
|
||||
/** Parallel array to {@linkcode dynamicPhaseQueues} - matches phase types to their queues */
|
||||
private dynamicPhaseTypes: Constructor<Phase>[];
|
||||
/** Holds priority queues for dynamically ordered phases */
|
||||
public dynamicQueueManager = new DynamicQueueManager();
|
||||
|
||||
/** The currently-running phase */
|
||||
private currentPhase: Phase;
|
||||
/** The phase put on standby if {@linkcode overridePhase} is called */
|
||||
private standbyPhase: Phase | null = null;
|
||||
|
||||
constructor() {
|
||||
this.dynamicPhaseQueues = [new PostSummonPhasePriorityQueue()];
|
||||
this.dynamicPhaseTypes = [PostSummonPhase];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all previously set phases, then add a new {@linkcode TitlePhase} to transition to the title screen.
|
||||
* @param addLogin - Whether to add a new {@linkcode LoginPhase} before the {@linkcode TitlePhase}
|
||||
@ -275,123 +274,76 @@ export class PhaseManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Adds a phase to the end of the queue
|
||||
* @param phase - The {@linkcode Phase} to add
|
||||
*/
|
||||
pushConditionalPhase(phase: Phase, condition: () => boolean): void {
|
||||
this.conditionalQueue.push([condition, phase]);
|
||||
public pushPhase(phase: Phase): void {
|
||||
this.phaseQueue.pushPhase(this.checkDynamic(phase));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a phase to nextCommandPhaseQueue, as long as boolean passed in is false
|
||||
* @param phase {@linkcode Phase} the phase to add
|
||||
* @param defer boolean on which queue to add to, defaults to false, and adds to phaseQueue
|
||||
* Queue a phase to be run immediately after the current phase finishes. \
|
||||
* Unshifted phases are run in FIFO order if multiple are queued during a single phase's execution.
|
||||
* @param phase - The {@linkcode Phase} to add
|
||||
*/
|
||||
pushPhase(phase: Phase, defer = false): void {
|
||||
if (this.getDynamicPhaseType(phase) !== undefined) {
|
||||
this.pushDynamicPhase(phase);
|
||||
} else {
|
||||
(!defer ? this.phaseQueue : this.nextCommandPhaseQueue).push(phase);
|
||||
}
|
||||
public unshiftPhase(phase: Phase): void {
|
||||
const toAdd = this.checkDynamic(phase);
|
||||
phase.is("MovePhase") ? this.phaseQueue.addAfter(toAdd, "MoveEndPhase") : this.phaseQueue.addPhase(toAdd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds Phase(s) to the end of phaseQueuePrepend, or at phaseQueuePrependSpliceIndex
|
||||
* @param phases {@linkcode Phase} the phase(s) to add
|
||||
* Helper method to queue a phase as dynamic if necessary
|
||||
* @param phase - The phase to check
|
||||
* @returns The {@linkcode Phase} or a {@linkcode DynamicPhaseMarker} to be used in its place
|
||||
*/
|
||||
unshiftPhase(...phases: Phase[]): void {
|
||||
if (this.phaseQueuePrependSpliceIndex === -1) {
|
||||
this.phaseQueuePrepend.push(...phases);
|
||||
} else {
|
||||
this.phaseQueuePrepend.splice(this.phaseQueuePrependSpliceIndex, 0, ...phases);
|
||||
private checkDynamic(phase: Phase): Phase {
|
||||
if (this.dynamicQueueManager.queueDynamicPhase(phase)) {
|
||||
return new DynamicPhaseMarker(phase.phaseName);
|
||||
}
|
||||
return phase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the phaseQueue
|
||||
* @param leaveUnshifted - If `true`, leaves the top level of the tree intact; default `false`
|
||||
*/
|
||||
clearPhaseQueue(): void {
|
||||
this.phaseQueue.splice(0, this.phaseQueue.length);
|
||||
public clearPhaseQueue(leaveUnshifted = false): void {
|
||||
this.phaseQueue.clear(leaveUnshifted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all phase-related stuff, including all phase queues, the current and standby phases, and a splice index
|
||||
*/
|
||||
clearAllPhases(): void {
|
||||
for (const queue of [this.phaseQueue, this.phaseQueuePrepend, this.conditionalQueue, this.nextCommandPhaseQueue]) {
|
||||
queue.splice(0, queue.length);
|
||||
}
|
||||
this.dynamicPhaseQueues.forEach(queue => queue.clear());
|
||||
/** Clears all phase queues and the standby phase */
|
||||
public clearAllPhases(): void {
|
||||
this.clearPhaseQueue();
|
||||
this.dynamicQueueManager.clearQueues();
|
||||
this.standbyPhase = null;
|
||||
this.clearPhaseQueueSplice();
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by function unshiftPhase(), sets index to start inserting at current length instead of the end of the array, useful if phaseQueuePrepend gets longer with Phases
|
||||
* Determines the next phase to run and starts it.
|
||||
* @privateRemarks
|
||||
* This is called by {@linkcode Phase.end} by default, and should not be called by other methods.
|
||||
*/
|
||||
setPhaseQueueSplice(): void {
|
||||
this.phaseQueuePrependSpliceIndex = this.phaseQueuePrepend.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets phaseQueuePrependSpliceIndex to -1, implies that calls to unshiftPhase will insert at end of phaseQueuePrepend
|
||||
*/
|
||||
clearPhaseQueueSplice(): void {
|
||||
this.phaseQueuePrependSpliceIndex = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is called by each Phase implementations "end()" by default
|
||||
* We dump everything from phaseQueuePrepend to the start of of phaseQueue
|
||||
* then removes first Phase and starts it
|
||||
*/
|
||||
shiftPhase(): void {
|
||||
public shiftPhase(): void {
|
||||
if (this.standbyPhase) {
|
||||
this.currentPhase = this.standbyPhase;
|
||||
this.standbyPhase = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.phaseQueuePrependSpliceIndex > -1) {
|
||||
this.clearPhaseQueueSplice();
|
||||
}
|
||||
this.phaseQueue.unshift(...this.phaseQueuePrepend);
|
||||
this.phaseQueuePrepend.splice(0);
|
||||
let nextPhase = this.phaseQueue.getNextPhase();
|
||||
|
||||
const unactivatedConditionalPhases: [() => boolean, Phase][] = [];
|
||||
// Check if there are any conditional phases queued
|
||||
for (const [condition, phase] of this.conditionalQueue) {
|
||||
// Evaluate the condition associated with the phase
|
||||
if (condition()) {
|
||||
// If the condition is met, add the phase to the phase queue
|
||||
this.pushPhase(phase);
|
||||
} else {
|
||||
// If the condition is not met, re-add the phase back to the end of the conditional queue
|
||||
unactivatedConditionalPhases.push([condition, phase]);
|
||||
}
|
||||
if (nextPhase?.is("DynamicPhaseMarker")) {
|
||||
nextPhase = this.dynamicQueueManager.popNextPhase(nextPhase.phaseType);
|
||||
}
|
||||
|
||||
this.conditionalQueue = unactivatedConditionalPhases;
|
||||
|
||||
// If no phases are left, unshift phases to start a new turn.
|
||||
if (this.phaseQueue.length === 0) {
|
||||
this.populatePhaseQueue();
|
||||
// Clear the conditionalQueue if there are no phases left in the phaseQueue
|
||||
this.conditionalQueue = [];
|
||||
if (nextPhase == null) {
|
||||
this.turnStart();
|
||||
} else {
|
||||
this.currentPhase = nextPhase;
|
||||
}
|
||||
|
||||
// Bang is justified as `populatePhaseQueue` ensures we always have _something_ in the queue at all times
|
||||
this.currentPhase = this.phaseQueue.shift()!;
|
||||
|
||||
this.startCurrentPhase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to start and log the current phase.
|
||||
*/
|
||||
@ -400,7 +352,14 @@ export class PhaseManager {
|
||||
this.currentPhase.start();
|
||||
}
|
||||
|
||||
overridePhase(phase: Phase): boolean {
|
||||
/**
|
||||
* Overrides the currently running phase with another
|
||||
* @param phase - The {@linkcode Phase} to override the current one with
|
||||
* @returns If the override succeeded
|
||||
*
|
||||
* @todo This is antithetical to the phase structure and used a single time. Remove it.
|
||||
*/
|
||||
public overridePhase(phase: Phase): boolean {
|
||||
if (this.standbyPhase) {
|
||||
return false;
|
||||
}
|
||||
@ -413,173 +372,47 @@ export class PhaseManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a specific {@linkcode Phase} in the phase queue.
|
||||
* Determine if there is a queued {@linkcode Phase} meeting the specified conditions.
|
||||
* @param type - The {@linkcode PhaseString | type} of phase to search for
|
||||
* @param condition - An optional {@linkcode PhaseConditionFunc} to add conditions to the search
|
||||
* @returns Whether a matching phase exists
|
||||
*/
|
||||
public hasPhaseOfType<T extends PhaseString>(type: T, condition?: PhaseConditionFunc<T>): boolean {
|
||||
return this.dynamicQueueManager.exists(type, condition) || this.phaseQueue.exists(type, condition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to find and remove the first queued {@linkcode Phase} matching the given conditions.
|
||||
* @param type - The {@linkcode PhaseString | type} of phase to search for
|
||||
* @param phaseFilter - An optional {@linkcode PhaseConditionFunc} to add conditions to the search
|
||||
* @returns Whether a phase was successfully removed
|
||||
*/
|
||||
public tryRemovePhase<T extends PhaseString>(type: T, phaseFilter?: PhaseConditionFunc<T>): boolean {
|
||||
if (this.dynamicQueueManager.removePhase(type, phaseFilter)) {
|
||||
return true;
|
||||
}
|
||||
return this.phaseQueue.remove(type, phaseFilter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all {@linkcode Phase}s of the given type from the queue
|
||||
* @param phaseType - The {@linkcode PhaseString | type} of phase to search for
|
||||
*
|
||||
* @param phaseFilter filter function to use to find the wanted phase
|
||||
* @returns the found phase or undefined if none found
|
||||
* @remarks
|
||||
* This is not intended to be used with dynamically ordered phases, and does not operate on the dynamic queue. \
|
||||
* However, it does remove {@linkcode DynamicPhaseMarker}s and so would prevent such phases from activating.
|
||||
*/
|
||||
findPhase<P extends Phase = Phase>(phaseFilter: (phase: P) => boolean): P | undefined {
|
||||
return this.phaseQueue.find(phaseFilter) as P | undefined;
|
||||
}
|
||||
|
||||
tryReplacePhase(phaseFilter: (phase: Phase) => boolean, phase: Phase): boolean {
|
||||
const phaseIndex = this.phaseQueue.findIndex(phaseFilter);
|
||||
if (phaseIndex > -1) {
|
||||
this.phaseQueue[phaseIndex] = phase;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
tryRemovePhase(phaseFilter: (phase: Phase) => boolean): boolean {
|
||||
const phaseIndex = this.phaseQueue.findIndex(phaseFilter);
|
||||
if (phaseIndex > -1) {
|
||||
this.phaseQueue.splice(phaseIndex, 1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
public removeAllPhasesOfType(type: PhaseString): void {
|
||||
this.phaseQueue.removeAll(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
tryRemoveUnshiftedPhase(phaseFilter: (phase: Phase) => boolean): boolean {
|
||||
const phaseIndex = this.phaseQueuePrepend.findIndex(phaseFilter);
|
||||
if (phaseIndex > -1) {
|
||||
this.phaseQueuePrepend.splice(phaseIndex, 1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 targetPhase - The phase to search for in phaseQueue
|
||||
* @returns boolean if a targetPhase was found and added
|
||||
*/
|
||||
prependToPhase(phase: Phase | Phase[], targetPhase: PhaseString): boolean {
|
||||
phase = coerceArray(phase);
|
||||
const target = PHASES[targetPhase];
|
||||
const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof target);
|
||||
|
||||
if (targetIndex !== -1) {
|
||||
this.phaseQueue.splice(targetIndex, 0, ...phase);
|
||||
return true;
|
||||
}
|
||||
this.unshiftPhase(...phase);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to add the input phase(s) to index after target phase in the {@linkcode phaseQueue}, else simply calls {@linkcode unshiftPhase()}
|
||||
* @param phase {@linkcode Phase} the phase(s) to be added
|
||||
* @param targetPhase {@linkcode Phase} the type of phase to search for in {@linkcode phaseQueue}
|
||||
* @param condition Condition the target phase must meet to be appended to
|
||||
* @returns `true` if a `targetPhase` was found to append to
|
||||
*/
|
||||
appendToPhase(phase: Phase | Phase[], targetPhase: PhaseString, condition?: (p: Phase) => boolean): boolean {
|
||||
phase = coerceArray(phase);
|
||||
const target = PHASES[targetPhase];
|
||||
const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof target && (!condition || condition(ph)));
|
||||
|
||||
if (targetIndex !== -1 && this.phaseQueue.length > targetIndex) {
|
||||
this.phaseQueue.splice(targetIndex + 1, 0, ...phase);
|
||||
return true;
|
||||
}
|
||||
this.unshiftPhase(...phase);
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to remove one or more Phases from the given DynamicPhaseQueue, removing the equivalent amount of {@linkcode ActivatePriorityQueuePhase}s from the queue.
|
||||
* @param type - The {@linkcode DynamicPhaseType} to check
|
||||
* @param phaseFilter - The function to select phases for removal
|
||||
* @param removeCount - The maximum number of phases to remove, or `all` to remove all matching phases;
|
||||
* default `1`
|
||||
* @todo Remove this eventually once the patchwork bug this is used for is fixed
|
||||
*/
|
||||
public tryRemoveDynamicPhase(
|
||||
type: DynamicPhaseType,
|
||||
phaseFilter: (phase: Phase) => boolean,
|
||||
removeCount: number | "all" = 1,
|
||||
): void {
|
||||
const numRemoved = this.dynamicPhaseQueues[type].tryRemovePhase(phaseFilter, removeCount);
|
||||
for (let x = 0; x < numRemoved; x++) {
|
||||
this.tryRemovePhase(p => p.is("ActivatePriorityQueuePhase"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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` to the queue
|
||||
* @param message - string for MessagePhase
|
||||
* @param callbackDelay - optional param for MessagePhase constructor
|
||||
* @param prompt - optional param for MessagePhase constructor
|
||||
* @param promptDelay - optional param for MessagePhase constructor
|
||||
* @param defer - Whether to allow the phase to be deferred
|
||||
* @param defer - If `true`, push the phase instead of unshifting; default `false`
|
||||
*
|
||||
* @see {@linkcode MessagePhase} for more details on the parameters
|
||||
*/
|
||||
@ -591,20 +424,18 @@ export class PhaseManager {
|
||||
defer?: boolean | null,
|
||||
) {
|
||||
const phase = new MessagePhase(message, callbackDelay, prompt, promptDelay);
|
||||
if (!defer) {
|
||||
// adds to the end of PhaseQueuePrepend
|
||||
this.unshiftPhase(phase);
|
||||
} else {
|
||||
//remember that pushPhase adds it to nextCommandPhaseQueue
|
||||
if (defer) {
|
||||
this.pushPhase(phase);
|
||||
} else {
|
||||
this.unshiftPhase(phase);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue a phase to show or hide the ability flyout bar.
|
||||
* Queues an ability bar flyout phase via {@linkcode unshiftPhase}
|
||||
* @param pokemon - The {@linkcode Pokemon} whose ability is being activated
|
||||
* @param passive - Whether the ability is a passive
|
||||
* @param show - Whether to show or hide the bar
|
||||
* @param show - If `true`, show the bar. Otherwise, hide it
|
||||
*/
|
||||
public queueAbilityDisplay(pokemon: Pokemon, passive: boolean, show: boolean): void {
|
||||
this.unshiftPhase(show ? new ShowAbilityPhase(pokemon.getBattlerIndex(), passive) : new HideAbilityPhase());
|
||||
@ -620,14 +451,12 @@ export class PhaseManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves everything from nextCommandPhaseQueue to phaseQueue (keeping order)
|
||||
* Clear all dynamic queues and begin a new {@linkcode TurnInitPhase} for the new turn.
|
||||
* Called whenever the current phase queue is empty.
|
||||
*/
|
||||
private populatePhaseQueue(): void {
|
||||
if (this.nextCommandPhaseQueue.length > 0) {
|
||||
this.phaseQueue.push(...this.nextCommandPhaseQueue);
|
||||
this.nextCommandPhaseQueue.splice(0, this.nextCommandPhaseQueue.length);
|
||||
}
|
||||
this.phaseQueue.push(new TurnInitPhase());
|
||||
private turnStart(): void {
|
||||
this.dynamicQueueManager.clearQueues();
|
||||
this.currentPhase = new TurnInitPhase();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -669,50 +498,119 @@ export class PhaseManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new phase and immediately prepend it to an existing phase in the phase queue.
|
||||
* Equivalent to calling {@linkcode create} followed by {@linkcode prependToPhase}.
|
||||
* @param targetPhase - The phase to search for in phaseQueue
|
||||
* @param phase - The name of the phase to create
|
||||
* Add a {@linkcode FaintPhase} to the queue
|
||||
* @param args - The arguments to pass to the phase constructor
|
||||
* @returns `true` if a `targetPhase` was found to prepend to
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* Faint phases are ordered in a special way to allow battle effects to settle before the pokemon faints.
|
||||
* @see {@linkcode PhaseTree.addPhase}
|
||||
*/
|
||||
public prependNewToPhase<T extends PhaseString>(
|
||||
targetPhase: PhaseString,
|
||||
phase: T,
|
||||
...args: ConstructorParameters<PhaseConstructorMap[T]>
|
||||
): boolean {
|
||||
return this.prependToPhase(this.create(phase, ...args), targetPhase);
|
||||
public queueFaintPhase(...args: ConstructorParameters<PhaseConstructorMap["FaintPhase"]>): void {
|
||||
this.phaseQueue.addPhase(this.create("FaintPhase", ...args), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new phase and immediately append it to an existing phase the phase queue.
|
||||
* Equivalent to calling {@linkcode create} followed by {@linkcode appendToPhase}.
|
||||
* @param targetPhase - The phase to search for in phaseQueue
|
||||
* @param phase - The name of the phase to create
|
||||
* @param args - The arguments to pass to the phase constructor
|
||||
* @returns `true` if a `targetPhase` was found to append to
|
||||
* Attempts to add {@linkcode PostSummonPhase}s for the enemy pokemon
|
||||
*
|
||||
* This is used to ensure that wild pokemon (which have no {@linkcode SummonPhase}) do not queue a {@linkcode PostSummonPhase}
|
||||
* until all pokemon are on the field.
|
||||
*/
|
||||
public appendNewToPhase<T extends PhaseString>(
|
||||
targetPhase: PhaseString,
|
||||
phase: T,
|
||||
...args: ConstructorParameters<PhaseConstructorMap[T]>
|
||||
): boolean {
|
||||
return this.appendToPhase(this.create(phase, ...args), targetPhase);
|
||||
public tryAddEnemyPostSummonPhases(): void {
|
||||
if (
|
||||
![BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(globalScene.currentBattle.battleType)
|
||||
&& !this.phaseQueue.exists("SummonPhase")
|
||||
) {
|
||||
globalScene.getEnemyField().forEach(p => {
|
||||
this.pushPhase(new PostSummonPhase(p.getBattlerIndex(), "SummonPhase"));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public startNewDynamicPhase<T extends PhaseString>(
|
||||
/**
|
||||
* Create a new phase and queue it to run after all others queued by the currently running phase.
|
||||
* @param phase - The name of the phase to create
|
||||
* @param args - The arguments to pass to the phase constructor
|
||||
*
|
||||
* @deprecated Only used for switches and should be phased out eventually.
|
||||
*/
|
||||
public queueDeferred<const T extends "SwitchPhase" | "SwitchSummonPhase">(
|
||||
phase: T,
|
||||
...args: ConstructorParameters<PhaseConstructorMap[T]>
|
||||
): void {
|
||||
this.startDynamicPhase(this.create(phase, ...args));
|
||||
this.phaseQueue.unshiftToCurrent(this.create(phase, ...args));
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the first {@linkcode MovePhase} meeting the condition
|
||||
* @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function
|
||||
* @returns The MovePhase, or `undefined` if it does not exist
|
||||
*/
|
||||
public getMovePhase(phaseCondition: PhaseConditionFunc<"MovePhase">): MovePhase | undefined {
|
||||
return this.dynamicQueueManager.getMovePhase(phaseCondition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and cancels the first {@linkcode MovePhase} meeting the condition
|
||||
* @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function
|
||||
*/
|
||||
public cancelMove(phaseCondition: PhaseConditionFunc<"MovePhase">): void {
|
||||
this.dynamicQueueManager.cancelMovePhase(phaseCondition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the first {@linkcode MovePhase} meeting the condition and forces it next
|
||||
* @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function
|
||||
*/
|
||||
public forceMoveNext(phaseCondition: PhaseConditionFunc<"MovePhase">): void {
|
||||
this.dynamicQueueManager.setMoveTimingModifier(phaseCondition, MovePhaseTimingModifier.FIRST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the first {@linkcode MovePhase} meeting the condition and forces it last
|
||||
* @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function
|
||||
*/
|
||||
public forceMoveLast(phaseCondition: PhaseConditionFunc<"MovePhase">): void {
|
||||
this.dynamicQueueManager.setMoveTimingModifier(phaseCondition, MovePhaseTimingModifier.LAST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the first {@linkcode MovePhase} meeting the condition and changes its move
|
||||
* @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function
|
||||
* @param move - The {@linkcode PokemonMove | move} to use in replacement
|
||||
*/
|
||||
public changePhaseMove(phaseCondition: PhaseConditionFunc<"MovePhase">, move: PokemonMove): void {
|
||||
this.dynamicQueueManager.setMoveForPhase(phaseCondition, move);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects moves which were targeted at a {@linkcode Pokemon} that has been removed
|
||||
* @param removedPokemon - The removed {@linkcode Pokemon}
|
||||
* @param allyPokemon - The ally of the removed pokemon
|
||||
*/
|
||||
public redirectMoves(removedPokemon: Pokemon, allyPokemon: Pokemon): void {
|
||||
this.dynamicQueueManager.redirectMoves(removedPokemon, allyPokemon);
|
||||
}
|
||||
|
||||
/** Queues phases which run at the end of each turn */
|
||||
public queueTurnEndPhases(): void {
|
||||
turnEndPhases.forEach(p => {
|
||||
this.pushNew(p);
|
||||
});
|
||||
}
|
||||
|
||||
/** Prevents end of turn effects from triggering when transitioning to a new biome on a X0 wave */
|
||||
public onInterlude(): void {
|
||||
const phasesToRemove = ["WeatherEffectPhase", "BerryPhase", "CheckStatusEffectPhase"];
|
||||
this.phaseQueue = this.phaseQueue.filter(p => !phasesToRemove.includes(p.phaseName));
|
||||
const phasesToRemove: readonly PhaseString[] = [
|
||||
"WeatherEffectPhase",
|
||||
"BerryPhase",
|
||||
"CheckStatusEffectPhase",
|
||||
] as const;
|
||||
for (const phaseType of phasesToRemove) {
|
||||
this.phaseQueue.removeAll(phaseType);
|
||||
}
|
||||
|
||||
const turnEndPhase = this.findPhase<TurnEndPhase>(p => p.phaseName === "TurnEndPhase");
|
||||
const turnEndPhase = this.phaseQueue.find("TurnEndPhase");
|
||||
if (turnEndPhase) {
|
||||
turnEndPhase.upcomingInterlude = true;
|
||||
}
|
||||
|