Merge remote-tracking branch 'upstream/beta' into phase-interceptor

This commit is contained in:
Bertie690 2025-09-21 18:47:57 -04:00
commit 88e8d64790
201 changed files with 3007 additions and 2038 deletions

View File

@ -48,7 +48,7 @@
"lefthook": "^1.12.2", "lefthook": "^1.12.2",
"msw": "^2.10.4", "msw": "^2.10.4",
"phaser3spectorjs": "^0.0.8", "phaser3spectorjs": "^0.0.8",
"typedoc": "0.28.7", "typedoc": "^0.28.13",
"typedoc-github-theme": "^0.3.1", "typedoc-github-theme": "^0.3.1",
"typedoc-plugin-coverage": "^4.0.1", "typedoc-plugin-coverage": "^4.0.1",
"typedoc-plugin-mdn-links": "^5.0.9", "typedoc-plugin-mdn-links": "^5.0.9",
@ -74,5 +74,5 @@
"engines": { "engines": {
"node": ">=22.0.0" "node": ">=22.0.0"
}, },
"packageManager": "pnpm@10.16.1" "packageManager": "pnpm@10.17.0"
} }

View File

@ -88,17 +88,17 @@ importers:
specifier: ^0.0.8 specifier: ^0.0.8
version: 0.0.8 version: 0.0.8
typedoc: typedoc:
specifier: 0.28.7 specifier: ^0.28.13
version: 0.28.7(typescript@5.9.2) version: 0.28.13(typescript@5.9.2)
typedoc-github-theme: typedoc-github-theme:
specifier: ^0.3.1 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: typedoc-plugin-coverage:
specifier: ^4.0.1 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: typedoc-plugin-mdn-links:
specifier: ^5.0.9 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: typescript:
specifier: ^5.9.2 specifier: ^5.9.2
version: 5.9.2 version: 5.9.2
@ -717,17 +717,17 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@shikijs/engine-oniguruma@3.12.2': '@shikijs/engine-oniguruma@3.13.0':
resolution: {integrity: sha512-hozwnFHsLvujK4/CPVHNo3Bcg2EsnG8krI/ZQ2FlBlCRpPZW4XAEQmEwqegJsypsTAN9ehu2tEYe30lYKSZW/w==} resolution: {integrity: sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg==}
'@shikijs/langs@3.12.2': '@shikijs/langs@3.13.0':
resolution: {integrity: sha512-bVx5PfuZHDSHoBal+KzJZGheFuyH4qwwcwG/n+MsWno5cTlKmaNtTsGzJpHYQ8YPbB5BdEdKU1rga5/6JGY8ww==} resolution: {integrity: sha512-672c3WAETDYHwrRP0yLy3W1QYB89Hbpj+pO4KhxK6FzIrDI2FoEXNiNCut6BQmEApYLfuYfpgOZaqbY+E9b8wQ==}
'@shikijs/themes@3.12.2': '@shikijs/themes@3.13.0':
resolution: {integrity: sha512-fTR3QAgnwYpfGczpIbzPjlRnxyONJOerguQv1iwpyQZ9QXX4qy/XFQqXlf17XTsorxnHoJGbH/LXBvwtqDsF5A==} resolution: {integrity: sha512-Vxw1Nm1/Od8jyA7QuAenaV78BG2nSr3/gCGdBkLpfLscddCkzkL36Q5b67SrLLfvAJTOUzW39x4FHVCFriPVgg==}
'@shikijs/types@3.12.2': '@shikijs/types@3.13.0':
resolution: {integrity: sha512-K5UIBzxCyv0YoxN3LMrKB9zuhp1bV+LgewxuVwHdl4Gz5oePoUFrr9EfgJlGlDeXCU1b/yhdnXeuRvAnz8HN8Q==} resolution: {integrity: sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw==}
'@shikijs/vscode-textmate@10.0.2': '@shikijs/vscode-textmate@10.0.2':
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
@ -1828,12 +1828,12 @@ packages:
peerDependencies: peerDependencies:
typedoc: 0.27.x || 0.28.x typedoc: 0.27.x || 0.28.x
typedoc@0.28.7: typedoc@0.28.13:
resolution: {integrity: sha512-lpz0Oxl6aidFkmS90VQDQjk/Qf2iw0IUvFqirdONBdj7jPSN9mGXhy66BcGNDxx5ZMyKKiBVAREvPEzT6Uxipw==} resolution: {integrity: sha512-dNWY8msnYB2a+7Audha+aTF1Pu3euiE7ySp53w8kEsXoYw7dMouV5A1UsTUY345aB152RHnmRMDiovuBi7BD+w==}
engines: {node: '>= 18', pnpm: '>= 10'} engines: {node: '>= 18', pnpm: '>= 10'}
hasBin: true hasBin: true
peerDependencies: 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: typescript@5.9.2:
resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==}
@ -2312,10 +2312,10 @@ snapshots:
'@gerrit0/mini-shiki@3.12.2': '@gerrit0/mini-shiki@3.12.2':
dependencies: dependencies:
'@shikijs/engine-oniguruma': 3.12.2 '@shikijs/engine-oniguruma': 3.13.0
'@shikijs/langs': 3.12.2 '@shikijs/langs': 3.13.0
'@shikijs/themes': 3.12.2 '@shikijs/themes': 3.13.0
'@shikijs/types': 3.12.2 '@shikijs/types': 3.13.0
'@shikijs/vscode-textmate': 10.0.2 '@shikijs/vscode-textmate': 10.0.2
'@inquirer/checkbox@4.2.0(@types/node@22.16.5)': '@inquirer/checkbox@4.2.0(@types/node@22.16.5)':
@ -2547,20 +2547,20 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.50.1': '@rollup/rollup-win32-x64-msvc@4.50.1':
optional: true optional: true
'@shikijs/engine-oniguruma@3.12.2': '@shikijs/engine-oniguruma@3.13.0':
dependencies: dependencies:
'@shikijs/types': 3.12.2 '@shikijs/types': 3.13.0
'@shikijs/vscode-textmate': 10.0.2 '@shikijs/vscode-textmate': 10.0.2
'@shikijs/langs@3.12.2': '@shikijs/langs@3.13.0':
dependencies: dependencies:
'@shikijs/types': 3.12.2 '@shikijs/types': 3.13.0
'@shikijs/themes@3.12.2': '@shikijs/themes@3.13.0':
dependencies: dependencies:
'@shikijs/types': 3.12.2 '@shikijs/types': 3.13.0
'@shikijs/types@3.12.2': '@shikijs/types@3.13.0':
dependencies: dependencies:
'@shikijs/vscode-textmate': 10.0.2 '@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.4 '@types/hast': 3.0.4
@ -3682,19 +3682,19 @@ snapshots:
type-fest@4.41.0: {} 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: 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: 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: 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: dependencies:
'@gerrit0/mini-shiki': 3.12.2 '@gerrit0/mini-shiki': 3.12.2
lunr: 2.3.9 lunr: 2.3.9

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 202 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 119 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 116 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 124 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 186 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 242 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 119 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 183 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 169 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 202 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 249 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 190 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 244 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 195 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 193 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 119 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 307 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 202 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 249 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 190 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 244 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 195 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 193 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 119 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 307 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 116 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 116 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 119 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 116 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 119 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 202 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 119 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 116 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 119 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 116 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 119 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 119 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 116 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 124 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 187 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 242 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 119 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 183 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 169 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 116 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 124 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 187 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 242 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 119 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 183 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 169 B

View File

@ -89,7 +89,8 @@ export type AbilityBattlerTagType =
| BattlerTagType.QUARK_DRIVE | BattlerTagType.QUARK_DRIVE
| BattlerTagType.UNBURDEN | BattlerTagType.UNBURDEN
| BattlerTagType.SLOW_START | BattlerTagType.SLOW_START
| BattlerTagType.TRUANT; | BattlerTagType.TRUANT
| BattlerTagType.SUPREME_OVERLORD;
/** Subset of {@linkcode BattlerTagType}s that provide type boosts */ /** Subset of {@linkcode BattlerTagType}s that provide type boosts */
export type TypeBoostTagType = BattlerTagType.FIRE_BOOST | BattlerTagType.CHARGED; export type TypeBoostTagType = BattlerTagType.FIRE_BOOST | BattlerTagType.CHARGED;

View 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">;

View File

@ -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 { PhaseConstructorMap } from "#app/phase-manager";
import type { ObjectValues } from "#types/type-helpers"; 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. // the centralized place for type definitions for the phase system.
export type * from "#app/phase-manager"; export type * from "#app/phase-manager";
// This file includes helpful types for the phase system. /** Map of phase names to constructors for said phase */
// It intentionally imports the phase constructor map from the phase manager (and re-exports it)
/**
* Map of phase names to constructors for said phase
*/
export type PhaseMap = { export type PhaseMap = {
[K in keyof PhaseConstructorMap]: InstanceType<PhaseConstructorMap[K]>; [K in keyof PhaseConstructorMap]: InstanceType<PhaseConstructorMap[K]>;
}; };
/** /** Union type of all phase constructors. */
* Union type of all phase constructors.
*/
export type PhaseClass = ObjectValues<PhaseConstructorMap>; 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; 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;
}

View File

@ -4,8 +4,10 @@ import type { BattleType } from "#enums/battle-type";
import type { GameModes } from "#enums/game-modes"; import type { GameModes } from "#enums/game-modes";
import type { MoveId } from "#enums/move-id"; import type { MoveId } from "#enums/move-id";
import type { MysteryEncounterType } from "#enums/mystery-encounter-type"; import type { MysteryEncounterType } from "#enums/mystery-encounter-type";
import type { Nature } from "#enums/nature";
import type { PlayerGender } from "#enums/player-gender"; import type { PlayerGender } from "#enums/player-gender";
import type { PokemonType } from "#enums/pokemon-type"; import type { PokemonType } from "#enums/pokemon-type";
import type { SpeciesId } from "#enums/species-id";
import type { MysteryEncounterSaveData } from "#mystery-encounters/mystery-encounter-save-data"; import type { MysteryEncounterSaveData } from "#mystery-encounters/mystery-encounter-save-data";
import type { Variant } from "#sprites/variant"; import type { Variant } from "#sprites/variant";
import type { ArenaData } from "#system/arena-data"; import type { ArenaData } from "#system/arena-data";
@ -108,6 +110,22 @@ export interface DexAttrProps {
formIndex: number; formIndex: number;
} }
export interface Starter {
speciesId: SpeciesId;
shiny: boolean;
variant: Variant;
formIndex: number;
female?: boolean;
abilityIndex: number;
passive: boolean;
nature: Nature;
moveset?: StarterMoveset;
pokerus: boolean;
nickname?: string;
teraType?: PokemonType;
ivs: number[];
}
export type RunHistoryData = Record<number, RunEntry>; export type RunHistoryData = Record<number, RunEntry>;
export interface RunEntry { export interface RunEntry {

View File

@ -104,7 +104,6 @@ import {
import { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterSaveData } from "#mystery-encounters/mystery-encounter-save-data"; import { MysteryEncounterSaveData } from "#mystery-encounters/mystery-encounter-save-data";
import { allMysteryEncounters, mysteryEncountersByBiome } from "#mystery-encounters/mystery-encounters"; import { allMysteryEncounters, mysteryEncountersByBiome } from "#mystery-encounters/mystery-encounters";
import type { MovePhase } from "#phases/move-phase";
import { expSpriteKeys } from "#sprites/sprite-keys"; import { expSpriteKeys } from "#sprites/sprite-keys";
import { hasExpSprite } from "#sprites/sprite-utils"; import { hasExpSprite } from "#sprites/sprite-utils";
import type { Variant } from "#sprites/variant"; import type { Variant } from "#sprites/variant";
@ -799,12 +798,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. * 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} * @returns array of {@linkcode EnemyPokemon}
*/ */
public getEnemyField(): EnemyPokemon[] { public getEnemyField(active = false): EnemyPokemon[] {
const party = this.getEnemyParty(); 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());
} }
/** /**
@ -829,25 +830,7 @@ export class BattleScene extends SceneBase {
* @param allyPokemon - The {@linkcode Pokemon} allied with the removed Pokemon; will have moves redirected to it * @param allyPokemon - The {@linkcode Pokemon} allied with the removed Pokemon; will have moves redirected to it
*/ */
redirectPokemonMoves(removedPokemon: Pokemon, allyPokemon: Pokemon): void { redirectPokemonMoves(removedPokemon: Pokemon, allyPokemon: Pokemon): void {
// failsafe: if not a double battle just return this.phaseManager.redirectMoves(removedPokemon, allyPokemon);
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);
}
} }
/** /**
@ -1447,7 +1430,7 @@ export class BattleScene extends SceneBase {
} }
if (lastBattle?.double && !newDouble) { if (lastBattle?.double && !newDouble) {
this.phaseManager.tryRemovePhase((p: Phase) => p.is("SwitchPhase")); this.phaseManager.tryRemovePhase("SwitchPhase");
for (const p of this.getPlayerField()) { for (const p of this.getPlayerField()) {
p.lapseTag(BattlerTagType.COMMANDED); p.lapseTag(BattlerTagType.COMMANDED);
} }

View File

@ -33,6 +33,7 @@ import { CommonAnim } from "#enums/move-anims-common";
import { MoveCategory } from "#enums/move-category"; import { MoveCategory } from "#enums/move-category";
import { MoveFlags } from "#enums/move-flags"; import { MoveFlags } from "#enums/move-flags";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
import { MoveResult } from "#enums/move-result"; import { MoveResult } from "#enums/move-result";
import { MoveTarget } from "#enums/move-target"; import { MoveTarget } from "#enums/move-target";
import { MoveUseMode } from "#enums/move-use-mode"; import { MoveUseMode } from "#enums/move-use-mode";
@ -2555,7 +2556,7 @@ export class PostIntimidateStatStageChangeAbAttr extends AbAttr {
override apply({ pokemon, simulated, cancelled }: AbAttrParamsWithCancel): void { override apply({ pokemon, simulated, cancelled }: AbAttrParamsWithCancel): void {
if (!simulated) { if (!simulated) {
globalScene.phaseManager.pushNew( globalScene.phaseManager.unshiftNew(
"StatStageChangePhase", "StatStageChangePhase",
pokemon.getBattlerIndex(), pokemon.getBattlerIndex(),
false, false,
@ -3240,6 +3241,7 @@ export class CommanderAbAttr extends AbAttr {
return ( return (
globalScene.currentBattle?.double globalScene.currentBattle?.double
&& ally != null && ally != null
&& ally.isActive(true)
&& ally.species.speciesId === SpeciesId.DONDOZO && ally.species.speciesId === SpeciesId.DONDOZO
&& !(ally.isFainted() || ally.getTag(BattlerTagType.COMMANDED)) && !(ally.isFainted() || ally.getTag(BattlerTagType.COMMANDED))
); );
@ -3254,7 +3256,7 @@ export class CommanderAbAttr extends AbAttr {
// Apply boosts from this effect to the ally Dondozo // Apply boosts from this effect to the ally Dondozo
pokemon.getAlly()?.addTag(BattlerTagType.COMMANDED, 0, MoveId.NONE, pokemon.id); pokemon.getAlly()?.addTag(BattlerTagType.COMMANDED, 0, MoveId.NONE, pokemon.id);
// Cancel the source Pokemon's next move (if a move is queued) // 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);
} }
} }
} }
@ -5004,7 +5006,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 the move is an AttackMove or a StatusMove the Dancer must replicate the move on the source of the Dance
if (move.getMove().is("AttackMove") || move.getMove().is("StatusMove")) { if (move.getMove().is("AttackMove") || move.getMove().is("StatusMove")) {
const target = this.getTarget(pokemon, source, targets); const target = this.getTarget(pokemon, source, targets);
globalScene.phaseManager.unshiftNew("MovePhase", pokemon, target, move, MoveUseMode.INDIRECT); globalScene.phaseManager.unshiftNew(
"MovePhase",
pokemon,
target,
move,
MoveUseMode.INDIRECT,
MovePhaseTimingModifier.FIRST,
);
} else if (move.getMove().is("SelfStatusMove")) { } else if (move.getMove().is("SelfStatusMove")) {
// If the move is a SelfStatusMove (ie. Swords Dance) the Dancer should replicate it on itself // If the move is a SelfStatusMove (ie. Swords Dance) the Dancer should replicate it on itself
globalScene.phaseManager.unshiftNew( globalScene.phaseManager.unshiftNew(
@ -5013,6 +5022,7 @@ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr {
[pokemon.getBattlerIndex()], [pokemon.getBattlerIndex()],
move, move,
MoveUseMode.INDIRECT, MoveUseMode.INDIRECT,
MovePhaseTimingModifier.FIRST,
); );
} }
} }
@ -6028,11 +6038,6 @@ export class IllusionPostBattleAbAttr extends PostBattleAbAttr {
} }
} }
export interface BypassSpeedChanceAbAttrParams extends AbAttrBaseParams {
/** Holds whether the speed check is bypassed after ability application */
bypass: BooleanHolder;
}
/** /**
* If a Pokémon with this Ability selects a damaging move, it has a 30% chance of going first in its priority bracket. If the Ability activates, this is announced at the start of the turn (after move selection). * If a Pokémon with this Ability selects a damaging move, it has a 30% chance of going first in its priority bracket. If the Ability activates, this is announced at the start of the turn (after move selection).
* @sealed * @sealed
@ -6048,26 +6053,28 @@ export class BypassSpeedChanceAbAttr extends AbAttr {
this.chance = chance; this.chance = chance;
} }
override canApply({ bypass, simulated, pokemon }: BypassSpeedChanceAbAttrParams): boolean { override canApply({ simulated, pokemon }: AbAttrBaseParams): boolean {
// TODO: Consider whether we can move the simulated check to the `apply` method // TODO: Consider whether we can move the simulated check to the `apply` method
// May be difficult as we likely do not want to modify the randBattleSeed // May be difficult as we likely do not want to modify the randBattleSeed
const turnCommand = globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()]; const turnCommand = globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()];
const isCommandFight = turnCommand?.command === Command.FIGHT;
const move = turnCommand?.move?.move ? allMoves[turnCommand.move.move] : null; const move = turnCommand?.move?.move ? allMoves[turnCommand.move.move] : null;
const isDamageMove = move?.category === MoveCategory.PHYSICAL || move?.category === MoveCategory.SPECIAL; const isDamageMove = move?.category === MoveCategory.PHYSICAL || move?.category === MoveCategory.SPECIAL;
return ( return (
!simulated && !bypass.value && pokemon.randBattleSeedInt(100) < this.chance && isCommandFight && isDamageMove !simulated
&& pokemon.randBattleSeedInt(100) < this.chance
&& isDamageMove
&& pokemon.canAddTag(BattlerTagType.BYPASS_SPEED)
); );
} }
/** /**
* bypass move order in their priority bracket when pokemon choose damaging move * bypass move order in their priority bracket when pokemon choose damaging move
*/ */
override apply({ bypass }: BypassSpeedChanceAbAttrParams): void { override apply({ pokemon }: AbAttrBaseParams): void {
bypass.value = true; pokemon.addTag(BattlerTagType.BYPASS_SPEED);
} }
override getTriggerMessage({ pokemon }: BypassSpeedChanceAbAttrParams, _abilityName: string): string { override getTriggerMessage({ pokemon }: AbAttrBaseParams, _abilityName: string): string {
return i18next.t("abilityTriggers:quickDraw", { pokemonName: getPokemonNameWithAffix(pokemon) }); return i18next.t("abilityTriggers:quickDraw", { pokemonName: getPokemonNameWithAffix(pokemon) });
} }
} }
@ -6075,8 +6082,6 @@ export class BypassSpeedChanceAbAttr extends AbAttr {
export interface PreventBypassSpeedChanceAbAttrParams extends AbAttrBaseParams { export interface PreventBypassSpeedChanceAbAttrParams extends AbAttrBaseParams {
/** Holds whether the speed check is bypassed after ability application */ /** Holds whether the speed check is bypassed after ability application */
bypass: BooleanHolder; bypass: BooleanHolder;
/** Holds whether the Pokemon can check held items for Quick Claw's effects */
canCheckHeldItems: BooleanHolder;
} }
/** /**
@ -6103,9 +6108,8 @@ export class PreventBypassSpeedChanceAbAttr extends AbAttr {
return isCommandFight && this.condition(pokemon, move!); return isCommandFight && this.condition(pokemon, move!);
} }
override apply({ bypass, canCheckHeldItems }: PreventBypassSpeedChanceAbAttrParams): void { override apply({ bypass }: PreventBypassSpeedChanceAbAttrParams): void {
bypass.value = false; bypass.value = false;
canCheckHeldItems.value = false;
} }
} }
@ -6203,8 +6207,7 @@ class ForceSwitchOutHelper {
if (switchOutTarget.hp > 0) { if (switchOutTarget.hp > 0) {
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH); switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
globalScene.phaseManager.prependNewToPhase( globalScene.phaseManager.queueDeferred(
"MoveEndPhase",
"SwitchPhase", "SwitchPhase",
this.switchType, this.switchType,
switchOutTarget.getFieldIndex(), switchOutTarget.getFieldIndex(),
@ -6226,8 +6229,7 @@ class ForceSwitchOutHelper {
const summonIndex = globalScene.currentBattle.trainer const summonIndex = globalScene.currentBattle.trainer
? globalScene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) ? globalScene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot)
: 0; : 0;
globalScene.phaseManager.prependNewToPhase( globalScene.phaseManager.queueDeferred(
"MoveEndPhase",
"SwitchSummonPhase", "SwitchSummonPhase",
this.switchType, this.switchType,
switchOutTarget.getFieldIndex(), switchOutTarget.getFieldIndex(),
@ -6949,7 +6951,7 @@ export function initAbilities() {
.attr(TypeImmunityStatStageChangeAbAttr, PokemonType.ELECTRIC, Stat.SPD, 1) .attr(TypeImmunityStatStageChangeAbAttr, PokemonType.ELECTRIC, Stat.SPD, 1)
.ignorable(), .ignorable(),
new Ability(AbilityId.RIVALRY, 4) 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), .attr(MovePowerBoostAbAttr, (user, target, _move) => user?.gender !== Gender.GENDERLESS && target?.gender !== Gender.GENDERLESS && user?.gender !== target?.gender, 0.75),
new Ability(AbilityId.STEADFAST, 4) new Ability(AbilityId.STEADFAST, 4)
.attr(FlinchStatStageChangeAbAttr, [ Stat.SPD ], 1), .attr(FlinchStatStageChangeAbAttr, [ Stat.SPD ], 1),
@ -7161,7 +7163,7 @@ export function initAbilities() {
new Ability(AbilityId.ANALYTIC, 5) new Ability(AbilityId.ANALYTIC, 5)
.attr(MovePowerBoostAbAttr, (user) => .attr(MovePowerBoostAbAttr, (user) =>
// Boost power if all other Pokemon have already moved (no other moves are slated to execute) // Boost power if all other Pokemon have already moved (no other moves are slated to execute)
!globalScene.phaseManager.findPhase((phase) => phase.is("MovePhase") && phase.pokemon.id !== user?.id), !globalScene.phaseManager.hasPhaseOfType("MovePhase", phase => phase.pokemon.id !== user?.id),
1.3), 1.3),
new Ability(AbilityId.ILLUSION, 5) new Ability(AbilityId.ILLUSION, 5)
// The Pokemon generate an illusion if it's available // The Pokemon generate an illusion if it's available
@ -7742,8 +7744,8 @@ export function initAbilities() {
new Ability(AbilityId.SHARPNESS, 9) new Ability(AbilityId.SHARPNESS, 9)
.attr(MovePowerBoostAbAttr, (_user, _target, move) => move.hasFlag(MoveFlags.SLICING_MOVE), 1.5), .attr(MovePowerBoostAbAttr, (_user, _target, move) => move.hasFlag(MoveFlags.SLICING_MOVE), 1.5),
new Ability(AbilityId.SUPREME_OVERLORD, 9) new Ability(AbilityId.SUPREME_OVERLORD, 9)
.attr(VariableMovePowerBoostAbAttr, (user, _target, _move) => 1 + 0.1 * Math.min(user.isPlayer() ? globalScene.arena.playerFaints : globalScene.currentBattle.enemyFaints, 5)) .conditionalAttr((p) => (p.isPlayer() ? globalScene.arena.playerFaints : globalScene.currentBattle.enemyFaints) > 0, PostSummonAddBattlerTagAbAttr, BattlerTagType.SUPREME_OVERLORD, 0, true)
.partial(), // Should only boost once, on summon .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) new Ability(AbilityId.COSTAR, 9, -2)
.attr(PostSummonCopyAllyStatsAbAttr), .attr(PostSummonCopyAllyStatsAbAttr),
new Ability(AbilityId.TOXIC_DEBRIS, 9) new Ability(AbilityId.TOXIC_DEBRIS, 9)

View File

@ -74,7 +74,6 @@ function applyAbAttrsInternal<T extends CallableAbAttrString>(
for (const passive of [false, true]) { for (const passive of [false, true]) {
params.passive = passive; params.passive = passive;
applySingleAbAttrs(attrType, params, gainedMidTurn, messages); 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 // 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. // this is necessary in case this method is called with an object that is reused.

View File

@ -56,6 +56,7 @@ import { allMoves } from "#data/data-lists";
import { AbilityId } from "#enums/ability-id"; import { AbilityId } from "#enums/ability-id";
import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagSide } from "#enums/arena-tag-side";
import { ArenaTagType } from "#enums/arena-tag-type"; import { ArenaTagType } from "#enums/arena-tag-type";
import type { BattlerIndex } from "#enums/battler-index";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { HitResult } from "#enums/hit-result"; import { HitResult } from "#enums/hit-result";
import { CommonAnim } from "#enums/move-anims-common"; import { CommonAnim } from "#enums/move-anims-common";
@ -84,6 +85,10 @@ interface BaseArenaTag {
* The tag's remaining duration. Setting to any number `<=0` will make the tag's duration effectively infinite. * The tag's remaining duration. Setting to any number `<=0` will make the tag's duration effectively infinite.
*/ */
turnCount: number; turnCount: number;
/**
* The tag's max duration.
*/
maxDuration: number;
/** /**
* The {@linkcode MoveId} that created this tag, or `undefined` if not set by a move. * The {@linkcode MoveId} that created this tag, or `undefined` if not set by a move.
*/ */
@ -110,12 +115,14 @@ export abstract class ArenaTag implements BaseArenaTag {
/** The type of the arena tag */ /** The type of the arena tag */
public abstract readonly tagType: ArenaTagType; public abstract readonly tagType: ArenaTagType;
public turnCount: number; public turnCount: number;
public maxDuration: number;
public sourceMove?: MoveId; public sourceMove?: MoveId;
public sourceId: number | undefined; public sourceId: number | undefined;
public side: ArenaTagSide; public side: ArenaTagSide;
constructor(turnCount: number, sourceMove?: MoveId, sourceId?: number, side: ArenaTagSide = ArenaTagSide.BOTH) { constructor(turnCount: number, sourceMove?: MoveId, sourceId?: number, side: ArenaTagSide = ArenaTagSide.BOTH) {
this.turnCount = turnCount; this.turnCount = turnCount;
this.maxDuration = turnCount;
this.sourceMove = sourceMove; this.sourceMove = sourceMove;
this.sourceId = sourceId; this.sourceId = sourceId;
this.side = side; this.side = side;
@ -164,6 +171,7 @@ export abstract class ArenaTag implements BaseArenaTag {
*/ */
loadTag<const T extends this>(source: BaseArenaTag & Pick<T, "tagType">): void { loadTag<const T extends this>(source: BaseArenaTag & Pick<T, "tagType">): void {
this.turnCount = source.turnCount; this.turnCount = source.turnCount;
this.maxDuration = source.maxDuration;
this.sourceMove = source.sourceMove; this.sourceMove = source.sourceMove;
this.sourceId = source.sourceId; this.sourceId = source.sourceId;
this.side = source.side; this.side = source.side;
@ -1590,6 +1598,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 // TODO: swap `sourceMove` and `sourceId` and make `sourceMove` an optional parameter
export function getArenaTag( export function getArenaTag(
tagType: ArenaTagType, tagType: ArenaTagType,
@ -1653,6 +1800,8 @@ export function getArenaTag(
return new FairyLockTag(turnCount, sourceId); return new FairyLockTag(turnCount, sourceId);
case ArenaTagType.NEUTRALIZING_GAS: case ArenaTagType.NEUTRALIZING_GAS:
return new SuppressAbilitiesTag(sourceId); return new SuppressAbilitiesTag(sourceId);
case ArenaTagType.PENDING_HEAL:
return new PendingHealTag();
default: default:
return null; return null;
} }
@ -1701,5 +1850,6 @@ export type ArenaTagTypeMap = {
[ArenaTagType.GRASS_WATER_PLEDGE]: GrassWaterPledgeTag; [ArenaTagType.GRASS_WATER_PLEDGE]: GrassWaterPledgeTag;
[ArenaTagType.FAIRY_LOCK]: FairyLockTag; [ArenaTagType.FAIRY_LOCK]: FairyLockTag;
[ArenaTagType.NEUTRALIZING_GAS]: SuppressAbilitiesTag; [ArenaTagType.NEUTRALIZING_GAS]: SuppressAbilitiesTag;
[ArenaTagType.PENDING_HEAL]: PendingHealTag;
[ArenaTagType.NONE]: NoneTag; [ArenaTagType.NONE]: NoneTag;
}; };

View File

@ -1119,7 +1119,7 @@ export const biomePokemonPools: BiomePokemonPools = {
}, },
[BiomePoolTier.RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.HITMONLEE, SpeciesId.HITMONCHAN, SpeciesId.LUCARIO, SpeciesId.THROH, SpeciesId.SAWK, { 1: [ SpeciesId.PANCHAM ], 52: [ SpeciesId.PANGORO ] } ] }, [BiomePoolTier.RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.HITMONLEE, SpeciesId.HITMONCHAN, SpeciesId.LUCARIO, SpeciesId.THROH, SpeciesId.SAWK, { 1: [ SpeciesId.PANCHAM ], 52: [ SpeciesId.PANGORO ] } ] },
[BiomePoolTier.SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.HITMONTOP, SpeciesId.GALLADE, SpeciesId.GALAR_FARFETCHD ] }, [BiomePoolTier.SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.HITMONTOP, SpeciesId.GALLADE, SpeciesId.GALAR_FARFETCHD ] },
[BiomePoolTier.ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.TERRAKION, { 1: [ SpeciesId.KUBFU ], 60: [ SpeciesId.URSHIFU] }, SpeciesId.GALAR_ZAPDOS ] }, [BiomePoolTier.ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.TERRAKION, { 1: [ SpeciesId.KUBFU ], 60: [ SpeciesId.URSHIFU ] }, SpeciesId.GALAR_ZAPDOS ] },
[BiomePoolTier.BOSS]: { [BiomePoolTier.BOSS]: {
[TimeOfDay.DAWN]: [], [TimeOfDay.DAWN]: [],
[TimeOfDay.DAY]: [], [TimeOfDay.DAY]: [],
@ -1128,7 +1128,7 @@ export const biomePokemonPools: BiomePokemonPools = {
[TimeOfDay.ALL]: [ SpeciesId.HITMONLEE, SpeciesId.HITMONCHAN, SpeciesId.HARIYAMA, SpeciesId.MEDICHAM, SpeciesId.LUCARIO, SpeciesId.TOXICROAK, SpeciesId.THROH, SpeciesId.SAWK, SpeciesId.SCRAFTY, SpeciesId.MIENSHAO, SpeciesId.BEWEAR, SpeciesId.GRAPPLOCT, SpeciesId.ANNIHILAPE ] [TimeOfDay.ALL]: [ SpeciesId.HITMONLEE, SpeciesId.HITMONCHAN, SpeciesId.HARIYAMA, SpeciesId.MEDICHAM, SpeciesId.LUCARIO, SpeciesId.TOXICROAK, SpeciesId.THROH, SpeciesId.SAWK, SpeciesId.SCRAFTY, SpeciesId.MIENSHAO, SpeciesId.BEWEAR, SpeciesId.GRAPPLOCT, SpeciesId.ANNIHILAPE ]
}, },
[BiomePoolTier.BOSS_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.HITMONTOP, SpeciesId.GALLADE, SpeciesId.PANGORO, SpeciesId.SIRFETCHD, SpeciesId.HISUI_DECIDUEYE ] }, [BiomePoolTier.BOSS_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.HITMONTOP, SpeciesId.GALLADE, SpeciesId.PANGORO, SpeciesId.SIRFETCHD, SpeciesId.HISUI_DECIDUEYE ] },
[BiomePoolTier.BOSS_SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.TERRAKION, { 1: [ SpeciesId.KUBFU ], 60: [ SpeciesId.URSHIFU] } ] }, [BiomePoolTier.BOSS_SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.TERRAKION, { 1: [ SpeciesId.KUBFU ], 60: [ SpeciesId.URSHIFU ] } ] },
[BiomePoolTier.BOSS_ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.ZAMAZENTA, SpeciesId.GALAR_ZAPDOS ] } [BiomePoolTier.BOSS_ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.ZAMAZENTA, SpeciesId.GALAR_ZAPDOS ] }
}, },
[BiomeId.FACTORY]: { [BiomeId.FACTORY]: {
@ -1597,10 +1597,10 @@ export const biomePokemonPools: BiomePokemonPools = {
[BiomePoolTier.UNCOMMON]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ { 1: [ SpeciesId.SOLOSIS ], 32: [ SpeciesId.DUOSION ], 41: [ SpeciesId.REUNICLUS ] } ] }, [BiomePoolTier.UNCOMMON]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ { 1: [ SpeciesId.SOLOSIS ], 32: [ SpeciesId.DUOSION ], 41: [ SpeciesId.REUNICLUS ] } ] },
[BiomePoolTier.RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.DITTO, { 1: [ SpeciesId.PORYGON ], 30: [ SpeciesId.PORYGON2 ] } ] }, [BiomePoolTier.RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.DITTO, { 1: [ SpeciesId.PORYGON ], 30: [ SpeciesId.PORYGON2 ] } ] },
[BiomePoolTier.SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.ROTOM ] }, [BiomePoolTier.SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.ROTOM ] },
[BiomePoolTier.ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ { 1: [SpeciesId.TYPE_NULL], 60: [ SpeciesId.SILVALLY ] } ] }, [BiomePoolTier.ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ { 1: [ SpeciesId.TYPE_NULL ], 60: [ SpeciesId.SILVALLY ] } ] },
[BiomePoolTier.BOSS]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.MUK, SpeciesId.ELECTRODE, SpeciesId.BRONZONG, SpeciesId.MAGNEZONE, SpeciesId.PORYGON_Z, SpeciesId.REUNICLUS, SpeciesId.KLINKLANG ] }, [BiomePoolTier.BOSS]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.MUK, SpeciesId.ELECTRODE, SpeciesId.BRONZONG, SpeciesId.MAGNEZONE, SpeciesId.PORYGON_Z, SpeciesId.REUNICLUS, SpeciesId.KLINKLANG ] },
[BiomePoolTier.BOSS_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [] }, [BiomePoolTier.BOSS_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [] },
[BiomePoolTier.BOSS_SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.ROTOM, SpeciesId.ZYGARDE, { 1: [SpeciesId.TYPE_NULL], 60: [ SpeciesId.SILVALLY ] } ] }, [BiomePoolTier.BOSS_SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.ROTOM, SpeciesId.ZYGARDE, { 1: [ SpeciesId.TYPE_NULL ], 60: [ SpeciesId.SILVALLY ] } ] },
[BiomePoolTier.BOSS_ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.MEWTWO, SpeciesId.MIRAIDON ] } [BiomePoolTier.BOSS_ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.MEWTWO, SpeciesId.MIRAIDON ] }
}, },
[BiomeId.END]: { [BiomeId.END]: {
@ -5627,10 +5627,12 @@ export function initBiomes() {
] ]
], ],
[ SpeciesId.TYPE_NULL, PokemonType.NORMAL, -1, [ [ SpeciesId.TYPE_NULL, PokemonType.NORMAL, -1, [
[ BiomeId.LABORATORY, BiomePoolTier.ULTRA_RARE ] [ BiomeId.LABORATORY, BiomePoolTier.ULTRA_RARE ],
[ BiomeId.LABORATORY, BiomePoolTier.BOSS_SUPER_RARE ]
] ]
], ],
[ SpeciesId.SILVALLY, PokemonType.NORMAL, -1, [ [ SpeciesId.SILVALLY, PokemonType.NORMAL, -1, [
[ BiomeId.LABORATORY, BiomePoolTier.ULTRA_RARE ],
[ BiomeId.LABORATORY, BiomePoolTier.BOSS_SUPER_RARE ] [ BiomeId.LABORATORY, BiomePoolTier.BOSS_SUPER_RARE ]
] ]
], ],
@ -5773,10 +5775,12 @@ export function initBiomes() {
] ]
], ],
[ SpeciesId.POIPOLE, PokemonType.POISON, -1, [ [ SpeciesId.POIPOLE, PokemonType.POISON, -1, [
[ BiomeId.SWAMP, BiomePoolTier.ULTRA_RARE ] [ BiomeId.SWAMP, BiomePoolTier.ULTRA_RARE ],
[ BiomeId.SWAMP, BiomePoolTier.BOSS_SUPER_RARE ]
] ]
], ],
[ SpeciesId.NAGANADEL, PokemonType.POISON, PokemonType.DRAGON, [ [ SpeciesId.NAGANADEL, PokemonType.POISON, PokemonType.DRAGON, [
[ BiomeId.SWAMP, BiomePoolTier.ULTRA_RARE ],
[ BiomeId.SWAMP, BiomePoolTier.BOSS_SUPER_RARE ] [ BiomeId.SWAMP, BiomePoolTier.BOSS_SUPER_RARE ]
] ]
], ],
@ -6165,10 +6169,12 @@ export function initBiomes() {
] ]
], ],
[ SpeciesId.KUBFU, PokemonType.FIGHTING, -1, [ [ SpeciesId.KUBFU, PokemonType.FIGHTING, -1, [
[ BiomeId.DOJO, BiomePoolTier.ULTRA_RARE ] [ BiomeId.DOJO, BiomePoolTier.ULTRA_RARE ],
[ BiomeId.DOJO, BiomePoolTier.BOSS_SUPER_RARE ]
] ]
], ],
[ SpeciesId.URSHIFU, PokemonType.FIGHTING, PokemonType.DARK, [ [ SpeciesId.URSHIFU, PokemonType.FIGHTING, PokemonType.DARK, [
[ BiomeId.DOJO, BiomePoolTier.ULTRA_RARE ],
[ BiomeId.DOJO, BiomePoolTier.BOSS_SUPER_RARE ] [ BiomeId.DOJO, BiomePoolTier.BOSS_SUPER_RARE ]
] ]
], ],
@ -7209,7 +7215,8 @@ export function initBiomes() {
], ],
[ TrainerType.SCIENTIST, [ [ TrainerType.SCIENTIST, [
[ BiomeId.DESERT, BiomePoolTier.COMMON ], [ BiomeId.DESERT, BiomePoolTier.COMMON ],
[ BiomeId.RUINS, BiomePoolTier.COMMON ] [ BiomeId.RUINS, BiomePoolTier.COMMON ],
[ BiomeId.LABORATORY, BiomePoolTier.COMMON ]
] ]
], ],
[ TrainerType.SMASHER, []], [ TrainerType.SMASHER, []],
@ -7224,7 +7231,8 @@ export function initBiomes() {
] ]
], ],
[ TrainerType.SWIMMER, [ [ TrainerType.SWIMMER, [
[ BiomeId.SEA, BiomePoolTier.COMMON ] [ BiomeId.SEA, BiomePoolTier.COMMON ],
[ BiomeId.SEABED, BiomePoolTier.COMMON ]
] ]
], ],
[ TrainerType.TWINS, [ [ TrainerType.TWINS, [
@ -7590,11 +7598,13 @@ export function initBiomes() {
[ TrainerType.ALDER, []], [ TrainerType.ALDER, []],
[ TrainerType.IRIS, []], [ TrainerType.IRIS, []],
[ TrainerType.DIANTHA, []], [ TrainerType.DIANTHA, []],
[ TrainerType.KUKUI, []],
[ TrainerType.HAU, []], [ TrainerType.HAU, []],
[ TrainerType.LEON, []],
[ TrainerType.MUSTARD, []],
[ TrainerType.GEETA, []], [ TrainerType.GEETA, []],
[ TrainerType.NEMONA, []], [ TrainerType.NEMONA, []],
[ TrainerType.KIERAN, []], [ TrainerType.KIERAN, []],
[ TrainerType.LEON, []],
[ TrainerType.RIVAL, []] [ TrainerType.RIVAL, []]
]; ];

View File

@ -606,17 +606,7 @@ export class ShellTrapTag extends BattlerTag {
// Trap should only be triggered by opponent's Physical moves // Trap should only be triggered by opponent's Physical moves
if (phaseData?.move.category === MoveCategory.PHYSICAL && pokemon.isOpponent(phaseData.attacker)) { if (phaseData?.move.category === MoveCategory.PHYSICAL && pokemon.isOpponent(phaseData.attacker)) {
const shellTrapPhaseIndex = globalScene.phaseManager.phaseQueue.findIndex( globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === pokemon);
phase => phase.is("MovePhase") && phase.pokemon === pokemon,
);
const firstMovePhaseIndex = globalScene.phaseManager.phaseQueue.findIndex(phase => phase.is("MovePhase"));
// Only shift MovePhase timing if it's not already next up
if (shellTrapPhaseIndex !== -1 && shellTrapPhaseIndex !== firstMovePhaseIndex) {
const shellTrapMovePhase = globalScene.phaseManager.phaseQueue.splice(shellTrapPhaseIndex, 1)[0];
globalScene.phaseManager.prependToPhase(shellTrapMovePhase, "MovePhase");
}
this.activated = true; this.activated = true;
} }
@ -1279,22 +1269,9 @@ export class EncoreTag extends MoveRestrictionBattlerTag {
}), }),
); );
const movePhase = globalScene.phaseManager.findPhase(m => m.is("MovePhase") && m.pokemon === pokemon); const movesetMove = pokemon.getMoveset().find(m => m.moveId === this.moveId);
if (movePhase) { if (movesetMove) {
const movesetMove = pokemon.getMoveset().find(m => m.moveId === this.moveId); globalScene.phaseManager.changePhaseMove((phase: MovePhase) => phase.pokemon === pokemon, movesetMove);
if (movesetMove) {
const lastMove = pokemon.getLastXMoves(1)[0];
globalScene.phaseManager.tryReplacePhase(
m => m.is("MovePhase") && m.pokemon === pokemon,
globalScene.phaseManager.create(
"MovePhase",
pokemon,
lastMove.targets ?? [],
movesetMove,
MoveUseMode.NORMAL,
),
);
}
} }
} }
@ -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 * 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. * 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 * @param sourceId - The ID of the pokemon adding the tag
@ -3826,6 +3857,10 @@ export function getBattlerTag(
return new PsychoShiftTag(); return new PsychoShiftTag();
case BattlerTagType.MAGIC_COAT: case BattlerTagType.MAGIC_COAT:
return new MagicCoatTag(); return new MagicCoatTag();
case BattlerTagType.SUPREME_OVERLORD:
return new SupremeOverlordTag();
case BattlerTagType.BYPASS_SPEED:
return new BypassSpeedTag();
} }
} }
@ -3960,4 +3995,6 @@ export type BattlerTagTypeMap = {
[BattlerTagType.GRUDGE]: GrudgeTag; [BattlerTagType.GRUDGE]: GrudgeTag;
[BattlerTagType.PSYCHO_SHIFT]: PsychoShiftTag; [BattlerTagType.PSYCHO_SHIFT]: PsychoShiftTag;
[BattlerTagType.MAGIC_COAT]: MagicCoatTag; [BattlerTagType.MAGIC_COAT]: MagicCoatTag;
[BattlerTagType.SUPREME_OVERLORD]: SupremeOverlordTag;
[BattlerTagType.BYPASS_SPEED]: BypassSpeedTag;
}; };

View File

@ -6,7 +6,7 @@ import { PokemonSpecies } from "#data/pokemon-species";
import { BiomeId } from "#enums/biome-id"; import { BiomeId } from "#enums/biome-id";
import { PartyMemberStrength } from "#enums/party-member-strength"; import { PartyMemberStrength } from "#enums/party-member-strength";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import type { Starter } from "#ui/starter-select-ui-handler"; import type { Starter } from "#types/save-data";
import { randSeedGauss, randSeedInt, randSeedItem } from "#utils/common"; import { randSeedGauss, randSeedInt, randSeedItem } from "#utils/common";
import { getEnumValues } from "#utils/enums"; import { getEnumValues } from "#utils/enums";
import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils"; import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils";
@ -66,8 +66,11 @@ function getDailyRunStarter(starterSpeciesForm: PokemonSpeciesForm, startingLeve
const formIndex = starterSpeciesForm instanceof PokemonSpecies ? undefined : starterSpeciesForm.formIndex; const formIndex = starterSpeciesForm instanceof PokemonSpecies ? undefined : starterSpeciesForm.formIndex;
const pokemon = globalScene.addPlayerPokemon(starterSpecies, startingLevel, undefined, formIndex); const pokemon = globalScene.addPlayerPokemon(starterSpecies, startingLevel, undefined, formIndex);
const starter: Starter = { const starter: Starter = {
species: starterSpecies, speciesId: starterSpecies.speciesId,
dexAttr: pokemon.getDexAttr(), shiny: pokemon.shiny,
variant: pokemon.variant,
formIndex: pokemon.formIndex,
ivs: pokemon.ivs,
abilityIndex: pokemon.abilityIndex, abilityIndex: pokemon.abilityIndex,
passive: false, passive: false,
nature: pokemon.getNature(), nature: pokemon.getNature(),

View File

@ -6,7 +6,7 @@ import { loggedInUser } from "#app/account";
import type { GameMode } from "#app/game-mode"; import type { GameMode } from "#app/game-mode";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { getPokemonNameWithAffix } from "#app/messages"; 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 { WeakenMoveTypeTag } from "#data/arena-tag";
import { MoveChargeAnim } from "#data/battle-anims"; import { MoveChargeAnim } from "#data/battle-anims";
import { import {
@ -18,6 +18,7 @@ import {
ShellTrapTag, ShellTrapTag,
StockpilingTag, StockpilingTag,
SubstituteTag, SubstituteTag,
SupremeOverlordTag,
TrappedTag, TrappedTag,
TypeBoostTag, TypeBoostTag,
} from "#data/battler-tags"; } 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 { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSketchMoves, invalidSleepTalkMoves } from "#moves/invalid-moves";
import { frenzyMissFunc, getMoveTargets } from "#moves/move-utils"; import { frenzyMissFunc, getMoveTargets } from "#moves/move-utils";
import { PokemonMove } from "#moves/pokemon-move"; import { PokemonMove } from "#moves/pokemon-move";
import { MoveEndPhase } from "#phases/move-end-phase";
import { MovePhase } from "#phases/move-phase"; import { MovePhase } from "#phases/move-phase";
import { PokemonHealPhase } from "#phases/pokemon-heal-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 { AttackMoveResult } from "#types/attack-move-result";
import type { Localizable } from "#types/locales"; import type { Localizable } from "#types/locales";
import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString, MoveMessageFunc } from "#types/move-types"; 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 { toCamelCase, toTitleCase } from "#utils/strings";
import i18next from "i18next"; import i18next from "i18next";
import { applyChallenges } from "#utils/challenge-utils"; import { applyChallenges } from "#utils/challenge-utils";
import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier";
import type { AbstractConstructor } from "#types/type-helpers"; import type { AbstractConstructor } from "#types/type-helpers";
/** /**
@ -879,6 +879,8 @@ export abstract class Move implements Localizable {
power.value *= 1.5; power.value *= 1.5;
} }
power.value *= (source.getTag(BattlerTagType.SUPREME_OVERLORD) as SupremeOverlordTag | undefined)?.getBoost() ?? 1;
return power.value; return power.value;
} }
@ -888,6 +890,10 @@ export abstract class Move implements Localizable {
applyMoveAttrs("IncrementMovePriorityAttr", user, null, this, priority); applyMoveAttrs("IncrementMovePriorityAttr", user, null, this, priority);
applyAbAttrs("ChangeMovePriorityAbAttr", {pokemon: user, simulated, move: this, priority}); applyAbAttrs("ChangeMovePriorityAbAttr", {pokemon: user, simulated, move: this, priority});
if (user.getTag(BattlerTagType.BYPASS_SPEED)) {
priority.value += 0.2;
}
return priority.value; return priority.value;
} }
@ -2147,24 +2153,15 @@ export class SacrificialFullRestoreAttr extends SacrificialAttr {
return false; return false;
} }
// We don't know which party member will be chosen, so pick the highest max HP in the party // Add a tag to the field if it doesn't already exist, then queue a delayed healing effect in the user's current slot.
const party = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty(); globalScene.arena.addTag(ArenaTagType.PENDING_HEAL, 0, move.id, user.id); // Arguments after first go completely unused
const maxPartyMemberHp = party.map(p => p.getMaxHp()).reduce((maxHp: number, hp: number) => Math.max(hp, maxHp), 0); const tag = globalScene.arena.getTag(ArenaTagType.PENDING_HEAL) as PendingHealTag;
tag.queueHeal(user.getBattlerIndex(), {
const pm = globalScene.phaseManager; sourceId: user.id,
moveId: move.id,
pm.pushPhase( restorePP: this.restorePP,
pm.create("PokemonHealPhase", healMessage: i18next.t(this.moveMessage, { pokemonName: getPokemonNameWithAffix(user) }),
user.getBattlerIndex(), });
maxPartyMemberHp,
i18next.t(this.moveMessage, { pokemonName: getPokemonNameWithAffix(user) }),
true,
false,
false,
true,
false,
this.restorePP),
true);
return true; return true;
} }
@ -3304,7 +3301,7 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr {
const overridden = args[0] as BooleanHolder; const overridden = args[0] as BooleanHolder;
const allyMovePhase = globalScene.phaseManager.findPhase<MovePhase>((phase) => phase.is("MovePhase") && phase.pokemon.isPlayer() === user.isPlayer()); const allyMovePhase = globalScene.phaseManager.getMovePhase((phase) => phase.pokemon.isPlayer() === user.isPlayer());
if (allyMovePhase) { if (allyMovePhase) {
const allyMove = allyMovePhase.move.getMove(); const allyMove = allyMovePhase.move.getMove();
if (allyMove !== move && allyMove.hasAttr("AwaitCombinedPledgeAttr")) { if (allyMove !== move && allyMove.hasAttr("AwaitCombinedPledgeAttr")) {
@ -3317,11 +3314,7 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr {
})); }));
// Move the ally's MovePhase (if needed) so that the ally moves next // Move the ally's MovePhase (if needed) so that the ally moves next
const allyMovePhaseIndex = globalScene.phaseManager.phaseQueue.indexOf(allyMovePhase); globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === user.getAlly());
const firstMovePhaseIndex = globalScene.phaseManager.phaseQueue.findIndex((phase) => phase.is("MovePhase"));
if (allyMovePhaseIndex !== firstMovePhaseIndex) {
globalScene.phaseManager.prependToPhase(globalScene.phaseManager.phaseQueue.splice(allyMovePhaseIndex, 1)[0], "MovePhase");
}
overridden.value = true; overridden.value = true;
return true; return true;
@ -4556,28 +4549,7 @@ export class LastMoveDoublePowerAttr extends VariablePowerAttr {
*/ */
apply(user: Pokemon, _target: Pokemon, _move: Move, args: any[]): boolean { apply(user: Pokemon, _target: Pokemon, _move: Move, args: any[]): boolean {
const power = args[0] as NumberHolder; const power = args[0] as NumberHolder;
const enemy = user.getOpponent(0); for (const p of globalScene.phaseManager.dynamicQueueManager.getLastTurnOrder().slice(0, -1).reverse()) {
const pokemonActed: Pokemon[] = [];
if (enemy?.turnData.acted) {
pokemonActed.push(enemy);
}
if (globalScene.currentBattle.double) {
const userAlly = user.getAlly();
const enemyAlly = enemy?.getAlly();
if (userAlly?.turnData.acted) {
pokemonActed.push(userAlly);
}
if (enemyAlly?.turnData.acted) {
pokemonActed.push(enemyAlly);
}
}
pokemonActed.sort((a, b) => b.turnData.order - a.turnData.order);
for (const p of pokemonActed) {
const [ lastMove ] = p.getLastXMoves(1); const [ lastMove ] = p.getLastXMoves(1);
if (lastMove.result !== MoveResult.FAIL) { if (lastMove.result !== MoveResult.FAIL) {
if ((lastMove.result === MoveResult.SUCCESS) && (lastMove.move === this.move)) { if ((lastMove.result === MoveResult.SUCCESS) && (lastMove.move === this.move)) {
@ -4659,20 +4631,13 @@ export class CueNextRoundAttr extends MoveEffectAttr {
} }
override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean { override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean {
const nextRoundPhase = globalScene.phaseManager.findPhase<MovePhase>(phase => const nextRoundPhase = globalScene.phaseManager.getMovePhase(phase => phase.move.moveId === MoveId.ROUND);
phase.is("MovePhase") && phase.move.moveId === MoveId.ROUND
);
if (!nextRoundPhase) { if (!nextRoundPhase) {
return false; return false;
} }
// Update the phase queue so that the next Pokemon using Round moves next globalScene.phaseManager.forceMoveNext(phase => phase.move.moveId === MoveId.ROUND);
const nextRoundIndex = globalScene.phaseManager.phaseQueue.indexOf(nextRoundPhase);
const nextMoveIndex = globalScene.phaseManager.phaseQueue.findIndex(phase => phase.is("MovePhase"));
if (nextRoundIndex !== nextMoveIndex) {
globalScene.phaseManager.prependToPhase(globalScene.phaseManager.phaseQueue.splice(nextRoundIndex, 1)[0], "MovePhase");
}
// Mark the corresponding Pokemon as having "joined the Round" (for doubling power later) // Mark the corresponding Pokemon as having "joined the Round" (for doubling power later)
nextRoundPhase.pokemon.turnData.joinedRound = true; nextRoundPhase.pokemon.turnData.joinedRound = true;
@ -6297,11 +6262,11 @@ export class RevivalBlessingAttr extends MoveEffectAttr {
// Handle cases where revived pokemon needs to get switched in on same turn // Handle cases where revived pokemon needs to get switched in on same turn
if (allyPokemon.isFainted() || allyPokemon === pokemon) { if (allyPokemon.isFainted() || allyPokemon === pokemon) {
// Enemy switch phase should be removed and replaced with the revived pkmn switching in // Enemy switch phase should be removed and replaced with the revived pkmn switching in
globalScene.phaseManager.tryRemovePhase((phase: SwitchSummonPhase) => phase.is("SwitchSummonPhase") && phase.getPokemon() === pokemon); globalScene.phaseManager.tryRemovePhase("SwitchSummonPhase", phase => phase.getFieldIndex() === slotIndex);
// If the pokemon being revived was alive earlier in the turn, cancel its move // If the pokemon being revived was alive earlier in the turn, cancel its move
// (revived pokemon can't move in the turn they're brought back) // (revived pokemon can't move in the turn they're brought back)
// TODO: might make sense to move this to `FaintPhase` after checking for Rev Seed (rather than handling it in the move) // TODO: might make sense to move this to `FaintPhase` after checking for Rev Seed (rather than handling it in the move)
globalScene.phaseManager.findPhase((phase: MovePhase) => phase.pokemon === pokemon)?.cancel(); globalScene.phaseManager.getMovePhase((phase: MovePhase) => phase.pokemon === pokemon)?.cancel();
if (user.fieldPosition === FieldPosition.CENTER) { if (user.fieldPosition === FieldPosition.CENTER) {
user.setFieldPosition(FieldPosition.LEFT); user.setFieldPosition(FieldPosition.LEFT);
} }
@ -6382,8 +6347,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
if (this.switchType === SwitchType.FORCE_SWITCH) { if (this.switchType === SwitchType.FORCE_SWITCH) {
switchOutTarget.leaveField(true); switchOutTarget.leaveField(true);
const slotIndex = eligibleNewIndices[user.randBattleSeedInt(eligibleNewIndices.length)]; const slotIndex = eligibleNewIndices[user.randBattleSeedInt(eligibleNewIndices.length)];
globalScene.phaseManager.prependNewToPhase( globalScene.phaseManager.queueDeferred(
"MoveEndPhase",
"SwitchSummonPhase", "SwitchSummonPhase",
this.switchType, this.switchType,
switchOutTarget.getFieldIndex(), switchOutTarget.getFieldIndex(),
@ -6393,7 +6357,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
); );
} else { } else {
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH); switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
globalScene.phaseManager.prependNewToPhase("MoveEndPhase", globalScene.phaseManager.queueDeferred(
"SwitchPhase", "SwitchPhase",
this.switchType, this.switchType,
switchOutTarget.getFieldIndex(), switchOutTarget.getFieldIndex(),
@ -6422,7 +6386,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
if (this.switchType === SwitchType.FORCE_SWITCH) { if (this.switchType === SwitchType.FORCE_SWITCH) {
switchOutTarget.leaveField(true); switchOutTarget.leaveField(true);
const slotIndex = eligibleNewIndices[user.randBattleSeedInt(eligibleNewIndices.length)]; const slotIndex = eligibleNewIndices[user.randBattleSeedInt(eligibleNewIndices.length)];
globalScene.phaseManager.prependNewToPhase("MoveEndPhase", globalScene.phaseManager.queueDeferred(
"SwitchSummonPhase", "SwitchSummonPhase",
this.switchType, this.switchType,
switchOutTarget.getFieldIndex(), switchOutTarget.getFieldIndex(),
@ -6432,7 +6396,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
); );
} else { } else {
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH); switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
globalScene.phaseManager.prependNewToPhase("MoveEndPhase", globalScene.phaseManager.queueDeferred(
"SwitchSummonPhase", "SwitchSummonPhase",
this.switchType, this.switchType,
switchOutTarget.getFieldIndex(), switchOutTarget.getFieldIndex(),
@ -6863,7 +6827,7 @@ class CallMoveAttr extends OverrideMoveEffectAttr {
: moveTargets.targets[user.randBattleSeedInt(moveTargets.targets.length)]]; : moveTargets.targets[user.randBattleSeedInt(moveTargets.targets.length)]];
globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", move.id); globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", move.id);
globalScene.phaseManager.unshiftNew("MovePhase", user, targets, new PokemonMove(move.id), MoveUseMode.FOLLOW_UP); globalScene.phaseManager.unshiftNew("MovePhase", user, targets, new PokemonMove(move.id), MoveUseMode.FOLLOW_UP, MovePhaseTimingModifier.FIRST);
return true; 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 // Load the move's animation if we didn't already and unshift a new usage phase
globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", moveId); globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", moveId);
globalScene.phaseManager.unshiftNew("MovePhase", user, [ target.getBattlerIndex() ], new PokemonMove(moveId), MoveUseMode.FOLLOW_UP); globalScene.phaseManager.unshiftNew("MovePhase", user, [ target.getBattlerIndex() ], new PokemonMove(moveId), MoveUseMode.FOLLOW_UP, MovePhaseTimingModifier.FIRST);
return true; return true;
} }
} }
@ -7179,7 +7143,7 @@ export class RepeatMoveAttr extends MoveEffectAttr {
targetPokemonName: getPokemonNameWithAffix(target) targetPokemonName: getPokemonNameWithAffix(target)
})); }));
target.turnData.extraTurns++; target.turnData.extraTurns++;
globalScene.phaseManager.appendNewToPhase("MoveEndPhase", "MovePhase", target, moveTargets, movesetMove, MoveUseMode.NORMAL); globalScene.phaseManager.unshiftNew("MovePhase", target, moveTargets, movesetMove, MoveUseMode.NORMAL, MovePhaseTimingModifier.FIRST);
return true; return true;
} }
@ -7952,12 +7916,7 @@ export class AfterYouAttr extends MoveEffectAttr {
*/ */
override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean { override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:afterYou", { targetName: getPokemonNameWithAffix(target) })); globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:afterYou", { targetName: getPokemonNameWithAffix(target) }));
globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === target);
// Will find next acting phase of the targeted pokémon, delete it and queue it right after us.
const targetNextPhase = globalScene.phaseManager.findPhase<MovePhase>(phase => phase.pokemon === target);
if (targetNextPhase && globalScene.phaseManager.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) {
globalScene.phaseManager.prependToPhase(targetNextPhase, "MovePhase");
}
return true; return true;
} }
@ -7980,45 +7939,11 @@ export class ForceLastAttr extends MoveEffectAttr {
override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean { override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:forceLast", { targetPokemonName: getPokemonNameWithAffix(target) })); globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:forceLast", { targetPokemonName: getPokemonNameWithAffix(target) }));
// TODO: Refactor this to be more readable and less janky globalScene.phaseManager.forceMoveLast((phase: MovePhase) => phase.pokemon === target);
const targetMovePhase = globalScene.phaseManager.findPhase<MovePhase>((phase) => phase.pokemon === target);
if (targetMovePhase && !targetMovePhase.isForcedLast() && globalScene.phaseManager.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) {
// Finding the phase to insert the move in front of -
// Either the end of the turn or in front of another, slower move which has also been forced last
const prependPhase = globalScene.phaseManager.findPhase((phase) =>
[ MovePhase, MoveEndPhase ].every(cls => !(phase instanceof cls))
|| (phase.is("MovePhase")) && phaseForcedSlower(phase, target, !!globalScene.arena.getTag(ArenaTagType.TRICK_ROOM))
);
if (prependPhase) {
globalScene.phaseManager.phaseQueue.splice(
globalScene.phaseManager.phaseQueue.indexOf(prependPhase),
0,
globalScene.phaseManager.create("MovePhase", target, [ ...targetMovePhase.targets ], targetMovePhase.move, targetMovePhase.useMode, true)
);
}
}
return true; return true;
} }
} }
/**
* Returns whether a {@linkcode MovePhase} has been forced last and the corresponding pokemon is slower than {@linkcode target}.
* TODO:
- Make this a class method
- Make this look at speed order from TurnStartPhase
*/
const phaseForcedSlower = (phase: MovePhase, target: Pokemon, trickRoom: boolean): boolean => {
let slower: boolean;
// quashed pokemon still have speed ties
if (phase.pokemon.getEffectiveStat(Stat.SPD) === target.getEffectiveStat(Stat.SPD)) {
slower = !!target.randBattleSeedInt(2);
} else {
slower = !trickRoom ? phase.pokemon.getEffectiveStat(Stat.SPD) < target.getEffectiveStat(Stat.SPD) : phase.pokemon.getEffectiveStat(Stat.SPD) > target.getEffectiveStat(Stat.SPD);
}
return phase.isForcedLast() && slower;
};
const failOnGravityCondition: MoveConditionFunc = (user, target, move) => !globalScene.arena.getTag(ArenaTagType.GRAVITY); const failOnGravityCondition: MoveConditionFunc = (user, target, move) => !globalScene.arena.getTag(ArenaTagType.GRAVITY);
const failOnBossCondition: MoveConditionFunc = (user, target, move) => !target.isBossImmune(); const failOnBossCondition: MoveConditionFunc = (user, target, move) => !target.isBossImmune();
@ -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 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 failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => {
const party: Pokemon[] = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty(); const party: Pokemon[] = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty();

View File

@ -414,7 +414,7 @@ function summonPlayerPokemonAnimation(pokemon: PlayerPokemon): Promise<void> {
pokemon.resetTurnData(); pokemon.resetTurnData();
globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true); globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true);
globalScene.phaseManager.pushNew("PostSummonPhase", pokemon.getBattlerIndex()); globalScene.phaseManager.unshiftNew("PostSummonPhase", pokemon.getBattlerIndex());
resolve(); resolve();
}); });
}, },

View File

@ -669,7 +669,6 @@ function onGameOver() {
// Clear any leftover battle phases // Clear any leftover battle phases
globalScene.phaseManager.clearPhaseQueue(); globalScene.phaseManager.clearPhaseQueue();
globalScene.phaseManager.clearPhaseQueueSplice();
// Return enemy Pokemon // Return enemy Pokemon
const pokemon = globalScene.getEnemyPokemon(); const pokemon = globalScene.getEnemyPokemon();

View File

@ -738,7 +738,7 @@ export function setEncounterRewards(
if (customShopRewards) { if (customShopRewards) {
globalScene.phaseManager.unshiftNew("SelectModifierPhase", 0, undefined, customShopRewards); globalScene.phaseManager.unshiftNew("SelectModifierPhase", 0, undefined, customShopRewards);
} else { } else {
globalScene.phaseManager.tryRemovePhase(p => p.is("MysteryEncounterRewardsPhase")); globalScene.phaseManager.removeAllPhasesOfType("MysteryEncounterRewardsPhase");
} }
if (eggRewards) { if (eggRewards) {
@ -812,8 +812,7 @@ export function leaveEncounterWithoutBattle(
encounterMode: MysteryEncounterMode = MysteryEncounterMode.NO_BATTLE, encounterMode: MysteryEncounterMode = MysteryEncounterMode.NO_BATTLE,
) { ) {
globalScene.currentBattle.mysteryEncounter!.encounterMode = encounterMode; globalScene.currentBattle.mysteryEncounter!.encounterMode = encounterMode;
globalScene.phaseManager.clearPhaseQueue(); globalScene.phaseManager.clearPhaseQueue(true);
globalScene.phaseManager.clearPhaseQueueSplice();
handleMysteryEncounterVictory(addHealPhase); handleMysteryEncounterVictory(addHealPhase);
} }
@ -826,7 +825,7 @@ export function handleMysteryEncounterVictory(addHealPhase = false, doNotContinu
const allowedPkm = globalScene.getPlayerParty().filter(pkm => pkm.isAllowedInBattle()); const allowedPkm = globalScene.getPlayerParty().filter(pkm => pkm.isAllowedInBattle());
if (allowedPkm.length === 0) { if (allowedPkm.length === 0) {
globalScene.phaseManager.clearPhaseQueue(); globalScene.phaseManager.clearPhaseQueue(true);
globalScene.phaseManager.unshiftNew("GameOverPhase"); globalScene.phaseManager.unshiftNew("GameOverPhase");
return; return;
} }
@ -869,7 +868,7 @@ export function handleMysteryEncounterBattleFailed(addHealPhase = false, doNotCo
const allowedPkm = globalScene.getPlayerParty().filter(pkm => pkm.isAllowedInBattle()); const allowedPkm = globalScene.getPlayerParty().filter(pkm => pkm.isAllowedInBattle());
if (allowedPkm.length === 0) { if (allowedPkm.length === 0) {
globalScene.phaseManager.clearPhaseQueue(); globalScene.phaseManager.clearPhaseQueue(true);
globalScene.phaseManager.unshiftNew("GameOverPhase"); globalScene.phaseManager.unshiftNew("GameOverPhase");
return; return;
} }

View File

@ -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, false, speedReversed);
return speedReversed.value;
}

View File

@ -22,10 +22,12 @@ export interface SerializedTerrain {
export class Terrain { export class Terrain {
public terrainType: TerrainType; public terrainType: TerrainType;
public turnsLeft: number; public turnsLeft: number;
public maxDuration: number;
constructor(terrainType: TerrainType, turnsLeft?: number) { constructor(terrainType: TerrainType, turnsLeft = 0, maxDuration: number = turnsLeft) {
this.terrainType = terrainType; this.terrainType = terrainType;
this.turnsLeft = turnsLeft || 0; this.turnsLeft = turnsLeft;
this.maxDuration = maxDuration;
} }
lapse(): boolean { lapse(): boolean {

View File

@ -19,10 +19,12 @@ export interface SerializedWeather {
export class Weather { export class Weather {
public weatherType: WeatherType; public weatherType: WeatherType;
public turnsLeft: number; public turnsLeft: number;
public maxDuration: number;
constructor(weatherType: WeatherType, turnsLeft?: number) { constructor(weatherType: WeatherType, turnsLeft = 0, maxDuration: number = turnsLeft) {
this.weatherType = weatherType; this.weatherType = weatherType;
this.turnsLeft = !this.isImmutable() ? turnsLeft || 0 : 0; this.turnsLeft = this.isImmutable() ? 0 : turnsLeft;
this.maxDuration = this.isImmutable() ? 0 : maxDuration;
} }
lapse(): boolean { lapse(): boolean {

View File

@ -0,0 +1,187 @@
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",
"ShowAbilityPhase",
"HideAbilityPhase",
"ExpPhase",
"ShowPartyExpBarPhase",
"HidePartyExpBarPhase",
] 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);
}
}

View File

@ -1,3 +1,4 @@
// TODO: rename to something else (this isn't used only for arena tags)
export enum ArenaTagSide { export enum ArenaTagSide {
BOTH, BOTH,
PLAYER, PLAYER,

View File

@ -34,4 +34,5 @@ export enum ArenaTagType {
GRASS_WATER_PLEDGE = "GRASS_WATER_PLEDGE", GRASS_WATER_PLEDGE = "GRASS_WATER_PLEDGE",
FAIRY_LOCK = "FAIRY_LOCK", FAIRY_LOCK = "FAIRY_LOCK",
NEUTRALIZING_GAS = "NEUTRALIZING_GAS", NEUTRALIZING_GAS = "NEUTRALIZING_GAS",
PENDING_HEAL = "PENDING_HEAL",
} }

View File

@ -94,4 +94,6 @@ export enum BattlerTagType {
ENDURE_TOKEN = "ENDURE_TOKEN", ENDURE_TOKEN = "ENDURE_TOKEN",
POWDER = "POWDER", POWDER = "POWDER",
MAGIC_COAT = "MAGIC_COAT", MAGIC_COAT = "MAGIC_COAT",
SUPREME_OVERLORD = "SUPREME_OVERLORD",
BYPASS_SPEED = "BYPASS_SPEED",
} }

View File

@ -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,
}

Some files were not shown because too many files have changed in this diff Show More