Merge remote-tracking branch 'upstream/beta' into metronome

This commit is contained in:
Bertie690 2025-06-24 19:21:01 -04:00
commit d455582625
69 changed files with 7033 additions and 10770 deletions

View File

@ -1,6 +1,19 @@
/** @type {import('dependency-cruiser').IConfiguration} */ /** @type {import('dependency-cruiser').IConfiguration} */
module.exports = { module.exports = {
forbidden: [ forbidden: [
{
name: "no-non-type-@type-exports",
severity: "error",
comment:
"Files in @types should not export anything but types and interfaces. " +
"The folder is intended to house imports that are removed at runtime, " +
"and thus should not contain anything with a bearing on runtime code.",
from: {},
to: {
path: "(^|/)src/@types",
dependencyTypesNot: ["type-only"],
},
},
{ {
name: "only-type-imports", name: "only-type-imports",
severity: "error", severity: "error",
@ -310,7 +323,7 @@ module.exports = {
conditionNames: ["import", "require", "node", "default", "types"], conditionNames: ["import", "require", "node", "default", "types"],
/* /*
The extensions, by default are the same as the ones dependency-cruiser The extensions, by default are the same as the ones dependency-cruiser
can access (run `npx depcruise --info` to see which ones that are in can access (run `pnpm exec depcruise --info` to see which ones that are in
_your_ environment). If that list is larger than you need you can pass _your_ environment). If that list is larger than you need you can pass
the extensions you actually use (e.g. [".js", ".jsx"]). This can speed the extensions you actually use (e.g. [".js", ".jsx"]). This can speed
up module resolution, which is the most expensive step. up module resolution, which is the most expensive step.

View File

@ -68,8 +68,8 @@ Do the reviewers need to do something special in order to test your changes?
- [ ] The PR is self-contained and cannot be split into smaller PRs? - [ ] The PR is self-contained and cannot be split into smaller PRs?
- [ ] Have I provided a clear explanation of the changes? - [ ] Have I provided a clear explanation of the changes?
- [ ] Have I tested the changes manually? - [ ] Have I tested the changes manually?
- [ ] Are all unit tests still passing? (`npm run test:silent`) - [ ] Are all unit tests still passing? (`pnpm test:silent`)
- [ ] Have I created new automated tests (`npm run test:create`) or updated existing tests related to the PR's changes? - [ ] Have I created new automated tests (`pnpm test:create`) or updated existing tests related to the PR's changes?
- [ ] Have I provided screenshots/videos of the changes (if applicable)? - [ ] Have I provided screenshots/videos of the changes (if applicable)?
- [ ] Have I made sure that any UI change works for both UI themes (default and legacy)? - [ ] Have I made sure that any UI change works for both UI themes (default and legacy)?

View File

@ -18,15 +18,24 @@ jobs:
with: with:
submodules: "recursive" submodules: "recursive"
ref: ${{ vars.BETA_DEPLOY_BRANCH || 'beta'}} ref: ${{ vars.BETA_DEPLOY_BRANCH || 'beta'}}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
- name: Install dependencies - name: Install dependencies
run: npm ci run: pnpm i
- name: Build - name: Build
run: npm run build:beta run: pnpm build:beta
env: env:
NODE_ENV: production NODE_ENV: production
- name: Set up SSH - name: Set up SSH
run: | run: |
mkdir ~/.ssh mkdir ~/.ssh
@ -34,6 +43,7 @@ jobs:
echo "${{ secrets.BETA_SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519 echo "${{ secrets.BETA_SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/* chmod 600 ~/.ssh/*
ssh-keyscan -H ${{ secrets.BETA_SSH_HOST }} >> ~/.ssh/known_hosts ssh-keyscan -H ${{ secrets.BETA_SSH_HOST }} >> ~/.ssh/known_hosts
- name: Deploy build on server - name: Deploy build on server
run: | run: |
rsync --del --no-times --checksum -vrm dist/* ${{ secrets.BETA_SSH_USER }}@${{ secrets.BETA_SSH_HOST }}:${{ secrets.BETA_DESTINATION_DIR }} rsync --del --no-times --checksum -vrm dist/* ${{ secrets.BETA_SSH_USER }}@${{ secrets.BETA_SSH_HOST }}:${{ secrets.BETA_DESTINATION_DIR }}

View File

@ -16,15 +16,24 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
submodules: 'recursive' submodules: 'recursive'
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
- name: Install dependencies - name: Install dependencies
run: npm ci run: pnpm i
- name: Build - name: Build
run: npm run build run: pnpm build
env: env:
NODE_ENV: production NODE_ENV: production
- name: Set up SSH - name: Set up SSH
if: github.event_name == 'push' && github.ref_name == 'main' if: github.event_name == 'push' && github.ref_name == 'main'
run: | run: |
@ -33,11 +42,13 @@ jobs:
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519 echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/* chmod 600 ~/.ssh/*
ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts
- name: Deploy build on server - name: Deploy build on server
if: github.event_name == 'push' && github.ref_name == 'main' if: github.event_name == 'push' && github.ref_name == 'main'
run: | run: |
rsync --del --no-times --checksum -vrm dist/* ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:${{ secrets.DESTINATION_DIR }} rsync --del --no-times --checksum -vrm dist/* ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:${{ secrets.DESTINATION_DIR }}
ssh -t ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "~/prmanifest --inpath ${{ secrets.DESTINATION_DIR }} --outpath ${{ secrets.DESTINATION_DIR }}/manifest.json" ssh -t ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "~/prmanifest --inpath ${{ secrets.DESTINATION_DIR }} --outpath ${{ secrets.DESTINATION_DIR }}/manifest.json"
- name: Purge Cloudflare Cache - name: Purge Cloudflare Cache
if: github.event_name == 'push' && github.ref_name == 'main' if: github.event_name == 'push' && github.ref_name == 'main'
id: purge-cache id: purge-cache

View File

@ -34,6 +34,11 @@ jobs:
sudo apt update sudo apt update
sudo apt install -y git openssh-client sudo apt install -y git openssh-client
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node 22.14.1 - name: Setup Node 22.14.1
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
@ -50,13 +55,13 @@ jobs:
working-directory: ${{env.api-dir}} working-directory: ${{env.api-dir}}
run: | run: |
cd pokerogue_docs cd pokerogue_docs
npm ci pnpm i
- name: Generate Typedoc docs - name: Generate Typedoc docs
working-directory: ${{env.api-dir}} working-directory: ${{env.api-dir}}
run: | run: |
cd pokerogue_docs cd pokerogue_docs
npm run docs -- --out /tmp/docs --githubPages false --entryPoints ./src/ pnpm exec typedoc --out /tmp/docs --githubPages false --entryPoints ./src/
- name: Commit & Push docs - name: Commit & Push docs
if: github.event_name == 'push' if: github.event_name == 'push'

View File

@ -23,17 +23,22 @@ jobs:
with: with:
submodules: 'recursive' submodules: 'recursive'
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
cache: 'npm' cache: 'pnpm'
- name: Install Node.js dependencies - name: Install Node.js dependencies
run: npm ci run: pnpm i
- name: Lint with Biome - name: Lint with Biome
run: npm run biome-ci run: pnpm biome-ci
- name: Check dependencies with depcruise - name: Check dependencies with depcruise
run: npm run depcruise run: pnpm depcruise

View File

@ -28,12 +28,20 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
with: with:
submodules: "recursive" submodules: "recursive"
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: "npm" cache: "pnpm"
- name: Install Node.js dependencies - name: Install Node.js dependencies
run: npm ci run: pnpm i
- name: Run tests - name: Run tests
run: npx vitest --project ${{ inputs.project }} --no-isolate --shard=${{ inputs.shard }}/${{ inputs.totalShards }} ${{ !runner.debug && '--silent' || '' }} run: pnpm exec vitest --project ${{ inputs.project }} --no-isolate --shard=${{ inputs.shard }}/${{ inputs.totalShards }} ${{ !runner.debug && '--silent' || '' }}

View File

@ -1,16 +1,14 @@
name: Tests name: Tests
on: on:
# Trigger the workflow on push or pull request,
# but only for the main branch
push: push:
branches: branches:
- main # Trigger on push events to the main branch - main
- beta # Trigger on push events to the beta branch - beta
pull_request: pull_request:
branches: branches:
- main # Trigger on pull request events targeting the main branch - main
- beta # Trigger on pull request events targeting the beta branch - beta
merge_group: merge_group:
types: [checks_requested] types: [checks_requested]
@ -24,6 +22,7 @@ jobs:
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36
id: filter id: filter
with: with:

View File

@ -24,14 +24,14 @@ If you have the motivation and experience with Typescript/Javascript (or are wil
### Prerequisites ### Prerequisites
- node: >=22.14.0 - node: >=22.14.0 - [manage with pnpm](https://pnpm.io/cli/env) | [manage with fnm](https://github.com/Schniz/fnm) | [manage with nvm](https://github.com/nvm-sh/nvm)
- npm: [how to install](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) - pnpm: 10.x - [how to install](https://pnpm.io/installation) (not recommended to install via `npm` on Windows native) | [alternate method - volta.sh](https://volta.sh/)
### Running Locally ### Running Locally
1. Clone the repo and in the root directory run `npm install` 1. Clone the repo and in the root directory run `pnpm install`
- *if you run into any errors, reach out in the **#dev-corner** channel on Discord* - *if you run into any errors, reach out in the **#dev-corner** channel on Discord*
2. Run `npm run start:dev` to locally run the project in `localhost:8000` 2. Run `pnpm start:dev` to locally run the project at `localhost:8000`
### Linting ### Linting
@ -91,11 +91,11 @@ You can get help testing your specific changes, and you might have found a new o
> PokéRogue uses [Vitest](https://vitest.dev/) for automatic testing. Checking out the existing tests in the [test](./test/) folder is a great way to understand how this works, and to get familiar with the project as a whole. > PokéRogue uses [Vitest](https://vitest.dev/) for automatic testing. Checking out the existing tests in the [test](./test/) folder is a great way to understand how this works, and to get familiar with the project as a whole.
To make sure your changes didn't break any existing test cases, run `npm run test:silent` in your terminal. You can also provide an argument to the command: to run only the Dancer (ability) tests, you could write `npm run test:silent dancer`. To make sure your changes didn't break any existing test cases, run `pnpm test:silent` in your terminal. You can also provide an argument to the command: to run only the Dancer (ability) tests, you could write `pnpm test:silent dancer`.
- __Note that passing all test cases does *not* guarantee that everything is working properly__. The project does not have complete regression testing. - __Note that passing all test cases does *not* guarantee that everything is working properly__. The project does not have complete regression testing.
Most non-trivial changes (*especially bug fixes*) should come along with new test cases. Most non-trivial changes (*especially bug fixes*) should come along with new test cases.
- To make a new test file, run `npm run create-test` and follow the prompts. If the move/ability/etc. you're modifying already has tests, simply add new cases to the end of the file. As mentioned before, the easiest way to get familiar with the system and understand how to write your own tests is simply to read the existing tests, particularly ones similar to the tests you intend to write. - To make a new test file, run `pnpm test:create` and follow the prompts. If the move/ability/etc. you're modifying already has tests, simply add new cases to the end of the file. As mentioned before, the easiest way to get familiar with the system and understand how to write your own tests is simply to read the existing tests, particularly ones similar to the tests you intend to write.
- Ensure that new tests: - Ensure that new tests:
- Are deterministic. In other words, the test should never pass or fail when it shouldn't due to randomness. This involves primarily ensuring that abilities and moves are never randomly selected. - Are deterministic. In other words, the test should never pass or fail when it shouldn't due to randomness. This involves primarily ensuring that abilities and moves are never randomly selected.
- As much as possible, are unit tests. If you have made two distinct changes, they should be tested in two separate cases. - As much as possible, are unit tests. If you have made two distinct changes, they should be tested in two separate cases.

View File

@ -27,6 +27,7 @@
"!**/.github/**/*", "!**/.github/**/*",
"!**/node_modules/**/*", "!**/node_modules/**/*",
"!**/.vscode/**/*", "!**/.vscode/**/*",
"!**/typedoc/**/*",
// TODO: lint css and html? // TODO: lint css and html?
"!**/*.css", "!**/*.css",
"!**/*.html", "!**/*.html",

View File

@ -23,7 +23,7 @@ When formatted correctly, these comments are shown within VS Code or similar IDE
- Functions also show the comment for each parameter as you type them, making keeping track of arguments inside lengthy functions much more clear. - Functions also show the comment for each parameter as you type them, making keeping track of arguments inside lengthy functions much more clear.
They can also be used to generate a commentated overview of the codebase. There is a GitHub action that automatically updates [this docs site](https://pagefaultgames.github.io/pokerogue/main/index.html) They can also be used to generate a commentated overview of the codebase. There is a GitHub action that automatically updates [this docs site](https://pagefaultgames.github.io/pokerogue/main/index.html)
and you can generate it locally as well via `npm run docs` which will generate into the `typedoc/` directory. and you can generate it locally as well via `pnpm run docs` which will generate into the `typedoc/` directory.
## Syntax ## Syntax
For an example of how TSDoc comments work, here are some TSDoc comments taken from `src/data/moves/move.ts`: For an example of how TSDoc comments work, here are some TSDoc comments taken from `src/data/moves/move.ts`:

View File

@ -1,14 +1,10 @@
# Linting & Formatting # Linting & Formatting
> "Any fool can write code that a computer can understand. Good programmers write code that humans can understand."
>
> — Martin Fowler
Writing clean, readable code is important, and linters and formatters are an integral part of ensuring code quality and readability. Writing clean, readable code is important, and linters and formatters are an integral part of ensuring code quality and readability.
It is for this reason we are using [Biome](https://biomejs.dev), an opinionated linter/formatter (akin to Prettier) with a heavy focus on speed and performance. It is for this reason we are using [Biome](https://biomejs.dev), an opinionated linter/formatter (akin to Prettier) with a heavy focus on speed and performance.
### Installation ### Installation
You probably installed Biome already without noticing it - it's included inside `package.json` and should've been downloaded when you ran `npm install` after cloning the repo (assuming you followed proper instructions, that is). If you haven't done that yet, go do it. You probably installed Biome already without noticing it - it's included inside `package.json` and should've been downloaded when you ran `pnpm install` after cloning the repo. If you haven't done that yet, go do it.
# Using Biome # Using Biome
@ -24,17 +20,11 @@ You will **not** be able to push code with `error`-level linting problems - fix
We also have a [Github Action](../.github/workflows/quality.yml) to verify code quality each time a PR is updated, preventing bad code from inadvertently making its way upstream. We also have a [Github Action](../.github/workflows/quality.yml) to verify code quality each time a PR is updated, preventing bad code from inadvertently making its way upstream.
### Why am I getting errors for code I didn't write?
<!-- TODO: Remove this if/when we perform a project wide linting spree -->
To save time and minimize friction with existing code, both the pre-commit hook and workflow run will only check files **directly changed** by a given PR or commit.
As a result, changes to files not updated since Biome's introduction can cause any _prior_ linting errors in them to resurface and get flagged.
This should occur less and less often as time passes and more files are updated to the new standard.
## Running Biome via CLI ## Running Biome via CLI
If you want Biome to check your files manually, you can run it from the command line like so: If you want Biome to check your files manually, you can run it from the command line like so:
```sh ```sh
npx biome check --[flags] pnpm exec biome check --[flags]
``` ```
A full list of flags and options can be found on [their website](https://biomejs.dev/reference/cli/), but here's a few useful ones to keep in mind: A full list of flags and options can be found on [their website](https://biomejs.dev/reference/cli/), but here's a few useful ones to keep in mind:
@ -56,10 +46,3 @@ Some things to consider:
Any questions about linting rules should be brought up in the `#dev-corner` channel in the discord. Any questions about linting rules should be brought up in the `#dev-corner` channel in the discord.
[^1]: A complete list of rules can be found in the `biome.jsonc` file in the project root. [^1]: A complete list of rules can be found in the `biome.jsonc` file in the project root.
## What about ESLint?
<!-- Remove if/when we finally ditch eslint for good -->
Our project migrated away from ESLint around March 2025 due to it simply not scaling well enough with the codebase's ever-growing size. The [existing eslint rules](../eslint.config.js) are considered _deprecated_, only kept due to Biome lacking the corresponding rules in its current ruleset.
No additional ESLint rules should be added under any circumstances - even the few currently in circulation take longer to run than the entire Biome formatting/linting suite combined.

View File

@ -1,8 +1,7 @@
pre-commit: pre-commit:
parallel: true
commands: commands:
biome-lint: biome-lint:
run: npx biome check --write --reporter=summary --staged --no-errors-on-unmatched run: pnpm exec biome check --write --reporter=summary --staged --no-errors-on-unmatched
stage_fixed: true stage_fixed: true
skip: skip:
- merge - merge

6132
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -22,9 +22,9 @@
"docs": "typedoc", "docs": "typedoc",
"depcruise": "depcruise src test", "depcruise": "depcruise src test",
"depcruise:graph": "depcruise src --output-type dot | node dependency-graph.js > dependency-graph.svg", "depcruise:graph": "depcruise src --output-type dot | node dependency-graph.js > dependency-graph.svg",
"postinstall": "npx lefthook install && npx lefthook run post-merge", "postinstall": "lefthook install && lefthook run post-merge",
"update-version:patch": "npm version patch --force --no-git-tag-version", "update-version:patch": "pnpm version patch --force --no-git-tag-version",
"update-version:minor": "npm version minor --force --no-git-tag-version", "update-version:minor": "pnpm version minor --force --no-git-tag-version",
"update-locales:remote": "git submodule update --progress --init --recursive --force --remote" "update-locales:remote": "git submodule update --progress --init --recursive --force --remote"
}, },
"devDependencies": { "devDependencies": {
@ -33,6 +33,7 @@
"@types/jsdom": "^21.1.7", "@types/jsdom": "^21.1.7",
"@types/node": "^22.13.14", "@types/node": "^22.13.14",
"@vitest/coverage-istanbul": "^3.0.9", "@vitest/coverage-istanbul": "^3.0.9",
"chalk": "^5.4.1",
"dependency-cruiser": "^16.3.10", "dependency-cruiser": "^16.3.10",
"inquirer": "^12.4.2", "inquirer": "^12.4.2",
"jsdom": "^26.0.0", "jsdom": "^26.0.0",

3910
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
/** /**
* This script creates a test boilerplate file in the appropriate * This script creates a test boilerplate file in the appropriate
* directory based on the type selected. * directory based on the type selected.
* @example npm run test:create * @example pnpm test:create
*/ */
import chalk from "chalk"; import chalk from "chalk";

View File

@ -1,14 +1,14 @@
import type { AbAttr } from "#app/data/abilities/ability";
import type Move from "#app/data/moves/move"; import type Move from "#app/data/moves/move";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import type { BattleStat } from "#enums/stat"; import type { BattleStat } from "#enums/stat";
import type { AbAttrConstructorMap } from "#app/data/abilities/ability"; import type { AbAttrConstructorMap } from "#app/data/abilities/ability";
// Intentionally re-export all types from the ability attributes module // intentionally re-export all types from abilities to have this be the centralized place to import ability types
export type * from "#app/data/abilities/ability"; export type * from "#app/data/abilities/ability";
export type AbAttrApplyFunc<TAttr extends AbAttr> = (attr: TAttr, passive: boolean, ...args: any[]) => void; // biome-ignore lint/correctness/noUnusedImports: Used in a tsdoc comment
export type AbAttrSuccessFunc<TAttr extends AbAttr> = (attr: TAttr, passive: boolean, ...args: any[]) => boolean; import type { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
export type AbAttrCondition = (pokemon: Pokemon) => boolean; export type AbAttrCondition = (pokemon: Pokemon) => boolean;
export type PokemonAttackCondition = (user: Pokemon | null, target: Pokemon | null, move: Move) => boolean; export type PokemonAttackCondition = (user: Pokemon | null, target: Pokemon | null, move: Move) => boolean;
export type PokemonDefendCondition = (target: Pokemon, user: Pokemon, move: Move) => boolean; export type PokemonDefendCondition = (target: Pokemon, user: Pokemon, move: Move) => boolean;
@ -25,3 +25,22 @@ export type AbAttrString = keyof AbAttrConstructorMap;
export type AbAttrMap = { export type AbAttrMap = {
[K in keyof AbAttrConstructorMap]: InstanceType<AbAttrConstructorMap[K]>; [K in keyof AbAttrConstructorMap]: InstanceType<AbAttrConstructorMap[K]>;
}; };
/**
* Subset of ability attribute classes that may be passed to {@linkcode applyAbAttrs} method
*
* @remarks
* Our AbAttr classes violate Liskov Substitution Principle.
*
* AbAttrs that are not in this have subclasses with apply methods requiring different parameters than
* the base apply method.
*
* Such attributes may not be passed to the {@linkcode applyAbAttrs} method
*/
export type CallableAbAttrString =
| Exclude<AbAttrString, "PreDefendAbAttr" | "PreAttackAbAttr">
| "PreApplyBattlerTagAbAttr";
export type AbAttrParamMap = {
[K in keyof AbAttrMap]: Parameters<AbAttrMap[K]["apply"]>[0];
};

View File

@ -0,0 +1,34 @@
/*
* A collection of custom utility types that aid in type checking and ensuring strict type conformity
*/
// biome-ignore lint/correctness/noUnusedImports: Used in a tsdoc comment
import type { AbAttr } from "./ability-types";
/**
* Exactly matches the type of the argument, preventing adding additional properties.
*
* Should never be used with `extends`, as this will nullify the exactness of the type.
*
* As an example, used to ensure that the parameters of {@linkcode AbAttr.canApply} and {@linkcode AbAttr.getTriggerMessage} are compatible with
* the type of the apply method
*
* @typeParam T - The type to match exactly
*/
export type Exact<T> = {
[K in keyof T]: T[K];
};
/**
* Type hint that indicates that the type is intended to be closed to a specific shape.
* Does not actually do anything special, is really just an alias for X.
*/
export type Closed<X> = X;
/**
* Remove `readonly` from all properties of the provided type
* @typeParam T - The type to make mutable
*/
export type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};

View File

@ -67,7 +67,7 @@ import { modifierTypes } from "./data/data-lists";
import { getModifierPoolForType } from "./utils/modifier-utils"; import { getModifierPoolForType } from "./utils/modifier-utils";
import { ModifierPoolType } from "#enums/modifier-pool-type"; import { ModifierPoolType } from "#enums/modifier-pool-type";
import AbilityBar from "#app/ui/ability-bar"; import AbilityBar from "#app/ui/ability-bar";
import { applyAbAttrs, applyPostBattleInitAbAttrs, applyPostItemLostAbAttrs } from "./data/abilities/apply-ab-attrs"; import { applyAbAttrs } from "./data/abilities/apply-ab-attrs";
import { allAbilities } from "./data/data-lists"; import { allAbilities } from "./data/data-lists";
import type { FixedBattleConfig } from "#app/battle"; import type { FixedBattleConfig } from "#app/battle";
import Battle from "#app/battle"; import Battle from "#app/battle";
@ -894,9 +894,19 @@ export default class BattleScene extends SceneBase {
return activeOnly ? this.infoToggles.filter(t => t?.isActive()) : this.infoToggles; return activeOnly ? this.infoToggles.filter(t => t?.isActive()) : this.infoToggles;
} }
getPokemonById(pokemonId: number): Pokemon | null { /**
const findInParty = (party: Pokemon[]) => party.find(p => p.id === pokemonId); * Return the {@linkcode Pokemon} associated with a given ID.
return (findInParty(this.getPlayerParty()) || findInParty(this.getEnemyParty())) ?? null; * @param pokemonId - The ID whose Pokemon will be retrieved.
* @returns The {@linkcode Pokemon} associated with the given id.
* Returns `null` if the ID is `undefined` or not present in either party.
*/
getPokemonById(pokemonId: number | undefined): Pokemon | null {
if (isNullOrUndefined(pokemonId)) {
return null;
}
const party = (this.getPlayerParty() as Pokemon[]).concat(this.getEnemyParty());
return party.find(p => p.id === pokemonId) ?? null;
} }
addPlayerPokemon( addPlayerPokemon(
@ -1256,7 +1266,7 @@ export default class BattleScene extends SceneBase {
const doubleChance = new NumberHolder(newWaveIndex % 10 === 0 ? 32 : 8); const doubleChance = new NumberHolder(newWaveIndex % 10 === 0 ? 32 : 8);
this.applyModifiers(DoubleBattleChanceBoosterModifier, true, doubleChance); this.applyModifiers(DoubleBattleChanceBoosterModifier, true, doubleChance);
for (const p of playerField) { for (const p of playerField) {
applyAbAttrs("DoubleBattleChanceAbAttr", p, null, false, doubleChance); applyAbAttrs("DoubleBattleChanceAbAttr", { pokemon: p, chance: doubleChance });
} }
return Math.max(doubleChance.value, 1); return Math.max(doubleChance.value, 1);
} }
@ -1461,7 +1471,7 @@ export default class BattleScene extends SceneBase {
for (const pokemon of this.getPlayerParty()) { for (const pokemon of this.getPlayerParty()) {
pokemon.resetBattleAndWaveData(); pokemon.resetBattleAndWaveData();
pokemon.resetTera(); pokemon.resetTera();
applyPostBattleInitAbAttrs("PostBattleInitAbAttr", pokemon); applyAbAttrs("PostBattleInitAbAttr", { pokemon });
if ( if (
pokemon.hasSpecies(SpeciesId.TERAPAGOS) || pokemon.hasSpecies(SpeciesId.TERAPAGOS) ||
(this.gameMode.isClassic && this.currentBattle.waveIndex > 180 && this.currentBattle.waveIndex <= 190) (this.gameMode.isClassic && this.currentBattle.waveIndex > 180 && this.currentBattle.waveIndex <= 190)
@ -2743,7 +2753,7 @@ export default class BattleScene extends SceneBase {
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
if (source && source.isPlayer() !== target.isPlayer()) { if (source && source.isPlayer() !== target.isPlayer()) {
applyAbAttrs("BlockItemTheftAbAttr", source, cancelled); applyAbAttrs("BlockItemTheftAbAttr", { pokemon: source, cancelled });
} }
if (cancelled.value) { if (cancelled.value) {
@ -2783,13 +2793,13 @@ export default class BattleScene extends SceneBase {
if (target.isPlayer()) { if (target.isPlayer()) {
this.addModifier(newItemModifier, ignoreUpdate, playSound, false, instant); this.addModifier(newItemModifier, ignoreUpdate, playSound, false, instant);
if (source && itemLost) { if (source && itemLost) {
applyPostItemLostAbAttrs("PostItemLostAbAttr", source, false); applyAbAttrs("PostItemLostAbAttr", { pokemon: source });
} }
return true; return true;
} }
this.addEnemyModifier(newItemModifier, ignoreUpdate, instant); this.addEnemyModifier(newItemModifier, ignoreUpdate, instant);
if (source && itemLost) { if (source && itemLost) {
applyPostItemLostAbAttrs("PostItemLostAbAttr", source, false); applyAbAttrs("PostItemLostAbAttr", { pokemon: source });
} }
return true; return true;
} }
@ -2812,7 +2822,7 @@ export default class BattleScene extends SceneBase {
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
if (source && source.isPlayer() !== target.isPlayer()) { if (source && source.isPlayer() !== target.isPlayer()) {
applyAbAttrs("BlockItemTheftAbAttr", source, cancelled); applyAbAttrs("BlockItemTheftAbAttr", { pokemon: source, cancelled });
} }
if (cancelled.value) { if (cancelled.value) {

File diff suppressed because it is too large Load Diff

View File

@ -1,63 +1,14 @@
import type { AbAttrApplyFunc, AbAttrMap, AbAttrString, AbAttrSuccessFunc } from "#app/@types/ability-types"; import type { AbAttrParamMap } from "#app/@types/ability-types";
import type Pokemon from "#app/field/pokemon"; import type { AbAttrBaseParams, AbAttrString, CallableAbAttrString } from "#app/@types/ability-types";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import type { BooleanHolder, NumberHolder } from "#app/utils/common";
import type { BattlerIndex } from "#enums/battler-index";
import type { HitResult } from "#enums/hit-result";
import type { BattleStat, Stat } from "#enums/stat";
import type { StatusEffect } from "#enums/status-effect";
import type { WeatherType } from "#enums/weather-type";
import type { BattlerTag } from "../battler-tags";
import type Move from "../moves/move";
import type { PokemonMove } from "../moves/pokemon-move";
import type { TerrainType } from "../terrain";
import type { Weather } from "../weather";
import type {
PostBattleInitAbAttr,
PreDefendAbAttr,
PostDefendAbAttr,
PostMoveUsedAbAttr,
StatMultiplierAbAttr,
AllyStatMultiplierAbAttr,
PostSetStatusAbAttr,
PostDamageAbAttr,
FieldMultiplyStatAbAttr,
PreAttackAbAttr,
ExecutedMoveAbAttr,
PostAttackAbAttr,
PostKnockOutAbAttr,
PostVictoryAbAttr,
PostSummonAbAttr,
PreSummonAbAttr,
PreSwitchOutAbAttr,
PreLeaveFieldAbAttr,
PreStatStageChangeAbAttr,
PostStatStageChangeAbAttr,
PreSetStatusAbAttr,
PreApplyBattlerTagAbAttr,
PreWeatherEffectAbAttr,
PreWeatherDamageAbAttr,
PostTurnAbAttr,
PostWeatherChangeAbAttr,
PostWeatherLapseAbAttr,
PostTerrainChangeAbAttr,
CheckTrappedAbAttr,
PostBattleAbAttr,
PostFaintAbAttr,
PostItemLostAbAttr,
} from "./ability";
function applySingleAbAttrs<T extends AbAttrString>( function applySingleAbAttrs<T extends AbAttrString>(
pokemon: Pokemon,
passive: boolean,
attrType: T, attrType: T,
applyFunc: AbAttrApplyFunc<AbAttrMap[T]>, params: AbAttrParamMap[T],
successFunc: AbAttrSuccessFunc<AbAttrMap[T]>,
args: any[],
gainedMidTurn = false, gainedMidTurn = false,
simulated = false,
messages: string[] = [], messages: string[] = [],
) { ) {
const { simulated = false, passive = false, pokemon } = params;
if (!pokemon?.canApplyAbility(passive) || (passive && pokemon.getPassiveAbility().id === pokemon.getAbility().id)) { if (!pokemon?.canApplyAbility(passive) || (passive && pokemon.getPassiveAbility().id === pokemon.getAbility().id)) {
return; return;
} }
@ -75,7 +26,11 @@ function applySingleAbAttrs<T extends AbAttrString>(
for (const attr of ability.getAttrs(attrType)) { for (const attr of ability.getAttrs(attrType)) {
const condition = attr.getCondition(); const condition = attr.getCondition();
let abShown = false; let abShown = false;
if ((condition && !condition(pokemon)) || !successFunc(attr, passive)) { // We require an `as any` cast to suppress an error about the `params` type not being assignable to
// the type of the argument expected by `attr.canApply()`. This is OK, because we know that
// `attr` is an instance of the `attrType` class provided to the method, and typescript _will_ check
// that the `params` object has the correct properties for that class at the callsites.
if ((condition && !condition(pokemon)) || !attr.canApply(params as any)) {
continue; continue;
} }
@ -85,15 +40,16 @@ function applySingleAbAttrs<T extends AbAttrString>(
globalScene.phaseManager.queueAbilityDisplay(pokemon, passive, true); globalScene.phaseManager.queueAbilityDisplay(pokemon, passive, true);
abShown = true; abShown = true;
} }
const message = attr.getTriggerMessage(pokemon, ability.name, args);
const message = attr.getTriggerMessage(params as any, ability.name);
if (message) { if (message) {
if (!simulated) { if (!simulated) {
globalScene.phaseManager.queueMessage(message); globalScene.phaseManager.queueMessage(message);
} }
messages.push(message); messages.push(message);
} }
// The `as any` cast here uses the same reasoning as above.
applyFunc(attr, passive); attr.apply(params as any);
if (abShown) { if (abShown) {
globalScene.phaseManager.queueAbilityDisplay(pokemon, passive, false); globalScene.phaseManager.queueAbilityDisplay(pokemon, passive, false);
@ -107,726 +63,60 @@ function applySingleAbAttrs<T extends AbAttrString>(
} }
} }
function applyAbAttrsInternal<T extends AbAttrString>( function applyAbAttrsInternal<T extends CallableAbAttrString>(
attrType: T, attrType: T,
pokemon: Pokemon | null, params: AbAttrParamMap[T],
applyFunc: AbAttrApplyFunc<AbAttrMap[T]>,
successFunc: AbAttrSuccessFunc<AbAttrMap[T]>,
args: any[],
simulated = false,
messages: string[] = [], messages: string[] = [],
gainedMidTurn = false, gainedMidTurn = false,
) { ) {
for (const passive of [false, true]) { // If the pokemon is not defined, no ability attributes to be applied.
if (pokemon) { // TODO: Evaluate whether this check is even necessary anymore
applySingleAbAttrs(pokemon, passive, attrType, applyFunc, successFunc, args, gainedMidTurn, simulated, messages); if (!params.pokemon) {
globalScene.phaseManager.clearPhaseQueueSplice(); return;
}
} }
if (params.passive !== undefined) {
applySingleAbAttrs(attrType, params, gainedMidTurn, messages);
return;
}
for (const passive of [false, true]) {
params.passive = passive;
applySingleAbAttrs(attrType, params, gainedMidTurn, messages);
globalScene.phaseManager.clearPhaseQueueSplice();
}
// We need to restore passive to its original state in the case that it was undefined on entry
// this is necessary in case this method is called with an object that is reused.
params.passive = undefined;
} }
export function applyAbAttrs<T extends AbAttrString>( /**
* @param attrType - The type of the ability attribute to apply. (note: may not be any attribute that extends PostSummonAbAttr)
* @param params - The parameters to pass to the ability attribute's apply method
* @param messages - An optional array to which ability trigger messges will be added
*/
export function applyAbAttrs<T extends CallableAbAttrString>(
attrType: T, attrType: T,
pokemon: Pokemon, params: AbAttrParamMap[T],
cancelled: BooleanHolder | null, messages?: string[],
simulated = false,
...args: any[]
): void { ): void {
applyAbAttrsInternal<T>( applyAbAttrsInternal(attrType, params, messages);
attrType,
pokemon,
// @ts-expect-error: TODO: fix the error on `cancelled`
(attr, passive) => attr.apply(pokemon, passive, simulated, cancelled, args),
(attr, passive) => attr.canApply(pokemon, passive, simulated, args),
args,
simulated,
);
} }
// TODO: Improve the type signatures of the following methods / refactor the apply methods // TODO: Improve the type signatures of the following methods / refactor the apply methods
export function applyPostBattleInitAbAttrs<K extends AbAttrString>(
attrType: AbAttrMap[K] extends PostBattleInitAbAttr ? K : never,
pokemon: Pokemon,
simulated = false,
...args: any[]
): void {
applyAbAttrsInternal(
attrType,
pokemon,
(attr, passive) => (attr as PostBattleInitAbAttr).applyPostBattleInit(pokemon, passive, simulated, args),
(attr, passive) => (attr as PostBattleInitAbAttr).canApplyPostBattleInit(pokemon, passive, simulated, args),
args,
simulated,
);
}
export function applyPreDefendAbAttrs<K extends AbAttrString>(
attrType: AbAttrMap[K] extends PreDefendAbAttr ? K : never,
pokemon: Pokemon,
attacker: Pokemon,
move: Move | null,
cancelled: BooleanHolder | null,
simulated = false,
...args: any[]
): void {
applyAbAttrsInternal(
attrType,
pokemon,
(attr, passive) =>
(attr as PreDefendAbAttr).applyPreDefend(pokemon, passive, simulated, attacker, move, cancelled, args),
(attr, passive) =>
(attr as PreDefendAbAttr).canApplyPreDefend(pokemon, passive, simulated, attacker, move, cancelled, args),
args,
simulated,
);
}
export function applyPostDefendAbAttrs<K extends AbAttrString>(
attrType: AbAttrMap[K] extends PostDefendAbAttr ? K : never,
pokemon: Pokemon,
attacker: Pokemon,
move: Move,
hitResult: HitResult | null,
simulated = false,
...args: any[]
): void {
applyAbAttrsInternal(
attrType,
pokemon,
(attr, passive) =>
(attr as PostDefendAbAttr).applyPostDefend(pokemon, passive, simulated, attacker, move, hitResult, args),
(attr, passive) =>
(attr as PostDefendAbAttr).canApplyPostDefend(pokemon, passive, simulated, attacker, move, hitResult, args),
args,
simulated,
);
}
export function applyPostMoveUsedAbAttrs<K extends AbAttrString>(
attrType: AbAttrMap[K] extends PostMoveUsedAbAttr ? K : never,
pokemon: Pokemon,
move: PokemonMove,
source: Pokemon,
targets: BattlerIndex[],
simulated = false,
...args: any[]
): void {
applyAbAttrsInternal(
attrType,
pokemon,
(attr, _passive) => (attr as PostMoveUsedAbAttr).applyPostMoveUsed(pokemon, move, source, targets, simulated, args),
(attr, _passive) =>
(attr as PostMoveUsedAbAttr).canApplyPostMoveUsed(pokemon, move, source, targets, simulated, args),
args,
simulated,
);
}
export function applyStatMultiplierAbAttrs<K extends AbAttrString>(
attrType: AbAttrMap[K] extends StatMultiplierAbAttr ? K : never,
pokemon: Pokemon,
stat: BattleStat,
statValue: NumberHolder,
simulated = false,
...args: any[]
): void {
applyAbAttrsInternal(
attrType,
pokemon,
(attr, passive) =>
(attr as StatMultiplierAbAttr).applyStatStage(pokemon, passive, simulated, stat, statValue, args),
(attr, passive) =>
(attr as StatMultiplierAbAttr).canApplyStatStage(pokemon, passive, simulated, stat, statValue, args),
args,
);
}
/**
* Applies an ally's Stat multiplier attribute
* @param attrType - {@linkcode AllyStatMultiplierAbAttr} should always be AllyStatMultiplierAbAttr for the time being
* @param pokemon - The {@linkcode Pokemon} with the ability
* @param stat - The type of the checked {@linkcode Stat}
* @param statValue - {@linkcode NumberHolder} containing the value of the checked stat
* @param checkedPokemon - The {@linkcode Pokemon} with the checked stat
* @param ignoreAbility - Whether or not the ability should be ignored by the pokemon or its move.
* @param args - unused
*/
export function applyAllyStatMultiplierAbAttrs<K extends AbAttrString>(
attrType: AbAttrMap[K] extends AllyStatMultiplierAbAttr ? K : never,
pokemon: Pokemon,
stat: BattleStat,
statValue: NumberHolder,
simulated = false,
checkedPokemon: Pokemon,
ignoreAbility: boolean,
...args: any[]
): void {
applyAbAttrsInternal(
attrType,
pokemon,
(attr, passive) =>
(attr as AllyStatMultiplierAbAttr).applyAllyStat(
pokemon,
passive,
simulated,
stat,
statValue,
checkedPokemon,
ignoreAbility,
args,
),
(attr, passive) =>
(attr as AllyStatMultiplierAbAttr).canApplyAllyStat(
pokemon,
passive,
simulated,
stat,
statValue,
checkedPokemon,
ignoreAbility,
args,
),
args,
simulated,
);
}
export function applyPostSetStatusAbAttrs<K extends AbAttrString>(
attrType: AbAttrMap[K] extends PostSetStatusAbAttr ? K : never,
pokemon: Pokemon,
effect: StatusEffect,
sourcePokemon?: Pokemon | null,
simulated = false,
...args: any[]
): void {
applyAbAttrsInternal(
attrType,
pokemon,
(attr, passive) =>
(attr as PostSetStatusAbAttr).applyPostSetStatus(pokemon, sourcePokemon, passive, effect, simulated, args),
(attr, passive) =>
(attr as PostSetStatusAbAttr).canApplyPostSetStatus(pokemon, sourcePokemon, passive, effect, simulated, args),
args,
simulated,
);
}
export function applyPostDamageAbAttrs<K extends AbAttrString>(
attrType: AbAttrMap[K] extends PostDamageAbAttr ? K : never,
pokemon: Pokemon,
damage: number,
_passive: boolean,
simulated = false,
args: any[],
source?: Pokemon,
): void {
applyAbAttrsInternal(
attrType,
pokemon,
(attr, passive) => (attr as PostDamageAbAttr).applyPostDamage(pokemon, damage, passive, simulated, args, source),
(attr, passive) => (attr as PostDamageAbAttr).canApplyPostDamage(pokemon, damage, passive, simulated, args, source),
args,
);
}
/**
* Applies a field Stat multiplier attribute
* @param attrType {@linkcode FieldMultiplyStatAbAttr} should always be FieldMultiplyBattleStatAbAttr for the time being
* @param pokemon {@linkcode Pokemon} the Pokemon applying this ability
* @param stat {@linkcode Stat} the type of the checked stat
* @param statValue {@linkcode NumberHolder} the value of the checked stat
* @param checkedPokemon {@linkcode Pokemon} the Pokemon with the checked stat
* @param hasApplied {@linkcode BooleanHolder} whether or not a FieldMultiplyBattleStatAbAttr has already affected this stat
* @param args unused
*/
export function applyFieldStatMultiplierAbAttrs<K extends AbAttrString>(
attrType: AbAttrMap[K] extends FieldMultiplyStatAbAttr ? K : never,
pokemon: Pokemon,
stat: Stat,
statValue: NumberHolder,
checkedPokemon: Pokemon,
hasApplied: BooleanHolder,
simulated = false,
...args: any[]
): void {
applyAbAttrsInternal(
attrType,
pokemon,
(attr, passive) =>
(attr as FieldMultiplyStatAbAttr).applyFieldStat(
pokemon,
passive,
simulated,
stat,
statValue,
checkedPokemon,
hasApplied,
args,
),
(attr, passive) =>
(attr as FieldMultiplyStatAbAttr).canApplyFieldStat(
pokemon,
passive,
simulated,
stat,
statValue,
checkedPokemon,
hasApplied,
args,
),
args,
);
}
export function applyPreAttackAbAttrs<K extends AbAttrString>(
attrType: AbAttrMap[K] extends PreAttackAbAttr ? K : never,
pokemon: Pokemon,
defender: Pokemon | null,
move: Move,
simulated = false,
...args: any[]
): void {
applyAbAttrsInternal(
attrType,
pokemon,
(attr, passive) => (attr as PreAttackAbAttr).applyPreAttack(pokemon, passive, simulated, defender, move, args),
(attr, passive) => (attr as PreAttackAbAttr).canApplyPreAttack(pokemon, passive, simulated, defender, move, args),
args,
simulated,
);
}
export function applyExecutedMoveAbAttrs<K extends AbAttrString>(
attrType: AbAttrMap[K] extends ExecutedMoveAbAttr ? K : never,
pokemon: Pokemon,
simulated = false,
...args: any[]
): void {
applyAbAttrsInternal(
attrType,
pokemon,
attr => (attr as ExecutedMoveAbAttr).applyExecutedMove(pokemon, simulated),
attr => (attr as ExecutedMoveAbAttr).canApplyExecutedMove(pokemon, simulated),
args,
simulated,
);
}
export function applyPostAttackAbAttrs<K extends AbAttrString>(
attrType: AbAttrMap[K] extends PostAttackAbAttr ? K : never,
pokemon: Pokemon,
defender: Pokemon,
move: Move,
hitResult: HitResult | null,
simulated = false,
...args: any[]
): void {
applyAbAttrsInternal(
attrType,
pokemon,
(attr, passive) =>
(attr as PostAttackAbAttr).applyPostAttack(pokemon, passive, simulated, defender, move, hitResult, args),
(attr, passive) =>
(attr as PostAttackAbAttr).canApplyPostAttack(pokemon, passive, simulated, defender, move, hitResult, args),
args,
simulated,
);
}
export function applyPostKnockOutAbAttrs<K extends AbAttrString>(
attrType: AbAttrMap[K] extends PostKnockOutAbAttr ? K : never,
pokemon: Pokemon,
knockedOut: Pokemon,
simulated = false,
...args: any[]
): void {
applyAbAttrsInternal(
attrType,
pokemon,
(attr, passive) => (attr as PostKnockOutAbAttr).applyPostKnockOut(pokemon, passive, simulated, knockedOut, args),
(attr, passive) => (attr as PostKnockOutAbAttr).canApplyPostKnockOut(pokemon, passive, simulated, knockedOut, args),
args,
simulated,
);
}
export function applyPostVictoryAbAttrs<K extends AbAttrString>(
attrType: AbAttrMap[K] extends PostVictoryAbAttr ? K : never,
pokemon: Pokemon,
simulated = false,
...args: any[]
): void {
applyAbAttrsInternal(
attrType,
pokemon,
(attr, passive) => (attr as PostVictoryAbAttr).applyPostVictory(pokemon, passive, simulated, args),
(attr, passive) => (attr as PostVictoryAbAttr).canApplyPostVictory(pokemon, passive, simulated, args),
args,
simulated,
);
}
export function applyPostSummonAbAttrs<K extends AbAttrString>(
attrType: AbAttrMap[K] extends PostSummonAbAttr ? K : never,
pokemon: Pokemon,
passive = false,
simulated = false,
...args: any[]
): void {
applySingleAbAttrs(
pokemon,
passive,
attrType,
(attr, passive) => (attr as PostSummonAbAttr).applyPostSummon(pokemon, passive, simulated, args),
(attr, passive) => (attr as PostSummonAbAttr).canApplyPostSummon(pokemon, passive, simulated, args),
args,
false,
simulated,
);
}
export function applyPreSummonAbAttrs<K extends AbAttrString>(
attrType: AbAttrMap[K] extends PreSummonAbAttr ? K : never,
pokemon: Pokemon,
...args: any[]
): void {
applyAbAttrsInternal(
attrType,
pokemon,
(attr, passive) => (attr as PreSummonAbAttr).applyPreSummon(pokemon, passive, args),
(attr, passive) => (attr as PreSummonAbAttr).canApplyPreSummon(pokemon, passive, args),
args,
);
}
export function applyPreSwitchOutAbAttrs<K extends AbAttrString>(
attrType: AbAttrMap[K] extends PreSwitchOutAbAttr ? K : never,
pokemon: Pokemon,
simulated = false,
...args: any[]
): void {
applyAbAttrsInternal(
attrType,
pokemon,
(attr, passive) => (attr as PreSwitchOutAbAttr).applyPreSwitchOut(pokemon, passive, simulated, args),
(attr, passive) => (attr as PreSwitchOutAbAttr).canApplyPreSwitchOut(pokemon, passive, simulated, args),
args,
simulated,
);
}
export function applyPreLeaveFieldAbAttrs<K extends AbAttrString>(
attrType: AbAttrMap[K] extends PreLeaveFieldAbAttr ? K : never,
pokemon: Pokemon,
simulated = false,
...args: any[]
): void {
applyAbAttrsInternal(
attrType,
pokemon,
(attr, passive) => (attr as PreLeaveFieldAbAttr).applyPreLeaveField(pokemon, passive, simulated, args),
(attr, passive) => (attr as PreLeaveFieldAbAttr).canApplyPreLeaveField(pokemon, passive, simulated, args),
args,
simulated,
);
}
export function applyPreStatStageChangeAbAttrs<K extends AbAttrString>(
attrType: AbAttrMap[K] extends PreStatStageChangeAbAttr ? K : never,
pokemon: Pokemon | null,
stat: BattleStat,
cancelled: BooleanHolder,
simulated = false,
...args: any[]
): void {
applyAbAttrsInternal(
attrType,
pokemon,
(attr, passive) =>
(attr as PreStatStageChangeAbAttr).applyPreStatStageChange(pokemon, passive, simulated, stat, cancelled, args),
(attr, passive) =>
(attr as PreStatStageChangeAbAttr).canApplyPreStatStageChange(pokemon, passive, simulated, stat, cancelled, args),
args,
simulated,
);
}
export function applyPostStatStageChangeAbAttrs<K extends AbAttrString>(
attrType: AbAttrMap[K] extends PostStatStageChangeAbAttr ? K : never,
pokemon: Pokemon,
stats: BattleStat[],
stages: number,
selfTarget: boolean,
simulated = false,
...args: any[]
): void {
applyAbAttrsInternal(
attrType,
pokemon,
(attr, _passive) =>
(attr as PostStatStageChangeAbAttr).applyPostStatStageChange(pokemon, simulated, stats, stages, selfTarget, args),
(attr, _passive) =>
(attr as PostStatStageChangeAbAttr).canApplyPostStatStageChange(
pokemon,
simulated,
stats,
stages,
selfTarget,
args,
),
args,
simulated,
);
}
export function applyPreSetStatusAbAttrs<K extends AbAttrString>(
attrType: AbAttrMap[K] extends PreSetStatusAbAttr ? K : never,
pokemon: Pokemon,
effect: StatusEffect | undefined,
cancelled: BooleanHolder,
simulated = false,
...args: any[]
): void {
applyAbAttrsInternal(
attrType,
pokemon,
(attr, passive) =>
(attr as PreSetStatusAbAttr).applyPreSetStatus(pokemon, passive, simulated, effect, cancelled, args),
(attr, passive) =>
(attr as PreSetStatusAbAttr).canApplyPreSetStatus(pokemon, passive, simulated, effect, cancelled, args),
args,
simulated,
);
}
export function applyPreApplyBattlerTagAbAttrs<K extends AbAttrString>(
attrType: AbAttrMap[K] extends PreApplyBattlerTagAbAttr ? K : never,
pokemon: Pokemon,
tag: BattlerTag,
cancelled: BooleanHolder,
simulated = false,
...args: any[]
): void {
applyAbAttrsInternal(
attrType,
pokemon,
(attr, passive) =>
(attr as PreApplyBattlerTagAbAttr).applyPreApplyBattlerTag(pokemon, passive, simulated, tag, cancelled, args),
(attr, passive) =>
(attr as PreApplyBattlerTagAbAttr).canApplyPreApplyBattlerTag(pokemon, passive, simulated, tag, cancelled, args),
args,
simulated,
);
}
export function applyPreWeatherEffectAbAttrs<K extends AbAttrString>(
attrType: AbAttrMap[K] extends PreWeatherEffectAbAttr ? K : never,
pokemon: Pokemon,
weather: Weather | null,
cancelled: BooleanHolder,
simulated = false,
...args: any[]
): void {
applyAbAttrsInternal(
attrType,
pokemon,
(attr, passive) =>
(attr as PreWeatherDamageAbAttr).applyPreWeatherEffect(pokemon, passive, simulated, weather, cancelled, args),
(attr, passive) =>
(attr as PreWeatherDamageAbAttr).canApplyPreWeatherEffect(pokemon, passive, simulated, weather, cancelled, args),
args,
simulated,
);
}
export function applyPostTurnAbAttrs<K extends AbAttrString>(
attrType: AbAttrMap[K] extends PostTurnAbAttr ? K : never,
pokemon: Pokemon,
simulated = false,
...args: any[]
): void {
applyAbAttrsInternal(
attrType,
pokemon,
(attr, passive) => (attr as PostTurnAbAttr).applyPostTurn(pokemon, passive, simulated, args),
(attr, passive) => (attr as PostTurnAbAttr).canApplyPostTurn(pokemon, passive, simulated, args),
args,
simulated,
);
}
export function applyPostWeatherChangeAbAttrs<K extends AbAttrString>(
attrType: AbAttrMap[K] extends PostWeatherChangeAbAttr ? K : never,
pokemon: Pokemon,
weather: WeatherType,
simulated = false,
...args: any[]
): void {
applyAbAttrsInternal(
attrType,
pokemon,
(attr, passive) =>
(attr as PostWeatherChangeAbAttr).applyPostWeatherChange(pokemon, passive, simulated, weather, args),
(attr, passive) =>
(attr as PostWeatherChangeAbAttr).canApplyPostWeatherChange(pokemon, passive, simulated, weather, args),
args,
simulated,
);
}
export function applyPostWeatherLapseAbAttrs<K extends AbAttrString>(
attrType: AbAttrMap[K] extends PostWeatherLapseAbAttr ? K : never,
pokemon: Pokemon,
weather: Weather | null,
simulated = false,
...args: any[]
): void {
applyAbAttrsInternal(
attrType,
pokemon,
(attr, passive) =>
(attr as PostWeatherLapseAbAttr).applyPostWeatherLapse(pokemon, passive, simulated, weather, args),
(attr, passive) =>
(attr as PostWeatherLapseAbAttr).canApplyPostWeatherLapse(pokemon, passive, simulated, weather, args),
args,
simulated,
);
}
export function applyPostTerrainChangeAbAttrs<K extends AbAttrString>(
attrType: AbAttrMap[K] extends PostTerrainChangeAbAttr ? K : never,
pokemon: Pokemon,
terrain: TerrainType,
simulated = false,
...args: any[]
): void {
applyAbAttrsInternal(
attrType,
pokemon,
(attr, passive) =>
(attr as PostTerrainChangeAbAttr).applyPostTerrainChange(pokemon, passive, simulated, terrain, args),
(attr, passive) =>
(attr as PostTerrainChangeAbAttr).canApplyPostTerrainChange(pokemon, passive, simulated, terrain, args),
args,
simulated,
);
}
export function applyCheckTrappedAbAttrs<K extends AbAttrString>(
attrType: AbAttrMap[K] extends CheckTrappedAbAttr ? K : never,
pokemon: Pokemon,
trapped: BooleanHolder,
otherPokemon: Pokemon,
messages: string[],
simulated = false,
...args: any[]
): void {
applyAbAttrsInternal(
attrType,
pokemon,
(attr, passive) =>
(attr as CheckTrappedAbAttr).applyCheckTrapped(pokemon, passive, simulated, trapped, otherPokemon, args),
(attr, passive) =>
(attr as CheckTrappedAbAttr).canApplyCheckTrapped(pokemon, passive, simulated, trapped, otherPokemon, args),
args,
simulated,
messages,
);
}
export function applyPostBattleAbAttrs<K extends AbAttrString>(
attrType: AbAttrMap[K] extends PostBattleAbAttr ? K : never,
pokemon: Pokemon,
simulated = false,
...args: any[]
): void {
applyAbAttrsInternal(
attrType,
pokemon,
(attr, passive) => (attr as PostBattleAbAttr).applyPostBattle(pokemon, passive, simulated, args),
(attr, passive) => (attr as PostBattleAbAttr).canApplyPostBattle(pokemon, passive, simulated, args),
args,
simulated,
);
}
export function applyPostFaintAbAttrs<K extends AbAttrString>(
attrType: AbAttrMap[K] extends PostFaintAbAttr ? K : never,
pokemon: Pokemon,
attacker?: Pokemon,
move?: Move,
hitResult?: HitResult,
simulated = false,
...args: any[]
): void {
applyAbAttrsInternal(
attrType,
pokemon,
(attr, passive) =>
(attr as PostFaintAbAttr).applyPostFaint(pokemon, passive, simulated, attacker, move, hitResult, args),
(attr, passive) =>
(attr as PostFaintAbAttr).canApplyPostFaint(pokemon, passive, simulated, attacker, move, hitResult, args),
args,
simulated,
);
}
export function applyPostItemLostAbAttrs<K extends AbAttrString>(
attrType: AbAttrMap[K] extends PostItemLostAbAttr ? K : never,
pokemon: Pokemon,
simulated = false,
...args: any[]
): void {
applyAbAttrsInternal(
attrType,
pokemon,
(attr, _passive) => (attr as PostItemLostAbAttr).applyPostItemLost(pokemon, simulated, args),
(attr, _passive) => (attr as PostItemLostAbAttr).canApplyPostItemLost(pokemon, simulated, args),
args,
);
}
/** /**
* Applies abilities when they become active mid-turn (ability switch) * Applies abilities when they become active mid-turn (ability switch)
* *
* Ignores passives as they don't change and shouldn't be reapplied when main abilities change * Ignores passives as they don't change and shouldn't be reapplied when main abilities change
*/ */
export function applyOnGainAbAttrs(pokemon: Pokemon, passive = false, simulated = false, ...args: any[]): void { export function applyOnGainAbAttrs(params: AbAttrBaseParams): void {
applySingleAbAttrs( applySingleAbAttrs("PostSummonAbAttr", params, true);
pokemon,
passive,
"PostSummonAbAttr",
(attr, passive) => attr.applyPostSummon(pokemon, passive, simulated, args),
(attr, passive) => attr.canApplyPostSummon(pokemon, passive, simulated, args),
args,
true,
simulated,
);
} }
/** /**
* Applies ability attributes which activate when the ability is lost or suppressed (i.e. primal weather) * Applies ability attributes which activate when the ability is lost or suppressed (i.e. primal weather)
*/ */
export function applyOnLoseAbAttrs(pokemon: Pokemon, passive = false, simulated = false, ...args: any[]): void { export function applyOnLoseAbAttrs(params: AbAttrBaseParams): void {
applySingleAbAttrs( applySingleAbAttrs("PreLeaveFieldAbAttr", params, true);
pokemon,
passive,
"PreLeaveFieldAbAttr",
(attr, passive) => attr.applyPreLeaveField(pokemon, passive, simulated, [...args, true]),
(attr, passive) => attr.canApplyPreLeaveField(pokemon, passive, simulated, [...args, true]),
args,
true,
simulated,
);
applySingleAbAttrs( applySingleAbAttrs("IllusionBreakAbAttr", params, true);
pokemon,
passive,
"IllusionBreakAbAttr",
(attr, passive) => attr.apply(pokemon, passive, simulated, null, args),
(attr, passive) => attr.canApply(pokemon, passive, simulated, args),
args,
true,
simulated,
);
} }

View File

@ -72,10 +72,11 @@ export abstract class ArenaTag {
/** /**
* Helper function that retrieves the source Pokemon * Helper function that retrieves the source Pokemon
* @returns The source {@linkcode Pokemon} or `null` if none is found * @returns - The source {@linkcode Pokemon} for this tag.
* Returns `null` if `this.sourceId` is `undefined`
*/ */
public getSourcePokemon(): Pokemon | null { public getSourcePokemon(): Pokemon | null {
return this.sourceId ? globalScene.getPokemonById(this.sourceId) : null; return globalScene.getPokemonById(this.sourceId);
} }
/** /**
@ -107,19 +108,22 @@ export class MistTag extends ArenaTag {
onAdd(arena: Arena, quiet = false): void { onAdd(arena: Arena, quiet = false): void {
super.onAdd(arena); super.onAdd(arena);
if (this.sourceId) { // We assume `quiet=true` means "just add the bloody tag no questions asked"
const source = globalScene.getPokemonById(this.sourceId); if (quiet) {
return;
if (!quiet && source) {
globalScene.phaseManager.queueMessage(
i18next.t("arenaTag:mistOnAdd", {
pokemonNameWithAffix: getPokemonNameWithAffix(source),
}),
);
} else if (!quiet) {
console.warn("Failed to get source for MistTag onAdd");
}
} }
const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for MistTag on add message; id: ${this.sourceId}`);
return;
}
globalScene.phaseManager.queueMessage(
i18next.t("arenaTag:mistOnAdd", {
pokemonNameWithAffix: getPokemonNameWithAffix(source),
}),
);
} }
/** /**
@ -137,7 +141,7 @@ export class MistTag extends ArenaTag {
if (attacker) { if (attacker) {
const bypassed = new BooleanHolder(false); const bypassed = new BooleanHolder(false);
// TODO: Allow this to be simulated // TODO: Allow this to be simulated
applyAbAttrs("InfiltratorAbAttr", attacker, null, false, bypassed); applyAbAttrs("InfiltratorAbAttr", { pokemon: attacker, simulated: false, bypassed });
if (bypassed.value) { if (bypassed.value) {
return false; return false;
} }
@ -202,7 +206,7 @@ export class WeakenMoveScreenTag extends ArenaTag {
): boolean { ): boolean {
if (this.weakenedCategories.includes(moveCategory)) { if (this.weakenedCategories.includes(moveCategory)) {
const bypassed = new BooleanHolder(false); const bypassed = new BooleanHolder(false);
applyAbAttrs("InfiltratorAbAttr", attacker, null, false, bypassed); applyAbAttrs("InfiltratorAbAttr", { pokemon: attacker, bypassed });
if (bypassed.value) { if (bypassed.value) {
return false; return false;
} }
@ -440,18 +444,18 @@ class MatBlockTag extends ConditionalProtectTag {
} }
onAdd(_arena: Arena) { onAdd(_arena: Arena) {
if (this.sourceId) { const source = this.getSourcePokemon();
const source = globalScene.getPokemonById(this.sourceId); if (!source) {
if (source) { console.warn(`Failed to get source Pokemon for Mat Block message; id: ${this.sourceId}`);
globalScene.phaseManager.queueMessage( return;
i18next.t("arenaTag:matBlockOnAdd", {
pokemonNameWithAffix: getPokemonNameWithAffix(source),
}),
);
} else {
console.warn("Failed to get source for MatBlockTag onAdd");
}
} }
super.onAdd(_arena);
globalScene.phaseManager.queueMessage(
i18next.t("arenaTag:matBlockOnAdd", {
pokemonNameWithAffix: getPokemonNameWithAffix(source),
}),
);
} }
} }
@ -511,7 +515,12 @@ export class NoCritTag extends ArenaTag {
/** Queues a message upon removing this effect from the field */ /** Queues a message upon removing this effect from the field */
onRemove(_arena: Arena): void { onRemove(_arena: Arena): void {
const source = globalScene.getPokemonById(this.sourceId!); // TODO: is this bang correct? const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for NoCritTag on remove message; id: ${this.sourceId}`);
return;
}
globalScene.phaseManager.queueMessage( globalScene.phaseManager.queueMessage(
i18next.t("arenaTag:noCritOnRemove", { i18next.t("arenaTag:noCritOnRemove", {
pokemonNameWithAffix: getPokemonNameWithAffix(source ?? undefined), pokemonNameWithAffix: getPokemonNameWithAffix(source ?? undefined),
@ -522,7 +531,7 @@ export class NoCritTag extends ArenaTag {
} }
/** /**
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Wish_(move) Wish}. * Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Wish_(move) | Wish}.
* Heals the Pokémon in the user's position the turn after Wish is used. * Heals the Pokémon in the user's position the turn after Wish is used.
*/ */
class WishTag extends ArenaTag { class WishTag extends ArenaTag {
@ -535,18 +544,20 @@ class WishTag extends ArenaTag {
} }
onAdd(_arena: Arena): void { onAdd(_arena: Arena): void {
if (this.sourceId) { const source = this.getSourcePokemon();
const user = globalScene.getPokemonById(this.sourceId); if (!source) {
if (user) { console.warn(`Failed to get source Pokemon for WishTag on add message; id: ${this.sourceId}`);
this.battlerIndex = user.getBattlerIndex(); return;
this.triggerMessage = i18next.t("arenaTag:wishTagOnAdd", {
pokemonNameWithAffix: getPokemonNameWithAffix(user),
});
this.healHp = toDmgValue(user.getMaxHp() / 2);
} else {
console.warn("Failed to get source for WishTag onAdd");
}
} }
super.onAdd(_arena);
this.healHp = toDmgValue(source.getMaxHp() / 2);
globalScene.phaseManager.queueMessage(
i18next.t("arenaTag:wishTagOnAdd", {
pokemonNameWithAffix: getPokemonNameWithAffix(source),
}),
);
} }
onRemove(_arena: Arena): void { onRemove(_arena: Arena): void {
@ -741,15 +752,23 @@ class SpikesTag extends ArenaTrapTag {
onAdd(arena: Arena, quiet = false): void { onAdd(arena: Arena, quiet = false): void {
super.onAdd(arena); super.onAdd(arena);
const source = this.sourceId ? globalScene.getPokemonById(this.sourceId) : null; // We assume `quiet=true` means "just add the bloody tag no questions asked"
if (!quiet && source) { if (quiet) {
globalScene.phaseManager.queueMessage( return;
i18next.t("arenaTag:spikesOnAdd", {
moveName: this.getMoveName(),
opponentDesc: source.getOpponentDescriptor(),
}),
);
} }
const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for SpikesTag on add message; id: ${this.sourceId}`);
return;
}
globalScene.phaseManager.queueMessage(
i18next.t("arenaTag:spikesOnAdd", {
moveName: this.getMoveName(),
opponentDesc: source.getOpponentDescriptor(),
}),
);
} }
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean { override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
@ -758,7 +777,7 @@ class SpikesTag extends ArenaTrapTag {
} }
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelled); applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled });
if (simulated || cancelled.value) { if (simulated || cancelled.value) {
return !cancelled.value; return !cancelled.value;
} }
@ -794,15 +813,23 @@ class ToxicSpikesTag extends ArenaTrapTag {
onAdd(arena: Arena, quiet = false): void { onAdd(arena: Arena, quiet = false): void {
super.onAdd(arena); super.onAdd(arena);
const source = this.sourceId ? globalScene.getPokemonById(this.sourceId) : null; if (quiet) {
if (!quiet && source) { // We assume `quiet=true` means "just add the bloody tag no questions asked"
globalScene.phaseManager.queueMessage( return;
i18next.t("arenaTag:toxicSpikesOnAdd", {
moveName: this.getMoveName(),
opponentDesc: source.getOpponentDescriptor(),
}),
);
} }
const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for ToxicSpikesTag on add message; id: ${this.sourceId}`);
return;
}
globalScene.phaseManager.queueMessage(
i18next.t("arenaTag:toxicSpikesOnAdd", {
moveName: this.getMoveName(),
opponentDesc: source.getOpponentDescriptor(),
}),
);
} }
onRemove(arena: Arena): void { onRemove(arena: Arena): void {
@ -905,7 +932,11 @@ class StealthRockTag extends ArenaTrapTag {
onAdd(arena: Arena, quiet = false): void { onAdd(arena: Arena, quiet = false): void {
super.onAdd(arena); super.onAdd(arena);
const source = this.sourceId ? globalScene.getPokemonById(this.sourceId) : null; if (quiet) {
return;
}
const source = this.getSourcePokemon();
if (!quiet && source) { if (!quiet && source) {
globalScene.phaseManager.queueMessage( globalScene.phaseManager.queueMessage(
i18next.t("arenaTag:stealthRockOnAdd", { i18next.t("arenaTag:stealthRockOnAdd", {
@ -946,7 +977,7 @@ class StealthRockTag extends ArenaTrapTag {
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean { override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelled); applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled });
if (cancelled.value) { if (cancelled.value) {
return false; return false;
} }
@ -989,21 +1020,35 @@ class StickyWebTag extends ArenaTrapTag {
onAdd(arena: Arena, quiet = false): void { onAdd(arena: Arena, quiet = false): void {
super.onAdd(arena); super.onAdd(arena);
const source = this.sourceId ? globalScene.getPokemonById(this.sourceId) : null;
if (!quiet && source) { // We assume `quiet=true` means "just add the bloody tag no questions asked"
globalScene.phaseManager.queueMessage( if (quiet) {
i18next.t("arenaTag:stickyWebOnAdd", { return;
moveName: this.getMoveName(),
opponentDesc: source.getOpponentDescriptor(),
}),
);
} }
const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for SpikesTag on add message; id: ${this.sourceId}`);
return;
}
globalScene.phaseManager.queueMessage(
i18next.t("arenaTag:stickyWebOnAdd", {
moveName: this.getMoveName(),
opponentDesc: source.getOpponentDescriptor(),
}),
);
} }
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean { override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
if (pokemon.isGrounded()) { if (pokemon.isGrounded()) {
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyAbAttrs("ProtectStatAbAttr", pokemon, cancelled); applyAbAttrs("ProtectStatAbAttr", {
pokemon,
cancelled,
stat: Stat.SPD,
stages: -1,
});
if (simulated) { if (simulated) {
return !cancelled.value; return !cancelled.value;
@ -1061,14 +1106,20 @@ export class TrickRoomTag extends ArenaTag {
} }
onAdd(_arena: Arena): void { onAdd(_arena: Arena): void {
const source = this.sourceId ? globalScene.getPokemonById(this.sourceId) : null; super.onAdd(_arena);
if (source) {
globalScene.phaseManager.queueMessage( const source = this.getSourcePokemon();
i18next.t("arenaTag:trickRoomOnAdd", { if (!source) {
pokemonNameWithAffix: getPokemonNameWithAffix(source), console.warn(`Failed to get source Pokemon for TrickRoomTag on add message; id: ${this.sourceId}`);
}), return;
);
} }
globalScene.phaseManager.queueMessage(
i18next.t("arenaTag:trickRoomOnAdd", {
moveName: this.getMoveName(),
opponentDesc: source.getOpponentDescriptor(),
}),
);
} }
onRemove(_arena: Arena): void { onRemove(_arena: Arena): void {
@ -1115,6 +1166,13 @@ class TailwindTag extends ArenaTag {
} }
onAdd(_arena: Arena, quiet = false): void { onAdd(_arena: Arena, quiet = false): void {
const source = this.getSourcePokemon();
if (!source) {
return;
}
super.onAdd(_arena, quiet);
if (!quiet) { if (!quiet) {
globalScene.phaseManager.queueMessage( globalScene.phaseManager.queueMessage(
i18next.t( i18next.t(
@ -1123,15 +1181,14 @@ class TailwindTag extends ArenaTag {
); );
} }
const source = globalScene.getPokemonById(this.sourceId!); //TODO: this bang is questionable! const field = source.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField();
const party = (source?.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField()) ?? [];
const phaseManager = globalScene.phaseManager;
for (const pokemon of party) { for (const pokemon of field) {
// Apply the CHARGED tag to party members with the WIND_POWER ability // Apply the CHARGED tag to party members with the WIND_POWER ability
// TODO: This should not be handled here
if (pokemon.hasAbility(AbilityId.WIND_POWER) && !pokemon.getTag(BattlerTagType.CHARGED)) { if (pokemon.hasAbility(AbilityId.WIND_POWER) && !pokemon.getTag(BattlerTagType.CHARGED)) {
pokemon.addTag(BattlerTagType.CHARGED); pokemon.addTag(BattlerTagType.CHARGED);
phaseManager.queueMessage( globalScene.phaseManager.queueMessage(
i18next.t("abilityTriggers:windPowerCharged", { i18next.t("abilityTriggers:windPowerCharged", {
pokemonName: getPokemonNameWithAffix(pokemon), pokemonName: getPokemonNameWithAffix(pokemon),
moveName: this.getMoveName(), moveName: this.getMoveName(),
@ -1142,9 +1199,16 @@ class TailwindTag extends ArenaTag {
// Raise attack by one stage if party member has WIND_RIDER ability // Raise attack by one stage if party member has WIND_RIDER ability
// TODO: Ability displays should be handled by the ability // TODO: Ability displays should be handled by the ability
if (pokemon.hasAbility(AbilityId.WIND_RIDER)) { if (pokemon.hasAbility(AbilityId.WIND_RIDER)) {
phaseManager.queueAbilityDisplay(pokemon, false, true); globalScene.phaseManager.queueAbilityDisplay(pokemon, false, true);
phaseManager.unshiftNew("StatStageChangePhase", pokemon.getBattlerIndex(), true, [Stat.ATK], 1, true); globalScene.phaseManager.unshiftNew(
phaseManager.queueAbilityDisplay(pokemon, false, false); "StatStageChangePhase",
pokemon.getBattlerIndex(),
true,
[Stat.ATK],
1,
true,
);
globalScene.phaseManager.queueAbilityDisplay(pokemon, false, false);
} }
} }
} }
@ -1216,24 +1280,26 @@ class ImprisonTag extends ArenaTrapTag {
} }
/** /**
* This function applies the effects of Imprison to the opposing Pokemon already present on the field. * Apply the effects of Imprison to all opposing on-field Pokemon.
* @param arena
*/ */
override onAdd() { override onAdd() {
const source = this.getSourcePokemon(); const source = this.getSourcePokemon();
if (source) { if (!source) {
const party = this.getAffectedPokemon(); return;
party?.forEach((p: Pokemon) => {
if (p.isAllowedInBattle()) {
p.addTag(BattlerTagType.IMPRISON, 1, MoveId.IMPRISON, this.sourceId);
}
});
globalScene.phaseManager.queueMessage(
i18next.t("battlerTags:imprisonOnAdd", {
pokemonNameWithAffix: getPokemonNameWithAffix(source),
}),
);
} }
const party = this.getAffectedPokemon();
party.forEach(p => {
if (p.isAllowedInBattle()) {
p.addTag(BattlerTagType.IMPRISON, 1, MoveId.IMPRISON, this.sourceId);
}
});
globalScene.phaseManager.queueMessage(
i18next.t("battlerTags:imprisonOnAdd", {
pokemonNameWithAffix: getPokemonNameWithAffix(source),
}),
);
} }
/** /**
@ -1243,7 +1309,7 @@ class ImprisonTag extends ArenaTrapTag {
*/ */
override lapse(): boolean { override lapse(): boolean {
const source = this.getSourcePokemon(); const source = this.getSourcePokemon();
return source ? source.isActive(true) : false; return !!source?.isActive(true);
} }
/** /**
@ -1265,9 +1331,7 @@ class ImprisonTag extends ArenaTrapTag {
*/ */
override onRemove(): void { override onRemove(): void {
const party = this.getAffectedPokemon(); const party = this.getAffectedPokemon();
party?.forEach((p: Pokemon) => { party.forEach(p => p.removeTag(BattlerTagType.IMPRISON));
p.removeTag(BattlerTagType.IMPRISON);
});
} }
} }
@ -1416,7 +1480,9 @@ export class SuppressAbilitiesTag extends ArenaTag {
for (const fieldPokemon of globalScene.getField(true)) { for (const fieldPokemon of globalScene.getField(true)) {
if (fieldPokemon && fieldPokemon.id !== pokemon.id) { if (fieldPokemon && fieldPokemon.id !== pokemon.id) {
[true, false].forEach(passive => applyOnLoseAbAttrs(fieldPokemon, passive)); // TODO: investigate whether we can just remove the foreach and call `applyAbAttrs` directly, providing
// the appropriate attributes (preLEaveField and IllusionBreak)
[true, false].forEach(passive => applyOnLoseAbAttrs({ pokemon: fieldPokemon, passive }));
} }
} }
} }
@ -1438,7 +1504,10 @@ export class SuppressAbilitiesTag extends ArenaTag {
const setter = globalScene const setter = globalScene
.getField() .getField()
.filter(p => p?.hasAbilityWithAttr("PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr", false))[0]; .filter(p => p?.hasAbilityWithAttr("PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr", false))[0];
applyOnGainAbAttrs(setter, setter.getAbility().hasAttr("PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr")); applyOnGainAbAttrs({
pokemon: setter,
passive: setter.getAbility().hasAttr("PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr"),
});
} }
} }
@ -1451,7 +1520,7 @@ export class SuppressAbilitiesTag extends ArenaTag {
for (const pokemon of globalScene.getField(true)) { for (const pokemon of globalScene.getField(true)) {
// There is only one pokemon with this attr on the field on removal, so its abilities are already active // There is only one pokemon with this attr on the field on removal, so its abilities are already active
if (pokemon && !pokemon.hasAbilityWithAttr("PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr", false)) { if (pokemon && !pokemon.hasAbilityWithAttr("PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr", false)) {
[true, false].forEach(passive => applyOnGainAbAttrs(pokemon, passive)); [true, false].forEach(passive => applyOnGainAbAttrs({ pokemon, passive }));
} }
} }
} }

View File

@ -111,7 +111,7 @@ export class BattlerTag {
* @returns The source {@linkcode Pokemon}, or `null` if none is found * @returns The source {@linkcode Pokemon}, or `null` if none is found
*/ */
public getSourcePokemon(): Pokemon | null { public getSourcePokemon(): Pokemon | null {
return this.sourceId ? globalScene.getPokemonById(this.sourceId) : null; return globalScene.getPokemonById(this.sourceId);
} }
} }
@ -540,9 +540,13 @@ export class TrappedTag extends BattlerTag {
} }
canAdd(pokemon: Pokemon): boolean { canAdd(pokemon: Pokemon): boolean {
const source = globalScene.getPokemonById(this.sourceId!)!; const source = this.getSourcePokemon();
const move = allMoves[this.sourceMove]; if (!source) {
console.warn(`Failed to get source Pokemon for TrappedTag canAdd; id: ${this.sourceId}`);
return false;
}
const move = allMoves[this.sourceMove];
const isGhost = pokemon.isOfType(PokemonType.GHOST); const isGhost = pokemon.isOfType(PokemonType.GHOST);
const isTrapped = pokemon.getTag(TrappedTag); const isTrapped = pokemon.getTag(TrappedTag);
const hasSubstitute = move.hitsSubstitute(source, pokemon); const hasSubstitute = move.hitsSubstitute(source, pokemon);
@ -621,7 +625,7 @@ export class FlinchedTag extends BattlerTag {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
}), }),
); );
applyAbAttrs("FlinchEffectAbAttr", pokemon, null); applyAbAttrs("FlinchEffectAbAttr", { pokemon });
return true; return true;
} }
@ -763,12 +767,20 @@ export class DestinyBondTag extends BattlerTag {
if (lapseType !== BattlerTagLapseType.CUSTOM) { if (lapseType !== BattlerTagLapseType.CUSTOM) {
return super.lapse(pokemon, lapseType); return super.lapse(pokemon, lapseType);
} }
const source = this.sourceId ? globalScene.getPokemonById(this.sourceId) : null;
if (!source?.isFainted()) { const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for DestinyBondTag lapse; id: ${this.sourceId}`);
return false;
}
// Destiny bond stays active until the user faints
if (!source.isFainted()) {
return true; return true;
} }
if (source?.getAlly() === pokemon) { // Don't kill allies or opposing bosses.
if (source.getAlly() === pokemon) {
return false; return false;
} }
@ -781,6 +793,7 @@ export class DestinyBondTag extends BattlerTag {
return false; return false;
} }
// Drag the foe down with the user
globalScene.phaseManager.queueMessage( globalScene.phaseManager.queueMessage(
i18next.t("battlerTags:destinyBondLapse", { i18next.t("battlerTags:destinyBondLapse", {
pokemonNameWithAffix: getPokemonNameWithAffix(source), pokemonNameWithAffix: getPokemonNameWithAffix(source),
@ -798,17 +811,13 @@ export class InfatuatedTag extends BattlerTag {
} }
canAdd(pokemon: Pokemon): boolean { canAdd(pokemon: Pokemon): boolean {
if (this.sourceId) { const source = this.getSourcePokemon();
const pkm = globalScene.getPokemonById(this.sourceId); if (!source) {
console.warn(`Failed to get source Pokemon for InfatuatedTag canAdd; id: ${this.sourceId}`);
if (pkm) {
return pokemon.isOppositeGender(pkm);
}
console.warn("canAdd: this.sourceId is not a valid pokemon id!", this.sourceId);
return false; return false;
} }
console.warn("canAdd: this.sourceId is undefined");
return false; return pokemon.isOppositeGender(source);
} }
onAdd(pokemon: Pokemon): void { onAdd(pokemon: Pokemon): void {
@ -817,7 +826,7 @@ export class InfatuatedTag extends BattlerTag {
globalScene.phaseManager.queueMessage( globalScene.phaseManager.queueMessage(
i18next.t("battlerTags:infatuatedOnAdd", { i18next.t("battlerTags:infatuatedOnAdd", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
sourcePokemonName: getPokemonNameWithAffix(globalScene.getPokemonById(this.sourceId!) ?? undefined), // TODO: is that bang correct? sourcePokemonName: getPokemonNameWithAffix(this.getSourcePokemon()!), // Tag not added + console warns if no source
}), }),
); );
} }
@ -835,28 +844,36 @@ export class InfatuatedTag extends BattlerTag {
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
const ret = lapseType !== BattlerTagLapseType.CUSTOM || super.lapse(pokemon, lapseType); const ret = lapseType !== BattlerTagLapseType.CUSTOM || super.lapse(pokemon, lapseType);
const phaseManager = globalScene.phaseManager; if (!ret) {
return false;
if (ret) {
phaseManager.queueMessage(
i18next.t("battlerTags:infatuatedLapse", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
sourcePokemonName: getPokemonNameWithAffix(globalScene.getPokemonById(this.sourceId!) ?? undefined), // TODO: is that bang correct?
}),
);
phaseManager.unshiftNew("CommonAnimPhase", pokemon.getBattlerIndex(), undefined, CommonAnim.ATTRACT);
if (pokemon.randBattleSeedInt(2)) {
phaseManager.queueMessage(
i18next.t("battlerTags:infatuatedLapseImmobilize", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
}),
);
(phaseManager.getCurrentPhase() as MovePhase).cancel();
}
} }
return ret; const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for InfatuatedTag lapse; id: ${this.sourceId}`);
return false;
}
const phaseManager = globalScene.phaseManager;
phaseManager.queueMessage(
i18next.t("battlerTags:infatuatedLapse", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
sourcePokemonName: getPokemonNameWithAffix(globalScene.getPokemonById(this.sourceId!) ?? undefined), // TODO: is that bang correct?
}),
);
phaseManager.unshiftNew("CommonAnimPhase", pokemon.getBattlerIndex(), undefined, CommonAnim.ATTRACT);
// 50% chance to disrupt the target's action
if (pokemon.randBattleSeedInt(2)) {
phaseManager.queueMessage(
i18next.t("battlerTags:infatuatedLapseImmobilize", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
}),
);
(phaseManager.getCurrentPhase() as MovePhase).cancel();
}
return true;
} }
onRemove(pokemon: Pokemon): void { onRemove(pokemon: Pokemon): void {
@ -899,6 +916,12 @@ export class SeedTag extends BattlerTag {
} }
onAdd(pokemon: Pokemon): void { onAdd(pokemon: Pokemon): void {
const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for SeedTag onAdd; id: ${this.sourceId}`);
return;
}
super.onAdd(pokemon); super.onAdd(pokemon);
globalScene.phaseManager.queueMessage( globalScene.phaseManager.queueMessage(
@ -906,47 +929,51 @@ export class SeedTag extends BattlerTag {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
}), }),
); );
this.sourceIndex = globalScene.getPokemonById(this.sourceId!)!.getBattlerIndex(); // TODO: are those bangs correct? this.sourceIndex = source.getBattlerIndex();
} }
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
const ret = lapseType !== BattlerTagLapseType.CUSTOM || super.lapse(pokemon, lapseType); const ret = lapseType !== BattlerTagLapseType.CUSTOM || super.lapse(pokemon, lapseType);
if (ret) { if (!ret) {
const source = pokemon.getOpponents().find(o => o.getBattlerIndex() === this.sourceIndex); return false;
if (source) {
const cancelled = new BooleanHolder(false);
applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelled);
if (!cancelled.value) {
globalScene.phaseManager.unshiftNew(
"CommonAnimPhase",
source.getBattlerIndex(),
pokemon.getBattlerIndex(),
CommonAnim.LEECH_SEED,
);
const damage = pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8), { result: HitResult.INDIRECT });
const reverseDrain = pokemon.hasAbilityWithAttr("ReverseDrainAbAttr", false);
globalScene.phaseManager.unshiftNew(
"PokemonHealPhase",
source.getBattlerIndex(),
!reverseDrain ? damage : damage * -1,
!reverseDrain
? i18next.t("battlerTags:seededLapse", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
})
: i18next.t("battlerTags:seededLapseShed", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
}),
false,
true,
);
}
}
} }
return ret; // Check which opponent to restore HP to
const source = pokemon.getOpponents().find(o => o.getBattlerIndex() === this.sourceIndex);
if (!source) {
console.warn(`Failed to get source Pokemon for SeedTag lapse; id: ${this.sourceId}`);
return false;
}
const cancelled = new BooleanHolder(false);
applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled });
if (cancelled.value) {
return true;
}
globalScene.phaseManager.unshiftNew(
"CommonAnimPhase",
source.getBattlerIndex(),
pokemon.getBattlerIndex(),
CommonAnim.LEECH_SEED,
);
// Damage the target and restore our HP (or take damage in the case of liquid ooze)
const damage = pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8), { result: HitResult.INDIRECT });
const reverseDrain = pokemon.hasAbilityWithAttr("ReverseDrainAbAttr", false);
globalScene.phaseManager.unshiftNew(
"PokemonHealPhase",
source.getBattlerIndex(),
reverseDrain ? -damage : damage,
i18next.t(reverseDrain ? "battlerTags:seededLapseShed" : "battlerTags:seededLapse", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
}),
false,
true,
);
return true;
} }
getDescriptor(): string { getDescriptor(): string {
@ -1006,7 +1033,7 @@ export class PowderTag extends BattlerTag {
globalScene.phaseManager.unshiftNew("CommonAnimPhase", idx, idx, CommonAnim.POWDER); globalScene.phaseManager.unshiftNew("CommonAnimPhase", idx, idx, CommonAnim.POWDER);
const cancelDamage = new BooleanHolder(false); const cancelDamage = new BooleanHolder(false);
applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelDamage); applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled: cancelDamage });
if (!cancelDamage.value) { if (!cancelDamage.value) {
pokemon.damageAndUpdate(Math.floor(pokemon.getMaxHp() / 4), { result: HitResult.INDIRECT }); pokemon.damageAndUpdate(Math.floor(pokemon.getMaxHp() / 4), { result: HitResult.INDIRECT });
} }
@ -1056,7 +1083,7 @@ export class NightmareTag extends BattlerTag {
phaseManager.unshiftNew("CommonAnimPhase", pokemon.getBattlerIndex(), undefined, CommonAnim.CURSE); // TODO: Update animation type phaseManager.unshiftNew("CommonAnimPhase", pokemon.getBattlerIndex(), undefined, CommonAnim.CURSE); // TODO: Update animation type
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelled); applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled });
if (!cancelled.value) { if (!cancelled.value) {
pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 4), { result: HitResult.INDIRECT }); pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 4), { result: HitResult.INDIRECT });
@ -1195,9 +1222,15 @@ export class HelpingHandTag extends BattlerTag {
} }
onAdd(pokemon: Pokemon): void { onAdd(pokemon: Pokemon): void {
const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for HelpingHandTag onAdd; id: ${this.sourceId}`);
return;
}
globalScene.phaseManager.queueMessage( globalScene.phaseManager.queueMessage(
i18next.t("battlerTags:helpingHandOnAdd", { i18next.t("battlerTags:helpingHandOnAdd", {
pokemonNameWithAffix: getPokemonNameWithAffix(globalScene.getPokemonById(this.sourceId!) ?? undefined), // TODO: is that bang correct? pokemonNameWithAffix: getPokemonNameWithAffix(source),
pokemonName: getPokemonNameWithAffix(pokemon), pokemonName: getPokemonNameWithAffix(pokemon),
}), }),
); );
@ -1219,9 +1252,7 @@ export class IngrainTag extends TrappedTag {
* @returns boolean True if the tag can be added, false otherwise * @returns boolean True if the tag can be added, false otherwise
*/ */
canAdd(pokemon: Pokemon): boolean { canAdd(pokemon: Pokemon): boolean {
const isTrapped = pokemon.getTag(BattlerTagType.TRAPPED); return !pokemon.getTag(BattlerTagType.TRAPPED);
return !isTrapped;
} }
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
@ -1409,7 +1440,7 @@ export abstract class DamagingTrapTag extends TrappedTag {
phaseManager.unshiftNew("CommonAnimPhase", pokemon.getBattlerIndex(), undefined, this.commonAnim); phaseManager.unshiftNew("CommonAnimPhase", pokemon.getBattlerIndex(), undefined, this.commonAnim);
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelled); applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled });
if (!cancelled.value) { if (!cancelled.value) {
pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8), { result: HitResult.INDIRECT }); pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8), { result: HitResult.INDIRECT });
@ -1420,15 +1451,22 @@ export abstract class DamagingTrapTag extends TrappedTag {
} }
} }
// TODO: Condense all these tags into 1 singular tag with a modified message func
export class BindTag extends DamagingTrapTag { export class BindTag extends DamagingTrapTag {
constructor(turnCount: number, sourceId: number) { constructor(turnCount: number, sourceId: number) {
super(BattlerTagType.BIND, CommonAnim.BIND, turnCount, MoveId.BIND, sourceId); super(BattlerTagType.BIND, CommonAnim.BIND, turnCount, MoveId.BIND, sourceId);
} }
getTrapMessage(pokemon: Pokemon): string { getTrapMessage(pokemon: Pokemon): string {
const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for BindTag getTrapMessage; id: ${this.sourceId}`);
return "ERROR - CHECK CONSOLE AND REPORT";
}
return i18next.t("battlerTags:bindOnTrap", { return i18next.t("battlerTags:bindOnTrap", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
sourcePokemonName: getPokemonNameWithAffix(globalScene.getPokemonById(this.sourceId!) ?? undefined), // TODO: is that bang correct? sourcePokemonName: getPokemonNameWithAffix(source),
moveName: this.getMoveName(), moveName: this.getMoveName(),
}); });
} }
@ -1440,9 +1478,16 @@ export class WrapTag extends DamagingTrapTag {
} }
getTrapMessage(pokemon: Pokemon): string { getTrapMessage(pokemon: Pokemon): string {
const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for WrapTag getTrapMessage; id: ${this.sourceId}`);
return "ERROR - CHECK CONSOLE AND REPORT";
}
return i18next.t("battlerTags:wrapOnTrap", { return i18next.t("battlerTags:wrapOnTrap", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
sourcePokemonName: getPokemonNameWithAffix(globalScene.getPokemonById(this.sourceId!) ?? undefined), // TODO: is that bang correct? sourcePokemonName: getPokemonNameWithAffix(source),
moveName: this.getMoveName(),
}); });
} }
} }
@ -1473,8 +1518,14 @@ export class ClampTag extends DamagingTrapTag {
} }
getTrapMessage(pokemon: Pokemon): string { getTrapMessage(pokemon: Pokemon): string {
const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for ClampTag getTrapMessage; id: ${this.sourceId}`);
return "ERROR - CHECK CONSOLE AND REPORT ASAP";
}
return i18next.t("battlerTags:clampOnTrap", { return i18next.t("battlerTags:clampOnTrap", {
sourcePokemonNameWithAffix: getPokemonNameWithAffix(globalScene.getPokemonById(this.sourceId!) ?? undefined), // TODO: is that bang correct? sourcePokemonNameWithAffix: getPokemonNameWithAffix(source),
pokemonName: getPokemonNameWithAffix(pokemon), pokemonName: getPokemonNameWithAffix(pokemon),
}); });
} }
@ -1523,9 +1574,15 @@ export class ThunderCageTag extends DamagingTrapTag {
} }
getTrapMessage(pokemon: Pokemon): string { getTrapMessage(pokemon: Pokemon): string {
const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for ThunderCageTag getTrapMessage; id: ${this.sourceId}`);
return "ERROR - PLEASE REPORT ASAP";
}
return i18next.t("battlerTags:thunderCageOnTrap", { return i18next.t("battlerTags:thunderCageOnTrap", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
sourcePokemonNameWithAffix: getPokemonNameWithAffix(globalScene.getPokemonById(this.sourceId!) ?? undefined), // TODO: is that bang correct? sourcePokemonNameWithAffix: getPokemonNameWithAffix(source),
}); });
} }
} }
@ -1536,9 +1593,15 @@ export class InfestationTag extends DamagingTrapTag {
} }
getTrapMessage(pokemon: Pokemon): string { getTrapMessage(pokemon: Pokemon): string {
const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for InfestationTag getTrapMessage; id: ${this.sourceId}`);
return "ERROR - CHECK CONSOLE AND REPORT";
}
return i18next.t("battlerTags:infestationOnTrap", { return i18next.t("battlerTags:infestationOnTrap", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
sourcePokemonNameWithAffix: getPokemonNameWithAffix(globalScene.getPokemonById(this.sourceId!) ?? undefined), // TODO: is that bang correct? sourcePokemonNameWithAffix: getPokemonNameWithAffix(source),
}); });
} }
} }
@ -1642,7 +1705,7 @@ export class ContactDamageProtectedTag extends ContactProtectedTag {
*/ */
override onContact(attacker: Pokemon, user: Pokemon): void { override onContact(attacker: Pokemon, user: Pokemon): void {
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyAbAttrs("BlockNonDirectDamageAbAttr", user, cancelled); applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon: user, cancelled });
if (!cancelled.value) { if (!cancelled.value) {
attacker.damageAndUpdate(toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio)), { attacker.damageAndUpdate(toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio)), {
result: HitResult.INDIRECT, result: HitResult.INDIRECT,
@ -2221,14 +2284,19 @@ export class SaltCuredTag extends BattlerTag {
} }
onAdd(pokemon: Pokemon): void { onAdd(pokemon: Pokemon): void {
super.onAdd(pokemon); const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for SaltCureTag onAdd; id: ${this.sourceId}`);
return;
}
super.onAdd(pokemon);
globalScene.phaseManager.queueMessage( globalScene.phaseManager.queueMessage(
i18next.t("battlerTags:saltCuredOnAdd", { i18next.t("battlerTags:saltCuredOnAdd", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
}), }),
); );
this.sourceIndex = globalScene.getPokemonById(this.sourceId!)!.getBattlerIndex(); // TODO: are those bangs correct? this.sourceIndex = source.getBattlerIndex();
} }
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
@ -2243,7 +2311,7 @@ export class SaltCuredTag extends BattlerTag {
); );
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelled); applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled });
if (!cancelled.value) { if (!cancelled.value) {
const pokemonSteelOrWater = pokemon.isOfType(PokemonType.STEEL) || pokemon.isOfType(PokemonType.WATER); const pokemonSteelOrWater = pokemon.isOfType(PokemonType.STEEL) || pokemon.isOfType(PokemonType.WATER);
@ -2281,8 +2349,14 @@ export class CursedTag extends BattlerTag {
} }
onAdd(pokemon: Pokemon): void { onAdd(pokemon: Pokemon): void {
const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for CursedTag onAdd; id: ${this.sourceId}`);
return;
}
super.onAdd(pokemon); super.onAdd(pokemon);
this.sourceIndex = globalScene.getPokemonById(this.sourceId!)!.getBattlerIndex(); // TODO: are those bangs correct? this.sourceIndex = source.getBattlerIndex();
} }
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
@ -2297,7 +2371,7 @@ export class CursedTag extends BattlerTag {
); );
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelled); applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled });
if (!cancelled.value) { if (!cancelled.value) {
pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 4), { result: HitResult.INDIRECT }); pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 4), { result: HitResult.INDIRECT });
@ -2632,7 +2706,7 @@ export class GulpMissileTag extends BattlerTag {
} }
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyAbAttrs("BlockNonDirectDamageAbAttr", attacker, cancelled); applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon: attacker, cancelled });
if (!cancelled.value) { if (!cancelled.value) {
attacker.damageAndUpdate(Math.max(1, Math.floor(attacker.getMaxHp() / 4)), { result: HitResult.INDIRECT }); attacker.damageAndUpdate(Math.max(1, Math.floor(attacker.getMaxHp() / 4)), { result: HitResult.INDIRECT });
@ -2902,7 +2976,13 @@ export class SubstituteTag extends BattlerTag {
/** Sets the Substitute's HP and queues an on-add battle animation that initializes the Substitute's sprite. */ /** Sets the Substitute's HP and queues an on-add battle animation that initializes the Substitute's sprite. */
onAdd(pokemon: Pokemon): void { onAdd(pokemon: Pokemon): void {
this.hp = Math.floor(globalScene.getPokemonById(this.sourceId!)!.getMaxHp() / 4); const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for SubstituteTag onAdd; id: ${this.sourceId}`);
return;
}
this.hp = Math.floor(source.getMaxHp() / 4);
this.sourceInFocus = false; this.sourceInFocus = false;
// Queue battle animation and message // Queue battle animation and message
@ -3021,14 +3101,7 @@ export class MysteryEncounterPostSummonTag extends BattlerTag {
const ret = super.lapse(pokemon, lapseType); const ret = super.lapse(pokemon, lapseType);
if (lapseType === BattlerTagLapseType.CUSTOM) { if (lapseType === BattlerTagLapseType.CUSTOM) {
const cancelled = new BooleanHolder(false); pokemon.mysteryEncounterBattleEffects?.(pokemon);
applyAbAttrs("ProtectStatAbAttr", pokemon, cancelled);
applyAbAttrs("ConditionalUserFieldProtectStatAbAttr", pokemon, cancelled, false, pokemon);
if (!cancelled.value) {
if (pokemon.mysteryEncounterBattleEffects) {
pokemon.mysteryEncounterBattleEffects(pokemon);
}
}
} }
return ret; return ret;
@ -3182,13 +3255,14 @@ export class ImprisonTag extends MoveRestrictionBattlerTag {
*/ */
public override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { public override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
const source = this.getSourcePokemon(); const source = this.getSourcePokemon();
if (source) { if (!source) {
if (lapseType === BattlerTagLapseType.PRE_MOVE) { console.warn(`Failed to get source Pokemon for ImprisonTag lapse; id: ${this.sourceId}`);
return super.lapse(pokemon, lapseType) && source.isActive(true); return false;
}
return source.isActive(true);
} }
return false; if (lapseType === BattlerTagLapseType.PRE_MOVE) {
return super.lapse(pokemon, lapseType) && source.isActive(true);
}
return source.isActive(true);
} }
/** /**
@ -3248,12 +3322,20 @@ export class SyrupBombTag extends BattlerTag {
* Applies the single-stage speed down to the target Pokemon and decrements the tag's turn count * Applies the single-stage speed down to the target Pokemon and decrements the tag's turn count
* @param pokemon - The target {@linkcode Pokemon} * @param pokemon - The target {@linkcode Pokemon}
* @param _lapseType - N/A * @param _lapseType - N/A
* @returns `true` if the `turnCount` is still greater than `0`; `false` if the `turnCount` is `0` or the target or source Pokemon has been removed from the field * @returns Whether the tag should persist (`turnsRemaining > 0` and source still on field)
*/ */
override lapse(pokemon: Pokemon, _lapseType: BattlerTagLapseType): boolean { override lapse(pokemon: Pokemon, _lapseType: BattlerTagLapseType): boolean {
if (this.sourceId && !globalScene.getPokemonById(this.sourceId)?.isActive(true)) { const source = this.getSourcePokemon();
if (!source) {
console.warn(`Failed to get source Pokemon for SyrupBombTag lapse; id: ${this.sourceId}`);
return false; return false;
} }
// Syrup bomb clears immediately if source leaves field/faints
if (!source.isActive(true)) {
return false;
}
// Custom message in lieu of an animation in mainline // Custom message in lieu of an animation in mainline
globalScene.phaseManager.queueMessage( globalScene.phaseManager.queueMessage(
i18next.t("battlerTags:syrupBombLapse", { i18next.t("battlerTags:syrupBombLapse", {
@ -3270,7 +3352,7 @@ export class SyrupBombTag extends BattlerTag {
false, false,
true, true,
); );
return --this.turnCount > 0; return super.lapse(pokemon, _lapseType);
} }
} }

View File

@ -35,28 +35,28 @@ export function getBerryPredicate(berryType: BerryType): BerryPredicate {
case BerryType.APICOT: case BerryType.APICOT:
case BerryType.SALAC: case BerryType.SALAC:
return (pokemon: Pokemon) => { return (pokemon: Pokemon) => {
const threshold = new NumberHolder(0.25); const hpRatioReq = new NumberHolder(0.25);
// Offset BerryType such that LIECHI -> Stat.ATK = 1, GANLON -> Stat.DEF = 2, so on and so forth // Offset BerryType such that LIECHI -> Stat.ATK = 1, GANLON -> Stat.DEF = 2, so on and so forth
const stat: BattleStat = berryType - BerryType.ENIGMA; const stat: BattleStat = berryType - BerryType.ENIGMA;
applyAbAttrs("ReduceBerryUseThresholdAbAttr", pokemon, null, false, threshold); applyAbAttrs("ReduceBerryUseThresholdAbAttr", { pokemon, hpRatioReq });
return pokemon.getHpRatio() < threshold.value && pokemon.getStatStage(stat) < 6; return pokemon.getHpRatio() < hpRatioReq.value && pokemon.getStatStage(stat) < 6;
}; };
case BerryType.LANSAT: case BerryType.LANSAT:
return (pokemon: Pokemon) => { return (pokemon: Pokemon) => {
const threshold = new NumberHolder(0.25); const hpRatioReq = new NumberHolder(0.25);
applyAbAttrs("ReduceBerryUseThresholdAbAttr", pokemon, null, false, threshold); applyAbAttrs("ReduceBerryUseThresholdAbAttr", { pokemon, hpRatioReq });
return pokemon.getHpRatio() < 0.25 && !pokemon.getTag(BattlerTagType.CRIT_BOOST); return pokemon.getHpRatio() < 0.25 && !pokemon.getTag(BattlerTagType.CRIT_BOOST);
}; };
case BerryType.STARF: case BerryType.STARF:
return (pokemon: Pokemon) => { return (pokemon: Pokemon) => {
const threshold = new NumberHolder(0.25); const hpRatioReq = new NumberHolder(0.25);
applyAbAttrs("ReduceBerryUseThresholdAbAttr", pokemon, null, false, threshold); applyAbAttrs("ReduceBerryUseThresholdAbAttr", { pokemon, hpRatioReq });
return pokemon.getHpRatio() < 0.25; return pokemon.getHpRatio() < 0.25;
}; };
case BerryType.LEPPA: case BerryType.LEPPA:
return (pokemon: Pokemon) => { return (pokemon: Pokemon) => {
const threshold = new NumberHolder(0.25); const hpRatioReq = new NumberHolder(0.25);
applyAbAttrs("ReduceBerryUseThresholdAbAttr", pokemon, null, false, threshold); applyAbAttrs("ReduceBerryUseThresholdAbAttr", { pokemon, hpRatioReq });
return !!pokemon.getMoveset().find(m => !m.getPpRatio()); return !!pokemon.getMoveset().find(m => !m.getPpRatio());
}; };
} }
@ -72,7 +72,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc {
case BerryType.ENIGMA: case BerryType.ENIGMA:
{ {
const hpHealed = new NumberHolder(toDmgValue(consumer.getMaxHp() / 4)); const hpHealed = new NumberHolder(toDmgValue(consumer.getMaxHp() / 4));
applyAbAttrs("DoubleBerryEffectAbAttr", consumer, null, false, hpHealed); applyAbAttrs("DoubleBerryEffectAbAttr", { pokemon: consumer, effectValue: hpHealed });
globalScene.phaseManager.unshiftNew( globalScene.phaseManager.unshiftNew(
"PokemonHealPhase", "PokemonHealPhase",
consumer.getBattlerIndex(), consumer.getBattlerIndex(),
@ -105,7 +105,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc {
// Offset BerryType such that LIECHI --> Stat.ATK = 1, GANLON --> Stat.DEF = 2, etc etc. // Offset BerryType such that LIECHI --> Stat.ATK = 1, GANLON --> Stat.DEF = 2, etc etc.
const stat: BattleStat = berryType - BerryType.ENIGMA; const stat: BattleStat = berryType - BerryType.ENIGMA;
const statStages = new NumberHolder(1); const statStages = new NumberHolder(1);
applyAbAttrs("DoubleBerryEffectAbAttr", consumer, null, false, statStages); applyAbAttrs("DoubleBerryEffectAbAttr", { pokemon: consumer, effectValue: statStages });
globalScene.phaseManager.unshiftNew( globalScene.phaseManager.unshiftNew(
"StatStageChangePhase", "StatStageChangePhase",
consumer.getBattlerIndex(), consumer.getBattlerIndex(),
@ -126,7 +126,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc {
{ {
const randStat = randSeedInt(Stat.SPD, Stat.ATK); const randStat = randSeedInt(Stat.SPD, Stat.ATK);
const stages = new NumberHolder(2); const stages = new NumberHolder(2);
applyAbAttrs("DoubleBerryEffectAbAttr", consumer, null, false, stages); applyAbAttrs("DoubleBerryEffectAbAttr", { pokemon: consumer, effectValue: stages });
globalScene.phaseManager.unshiftNew( globalScene.phaseManager.unshiftNew(
"StatStageChangePhase", "StatStageChangePhase",
consumer.getBattlerIndex(), consumer.getBattlerIndex(),

View File

@ -33,11 +33,7 @@ import type { ArenaTrapTag } from "../arena-tag";
import { WeakenMoveTypeTag } from "../arena-tag"; import { WeakenMoveTypeTag } from "../arena-tag";
import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagSide } from "#enums/arena-tag-side";
import { import {
applyAbAttrs, applyAbAttrs
applyPostAttackAbAttrs,
applyPostItemLostAbAttrs,
applyPreAttackAbAttrs,
applyPreDefendAbAttrs
} from "../abilities/apply-ab-attrs"; } from "../abilities/apply-ab-attrs";
import { allAbilities, allMoves } from "../data-lists"; import { allAbilities, allMoves } from "../data-lists";
import { import {
@ -89,9 +85,10 @@ import { MoveEffectTrigger } from "#enums/MoveEffectTrigger";
import { MultiHitType } from "#enums/MultiHitType"; import { MultiHitType } from "#enums/MultiHitType";
import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSleepTalkMoves, invalidSketchMoves } from "./invalid-moves"; import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSleepTalkMoves, invalidSketchMoves } from "./invalid-moves";
import { isVirtual, MoveUseMode } from "#enums/move-use-mode"; import { isVirtual, MoveUseMode } from "#enums/move-use-mode";
import { ChargingMove, MoveAttrMap, MoveAttrString, MoveKindString, MoveClassMap } from "#app/@types/move-types"; import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveKindString, MoveClassMap } from "#app/@types/move-types";
import { applyMoveAttrs } from "./apply-attrs"; import { applyMoveAttrs } from "./apply-attrs";
import { frenzyMissFunc, getMoveTargets } from "./move-utils"; import { frenzyMissFunc, getMoveTargets } from "./move-utils";
import { AbAttrBaseParams, AbAttrParamsWithCancel, PreAttackModifyPowerAbAttrParams } from "../abilities/ability";
/** /**
* A function used to conditionally determine execution of a given {@linkcode MoveAttr}. * A function used to conditionally determine execution of a given {@linkcode MoveAttr}.
@ -347,7 +344,7 @@ export default abstract class Move implements Localizable {
const bypassed = new BooleanHolder(false); const bypassed = new BooleanHolder(false);
// TODO: Allow this to be simulated // TODO: Allow this to be simulated
applyAbAttrs("InfiltratorAbAttr", user, null, false, bypassed); applyAbAttrs("InfiltratorAbAttr", {pokemon: user, bypassed});
return !bypassed.value return !bypassed.value
&& !this.hasFlag(MoveFlags.SOUND_BASED) && !this.hasFlag(MoveFlags.SOUND_BASED)
@ -645,7 +642,7 @@ export default abstract class Move implements Localizable {
case MoveFlags.IGNORE_ABILITIES: case MoveFlags.IGNORE_ABILITIES:
if (user.hasAbilityWithAttr("MoveAbilityBypassAbAttr")) { if (user.hasAbilityWithAttr("MoveAbilityBypassAbAttr")) {
const abilityEffectsIgnored = new BooleanHolder(false); const abilityEffectsIgnored = new BooleanHolder(false);
applyAbAttrs("MoveAbilityBypassAbAttr", user, abilityEffectsIgnored, false, this); applyAbAttrs("MoveAbilityBypassAbAttr", {pokemon: user, cancelled: abilityEffectsIgnored, move: this});
if (abilityEffectsIgnored.value) { if (abilityEffectsIgnored.value) {
return true; return true;
} }
@ -762,7 +759,7 @@ export default abstract class Move implements Localizable {
const moveAccuracy = new NumberHolder(this.accuracy); const moveAccuracy = new NumberHolder(this.accuracy);
applyMoveAttrs("VariableAccuracyAttr", user, target, this, moveAccuracy); applyMoveAttrs("VariableAccuracyAttr", user, target, this, moveAccuracy);
applyPreDefendAbAttrs("WonderSkinAbAttr", target, user, this, { value: false }, simulated, moveAccuracy); applyAbAttrs("WonderSkinAbAttr", {pokemon: target, opponent: user, move: this, simulated, accuracy: moveAccuracy});
if (moveAccuracy.value === -1) { if (moveAccuracy.value === -1) {
return moveAccuracy.value; return moveAccuracy.value;
@ -805,17 +802,25 @@ export default abstract class Move implements Localizable {
const typeChangeMovePowerMultiplier = new NumberHolder(1); const typeChangeMovePowerMultiplier = new NumberHolder(1);
const typeChangeHolder = new NumberHolder(this.type); const typeChangeHolder = new NumberHolder(this.type);
applyPreAttackAbAttrs("MoveTypeChangeAbAttr", source, target, this, true, typeChangeHolder, typeChangeMovePowerMultiplier); applyAbAttrs("MoveTypeChangeAbAttr", {pokemon: source, opponent: target, move: this, simulated: true, moveType: typeChangeHolder, power: typeChangeMovePowerMultiplier});
const sourceTeraType = source.getTeraType(); const sourceTeraType = source.getTeraType();
if (source.isTerastallized && sourceTeraType === this.type && power.value < 60 && this.priority <= 0 && !this.hasAttr("MultiHitAttr") && !globalScene.findModifier(m => m instanceof PokemonMultiHitModifier && m.pokemonId === source.id)) { if (source.isTerastallized && sourceTeraType === this.type && power.value < 60 && this.priority <= 0 && !this.hasAttr("MultiHitAttr") && !globalScene.findModifier(m => m instanceof PokemonMultiHitModifier && m.pokemonId === source.id)) {
power.value = 60; power.value = 60;
} }
applyPreAttackAbAttrs("VariableMovePowerAbAttr", source, target, this, simulated, power); const abAttrParams: PreAttackModifyPowerAbAttrParams = {
pokemon: source,
opponent: target,
simulated,
power,
move: this,
}
applyAbAttrs("VariableMovePowerAbAttr", abAttrParams);
const ally = source.getAlly(); const ally = source.getAlly();
if (!isNullOrUndefined(ally)) { if (!isNullOrUndefined(ally)) {
applyPreAttackAbAttrs("AllyMoveCategoryPowerBoostAbAttr", ally, target, this, simulated, power); applyAbAttrs("AllyMoveCategoryPowerBoostAbAttr", {...abAttrParams, pokemon: ally});
} }
const fieldAuras = new Set( const fieldAuras = new Set(
@ -827,11 +832,12 @@ export default abstract class Move implements Localizable {
.flat(), .flat(),
); );
for (const aura of fieldAuras) { for (const aura of fieldAuras) {
aura.applyPreAttack(source, null, simulated, target, this, [ power ]); // TODO: Refactor the fieldAura attribute so that its apply method is not directly called
aura.apply({pokemon: source, simulated, opponent: target, move: this, power});
} }
const alliedField: Pokemon[] = source.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); const alliedField: Pokemon[] = source.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField();
alliedField.forEach(p => applyPreAttackAbAttrs("UserFieldMoveTypePowerBoostAbAttr", p, target, this, simulated, power)); alliedField.forEach(p => applyAbAttrs("UserFieldMoveTypePowerBoostAbAttr", {pokemon: p, opponent: target, move: this, simulated, power}));
power.value *= typeChangeMovePowerMultiplier.value; power.value *= typeChangeMovePowerMultiplier.value;
@ -858,7 +864,7 @@ export default abstract class Move implements Localizable {
const priority = new NumberHolder(this.priority); const priority = new NumberHolder(this.priority);
applyMoveAttrs("IncrementMovePriorityAttr", user, null, this, priority); applyMoveAttrs("IncrementMovePriorityAttr", user, null, this, priority);
applyAbAttrs("ChangeMovePriorityAbAttr", user, null, simulated, this, priority); applyAbAttrs("ChangeMovePriorityAbAttr", {pokemon: user, simulated, move: this, priority});
return priority.value; return priority.value;
} }
@ -1310,7 +1316,7 @@ export class MoveEffectAttr extends MoveAttr {
getMoveChance(user: Pokemon, target: Pokemon, move: Move, selfEffect?: Boolean, showAbility?: Boolean): number { getMoveChance(user: Pokemon, target: Pokemon, move: Move, selfEffect?: Boolean, showAbility?: Boolean): number {
const moveChance = new NumberHolder(this.effectChanceOverride ?? move.chance); const moveChance = new NumberHolder(this.effectChanceOverride ?? move.chance);
applyAbAttrs("MoveEffectChanceMultiplierAbAttr", user, null, !showAbility, moveChance, move); applyAbAttrs("MoveEffectChanceMultiplierAbAttr", {pokemon: user, simulated: !showAbility, chance: moveChance, move});
if ((!move.hasAttr("FlinchAttr") || moveChance.value <= move.chance) && !move.hasAttr("SecretPowerAttr")) { if ((!move.hasAttr("FlinchAttr") || moveChance.value <= move.chance) && !move.hasAttr("SecretPowerAttr")) {
const userSide = user.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; const userSide = user.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
@ -1318,7 +1324,7 @@ export class MoveEffectAttr extends MoveAttr {
} }
if (!selfEffect) { if (!selfEffect) {
applyPreDefendAbAttrs("IgnoreMoveEffectsAbAttr", target, user, null, null, !showAbility, moveChance); applyAbAttrs("IgnoreMoveEffectsAbAttr", {pokemon: target, move, simulated: !showAbility, chance: moveChance});
} }
return moveChance.value; return moveChance.value;
} }
@ -1709,8 +1715,9 @@ export class RecoilAttr extends MoveEffectAttr {
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
if (!this.unblockable) { if (!this.unblockable) {
applyAbAttrs("BlockRecoilDamageAttr", user, cancelled); const abAttrParams: AbAttrParamsWithCancel = {pokemon: user, cancelled};
applyAbAttrs("BlockNonDirectDamageAbAttr", user, cancelled); applyAbAttrs("BlockRecoilDamageAttr", abAttrParams);
applyAbAttrs("BlockNonDirectDamageAbAttr", abAttrParams);
} }
if (cancelled.value) { if (cancelled.value) {
@ -1843,7 +1850,7 @@ export class HalfSacrificialAttr extends MoveEffectAttr {
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
// Check to see if the Pokemon has an ability that blocks non-direct damage // Check to see if the Pokemon has an ability that blocks non-direct damage
applyAbAttrs("BlockNonDirectDamageAbAttr", user, cancelled); applyAbAttrs("BlockNonDirectDamageAbAttr", {pokemon: user, cancelled});
if (!cancelled.value) { if (!cancelled.value) {
user.damageAndUpdate(toDmgValue(user.getMaxHp() / 2), { result: HitResult.INDIRECT, ignoreSegments: true }); user.damageAndUpdate(toDmgValue(user.getMaxHp() / 2), { result: HitResult.INDIRECT, ignoreSegments: true });
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:cutHpPowerUpMove", { pokemonName: getPokemonNameWithAffix(user) })); // Queue recoil message globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:cutHpPowerUpMove", { pokemonName: getPokemonNameWithAffix(user) })); // Queue recoil message
@ -2042,7 +2049,7 @@ export class FlameBurstAttr extends MoveEffectAttr {
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
if (!isNullOrUndefined(targetAlly)) { if (!isNullOrUndefined(targetAlly)) {
applyAbAttrs("BlockNonDirectDamageAbAttr", targetAlly, cancelled); applyAbAttrs("BlockNonDirectDamageAbAttr", {pokemon: targetAlly, cancelled});
} }
if (cancelled.value || !targetAlly || targetAlly.switchOutStatus) { if (cancelled.value || !targetAlly || targetAlly.switchOutStatus) {
@ -2414,7 +2421,7 @@ export class MultiHitAttr extends MoveAttr {
{ {
const rand = user.randBattleSeedInt(20); const rand = user.randBattleSeedInt(20);
const hitValue = new NumberHolder(rand); const hitValue = new NumberHolder(rand);
applyAbAttrs("MaxMultiHitAbAttr", user, null, false, hitValue); applyAbAttrs("MaxMultiHitAbAttr", {pokemon: user, hits: hitValue});
if (hitValue.value >= 13) { if (hitValue.value >= 13) {
return 2; return 2;
} else if (hitValue.value >= 6) { } else if (hitValue.value >= 6) {
@ -2522,7 +2529,7 @@ export class StatusEffectAttr extends MoveEffectAttr {
} }
if (((!pokemon.status || this.overrideStatus) || (pokemon.status.effect === this.effect && moveChance < 0)) if (((!pokemon.status || this.overrideStatus) || (pokemon.status.effect === this.effect && moveChance < 0))
&& pokemon.trySetStatus(this.effect, true, user, this.turnsRemaining, null, this.overrideStatus, quiet)) { && pokemon.trySetStatus(this.effect, true, user, this.turnsRemaining, null, this.overrideStatus, quiet)) {
applyPostAttackAbAttrs("ConfusionOnStatusEffectAbAttr", user, target, move, null, false, this.effect); applyAbAttrs("ConfusionOnStatusEffectAbAttr", {pokemon: user, opponent: target, move, effect: this.effect});
return true; return true;
} }
} }
@ -2574,7 +2581,7 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr {
apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean { apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
const statusToApply: StatusEffect | undefined = user.status?.effect ?? (user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : undefined); const statusToApply: StatusEffect | undefined = user.status?.effect ?? (user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : undefined);
if (target.status) { if (target.status || !statusToApply) {
return false; return false;
} else { } else {
const canSetStatus = target.canSetStatus(statusToApply, true, false, user); const canSetStatus = target.canSetStatus(statusToApply, true, false, user);
@ -2590,7 +2597,8 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr {
} }
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
return !target.status && target.canSetStatus(user.status?.effect, true, false, user) ? -10 : 0; const statusToApply = user.status?.effect ?? (user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : undefined);
return !target.status && statusToApply && target.canSetStatus(statusToApply, true, false, user) ? -10 : 0;
} }
} }
@ -2678,7 +2686,7 @@ export class RemoveHeldItemAttr extends MoveEffectAttr {
// Check for abilities that block item theft // Check for abilities that block item theft
// TODO: This should not trigger if the target would faint beforehand // TODO: This should not trigger if the target would faint beforehand
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyAbAttrs("BlockItemTheftAbAttr", target, cancelled); applyAbAttrs("BlockItemTheftAbAttr", {pokemon: target, cancelled});
if (cancelled.value) { if (cancelled.value) {
return false; return false;
@ -2795,8 +2803,8 @@ export class EatBerryAttr extends MoveEffectAttr {
protected eatBerry(consumer: Pokemon, berryOwner: Pokemon = consumer, updateHarvest = consumer === berryOwner) { protected eatBerry(consumer: Pokemon, berryOwner: Pokemon = consumer, updateHarvest = consumer === berryOwner) {
// consumer eats berry, owner triggers unburden and similar effects // consumer eats berry, owner triggers unburden and similar effects
getBerryEffectFunc(this.chosenBerry.berryType)(consumer); getBerryEffectFunc(this.chosenBerry.berryType)(consumer);
applyPostItemLostAbAttrs("PostItemLostAbAttr", berryOwner, false); applyAbAttrs("PostItemLostAbAttr", {pokemon: berryOwner});
applyAbAttrs("HealFromBerryUseAbAttr", consumer, new BooleanHolder(false)); applyAbAttrs("HealFromBerryUseAbAttr", {pokemon: consumer});
consumer.recordEatenBerry(this.chosenBerry.berryType, updateHarvest); consumer.recordEatenBerry(this.chosenBerry.berryType, updateHarvest);
} }
} }
@ -2821,7 +2829,7 @@ export class StealEatBerryAttr extends EatBerryAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
// check for abilities that block item theft // check for abilities that block item theft
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyAbAttrs("BlockItemTheftAbAttr", target, cancelled); applyAbAttrs("BlockItemTheftAbAttr", {pokemon: target, cancelled});
if (cancelled.value === true) { if (cancelled.value === true) {
return false; return false;
} }
@ -2835,7 +2843,7 @@ export class StealEatBerryAttr extends EatBerryAttr {
// pick a random berry and eat it // pick a random berry and eat it
this.chosenBerry = heldBerries[user.randBattleSeedInt(heldBerries.length)]; this.chosenBerry = heldBerries[user.randBattleSeedInt(heldBerries.length)];
applyPostItemLostAbAttrs("PostItemLostAbAttr", target, false); applyAbAttrs("PostItemLostAbAttr", {pokemon: target});
const message = i18next.t("battle:stealEatBerry", { pokemonName: user.name, targetName: target.name, berryName: this.chosenBerry.type.name }); const message = i18next.t("battle:stealEatBerry", { pokemonName: user.name, targetName: target.name, berryName: this.chosenBerry.type.name });
globalScene.phaseManager.queueMessage(message); globalScene.phaseManager.queueMessage(message);
this.reduceBerryModifier(target); this.reduceBerryModifier(target);
@ -3026,7 +3034,7 @@ export class OneHitKOAttr extends MoveAttr {
getCondition(): MoveConditionFunc { getCondition(): MoveConditionFunc {
return (user, target, move) => { return (user, target, move) => {
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyAbAttrs("BlockOneHitKOAbAttr", target, cancelled); applyAbAttrs("BlockOneHitKOAbAttr", {pokemon: target, cancelled});
return !cancelled.value && user.level >= target.level; return !cancelled.value && user.level >= target.level;
}; };
} }
@ -5436,7 +5444,7 @@ export class NoEffectAttr extends MoveAttr {
const crashDamageFunc = (user: Pokemon, move: Move) => { const crashDamageFunc = (user: Pokemon, move: Move) => {
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyAbAttrs("BlockNonDirectDamageAbAttr", user, cancelled); applyAbAttrs("BlockNonDirectDamageAbAttr", {pokemon: user, cancelled});
if (cancelled.value) { if (cancelled.value) {
return false; return false;
} }
@ -6435,9 +6443,9 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
} }
getFailedText(_user: Pokemon, target: Pokemon, _move: Move): string | undefined { getFailedText(_user: Pokemon, target: Pokemon, _move: Move): string | undefined {
const blockedByAbility = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyAbAttrs("ForceSwitchOutImmunityAbAttr", target, blockedByAbility); applyAbAttrs("ForceSwitchOutImmunityAbAttr", {pokemon: target, cancelled});
if (blockedByAbility.value) { if (cancelled.value) {
return i18next.t("moveTriggers:cannotBeSwitchedOut", { pokemonName: getPokemonNameWithAffix(target) }); return i18next.t("moveTriggers:cannotBeSwitchedOut", { pokemonName: getPokemonNameWithAffix(target) });
} }
} }
@ -6476,7 +6484,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
} }
const blockedByAbility = new BooleanHolder(false); const blockedByAbility = new BooleanHolder(false);
applyAbAttrs("ForceSwitchOutImmunityAbAttr", target, blockedByAbility); applyAbAttrs("ForceSwitchOutImmunityAbAttr", {pokemon: target, cancelled: blockedByAbility});
if (blockedByAbility.value) { if (blockedByAbility.value) {
return false; return false;
} }
@ -6869,7 +6877,7 @@ export class RandomMovesetMoveAttr extends RandomMoveAttr {
* Whether to consider all moves from the user's party (`true`) or the user's own moveset (`false`); * Whether to consider all moves from the user's party (`true`) or the user's own moveset (`false`);
* default `false`. * default `false`.
*/ */
private includeParty = false private includeParty = false,
) { ) {
super(invalidMoves); super(invalidMoves);
} }
@ -7970,7 +7978,7 @@ const failIfSingleBattle: MoveConditionFunc = (user, target, move) => globalScen
const failIfDampCondition: MoveConditionFunc = (user, target, move) => { const failIfDampCondition: MoveConditionFunc = (user, target, move) => {
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
globalScene.getField(true).map(p=>applyAbAttrs("FieldPreventExplosiveMovesAbAttr", p, cancelled)); globalScene.getField(true).map(p=>applyAbAttrs("FieldPreventExplosiveMovesAbAttr", {pokemon: p, cancelled}));
// Queue a message if an ability prevented usage of the move // Queue a message if an ability prevented usage of the move
if (cancelled.value) { if (cancelled.value) {
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:cannotUseMove", { pokemonName: getPokemonNameWithAffix(user), moveName: move.name })); globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:cannotUseMove", { pokemonName: getPokemonNameWithAffix(user), moveName: move.name }));

View File

@ -328,7 +328,7 @@ export const DancingLessonsEncounter: MysteryEncounter = MysteryEncounterBuilder
.withOptionPhase(async () => { .withOptionPhase(async () => {
// Show the Oricorio a dance, and recruit it // Show the Oricorio a dance, and recruit it
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
const oricorio = encounter.misc.oricorioData.toPokemon(); const oricorio = encounter.misc.oricorioData.toPokemon() as EnemyPokemon;
oricorio.passive = true; oricorio.passive = true;
// Ensure the Oricorio's moveset gains the Dance move the player used // Ensure the Oricorio's moveset gains the Dance move the player used

View File

@ -24,7 +24,7 @@ import { PokemonType } from "#enums/pokemon-type";
import { BerryType } from "#enums/berry-type"; import { BerryType } from "#enums/berry-type";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { SpeciesFormChangeAbilityTrigger } from "#app/data/pokemon-forms/form-change-triggers"; import { SpeciesFormChangeAbilityTrigger } from "#app/data/pokemon-forms/form-change-triggers";
import { applyPostBattleInitAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
import { showEncounterDialogue, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { showEncounterDialogue, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
import i18next from "i18next"; import i18next from "i18next";
@ -221,7 +221,7 @@ function endTrainerBattleAndShowDialogue(): Promise<void> {
// Each trainer battle is supposed to be a new fight, so reset all per-battle activation effects // Each trainer battle is supposed to be a new fight, so reset all per-battle activation effects
pokemon.resetBattleAndWaveData(); pokemon.resetBattleAndWaveData();
applyPostBattleInitAbAttrs("PostBattleInitAbAttr", pokemon); applyAbAttrs("PostBattleInitAbAttr", { pokemon });
} }
globalScene.phaseManager.unshiftNew("ShowTrainerPhase"); globalScene.phaseManager.unshiftNew("ShowTrainerPhase");

View File

@ -20,11 +20,7 @@ import { ArenaTrapTag, getArenaTag } from "#app/data/arena-tag";
import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagSide } from "#enums/arena-tag-side";
import type { BattlerIndex } from "#enums/battler-index"; import type { BattlerIndex } from "#enums/battler-index";
import { Terrain, TerrainType } from "#app/data/terrain"; import { Terrain, TerrainType } from "#app/data/terrain";
import { import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
applyAbAttrs,
applyPostTerrainChangeAbAttrs,
applyPostWeatherChangeAbAttrs,
} from "#app/data/abilities/apply-ab-attrs";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import Overrides from "#app/overrides"; import Overrides from "#app/overrides";
import { TagAddedEvent, TagRemovedEvent, TerrainChangedEvent, WeatherChangedEvent } from "#app/events/arena"; import { TagAddedEvent, TagRemovedEvent, TerrainChangedEvent, WeatherChangedEvent } from "#app/events/arena";
@ -372,7 +368,7 @@ export class Arena {
pokemon.findAndRemoveTags( pokemon.findAndRemoveTags(
t => "weatherTypes" in t && !(t.weatherTypes as WeatherType[]).find(t => t === weather), t => "weatherTypes" in t && !(t.weatherTypes as WeatherType[]).find(t => t === weather),
); );
applyPostWeatherChangeAbAttrs("PostWeatherChangeAbAttr", pokemon, weather); applyAbAttrs("PostWeatherChangeAbAttr", { pokemon, weather });
}); });
return true; return true;
@ -461,8 +457,8 @@ export class Arena {
pokemon.findAndRemoveTags( pokemon.findAndRemoveTags(
t => "terrainTypes" in t && !(t.terrainTypes as TerrainType[]).find(t => t === terrain), t => "terrainTypes" in t && !(t.terrainTypes as TerrainType[]).find(t => t === terrain),
); );
applyPostTerrainChangeAbAttrs("PostTerrainChangeAbAttr", pokemon, terrain); applyAbAttrs("PostTerrainChangeAbAttr", { pokemon, terrain });
applyAbAttrs("TerrainEventTypeChangeAbAttr", pokemon, null, false); applyAbAttrs("TerrainEventTypeChangeAbAttr", { pokemon });
}); });
return true; return true;

View File

@ -108,23 +108,8 @@ import { WeatherType } from "#enums/weather-type";
import { NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag"; import { NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag";
import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagSide } from "#enums/arena-tag-side";
import type { SuppressAbilitiesTag } from "#app/data/arena-tag"; import type { SuppressAbilitiesTag } from "#app/data/arena-tag";
import type { Ability } from "#app/data/abilities/ability"; import type { Ability, PreAttackModifyDamageAbAttrParams } from "#app/data/abilities/ability";
import { import { applyAbAttrs, applyOnGainAbAttrs, applyOnLoseAbAttrs } from "#app/data/abilities/apply-ab-attrs";
applyAbAttrs,
applyStatMultiplierAbAttrs,
applyPreApplyBattlerTagAbAttrs,
applyPreAttackAbAttrs,
applyPreDefendAbAttrs,
applyPreSetStatusAbAttrs,
applyFieldStatMultiplierAbAttrs,
applyCheckTrappedAbAttrs,
applyPostDamageAbAttrs,
applyPostItemLostAbAttrs,
applyOnGainAbAttrs,
applyPreLeaveFieldAbAttrs,
applyOnLoseAbAttrs,
applyAllyStatMultiplierAbAttrs,
} from "#app/data/abilities/apply-ab-attrs";
import { allAbilities } from "#app/data/data-lists"; import { allAbilities } from "#app/data/data-lists";
import type PokemonData from "#app/system/pokemon-data"; import type PokemonData from "#app/system/pokemon-data";
import { BattlerIndex } from "#enums/battler-index"; import { BattlerIndex } from "#enums/battler-index";
@ -189,7 +174,7 @@ import { HitResult } from "#enums/hit-result";
import { AiType } from "#enums/ai-type"; import { AiType } from "#enums/ai-type";
import type { MoveResult } from "#enums/move-result"; import type { MoveResult } from "#enums/move-result";
import { PokemonMove } from "#app/data/moves/pokemon-move"; import { PokemonMove } from "#app/data/moves/pokemon-move";
import type { AbAttrMap, AbAttrString } from "#app/@types/ability-types"; import type { AbAttrMap, AbAttrString, TypeMultiplierAbAttrParams } from "#app/@types/ability-types";
/** Base typeclass for damage parameter methods, used for DRY */ /** Base typeclass for damage parameter methods, used for DRY */
type damageParams = { type damageParams = {
@ -1364,7 +1349,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
applyMoveAttrs("HighCritAttr", source, this, move, critStage); applyMoveAttrs("HighCritAttr", source, this, move, critStage);
globalScene.applyModifiers(CritBoosterModifier, source.isPlayer(), source, critStage); globalScene.applyModifiers(CritBoosterModifier, source.isPlayer(), source, critStage);
globalScene.applyModifiers(TempCritBoosterModifier, source.isPlayer(), critStage); globalScene.applyModifiers(TempCritBoosterModifier, source.isPlayer(), critStage);
applyAbAttrs("BonusCritAbAttr", source, null, false, critStage); applyAbAttrs("BonusCritAbAttr", { pokemon: source, critStage });
const critBoostTag = source.getTag(CritBoostTag); const critBoostTag = source.getTag(CritBoostTag);
if (critBoostTag) { if (critBoostTag) {
// Dragon cheer only gives +1 crit stage to non-dragon types // Dragon cheer only gives +1 crit stage to non-dragon types
@ -1415,46 +1400,52 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
simulated = true, simulated = true,
ignoreHeldItems = false, ignoreHeldItems = false,
): number { ): number {
const statValue = new NumberHolder(this.getStat(stat, false)); const statVal = new NumberHolder(this.getStat(stat, false));
if (!ignoreHeldItems) { if (!ignoreHeldItems) {
globalScene.applyModifiers(StatBoosterModifier, this.isPlayer(), this, stat, statValue); globalScene.applyModifiers(StatBoosterModifier, this.isPlayer(), this, stat, statVal);
} }
// The Ruin abilities here are never ignored, but they reveal themselves on summon anyway // The Ruin abilities here are never ignored, but they reveal themselves on summon anyway
const fieldApplied = new BooleanHolder(false); const fieldApplied = new BooleanHolder(false);
for (const pokemon of globalScene.getField(true)) { for (const pokemon of globalScene.getField(true)) {
applyFieldStatMultiplierAbAttrs( applyAbAttrs("FieldMultiplyStatAbAttr", {
"FieldMultiplyStatAbAttr",
pokemon, pokemon,
stat, stat,
statValue, statVal,
this, target: this,
fieldApplied, hasApplied: fieldApplied,
simulated, simulated,
); });
if (fieldApplied.value) { if (fieldApplied.value) {
break; break;
} }
} }
if (!ignoreAbility) { if (!ignoreAbility) {
applyStatMultiplierAbAttrs("StatMultiplierAbAttr", this, stat, statValue, simulated); applyAbAttrs("StatMultiplierAbAttr", {
pokemon: this,
stat,
statVal,
simulated,
// TODO: maybe just don't call this if the move is none?
move: move ?? allMoves[MoveId.NONE],
});
} }
const ally = this.getAlly(); const ally = this.getAlly();
if (!isNullOrUndefined(ally)) { if (!isNullOrUndefined(ally)) {
applyAllyStatMultiplierAbAttrs( applyAbAttrs("AllyStatMultiplierAbAttr", {
"AllyStatMultiplierAbAttr", pokemon: ally,
ally,
stat, stat,
statValue, statVal,
simulated, simulated,
this, // TODO: maybe just don't call this if the move is none?
move?.hasFlag(MoveFlags.IGNORE_ABILITIES) || ignoreAllyAbility, move: move ?? allMoves[MoveId.NONE],
); ignoreAbility: move?.hasFlag(MoveFlags.IGNORE_ABILITIES) || ignoreAllyAbility,
});
} }
let ret = let ret =
statValue.value * statVal.value *
this.getStatStageMultiplier(stat, opponent, move, ignoreOppAbility, isCritical, simulated, ignoreHeldItems); this.getStatStageMultiplier(stat, opponent, move, ignoreOppAbility, isCritical, simulated, ignoreHeldItems);
switch (stat) { switch (stat) {
@ -2045,20 +2036,20 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* @param ability New Ability * @param ability New Ability
*/ */
public setTempAbility(ability: Ability, passive = false): void { public setTempAbility(ability: Ability, passive = false): void {
applyOnLoseAbAttrs(this, passive); applyOnLoseAbAttrs({ pokemon: this, passive });
if (passive) { if (passive) {
this.summonData.passiveAbility = ability.id; this.summonData.passiveAbility = ability.id;
} else { } else {
this.summonData.ability = ability.id; this.summonData.ability = ability.id;
} }
applyOnGainAbAttrs(this, passive); applyOnGainAbAttrs({ pokemon: this, passive });
} }
/** /**
* Suppresses an ability and calls its onlose attributes * Suppresses an ability and calls its onlose attributes
*/ */
public suppressAbility() { public suppressAbility() {
[true, false].forEach(passive => applyOnLoseAbAttrs(this, passive)); [true, false].forEach(passive => applyOnLoseAbAttrs({ pokemon: this, passive }));
this.summonData.abilitySuppressed = true; this.summonData.abilitySuppressed = true;
} }
@ -2194,7 +2185,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const weight = new NumberHolder(this.species.weight - weightRemoved); const weight = new NumberHolder(this.species.weight - weightRemoved);
// This will trigger the ability overlay so only call this function when necessary // This will trigger the ability overlay so only call this function when necessary
applyAbAttrs("WeightMultiplierAbAttr", this, null, false, weight); applyAbAttrs("WeightMultiplierAbAttr", { pokemon: this, weight });
return Math.max(minWeight, weight.value); return Math.max(minWeight, weight.value);
} }
@ -2256,7 +2247,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return false; return false;
} }
const trappedByAbility = new BooleanHolder(false); /** Holds whether the pokemon is trapped due to an ability */
const trapped = new BooleanHolder(false);
/** /**
* Contains opposing Pokemon (Enemy/Player Pokemon) depending on perspective * Contains opposing Pokemon (Enemy/Player Pokemon) depending on perspective
* Afterwards, it filters out Pokemon that have been switched out of the field so trapped abilities/moves do not trigger * Afterwards, it filters out Pokemon that have been switched out of the field so trapped abilities/moves do not trigger
@ -2265,14 +2257,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const opposingField = opposingFieldUnfiltered.filter(enemyPkm => enemyPkm.switchOutStatus === false); const opposingField = opposingFieldUnfiltered.filter(enemyPkm => enemyPkm.switchOutStatus === false);
for (const opponent of opposingField) { for (const opponent of opposingField) {
applyCheckTrappedAbAttrs("CheckTrappedAbAttr", opponent, trappedByAbility, this, trappedAbMessages, simulated); applyAbAttrs("CheckTrappedAbAttr", { pokemon: opponent, trapped, opponent: this, simulated }, trappedAbMessages);
} }
const side = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; const side = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
return ( return (
trappedByAbility.value || trapped.value || !!this.getTag(TrappedTag) || !!globalScene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, side)
!!this.getTag(TrappedTag) ||
!!globalScene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, side)
); );
} }
@ -2287,7 +2277,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const moveTypeHolder = new NumberHolder(move.type); const moveTypeHolder = new NumberHolder(move.type);
applyMoveAttrs("VariableMoveTypeAttr", this, null, move, moveTypeHolder); applyMoveAttrs("VariableMoveTypeAttr", this, null, move, moveTypeHolder);
applyPreAttackAbAttrs("MoveTypeChangeAbAttr", this, null, move, simulated, moveTypeHolder);
const power = new NumberHolder(move.power);
applyAbAttrs("MoveTypeChangeAbAttr", {
pokemon: this,
move,
simulated,
moveType: moveTypeHolder,
power,
opponent: this,
});
// If the user is terastallized and the move is tera blast, or tera starstorm that is stellar type, // If the user is terastallized and the move is tera blast, or tera starstorm that is stellar type,
// then bypass the check for ion deluge and electrify // then bypass the check for ion deluge and electrify
@ -2351,17 +2350,31 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
const cancelledHolder = cancelled ?? new BooleanHolder(false); const cancelledHolder = cancelled ?? new BooleanHolder(false);
// TypeMultiplierAbAttrParams is shared amongst the type of AbAttrs we will be invoking
const commonAbAttrParams: TypeMultiplierAbAttrParams = {
pokemon: this,
opponent: source,
move,
cancelled: cancelledHolder,
simulated,
typeMultiplier,
};
if (!ignoreAbility) { if (!ignoreAbility) {
applyPreDefendAbAttrs("TypeImmunityAbAttr", this, source, move, cancelledHolder, simulated, typeMultiplier); applyAbAttrs("TypeImmunityAbAttr", commonAbAttrParams);
if (!cancelledHolder.value) { if (!cancelledHolder.value) {
applyPreDefendAbAttrs("MoveImmunityAbAttr", this, source, move, cancelledHolder, simulated, typeMultiplier); applyAbAttrs("MoveImmunityAbAttr", commonAbAttrParams);
} }
if (!cancelledHolder.value) { if (!cancelledHolder.value) {
const defendingSidePlayField = this.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); const defendingSidePlayField = this.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField();
defendingSidePlayField.forEach(p => defendingSidePlayField.forEach(p =>
applyPreDefendAbAttrs("FieldPriorityMoveImmunityAbAttr", p, source, move, cancelledHolder), applyAbAttrs("FieldPriorityMoveImmunityAbAttr", {
pokemon: p,
opponent: source,
move,
cancelled: cancelledHolder,
}),
); );
} }
} }
@ -2376,7 +2389,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
// Apply Tera Shell's effect to attacks after all immunities are accounted for // Apply Tera Shell's effect to attacks after all immunities are accounted for
if (!ignoreAbility && move.category !== MoveCategory.STATUS) { if (!ignoreAbility && move.category !== MoveCategory.STATUS) {
applyPreDefendAbAttrs("FullHpResistTypeAbAttr", this, source, move, cancelledHolder, simulated, typeMultiplier); applyAbAttrs("FullHpResistTypeAbAttr", commonAbAttrParams);
} }
if (move.category === MoveCategory.STATUS && move.hitsSubstitute(source, this)) { if (move.category === MoveCategory.STATUS && move.hitsSubstitute(source, this)) {
@ -2420,16 +2433,22 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
let multiplier = types let multiplier = types
.map(defType => { .map(defenderType => {
const multiplier = new NumberHolder(getTypeDamageMultiplier(moveType, defType)); const multiplier = new NumberHolder(getTypeDamageMultiplier(moveType, defenderType));
applyChallenges(ChallengeType.TYPE_EFFECTIVENESS, multiplier); applyChallenges(ChallengeType.TYPE_EFFECTIVENESS, multiplier);
if (move) { if (move) {
applyMoveAttrs("VariableMoveTypeChartAttr", null, this, move, multiplier, defType); applyMoveAttrs("VariableMoveTypeChartAttr", null, this, move, multiplier, defenderType);
} }
if (source) { if (source) {
const ignoreImmunity = new BooleanHolder(false); const ignoreImmunity = new BooleanHolder(false);
if (source.isActive(true) && source.hasAbilityWithAttr("IgnoreTypeImmunityAbAttr")) { if (source.isActive(true) && source.hasAbilityWithAttr("IgnoreTypeImmunityAbAttr")) {
applyAbAttrs("IgnoreTypeImmunityAbAttr", source, ignoreImmunity, simulated, moveType, defType); applyAbAttrs("IgnoreTypeImmunityAbAttr", {
pokemon: source,
cancelled: ignoreImmunity,
simulated,
moveType,
defenderType,
});
} }
if (ignoreImmunity.value) { if (ignoreImmunity.value) {
if (multiplier.value === 0) { if (multiplier.value === 0) {
@ -2438,7 +2457,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
const exposedTags = this.findTags(tag => tag instanceof ExposedTag) as ExposedTag[]; const exposedTags = this.findTags(tag => tag instanceof ExposedTag) as ExposedTag[];
if (exposedTags.some(t => t.ignoreImmunity(defType, moveType))) { if (exposedTags.some(t => t.ignoreImmunity(defenderType, moveType))) {
if (multiplier.value === 0) { if (multiplier.value === 0) {
return 1; return 1;
} }
@ -3383,7 +3402,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
} }
if (!ignoreOppAbility) { if (!ignoreOppAbility) {
applyAbAttrs("IgnoreOpponentStatStagesAbAttr", opponent, null, simulated, stat, ignoreStatStage); applyAbAttrs("IgnoreOpponentStatStagesAbAttr", {
pokemon: opponent,
ignored: ignoreStatStage,
stat,
simulated,
});
} }
if (move) { if (move) {
applyMoveAttrs("IgnoreOpponentStatStagesAttr", this, opponent, move, ignoreStatStage); applyMoveAttrs("IgnoreOpponentStatStagesAttr", this, opponent, move, ignoreStatStage);
@ -3422,8 +3446,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const ignoreAccStatStage = new BooleanHolder(false); const ignoreAccStatStage = new BooleanHolder(false);
const ignoreEvaStatStage = new BooleanHolder(false); const ignoreEvaStatStage = new BooleanHolder(false);
applyAbAttrs("IgnoreOpponentStatStagesAbAttr", target, null, false, Stat.ACC, ignoreAccStatStage); // TODO: consider refactoring this method to accept `simulated` and then pass simulated to these applyAbAttrs
applyAbAttrs("IgnoreOpponentStatStagesAbAttr", this, null, false, Stat.EVA, ignoreEvaStatStage); applyAbAttrs("IgnoreOpponentStatStagesAbAttr", { pokemon: target, stat: Stat.ACC, ignored: ignoreAccStatStage });
applyAbAttrs("IgnoreOpponentStatStagesAbAttr", { pokemon: this, stat: Stat.EVA, ignored: ignoreEvaStatStage });
applyMoveAttrs("IgnoreOpponentStatStagesAttr", this, target, sourceMove, ignoreEvaStatStage); applyMoveAttrs("IgnoreOpponentStatStagesAttr", this, target, sourceMove, ignoreEvaStatStage);
globalScene.applyModifiers(TempStatStageBoosterModifier, this.isPlayer(), Stat.ACC, userAccStage); globalScene.applyModifiers(TempStatStageBoosterModifier, this.isPlayer(), Stat.ACC, userAccStage);
@ -3443,33 +3468,40 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
: 3 / (3 + Math.min(targetEvaStage.value - userAccStage.value, 6)); : 3 / (3 + Math.min(targetEvaStage.value - userAccStage.value, 6));
} }
applyStatMultiplierAbAttrs("StatMultiplierAbAttr", this, Stat.ACC, accuracyMultiplier, false, sourceMove); applyAbAttrs("StatMultiplierAbAttr", {
pokemon: this,
stat: Stat.ACC,
statVal: accuracyMultiplier,
move: sourceMove,
});
const evasionMultiplier = new NumberHolder(1); const evasionMultiplier = new NumberHolder(1);
applyStatMultiplierAbAttrs("StatMultiplierAbAttr", target, Stat.EVA, evasionMultiplier); applyAbAttrs("StatMultiplierAbAttr", {
pokemon: target,
stat: Stat.EVA,
statVal: evasionMultiplier,
move: sourceMove,
});
const ally = this.getAlly(); const ally = this.getAlly();
if (!isNullOrUndefined(ally)) { if (!isNullOrUndefined(ally)) {
const ignore = const ignore =
this.hasAbilityWithAttr("MoveAbilityBypassAbAttr") || sourceMove.hasFlag(MoveFlags.IGNORE_ABILITIES); this.hasAbilityWithAttr("MoveAbilityBypassAbAttr") || sourceMove.hasFlag(MoveFlags.IGNORE_ABILITIES);
applyAllyStatMultiplierAbAttrs( applyAbAttrs("AllyStatMultiplierAbAttr", {
"AllyStatMultiplierAbAttr", pokemon: ally,
ally, stat: Stat.ACC,
Stat.ACC, statVal: accuracyMultiplier,
accuracyMultiplier, ignoreAbility: ignore,
false, move: sourceMove,
this, });
ignore,
); applyAbAttrs("AllyStatMultiplierAbAttr", {
applyAllyStatMultiplierAbAttrs( pokemon: ally,
"AllyStatMultiplierAbAttr", stat: Stat.EVA,
ally, statVal: evasionMultiplier,
Stat.EVA, ignoreAbility: ignore,
evasionMultiplier, move: sourceMove,
false, });
this,
ignore,
);
} }
return accuracyMultiplier.value / evasionMultiplier.value; return accuracyMultiplier.value / evasionMultiplier.value;
@ -3584,7 +3616,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
applyMoveAttrs("CombinedPledgeStabBoostAttr", source, this, move, stabMultiplier); applyMoveAttrs("CombinedPledgeStabBoostAttr", source, this, move, stabMultiplier);
if (!ignoreSourceAbility) { if (!ignoreSourceAbility) {
applyAbAttrs("StabBoostAbAttr", source, null, simulated, stabMultiplier); applyAbAttrs("StabBoostAbAttr", { pokemon: source, simulated, multiplier: stabMultiplier });
} }
if (source.isTerastallized && sourceTeraType === moveType && moveType !== PokemonType.STELLAR) { if (source.isTerastallized && sourceTeraType === moveType && moveType !== PokemonType.STELLAR) {
@ -3731,16 +3763,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
null, null,
multiStrikeEnhancementMultiplier, multiStrikeEnhancementMultiplier,
); );
if (!ignoreSourceAbility) { if (!ignoreSourceAbility) {
applyPreAttackAbAttrs( applyAbAttrs("AddSecondStrikeAbAttr", {
"AddSecondStrikeAbAttr", pokemon: source,
source,
this,
move, move,
simulated, simulated,
null, multiplier: multiStrikeEnhancementMultiplier,
multiStrikeEnhancementMultiplier, });
);
} }
/** Doubles damage if this Pokemon's last move was Glaive Rush */ /** Doubles damage if this Pokemon's last move was Glaive Rush */
@ -3751,7 +3781,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/** The damage multiplier when the given move critically hits */ /** The damage multiplier when the given move critically hits */
const criticalMultiplier = new NumberHolder(isCritical ? 1.5 : 1); const criticalMultiplier = new NumberHolder(isCritical ? 1.5 : 1);
applyAbAttrs("MultCritAbAttr", source, null, simulated, criticalMultiplier); applyAbAttrs("MultCritAbAttr", { pokemon: source, simulated, critMult: criticalMultiplier });
/** /**
* A multiplier for random damage spread in the range [0.85, 1] * A multiplier for random damage spread in the range [0.85, 1]
@ -3772,7 +3802,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
) { ) {
const burnDamageReductionCancelled = new BooleanHolder(false); const burnDamageReductionCancelled = new BooleanHolder(false);
if (!ignoreSourceAbility) { if (!ignoreSourceAbility) {
applyAbAttrs("BypassBurnDamageReductionAbAttr", source, burnDamageReductionCancelled, simulated); applyAbAttrs("BypassBurnDamageReductionAbAttr", {
pokemon: source,
cancelled: burnDamageReductionCancelled,
simulated,
});
} }
if (!burnDamageReductionCancelled.value) { if (!burnDamageReductionCancelled.value) {
burnMultiplier = 0.5; burnMultiplier = 0.5;
@ -3836,7 +3870,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/** Doubles damage if the attacker has Tinted Lens and is using a resisted move */ /** Doubles damage if the attacker has Tinted Lens and is using a resisted move */
if (!ignoreSourceAbility) { if (!ignoreSourceAbility) {
applyPreAttackAbAttrs("DamageBoostAbAttr", source, this, move, simulated, damage); applyAbAttrs("DamageBoostAbAttr", {
pokemon: source,
opponent: this,
move,
simulated,
damage,
});
} }
/** Apply the enemy's Damage and Resistance tokens */ /** Apply the enemy's Damage and Resistance tokens */
@ -3847,14 +3887,25 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
globalScene.applyModifiers(EnemyDamageReducerModifier, false, damage); globalScene.applyModifiers(EnemyDamageReducerModifier, false, damage);
} }
const abAttrParams: PreAttackModifyDamageAbAttrParams = {
pokemon: this,
opponent: source,
move,
simulated,
damage,
};
/** Apply this Pokemon's post-calc defensive modifiers (e.g. Fur Coat) */ /** Apply this Pokemon's post-calc defensive modifiers (e.g. Fur Coat) */
if (!ignoreAbility) { if (!ignoreAbility) {
applyPreDefendAbAttrs("ReceivedMoveDamageMultiplierAbAttr", this, source, move, cancelled, simulated, damage); applyAbAttrs("ReceivedMoveDamageMultiplierAbAttr", abAttrParams);
const ally = this.getAlly(); const ally = this.getAlly();
/** Additionally apply friend guard damage reduction if ally has it. */ /** Additionally apply friend guard damage reduction if ally has it. */
if (globalScene.currentBattle.double && !isNullOrUndefined(ally) && ally.isActive(true)) { if (globalScene.currentBattle.double && !isNullOrUndefined(ally) && ally.isActive(true)) {
applyPreDefendAbAttrs("AlliedFieldDamageReductionAbAttr", ally, source, move, cancelled, simulated, damage); applyAbAttrs("AlliedFieldDamageReductionAbAttr", {
...abAttrParams,
// Same parameters as before, except we are applying the ally's ability
pokemon: ally,
});
} }
} }
@ -3862,7 +3913,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
applyMoveAttrs("ModifiedDamageAttr", source, this, move, damage); applyMoveAttrs("ModifiedDamageAttr", source, this, move, damage);
if (this.isFullHp() && !ignoreAbility) { if (this.isFullHp() && !ignoreAbility) {
applyPreDefendAbAttrs("PreDefendFullHpEndureAbAttr", this, source, move, cancelled, false, damage); applyAbAttrs("PreDefendFullHpEndureAbAttr", abAttrParams);
} }
// debug message for when damage is applied (i.e. not simulated) // debug message for when damage is applied (i.e. not simulated)
@ -3900,7 +3951,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const alwaysCrit = new BooleanHolder(false); const alwaysCrit = new BooleanHolder(false);
applyMoveAttrs("CritOnlyAttr", source, this, move, alwaysCrit); applyMoveAttrs("CritOnlyAttr", source, this, move, alwaysCrit);
applyAbAttrs("ConditionalCritAbAttr", source, null, false, alwaysCrit, this, move); applyAbAttrs("ConditionalCritAbAttr", { pokemon: source, isCritical: alwaysCrit, target: this, move });
const alwaysCritTag = !!source.getTag(BattlerTagType.ALWAYS_CRIT); const alwaysCritTag = !!source.getTag(BattlerTagType.ALWAYS_CRIT);
const critChance = [24, 8, 2, 1][Phaser.Math.Clamp(this.getCritStage(source, move), 0, 3)]; const critChance = [24, 8, 2, 1][Phaser.Math.Clamp(this.getCritStage(source, move), 0, 3)];
@ -3911,7 +3962,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
// apply crit block effects from lucky chant & co., overriding previous effects // apply crit block effects from lucky chant & co., overriding previous effects
const blockCrit = new BooleanHolder(false); const blockCrit = new BooleanHolder(false);
applyAbAttrs("BlockCritAbAttr", this, null, false, blockCrit); applyAbAttrs("BlockCritAbAttr", { pokemon: this, blockCrit });
const blockCritTag = globalScene.arena.getTagOnSide( const blockCritTag = globalScene.arena.getTagOnSide(
NoCritTag, NoCritTag,
this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY,
@ -4023,7 +4074,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* Multi-hits are handled in move-effect-phase.ts for PostDamageAbAttr * Multi-hits are handled in move-effect-phase.ts for PostDamageAbAttr
*/ */
if (!source || source.turnData.hitCount <= 1) { if (!source || source.turnData.hitCount <= 1) {
applyPostDamageAbAttrs("PostDamageAbAttr", this, damage, this.hasPassive(), false, [], source); applyAbAttrs("PostDamageAbAttr", { pokemon: this, damage, source });
} }
return damage; return damage;
} }
@ -4071,11 +4122,17 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const stubTag = new BattlerTag(tagType, 0, 0); const stubTag = new BattlerTag(tagType, 0, 0);
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyPreApplyBattlerTagAbAttrs("BattlerTagImmunityAbAttr", this, stubTag, cancelled, true); applyAbAttrs("BattlerTagImmunityAbAttr", { pokemon: this, tag: stubTag, cancelled, simulated: true });
const userField = this.getAlliedField(); const userField = this.getAlliedField();
userField.forEach(pokemon => userField.forEach(pokemon =>
applyPreApplyBattlerTagAbAttrs("UserFieldBattlerTagImmunityAbAttr", pokemon, stubTag, cancelled, true, this), applyAbAttrs("UserFieldBattlerTagImmunityAbAttr", {
pokemon,
tag: stubTag,
cancelled,
simulated: true,
target: this,
}),
); );
return !cancelled.value; return !cancelled.value;
@ -4091,13 +4148,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const newTag = getBattlerTag(tagType, turnCount, sourceMove!, sourceId!); // TODO: are the bangs correct? const newTag = getBattlerTag(tagType, turnCount, sourceMove!, sourceId!); // TODO: are the bangs correct?
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyPreApplyBattlerTagAbAttrs("BattlerTagImmunityAbAttr", this, newTag, cancelled); applyAbAttrs("BattlerTagImmunityAbAttr", { pokemon: this, tag: newTag, cancelled });
if (cancelled.value) { if (cancelled.value) {
return false; return false;
} }
for (const pokemon of this.getAlliedField()) { for (const pokemon of this.getAlliedField()) {
applyPreApplyBattlerTagAbAttrs("UserFieldBattlerTagImmunityAbAttr", pokemon, newTag, cancelled, false, this); applyAbAttrs("UserFieldBattlerTagImmunityAbAttr", { pokemon, tag: newTag, cancelled, target: this });
if (cancelled.value) { if (cancelled.value) {
return false; return false;
} }
@ -4125,7 +4182,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
getTag<T extends BattlerTag>(tagType: Constructor<T>): T | undefined; getTag<T extends BattlerTag>(tagType: Constructor<T>): T | undefined;
getTag(tagType: BattlerTagType | Constructor<BattlerTag>): BattlerTag | undefined { getTag(tagType: BattlerTagType | Constructor<BattlerTag>): BattlerTag | undefined {
return tagType instanceof Function return typeof tagType === "function"
? this.summonData.tags.find(t => t instanceof tagType) ? this.summonData.tags.find(t => t instanceof tagType)
: this.summonData.tags.find(t => t.tagType === tagType); : this.summonData.tags.find(t => t.tagType === tagType);
} }
@ -4620,7 +4677,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* @param ignoreField Whether any field effects (weather, terrain, etc.) should be considered * @param ignoreField Whether any field effects (weather, terrain, etc.) should be considered
*/ */
canSetStatus( canSetStatus(
effect: StatusEffect | undefined, effect: StatusEffect,
quiet = false, quiet = false,
overrideStatus = false, overrideStatus = false,
sourcePokemon: Pokemon | null = null, sourcePokemon: Pokemon | null = null,
@ -4651,8 +4708,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
// Check if the source Pokemon has an ability that cancels the Poison/Toxic immunity // Check if the source Pokemon has an ability that cancels the Poison/Toxic immunity
const cancelImmunity = new BooleanHolder(false); const cancelImmunity = new BooleanHolder(false);
// TODO: Determine if we need to pass `quiet` as the value for simulated in this call
if (sourcePokemon) { if (sourcePokemon) {
applyAbAttrs("IgnoreTypeStatusEffectImmunityAbAttr", sourcePokemon, cancelImmunity, false, effect, defType); applyAbAttrs("IgnoreTypeStatusEffectImmunityAbAttr", {
pokemon: sourcePokemon,
cancelled: cancelImmunity,
statusEffect: effect,
defenderType: defType,
});
if (cancelImmunity.value) { if (cancelImmunity.value) {
return false; return false;
} }
@ -4701,21 +4764,20 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyPreSetStatusAbAttrs("StatusEffectImmunityAbAttr", this, effect, cancelled, quiet); applyAbAttrs("StatusEffectImmunityAbAttr", { pokemon: this, effect, cancelled, simulated: quiet });
if (cancelled.value) { if (cancelled.value) {
return false; return false;
} }
for (const pokemon of this.getAlliedField()) { for (const pokemon of this.getAlliedField()) {
applyPreSetStatusAbAttrs( applyAbAttrs("UserFieldStatusEffectImmunityAbAttr", {
"UserFieldStatusEffectImmunityAbAttr",
pokemon, pokemon,
effect, effect,
cancelled, cancelled,
quiet, simulated: quiet,
this, target: this,
sourcePokemon, source: sourcePokemon,
); });
if (cancelled.value) { if (cancelled.value) {
break; break;
} }
@ -4746,6 +4808,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
overrideStatus?: boolean, overrideStatus?: boolean,
quiet = true, quiet = true,
): boolean { ): boolean {
if (!effect) {
return false;
}
if (!this.canSetStatus(effect, quiet, overrideStatus, sourcePokemon)) { if (!this.canSetStatus(effect, quiet, overrideStatus, sourcePokemon)) {
return false; return false;
} }
@ -4804,7 +4869,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
sleepTurnsRemaining = sleepTurnsRemaining!; // tell TS compiler it's defined sleepTurnsRemaining = sleepTurnsRemaining!; // tell TS compiler it's defined
effect = effect!; // If `effect` is undefined then `trySetStatus()` will have already returned early via the `canSetStatus()` call
this.status = new Status(effect, 0, sleepTurnsRemaining?.value); this.status = new Status(effect, 0, sleepTurnsRemaining?.value);
return true; return true;
@ -4865,7 +4929,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (globalScene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, defendingSide)) { if (globalScene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, defendingSide)) {
const bypassed = new BooleanHolder(false); const bypassed = new BooleanHolder(false);
if (attacker) { if (attacker) {
applyAbAttrs("InfiltratorAbAttr", attacker, null, false, bypassed); applyAbAttrs("InfiltratorAbAttr", { pokemon: attacker, bypassed });
} }
return !bypassed.value; return !bypassed.value;
} }
@ -5411,7 +5475,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.hideInfo(); this.hideInfo();
} }
// Trigger abilities that activate upon leaving the field // Trigger abilities that activate upon leaving the field
applyPreLeaveFieldAbAttrs("PreLeaveFieldAbAttr", this); applyAbAttrs("PreLeaveFieldAbAttr", { pokemon: this });
this.setSwitchOutStatus(true); this.setSwitchOutStatus(true);
globalScene.triggerPokemonFormChange(this, SpeciesFormChangeActiveTrigger, true); globalScene.triggerPokemonFormChange(this, SpeciesFormChangeActiveTrigger, true);
globalScene.field.remove(this, destroy); globalScene.field.remove(this, destroy);
@ -5471,7 +5535,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
globalScene.removeModifier(heldItem, this.isEnemy()); globalScene.removeModifier(heldItem, this.isEnemy());
} }
if (forBattle) { if (forBattle) {
applyPostItemLostAbAttrs("PostItemLostAbAttr", this, false); applyAbAttrs("PostItemLostAbAttr", { pokemon: this });
} }
return true; return true;

View File

@ -42,7 +42,7 @@ import type {
import { getModifierType } from "#app/utils/modifier-utils"; import { getModifierType } from "#app/utils/modifier-utils";
import { Color, ShadowColor } from "#enums/color"; import { Color, ShadowColor } from "#enums/color";
import { FRIENDSHIP_GAIN_FROM_RARE_CANDY } from "#app/data/balance/starters"; import { FRIENDSHIP_GAIN_FROM_RARE_CANDY } from "#app/data/balance/starters";
import { applyAbAttrs, applyPostItemLostAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import type { ModifierInstanceMap, ModifierString } from "#app/@types/modifier-types"; import type { ModifierInstanceMap, ModifierString } from "#app/@types/modifier-types";
@ -751,7 +751,7 @@ export abstract class PokemonHeldItemModifier extends PersistentModifier {
} }
getPokemon(): Pokemon | undefined { getPokemon(): Pokemon | undefined {
return this.pokemonId ? (globalScene.getPokemonById(this.pokemonId) ?? undefined) : undefined; return globalScene.getPokemonById(this.pokemonId) ?? undefined;
} }
getScoreMultiplier(): number { getScoreMultiplier(): number {
@ -1879,7 +1879,7 @@ export class BerryModifier extends PokemonHeldItemModifier {
// munch the berry and trigger unburden-like effects // munch the berry and trigger unburden-like effects
getBerryEffectFunc(this.berryType)(pokemon); getBerryEffectFunc(this.berryType)(pokemon);
applyPostItemLostAbAttrs("PostItemLostAbAttr", pokemon, false); applyAbAttrs("PostItemLostAbAttr", { pokemon });
// Update berry eaten trackers for Belch, Harvest, Cud Chew, etc. // Update berry eaten trackers for Belch, Harvest, Cud Chew, etc.
// Don't recover it if we proc berry pouch (no item duplication) // Don't recover it if we proc berry pouch (no item duplication)
@ -1967,7 +1967,7 @@ export class PokemonInstantReviveModifier extends PokemonHeldItemModifier {
// Reapply Commander on the Pokemon's side of the field, if applicable // Reapply Commander on the Pokemon's side of the field, if applicable
const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField();
for (const p of field) { for (const p of field) {
applyAbAttrs("CommanderAbAttr", p, null, false); applyAbAttrs("CommanderAbAttr", { pokemon: p });
} }
return true; return true;
} }

View File

@ -1,4 +1,4 @@
import { applyAbAttrs, applyPreLeaveFieldAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";
import type { PlayerPokemon, EnemyPokemon } from "#app/field/pokemon"; import type { PlayerPokemon, EnemyPokemon } from "#app/field/pokemon";
@ -25,10 +25,10 @@ export class AttemptRunPhase extends PokemonPhase {
this.attemptRunAway(playerField, enemyField, escapeChance); this.attemptRunAway(playerField, enemyField, escapeChance);
applyAbAttrs("RunSuccessAbAttr", playerPokemon, null, false, escapeChance); applyAbAttrs("RunSuccessAbAttr", { pokemon: playerPokemon, chance: escapeChance });
if (playerPokemon.randBattleSeedInt(100) < escapeChance.value && !this.forceFailEscape) { if (playerPokemon.randBattleSeedInt(100) < escapeChance.value && !this.forceFailEscape) {
enemyField.forEach(enemyPokemon => applyPreLeaveFieldAbAttrs("PreLeaveFieldAbAttr", enemyPokemon)); enemyField.forEach(enemyPokemon => applyAbAttrs("PreLeaveFieldAbAttr", { pokemon: enemyPokemon }));
globalScene.playSound("se/flee"); globalScene.playSound("se/flee");
globalScene.phaseManager.queueMessage(i18next.t("battle:runAwaySuccess"), null, true, 500); globalScene.phaseManager.queueMessage(i18next.t("battle:runAwaySuccess"), null, true, 500);

View File

@ -1,5 +1,5 @@
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { applyPostBattleAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
import { LapsingPersistentModifier, LapsingPokemonHeldItemModifier } from "#app/modifier/modifier"; import { LapsingPersistentModifier, LapsingPokemonHeldItemModifier } from "#app/modifier/modifier";
import { BattlePhase } from "./battle-phase"; import { BattlePhase } from "./battle-phase";
@ -65,7 +65,7 @@ export class BattleEndPhase extends BattlePhase {
} }
for (const pokemon of globalScene.getPokemonAllowedInBattle()) { for (const pokemon of globalScene.getPokemonAllowedInBattle()) {
applyPostBattleAbAttrs("PostBattleAbAttr", pokemon, false, this.isVictory); applyAbAttrs("PostBattleAbAttr", { pokemon, victory: this.isVictory });
} }
if (globalScene.currentBattle.moneyScattered) { if (globalScene.currentBattle.moneyScattered) {

View File

@ -20,7 +20,7 @@ export class BerryPhase extends FieldPhase {
this.executeForAll(pokemon => { this.executeForAll(pokemon => {
this.eatBerries(pokemon); this.eatBerries(pokemon);
applyAbAttrs("RepeatBerryNextTurnAbAttr", pokemon, null); applyAbAttrs("CudChewConsumeBerryAbAttr", { pokemon });
}); });
this.end(); this.end();
@ -42,7 +42,7 @@ export class BerryPhase extends FieldPhase {
// TODO: If both opponents on field have unnerve, which one displays its message? // TODO: If both opponents on field have unnerve, which one displays its message?
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
pokemon.getOpponents().forEach(opp => applyAbAttrs("PreventBerryUseAbAttr", opp, cancelled)); pokemon.getOpponents().forEach(opp => applyAbAttrs("PreventBerryUseAbAttr", { pokemon: opp, cancelled }));
if (cancelled.value) { if (cancelled.value) {
globalScene.phaseManager.queueMessage( globalScene.phaseManager.queueMessage(
i18next.t("abilityTriggers:preventBerryUse", { i18next.t("abilityTriggers:preventBerryUse", {
@ -70,6 +70,6 @@ export class BerryPhase extends FieldPhase {
globalScene.updateModifiers(pokemon.isPlayer()); globalScene.updateModifiers(pokemon.isPlayer());
// AbilityId.CHEEK_POUCH only works once per round of nom noms // AbilityId.CHEEK_POUCH only works once per round of nom noms
applyAbAttrs("HealFromBerryUseAbAttr", pokemon, new BooleanHolder(false)); applyAbAttrs("HealFromBerryUseAbAttr", { pokemon });
} }
} }

View File

@ -2,7 +2,7 @@ import { BattlerIndex } from "#enums/battler-index";
import { BattleType } from "#enums/battle-type"; import { BattleType } from "#enums/battle-type";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { PLAYER_PARTY_MAX_SIZE } from "#app/constants"; import { PLAYER_PARTY_MAX_SIZE } from "#app/constants";
import { applyAbAttrs, applyPreSummonAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
import { initEncounterAnims, loadEncounterAnimAssets } from "#app/data/battle-anims"; import { initEncounterAnims, loadEncounterAnimAssets } from "#app/data/battle-anims";
import { getCharVariantFromDialogue } from "#app/data/dialogue"; import { getCharVariantFromDialogue } from "#app/data/dialogue";
import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
@ -128,7 +128,7 @@ export class EncounterPhase extends BattlePhase {
.slice(0, !battle.double ? 1 : 2) .slice(0, !battle.double ? 1 : 2)
.reverse() .reverse()
.forEach(playerPokemon => { .forEach(playerPokemon => {
applyAbAttrs("SyncEncounterNatureAbAttr", playerPokemon, null, false, battle.enemyParty[e]); applyAbAttrs("SyncEncounterNatureAbAttr", { pokemon: playerPokemon, target: battle.enemyParty[e] });
}); });
} }
} }
@ -249,7 +249,7 @@ export class EncounterPhase extends BattlePhase {
if (e < (battle.double ? 2 : 1)) { if (e < (battle.double ? 2 : 1)) {
if (battle.battleType === BattleType.WILD) { if (battle.battleType === BattleType.WILD) {
for (const pokemon of globalScene.getField()) { for (const pokemon of globalScene.getField()) {
applyPreSummonAbAttrs("PreSummonAbAttr", pokemon, []); applyAbAttrs("PreSummonAbAttr", { pokemon });
} }
globalScene.field.add(enemyPokemon); globalScene.field.add(enemyPokemon);
battle.seenEnemyPartyMemberIds.add(enemyPokemon.id); battle.seenEnemyPartyMemberIds.add(enemyPokemon.id);

View File

@ -23,6 +23,8 @@ export class EvolutionPhase extends Phase {
protected pokemon: PlayerPokemon; protected pokemon: PlayerPokemon;
protected lastLevel: number; protected lastLevel: number;
protected evoChain: Phaser.Tweens.TweenChain | null = null;
private preEvolvedPokemonName: string; private preEvolvedPokemonName: string;
private evolution: SpeciesFormEvolution | null; private evolution: SpeciesFormEvolution | null;
@ -40,13 +42,23 @@ export class EvolutionPhase extends Phase {
protected pokemonEvoSprite: Phaser.GameObjects.Sprite; protected pokemonEvoSprite: Phaser.GameObjects.Sprite;
protected pokemonEvoTintSprite: Phaser.GameObjects.Sprite; protected pokemonEvoTintSprite: Phaser.GameObjects.Sprite;
constructor(pokemon: PlayerPokemon, evolution: SpeciesFormEvolution | null, lastLevel: number) { /** Whether the evolution can be cancelled by the player */
protected canCancel: boolean;
/**
* @param pokemon - The Pokemon that is evolving
* @param evolution - The form being evolved into
* @param lastLevel - The level at which the Pokemon is evolving
* @param canCancel - Whether the evolution can be cancelled by the player
*/
constructor(pokemon: PlayerPokemon, evolution: SpeciesFormEvolution | null, lastLevel: number, canCancel = true) {
super(); super();
this.pokemon = pokemon; this.pokemon = pokemon;
this.evolution = evolution; this.evolution = evolution;
this.lastLevel = lastLevel; this.lastLevel = lastLevel;
this.fusionSpeciesEvolved = evolution instanceof FusionSpeciesFormEvolution; this.fusionSpeciesEvolved = evolution instanceof FusionSpeciesFormEvolution;
this.canCancel = canCancel;
} }
validate(): boolean { validate(): boolean {
@ -57,198 +69,227 @@ export class EvolutionPhase extends Phase {
return globalScene.ui.setModeForceTransition(UiMode.EVOLUTION_SCENE); return globalScene.ui.setModeForceTransition(UiMode.EVOLUTION_SCENE);
} }
start() { /**
super.start(); * Set up the following evolution assets
* - {@linkcode evolutionContainer}
* - {@linkcode evolutionBaseBg}
* - {@linkcode evolutionBg}
* - {@linkcode evolutionBgOverlay}
* - {@linkcode evolutionOverlay}
*
*/
private setupEvolutionAssets(): void {
this.evolutionHandler = globalScene.ui.getHandler() as EvolutionSceneHandler;
this.evolutionContainer = this.evolutionHandler.evolutionContainer;
this.evolutionBaseBg = globalScene.add.image(0, 0, "default_bg").setOrigin(0);
this.setMode().then(() => { this.evolutionBg = globalScene.add
if (!this.validate()) { .video(0, 0, "evo_bg")
return this.end(); .stop()
} .setOrigin(0)
.setScale(0.4359673025)
.setVisible(false);
globalScene.fadeOutBgm(undefined, false); this.evolutionBgOverlay = globalScene.add
.rectangle(0, 0, globalScene.game.canvas.width / 6, globalScene.game.canvas.height / 6, 0x262626)
.setOrigin(0)
.setAlpha(0);
this.evolutionContainer.add([this.evolutionBaseBg, this.evolutionBgOverlay, this.evolutionBg]);
this.evolutionHandler = globalScene.ui.getHandler() as EvolutionSceneHandler; this.evolutionOverlay = globalScene.add.rectangle(
0,
-globalScene.game.canvas.height / 6,
globalScene.game.canvas.width / 6,
globalScene.game.canvas.height / 6 - 48,
0xffffff,
);
this.evolutionOverlay.setOrigin(0).setAlpha(0);
globalScene.ui.add(this.evolutionOverlay);
}
this.evolutionContainer = this.evolutionHandler.evolutionContainer; /**
* Configure the sprite, setting its pipeline data
* @param pokemon - The pokemon object that the sprite information is configured from
* @param sprite - The sprite object to configure
* @param setPipeline - Whether to also set the pipeline; should be false
* if the sprite is only being updated with new sprite assets
*
*
* @returns The sprite object that was passed in
*/
protected configureSprite(pokemon: Pokemon, sprite: Phaser.GameObjects.Sprite, setPipeline = true): typeof sprite {
const spriteKey = pokemon.getSpriteKey(true);
try {
sprite.play(spriteKey);
} catch (err: unknown) {
console.error(`Failed to play animation for ${spriteKey}`, err);
}
this.evolutionBaseBg = globalScene.add.image(0, 0, "default_bg"); if (setPipeline) {
this.evolutionBaseBg.setOrigin(0, 0); sprite.setPipeline(globalScene.spritePipeline, {
this.evolutionContainer.add(this.evolutionBaseBg); tone: [0.0, 0.0, 0.0, 0.0],
hasShadow: false,
this.evolutionBg = globalScene.add.video(0, 0, "evo_bg").stop(); teraColor: getTypeRgb(pokemon.getTeraType()),
this.evolutionBg.setOrigin(0, 0); isTerastallized: pokemon.isTerastallized,
this.evolutionBg.setScale(0.4359673025); });
this.evolutionBg.setVisible(false); }
this.evolutionContainer.add(this.evolutionBg);
sprite
this.evolutionBgOverlay = globalScene.add.rectangle( .setPipelineData("ignoreTimeTint", true)
0, .setPipelineData("spriteKey", pokemon.getSpriteKey())
0, .setPipelineData("shiny", pokemon.shiny)
globalScene.game.canvas.width / 6, .setPipelineData("variant", pokemon.variant);
globalScene.game.canvas.height / 6,
0x262626, for (let k of ["spriteColors", "fusionSpriteColors"]) {
); if (pokemon.summonData.speciesForm) {
this.evolutionBgOverlay.setOrigin(0, 0); k += "Base";
this.evolutionBgOverlay.setAlpha(0); }
this.evolutionContainer.add(this.evolutionBgOverlay); sprite.pipelineData[k] = pokemon.getSprite().pipelineData[k];
}
const getPokemonSprite = () => {
const ret = globalScene.addPokemonSprite( return sprite;
this.pokemon, }
this.evolutionBaseBg.displayWidth / 2,
this.evolutionBaseBg.displayHeight / 2, private getPokemonSprite(): Phaser.GameObjects.Sprite {
"pkmn__sub", const sprite = globalScene.addPokemonSprite(
); this.pokemon,
ret.setPipeline(globalScene.spritePipeline, { this.evolutionBaseBg.displayWidth / 2,
tone: [0.0, 0.0, 0.0, 0.0], this.evolutionBaseBg.displayHeight / 2,
ignoreTimeTint: true, "pkmn__sub",
}); );
return ret; sprite.setPipeline(globalScene.spritePipeline, {
}; tone: [0.0, 0.0, 0.0, 0.0],
ignoreTimeTint: true,
this.evolutionContainer.add((this.pokemonSprite = getPokemonSprite())); });
this.evolutionContainer.add((this.pokemonTintSprite = getPokemonSprite())); return sprite;
this.evolutionContainer.add((this.pokemonEvoSprite = getPokemonSprite())); }
this.evolutionContainer.add((this.pokemonEvoTintSprite = getPokemonSprite()));
/**
this.pokemonTintSprite.setAlpha(0); * Initialize {@linkcode pokemonSprite}, {@linkcode pokemonTintSprite}, {@linkcode pokemonEvoSprite}, and {@linkcode pokemonEvoTintSprite}
this.pokemonTintSprite.setTintFill(0xffffff); * and add them to the {@linkcode evolutionContainer}
this.pokemonEvoSprite.setVisible(false); */
this.pokemonEvoTintSprite.setVisible(false); private setupPokemonSprites(): void {
this.pokemonEvoTintSprite.setTintFill(0xffffff); this.pokemonSprite = this.configureSprite(this.pokemon, this.getPokemonSprite());
this.pokemonTintSprite = this.configureSprite(
this.evolutionOverlay = globalScene.add.rectangle( this.pokemon,
0, this.getPokemonSprite().setAlpha(0).setTintFill(0xffffff),
-globalScene.game.canvas.height / 6, );
globalScene.game.canvas.width / 6, this.pokemonEvoSprite = this.configureSprite(this.pokemon, this.getPokemonSprite().setVisible(false));
globalScene.game.canvas.height / 6 - 48, this.pokemonEvoTintSprite = this.configureSprite(
0xffffff, this.pokemon,
); this.getPokemonSprite().setVisible(false).setTintFill(0xffffff),
this.evolutionOverlay.setOrigin(0, 0); );
this.evolutionOverlay.setAlpha(0);
globalScene.ui.add(this.evolutionOverlay); this.evolutionContainer.add([
this.pokemonSprite,
[this.pokemonSprite, this.pokemonTintSprite, this.pokemonEvoSprite, this.pokemonEvoTintSprite].map(sprite => { this.pokemonTintSprite,
const spriteKey = this.pokemon.getSpriteKey(true); this.pokemonEvoSprite,
try { this.pokemonEvoTintSprite,
sprite.play(spriteKey); ]);
} catch (err: unknown) { }
console.error(`Failed to play animation for ${spriteKey}`, err);
} async start() {
super.start();
sprite.setPipeline(globalScene.spritePipeline, { await this.setMode();
tone: [0.0, 0.0, 0.0, 0.0],
hasShadow: false, if (!this.validate()) {
teraColor: getTypeRgb(this.pokemon.getTeraType()), return this.end();
isTerastallized: this.pokemon.isTerastallized, }
}); this.setupEvolutionAssets();
sprite.setPipelineData("ignoreTimeTint", true); this.setupPokemonSprites();
sprite.setPipelineData("spriteKey", this.pokemon.getSpriteKey()); this.preEvolvedPokemonName = getPokemonNameWithAffix(this.pokemon);
sprite.setPipelineData("shiny", this.pokemon.shiny); this.doEvolution();
sprite.setPipelineData("variant", this.pokemon.variant); }
["spriteColors", "fusionSpriteColors"].map(k => {
if (this.pokemon.summonData.speciesForm) { /**
k += "Base"; * Update the sprites depicting the evolved Pokemon
} * @param evolvedPokemon - The evolved Pokemon
sprite.pipelineData[k] = this.pokemon.getSprite().pipelineData[k]; */
}); private updateEvolvedPokemonSprites(evolvedPokemon: Pokemon): void {
this.configureSprite(evolvedPokemon, this.pokemonEvoSprite, false);
this.configureSprite(evolvedPokemon, this.pokemonEvoTintSprite, false);
}
/**
* Adds the evolution tween and begins playing it
*/
private playEvolutionAnimation(evolvedPokemon: Pokemon): void {
globalScene.time.delayedCall(1000, () => {
this.evolutionBgm = globalScene.playSoundWithoutBgm("evolution");
globalScene.tweens.add({
targets: this.evolutionBgOverlay,
alpha: 1,
delay: 500,
duration: 1500,
ease: "Sine.easeOut",
onComplete: () => {
globalScene.time.delayedCall(1000, () => {
this.evolutionBg.setVisible(true).play();
});
globalScene.playSound("se/charge");
this.doSpiralUpward();
this.fadeOutPokemonSprite(evolvedPokemon);
},
}); });
this.preEvolvedPokemonName = getPokemonNameWithAffix(this.pokemon);
this.doEvolution();
}); });
} }
private fadeOutPokemonSprite(evolvedPokemon: Pokemon): void {
globalScene.tweens.addCounter({
from: 0,
to: 1,
duration: 2000,
onUpdate: t => {
this.pokemonTintSprite.setAlpha(t.getValue());
},
onComplete: () => {
this.pokemonSprite.setVisible(false);
globalScene.time.delayedCall(1100, () => {
globalScene.playSound("se/beam");
this.doArcDownward();
this.prepareForCycle(evolvedPokemon);
});
},
});
}
/**
* Prepares the evolution cycle by setting up the tint sprites and starting the cycle
*/
private prepareForCycle(evolvedPokemon: Pokemon): void {
globalScene.time.delayedCall(1500, () => {
this.pokemonEvoTintSprite.setScale(0.25).setVisible(true);
this.evolutionHandler.canCancel = this.canCancel;
this.doCycle(1, undefined, () => {
if (this.evolutionHandler.cancelled) {
this.handleFailedEvolution(evolvedPokemon);
} else {
this.handleSuccessEvolution(evolvedPokemon);
}
});
});
}
/**
* Show the evolution text and then commence the evolution animation
*/
doEvolution(): void { doEvolution(): void {
globalScene.ui.showText( globalScene.ui.showText(
i18next.t("menu:evolving", { pokemonName: this.preEvolvedPokemonName }), i18next.t("menu:evolving", { pokemonName: this.preEvolvedPokemonName }),
null, null,
() => { () => {
this.pokemon.cry(); this.pokemon.cry();
this.pokemon.getPossibleEvolution(this.evolution).then(evolvedPokemon => { this.pokemon.getPossibleEvolution(this.evolution).then(evolvedPokemon => {
[this.pokemonEvoSprite, this.pokemonEvoTintSprite].map(sprite => { this.updateEvolvedPokemonSprites(evolvedPokemon);
const spriteKey = evolvedPokemon.getSpriteKey(true); this.playEvolutionAnimation(evolvedPokemon);
try {
sprite.play(spriteKey);
} catch (err: unknown) {
console.error(`Failed to play animation for ${spriteKey}`, err);
}
sprite.setPipelineData("ignoreTimeTint", true);
sprite.setPipelineData("spriteKey", evolvedPokemon.getSpriteKey());
sprite.setPipelineData("shiny", evolvedPokemon.shiny);
sprite.setPipelineData("variant", evolvedPokemon.variant);
["spriteColors", "fusionSpriteColors"].map(k => {
if (evolvedPokemon.summonData.speciesForm) {
k += "Base";
}
sprite.pipelineData[k] = evolvedPokemon.getSprite().pipelineData[k];
});
});
globalScene.time.delayedCall(1000, () => {
this.evolutionBgm = globalScene.playSoundWithoutBgm("evolution");
globalScene.tweens.add({
targets: this.evolutionBgOverlay,
alpha: 1,
delay: 500,
duration: 1500,
ease: "Sine.easeOut",
onComplete: () => {
globalScene.time.delayedCall(1000, () => {
globalScene.tweens.add({
targets: this.evolutionBgOverlay,
alpha: 0,
duration: 250,
});
this.evolutionBg.setVisible(true);
this.evolutionBg.play();
});
globalScene.playSound("se/charge");
this.doSpiralUpward();
globalScene.tweens.addCounter({
from: 0,
to: 1,
duration: 2000,
onUpdate: t => {
this.pokemonTintSprite.setAlpha(t.getValue());
},
onComplete: () => {
this.pokemonSprite.setVisible(false);
globalScene.time.delayedCall(1100, () => {
globalScene.playSound("se/beam");
this.doArcDownward();
globalScene.time.delayedCall(1500, () => {
this.pokemonEvoTintSprite.setScale(0.25);
this.pokemonEvoTintSprite.setVisible(true);
this.evolutionHandler.canCancel = true;
this.doCycle(1).then(success => {
if (success) {
this.handleSuccessEvolution(evolvedPokemon);
} else {
this.handleFailedEvolution(evolvedPokemon);
}
});
});
});
},
});
},
});
});
}); });
}, },
1000, 1000,
); );
} }
/** /** Used exclusively by {@linkcode handleFailedEvolution} to fade out the evolution sprites and music */
* Handles a failed/stopped evolution private fadeOutEvolutionAssets(): void {
* @param evolvedPokemon - The evolved Pokemon
*/
private handleFailedEvolution(evolvedPokemon: Pokemon): void {
this.pokemonSprite.setVisible(true);
this.pokemonTintSprite.setScale(1);
globalScene.tweens.add({ globalScene.tweens.add({
targets: [this.evolutionBg, this.pokemonTintSprite, this.pokemonEvoSprite, this.pokemonEvoTintSprite], targets: [this.evolutionBg, this.pokemonTintSprite, this.pokemonEvoSprite, this.pokemonEvoTintSprite],
alpha: 0, alpha: 0,
@ -257,9 +298,40 @@ export class EvolutionPhase extends Phase {
this.evolutionBg.setVisible(false); this.evolutionBg.setVisible(false);
}, },
}); });
SoundFade.fadeOut(globalScene, this.evolutionBgm, 100); SoundFade.fadeOut(globalScene, this.evolutionBgm, 100);
}
/**
* Show the confirmation prompt for pausing evolutions
* @param endCallback - The callback to call after either option is selected.
* This should end the evolution phase
*/
private showPauseEvolutionConfirmation(endCallback: () => void): void {
globalScene.ui.setOverlayMode(
UiMode.CONFIRM,
() => {
globalScene.ui.revertMode();
this.pokemon.pauseEvolutions = true;
globalScene.ui.showText(
i18next.t("menu:evolutionsPaused", {
pokemonName: this.preEvolvedPokemonName,
}),
null,
endCallback,
3000,
);
},
() => {
globalScene.ui.revertMode();
globalScene.time.delayedCall(3000, endCallback);
},
);
}
/**
* Used exclusively by {@linkcode handleFailedEvolution} to show the failed evolution UI messages
*/
private showFailedEvolutionUI(evolvedPokemon: Pokemon): void {
globalScene.phaseManager.unshiftNew("EndEvolutionPhase"); globalScene.phaseManager.unshiftNew("EndEvolutionPhase");
globalScene.ui.showText( globalScene.ui.showText(
@ -280,25 +352,7 @@ export class EvolutionPhase extends Phase {
evolvedPokemon.destroy(); evolvedPokemon.destroy();
this.end(); this.end();
}; };
globalScene.ui.setOverlayMode( this.showPauseEvolutionConfirmation(end);
UiMode.CONFIRM,
() => {
globalScene.ui.revertMode();
this.pokemon.pauseEvolutions = true;
globalScene.ui.showText(
i18next.t("menu:evolutionsPaused", {
pokemonName: this.preEvolvedPokemonName,
}),
null,
end,
3000,
);
},
() => {
globalScene.ui.revertMode();
globalScene.time.delayedCall(3000, end);
},
);
}, },
); );
}, },
@ -307,6 +361,93 @@ export class EvolutionPhase extends Phase {
); );
} }
/**
* Fade out the evolution assets, show the failed evolution UI messages, and enqueue the EndEvolutionPhase
* @param evolvedPokemon - The evolved Pokemon
*/
private handleFailedEvolution(evolvedPokemon: Pokemon): void {
this.pokemonSprite.setVisible(true);
this.pokemonTintSprite.setScale(1);
this.fadeOutEvolutionAssets();
globalScene.phaseManager.unshiftNew("EndEvolutionPhase");
this.showFailedEvolutionUI(evolvedPokemon);
}
/**
* Fadeout evolution music, play the cry, show the evolution completed text, and end the phase
*/
private onEvolutionComplete(evolvedPokemon: Pokemon) {
SoundFade.fadeOut(globalScene, this.evolutionBgm, 100);
globalScene.time.delayedCall(250, () => {
this.pokemon.cry();
globalScene.time.delayedCall(1250, () => {
globalScene.playSoundWithoutBgm("evolution_fanfare");
evolvedPokemon.destroy();
globalScene.ui.showText(
i18next.t("menu:evolutionDone", {
pokemonName: this.preEvolvedPokemonName,
evolvedPokemonName: this.pokemon.species.getExpandedSpeciesName(),
}),
null,
() => this.end(),
null,
true,
fixedInt(4000),
);
globalScene.time.delayedCall(fixedInt(4250), () => globalScene.playBgm());
});
});
}
private postEvolve(evolvedPokemon: Pokemon): void {
const learnSituation: LearnMoveSituation = this.fusionSpeciesEvolved
? LearnMoveSituation.EVOLUTION_FUSED
: this.pokemon.fusionSpecies
? LearnMoveSituation.EVOLUTION_FUSED_BASE
: LearnMoveSituation.EVOLUTION;
const levelMoves = this.pokemon
.getLevelMoves(this.lastLevel + 1, true, false, false, learnSituation)
.filter(lm => lm[0] === EVOLVE_MOVE);
for (const lm of levelMoves) {
globalScene.phaseManager.unshiftNew("LearnMovePhase", globalScene.getPlayerParty().indexOf(this.pokemon), lm[1]);
}
globalScene.phaseManager.unshiftNew("EndEvolutionPhase");
globalScene.playSound("se/shine");
this.doSpray();
globalScene.tweens.chain({
targets: null,
tweens: [
{
targets: this.evolutionOverlay,
alpha: 1,
duration: 250,
easing: "Sine.easeIn",
onComplete: () => {
this.evolutionBgOverlay.setAlpha(1);
this.evolutionBg.setVisible(false);
},
},
{
targets: [this.evolutionOverlay, this.pokemonEvoTintSprite],
alpha: 0,
duration: 2000,
delay: 150,
easing: "Sine.easeIn",
},
{
targets: this.evolutionBgOverlay,
alpha: 0,
duration: 250,
onComplete: () => this.onEvolutionComplete(evolvedPokemon),
},
],
});
}
/** /**
* Handles a successful evolution * Handles a successful evolution
* @param evolvedPokemon - The evolved Pokemon * @param evolvedPokemon - The evolved Pokemon
@ -316,85 +457,15 @@ export class EvolutionPhase extends Phase {
this.pokemonEvoSprite.setVisible(true); this.pokemonEvoSprite.setVisible(true);
this.doCircleInward(); this.doCircleInward();
const onEvolutionComplete = () => {
SoundFade.fadeOut(globalScene, this.evolutionBgm, 100);
globalScene.time.delayedCall(250, () => {
this.pokemon.cry();
globalScene.time.delayedCall(1250, () => {
globalScene.playSoundWithoutBgm("evolution_fanfare");
evolvedPokemon.destroy();
globalScene.ui.showText(
i18next.t("menu:evolutionDone", {
pokemonName: this.preEvolvedPokemonName,
evolvedPokemonName: this.pokemon.species.getExpandedSpeciesName(),
}),
null,
() => this.end(),
null,
true,
fixedInt(4000),
);
globalScene.time.delayedCall(fixedInt(4250), () => globalScene.playBgm());
});
});
};
globalScene.time.delayedCall(900, () => { globalScene.time.delayedCall(900, () => {
this.evolutionHandler.canCancel = false; this.evolutionHandler.canCancel = this.canCancel;
this.pokemon.evolve(this.evolution, this.pokemon.species).then(() => { this.pokemon.evolve(this.evolution, this.pokemon.species).then(() => this.postEvolve(evolvedPokemon));
const learnSituation: LearnMoveSituation = this.fusionSpeciesEvolved
? LearnMoveSituation.EVOLUTION_FUSED
: this.pokemon.fusionSpecies
? LearnMoveSituation.EVOLUTION_FUSED_BASE
: LearnMoveSituation.EVOLUTION;
const levelMoves = this.pokemon
.getLevelMoves(this.lastLevel + 1, true, false, false, learnSituation)
.filter(lm => lm[0] === EVOLVE_MOVE);
for (const lm of levelMoves) {
globalScene.phaseManager.unshiftNew(
"LearnMovePhase",
globalScene.getPlayerParty().indexOf(this.pokemon),
lm[1],
);
}
globalScene.phaseManager.unshiftNew("EndEvolutionPhase");
globalScene.playSound("se/shine");
this.doSpray();
globalScene.tweens.add({
targets: this.evolutionOverlay,
alpha: 1,
duration: 250,
easing: "Sine.easeIn",
onComplete: () => {
this.evolutionBgOverlay.setAlpha(1);
this.evolutionBg.setVisible(false);
globalScene.tweens.add({
targets: [this.evolutionOverlay, this.pokemonEvoTintSprite],
alpha: 0,
duration: 2000,
delay: 150,
easing: "Sine.easeIn",
onComplete: () => {
globalScene.tweens.add({
targets: this.evolutionBgOverlay,
alpha: 0,
duration: 250,
onComplete: onEvolutionComplete,
});
},
});
},
});
});
}); });
} }
doSpiralUpward() { doSpiralUpward() {
let f = 0; let f = 0;
globalScene.tweens.addCounter({ globalScene.tweens.addCounter({
repeat: 64, repeat: 64,
duration: getFrameMs(1), duration: getFrameMs(1),
@ -430,34 +501,41 @@ export class EvolutionPhase extends Phase {
}); });
} }
doCycle(l: number, lastCycle = 15): Promise<boolean> { /**
return new Promise(resolve => { * Return a tween chain that cycles the evolution sprites
const isLastCycle = l === lastCycle; */
globalScene.tweens.add({ doCycle(cycles: number, lastCycle = 15, onComplete = () => {}): void {
targets: this.pokemonTintSprite, // Make our tween start both at the same time
scale: 0.25, const tweens: Phaser.Types.Tweens.TweenBuilderConfig[] = [];
for (let i = cycles; i <= lastCycle; i += 0.5) {
tweens.push({
targets: [this.pokemonTintSprite, this.pokemonEvoTintSprite],
scale: (_target, _key, _value, targetIndex: number, _totalTargets, _tween) => (targetIndex === 0 ? 0.25 : 1),
ease: "Cubic.easeInOut", ease: "Cubic.easeInOut",
duration: 500 / l, duration: 500 / i,
yoyo: !isLastCycle, yoyo: i !== lastCycle,
});
globalScene.tweens.add({
targets: this.pokemonEvoTintSprite,
scale: 1,
ease: "Cubic.easeInOut",
duration: 500 / l,
yoyo: !isLastCycle,
onComplete: () => { onComplete: () => {
if (this.evolutionHandler.cancelled) { if (this.evolutionHandler.cancelled) {
return resolve(false); // cause the tween chain to complete instantly, skipping the remaining tweens.
this.pokemonEvoTintSprite.setScale(1);
this.pokemonEvoTintSprite.setVisible(false);
this.evoChain?.complete?.();
return;
} }
if (l < lastCycle) { if (i === lastCycle) {
this.doCycle(l + 0.5, lastCycle).then(success => resolve(success)); this.pokemonEvoTintSprite.setScale(1);
} else {
this.pokemonTintSprite.setVisible(false);
resolve(true);
} }
}, },
}); });
}
this.evoChain = globalScene.tweens.chain({
targets: null,
tweens,
onComplete: () => {
this.evoChain = null;
onComplete();
},
}); });
} }

View File

@ -1,11 +1,7 @@
import type { BattlerIndex } from "#enums/battler-index"; import type { BattlerIndex } from "#enums/battler-index";
import { BattleType } from "#enums/battle-type"; import { BattleType } from "#enums/battle-type";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
applyPostFaintAbAttrs,
applyPostKnockOutAbAttrs,
applyPostVictoryAbAttrs,
} from "#app/data/abilities/apply-ab-attrs";
import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type"; import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
import { battleSpecDialogue } from "#app/data/dialogue"; import { battleSpecDialogue } from "#app/data/dialogue";
import { allMoves } from "#app/data/data-lists"; import { allMoves } from "#app/data/data-lists";
@ -117,29 +113,31 @@ export class FaintPhase extends PokemonPhase {
pokemon.resetTera(); pokemon.resetTera();
// TODO: this can be simplified by just checking whether lastAttack is defined
if (pokemon.turnData.attacksReceived?.length) { if (pokemon.turnData.attacksReceived?.length) {
const lastAttack = pokemon.turnData.attacksReceived[0]; const lastAttack = pokemon.turnData.attacksReceived[0];
applyPostFaintAbAttrs( applyAbAttrs("PostFaintAbAttr", {
"PostFaintAbAttr", pokemon: pokemon,
pokemon, // TODO: We should refactor lastAttack's sourceId to forbid null and just use undefined
globalScene.getPokemonById(lastAttack.sourceId)!, attacker: globalScene.getPokemonById(lastAttack.sourceId) ?? undefined,
new PokemonMove(lastAttack.move).getMove(), // TODO: improve the way that we provide the move that knocked out the pokemon...
lastAttack.result, move: new PokemonMove(lastAttack.move).getMove(),
); // TODO: is this bang correct? hitResult: lastAttack.result,
}); // TODO: is this bang correct?
} else { } else {
//If killed by indirect damage, apply post-faint abilities without providing a last move //If killed by indirect damage, apply post-faint abilities without providing a last move
applyPostFaintAbAttrs("PostFaintAbAttr", pokemon); applyAbAttrs("PostFaintAbAttr", { pokemon });
} }
const alivePlayField = globalScene.getField(true); const alivePlayField = globalScene.getField(true);
for (const p of alivePlayField) { for (const p of alivePlayField) {
applyPostKnockOutAbAttrs("PostKnockOutAbAttr", p, pokemon); applyAbAttrs("PostKnockOutAbAttr", { pokemon: p, victim: pokemon });
} }
if (pokemon.turnData.attacksReceived?.length) { if (pokemon.turnData.attacksReceived?.length) {
const defeatSource = this.source; const defeatSource = this.source;
if (defeatSource?.isOnField()) { if (defeatSource?.isOnField()) {
applyPostVictoryAbAttrs("PostVictoryAbAttr", defeatSource); applyAbAttrs("PostVictoryAbAttr", { pokemon: defeatSource });
const pvmove = allMoves[pokemon.turnData.attacksReceived[0].move]; const pvmove = allMoves[pokemon.turnData.attacksReceived[0].move];
const pvattrs = pvmove.getAttrs("PostVictoryStatStageChangeAttr"); const pvattrs = pvmove.getAttrs("PostVictoryStatStageChangeAttr");
if (pvattrs.length) { if (pvattrs.length) {

View File

@ -3,7 +3,7 @@ import { fixedInt } from "#app/utils/common";
import { achvs } from "../system/achv"; import { achvs } from "../system/achv";
import type { SpeciesFormChange } from "../data/pokemon-forms"; import type { SpeciesFormChange } from "../data/pokemon-forms";
import { getSpeciesFormChangeMessage } from "#app/data/pokemon-forms/form-change-triggers"; import { getSpeciesFormChangeMessage } from "#app/data/pokemon-forms/form-change-triggers";
import type { PlayerPokemon } from "../field/pokemon"; import type { default as Pokemon, PlayerPokemon } from "../field/pokemon";
import { UiMode } from "#enums/ui-mode"; import { UiMode } from "#enums/ui-mode";
import type PartyUiHandler from "../ui/party-ui-handler"; import type PartyUiHandler from "../ui/party-ui-handler";
import { getPokemonNameWithAffix } from "../messages"; import { getPokemonNameWithAffix } from "../messages";
@ -34,146 +34,158 @@ export class FormChangePhase extends EvolutionPhase {
return globalScene.ui.setOverlayMode(UiMode.EVOLUTION_SCENE); return globalScene.ui.setOverlayMode(UiMode.EVOLUTION_SCENE);
} }
doEvolution(): void { /**
const preName = getPokemonNameWithAffix(this.pokemon); * Commence the tweens that play after the form change animation finishes
* @param transformedPokemon - The Pokemon after the evolution
this.pokemon.getPossibleForm(this.formChange).then(transformedPokemon => { * @param preName - The name of the Pokemon before the evolution
[this.pokemonEvoSprite, this.pokemonEvoTintSprite].map(sprite => { */
const spriteKey = transformedPokemon.getSpriteKey(true); private postFormChangeTweens(transformedPokemon: Pokemon, preName: string): void {
try { globalScene.tweens.chain({
sprite.play(spriteKey); targets: null,
} catch (err: unknown) { tweens: [
console.error(`Failed to play animation for ${spriteKey}`, err); {
targets: this.evolutionOverlay,
alpha: 1,
duration: 250,
easing: "Sine.easeIn",
onComplete: () => {
this.evolutionBgOverlay.setAlpha(1);
this.evolutionBg.setVisible(false);
},
},
{
targets: [this.evolutionOverlay, this.pokemonEvoTintSprite],
alpha: 0,
duration: 2000,
delay: 150,
easing: "Sine.easeIn",
},
{
targets: this.evolutionBgOverlay,
alpha: 0,
duration: 250,
completeDelay: 250,
onComplete: () => this.pokemon.cry(),
},
],
// 1.25 seconds after the pokemon cry
completeDelay: 1250,
onComplete: () => {
let playEvolutionFanfare = false;
if (this.formChange.formKey.indexOf(SpeciesFormKey.MEGA) > -1) {
globalScene.validateAchv(achvs.MEGA_EVOLVE);
playEvolutionFanfare = true;
} else if (
this.formChange.formKey.indexOf(SpeciesFormKey.GIGANTAMAX) > -1 ||
this.formChange.formKey.indexOf(SpeciesFormKey.ETERNAMAX) > -1
) {
globalScene.validateAchv(achvs.GIGANTAMAX);
playEvolutionFanfare = true;
} }
sprite.setPipelineData("ignoreTimeTint", true); const delay = playEvolutionFanfare ? 4000 : 1750;
sprite.setPipelineData("spriteKey", transformedPokemon.getSpriteKey()); globalScene.playSoundWithoutBgm(playEvolutionFanfare ? "evolution_fanfare" : "minor_fanfare");
sprite.setPipelineData("shiny", transformedPokemon.shiny); transformedPokemon.destroy();
sprite.setPipelineData("variant", transformedPokemon.variant); globalScene.ui.showText(
["spriteColors", "fusionSpriteColors"].map(k => { getSpeciesFormChangeMessage(this.pokemon, this.formChange, preName),
if (transformedPokemon.summonData.speciesForm) { null,
k += "Base"; () => this.end(),
} null,
sprite.pipelineData[k] = transformedPokemon.getSprite().pipelineData[k]; true,
}); fixedInt(delay),
}); );
globalScene.time.delayedCall(fixedInt(delay + 250), () => globalScene.playBgm());
},
});
}
globalScene.time.delayedCall(250, () => { /**
globalScene.tweens.add({ * Commence the animations that occur once the form change evolution cycle ({@linkcode doCycle}) is complete
*
* @privateRemarks
* This would prefer {@linkcode doCycle} to be refactored and de-promisified so this can be moved into {@linkcode beginTweens}
* @param preName - The name of the Pokemon before the evolution
* @param transformedPokemon - The Pokemon being transformed into
*/
private afterCycle(preName: string, transformedPokemon: Pokemon): void {
globalScene.playSound("se/sparkle");
this.pokemonEvoSprite.setVisible(true);
this.doCircleInward();
globalScene.time.delayedCall(900, () => {
this.pokemon.changeForm(this.formChange).then(() => {
if (!this.modal) {
globalScene.phaseManager.unshiftNew("EndEvolutionPhase");
}
globalScene.playSound("se/shine");
this.doSpray();
this.postFormChangeTweens(transformedPokemon, preName);
});
});
}
/**
* Commence the sequence of tweens and events that occur during the evolution animation
* @param preName The name of the Pokemon before the evolution
* @param transformedPokemon The Pokemon after the evolution
*/
private beginTweens(preName: string, transformedPokemon: Pokemon): void {
globalScene.tweens.chain({
// Starts 250ms after sprites have been configured
targets: null,
tweens: [
// Step 1: Fade in the background overlay
{
delay: 250,
targets: this.evolutionBgOverlay, targets: this.evolutionBgOverlay,
alpha: 1, alpha: 1,
delay: 500,
duration: 1500, duration: 1500,
ease: "Sine.easeOut", ease: "Sine.easeOut",
// We want the backkground overlay to fade out after it fades in
onComplete: () => { onComplete: () => {
globalScene.time.delayedCall(1000, () => { globalScene.tweens.add({
globalScene.tweens.add({ targets: this.evolutionBgOverlay,
targets: this.evolutionBgOverlay, alpha: 0,
alpha: 0, duration: 250,
duration: 250, delay: 1000,
});
this.evolutionBg.setVisible(true);
this.evolutionBg.play();
}); });
this.evolutionBg.setVisible(true).play();
},
},
// Step 2: Play the sounds and fade in the tint sprite
{
targets: this.pokemonTintSprite,
alpha: { from: 0, to: 1 },
duration: 2000,
onStart: () => {
globalScene.playSound("se/charge"); globalScene.playSound("se/charge");
this.doSpiralUpward(); this.doSpiralUpward();
globalScene.tweens.addCounter({
from: 0,
to: 1,
duration: 2000,
onUpdate: t => {
this.pokemonTintSprite.setAlpha(t.getValue());
},
onComplete: () => {
this.pokemonSprite.setVisible(false);
globalScene.time.delayedCall(1100, () => {
globalScene.playSound("se/beam");
this.doArcDownward();
globalScene.time.delayedCall(1000, () => {
this.pokemonEvoTintSprite.setScale(0.25);
this.pokemonEvoTintSprite.setVisible(true);
this.doCycle(1, 1).then(_success => {
globalScene.playSound("se/sparkle");
this.pokemonEvoSprite.setVisible(true);
this.doCircleInward();
globalScene.time.delayedCall(900, () => {
this.pokemon.changeForm(this.formChange).then(() => {
if (!this.modal) {
globalScene.phaseManager.unshiftNew("EndEvolutionPhase");
}
globalScene.playSound("se/shine");
this.doSpray();
globalScene.tweens.add({
targets: this.evolutionOverlay,
alpha: 1,
duration: 250,
easing: "Sine.easeIn",
onComplete: () => {
this.evolutionBgOverlay.setAlpha(1);
this.evolutionBg.setVisible(false);
globalScene.tweens.add({
targets: [this.evolutionOverlay, this.pokemonEvoTintSprite],
alpha: 0,
duration: 2000,
delay: 150,
easing: "Sine.easeIn",
onComplete: () => {
globalScene.tweens.add({
targets: this.evolutionBgOverlay,
alpha: 0,
duration: 250,
onComplete: () => {
globalScene.time.delayedCall(250, () => {
this.pokemon.cry();
globalScene.time.delayedCall(1250, () => {
let playEvolutionFanfare = false;
if (this.formChange.formKey.indexOf(SpeciesFormKey.MEGA) > -1) {
globalScene.validateAchv(achvs.MEGA_EVOLVE);
playEvolutionFanfare = true;
} else if (
this.formChange.formKey.indexOf(SpeciesFormKey.GIGANTAMAX) > -1 ||
this.formChange.formKey.indexOf(SpeciesFormKey.ETERNAMAX) > -1
) {
globalScene.validateAchv(achvs.GIGANTAMAX);
playEvolutionFanfare = true;
}
const delay = playEvolutionFanfare ? 4000 : 1750;
globalScene.playSoundWithoutBgm(
playEvolutionFanfare ? "evolution_fanfare" : "minor_fanfare",
);
transformedPokemon.destroy();
globalScene.ui.showText(
getSpeciesFormChangeMessage(this.pokemon, this.formChange, preName),
null,
() => this.end(),
null,
true,
fixedInt(delay),
);
globalScene.time.delayedCall(fixedInt(delay + 250), () =>
globalScene.playBgm(),
);
});
});
},
});
},
});
},
});
});
});
});
});
});
},
});
}, },
onComplete: () => {
this.pokemonSprite.setVisible(false);
},
},
],
// Step 3: Commence the form change animation via doCycle then continue the animation chain with afterCycle
completeDelay: 1100,
onComplete: () => {
globalScene.playSound("se/beam");
this.doArcDownward();
globalScene.time.delayedCall(1000, () => {
this.pokemonEvoTintSprite.setScale(0.25).setVisible(true);
this.doCycle(1, 1, () => this.afterCycle(preName, transformedPokemon));
}); });
}); },
});
}
doEvolution(): void {
const preName = getPokemonNameWithAffix(this.pokemon, false);
this.pokemon.getPossibleForm(this.formChange).then(transformedPokemon => {
this.configureSprite(transformedPokemon, this.pokemonEvoSprite, false);
this.configureSprite(transformedPokemon, this.pokemonEvoTintSprite, false);
this.beginTweens(preName, transformedPokemon);
}); });
} }

View File

@ -1,12 +1,6 @@
import { BattlerIndex } from "#enums/battler-index"; import { BattlerIndex } from "#enums/battler-index";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
applyExecutedMoveAbAttrs,
applyPostAttackAbAttrs,
applyPostDamageAbAttrs,
applyPostDefendAbAttrs,
applyPreAttackAbAttrs,
} from "#app/data/abilities/apply-ab-attrs";
import { ConditionalProtectTag } from "#app/data/arena-tag"; import { ConditionalProtectTag } from "#app/data/arena-tag";
import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagSide } from "#enums/arena-tag-side";
import { MoveAnim } from "#app/data/battle-anims"; import { MoveAnim } from "#app/data/battle-anims";
@ -322,7 +316,7 @@ export class MoveEffectPhase extends PokemonPhase {
// Assume single target for multi hit // Assume single target for multi hit
applyMoveAttrs("MultiHitAttr", user, this.getFirstTarget() ?? null, move, hitCount); applyMoveAttrs("MultiHitAttr", user, this.getFirstTarget() ?? null, move, hitCount);
// If Parental Bond is applicable, add another hit // If Parental Bond is applicable, add another hit
applyPreAttackAbAttrs("AddSecondStrikeAbAttr", user, null, move, false, hitCount, null); applyAbAttrs("AddSecondStrikeAbAttr", { pokemon: user, move, hitCount });
// If Multi-Lens is applicable, add hits equal to the number of held Multi-Lenses // If Multi-Lens is applicable, add hits equal to the number of held Multi-Lenses
globalScene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, move.id, hitCount); globalScene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, move.id, hitCount);
// Set the user's relevant turnData fields to reflect the final hit count // Set the user's relevant turnData fields to reflect the final hit count
@ -370,7 +364,7 @@ export class MoveEffectPhase extends PokemonPhase {
// Add to the move history entry // Add to the move history entry
if (this.firstHit) { if (this.firstHit) {
user.pushMoveHistory(this.moveHistoryEntry); user.pushMoveHistory(this.moveHistoryEntry);
applyExecutedMoveAbAttrs("ExecutedMoveAbAttr", user); applyAbAttrs("ExecutedMoveAbAttr", { pokemon: user });
} }
try { try {
@ -439,7 +433,7 @@ export class MoveEffectPhase extends PokemonPhase {
* @param hitResult - The {@linkcode HitResult} of the attempted move * @param hitResult - The {@linkcode HitResult} of the attempted move
*/ */
protected applyOnGetHitAbEffects(user: Pokemon, target: Pokemon, hitResult: HitResult): void { protected applyOnGetHitAbEffects(user: Pokemon, target: Pokemon, hitResult: HitResult): void {
applyPostDefendAbAttrs("PostDefendAbAttr", target, user, this.move, hitResult); applyAbAttrs("PostDefendAbAttr", { pokemon: target, opponent: user, move: this.move, hitResult });
target.lapseTags(BattlerTagLapseType.AFTER_HIT); target.lapseTags(BattlerTagLapseType.AFTER_HIT);
} }
@ -805,7 +799,9 @@ export class MoveEffectPhase extends PokemonPhase {
// Multi-hit check for Wimp Out/Emergency Exit // Multi-hit check for Wimp Out/Emergency Exit
if (user.turnData.hitCount > 1) { if (user.turnData.hitCount > 1) {
applyPostDamageAbAttrs("PostDamageAbAttr", target, 0, target.hasPassive(), false, [], user); // TODO: Investigate why 0 is being passed for damage amount here
// and then determing if refactoring `applyMove` to return the damage dealt is appropriate.
applyAbAttrs("PostDamageAbAttr", { pokemon: target, damage: 0, source: user });
} }
} }
} }
@ -999,7 +995,7 @@ export class MoveEffectPhase extends PokemonPhase {
this.triggerMoveEffects(MoveEffectTrigger.POST_APPLY, user, target, firstTarget, false); this.triggerMoveEffects(MoveEffectTrigger.POST_APPLY, user, target, firstTarget, false);
this.applyHeldItemFlinchCheck(user, target, dealsDamage); this.applyHeldItemFlinchCheck(user, target, dealsDamage);
this.applyOnGetHitAbEffects(user, target, hitResult); this.applyOnGetHitAbEffects(user, target, hitResult);
applyPostAttackAbAttrs("PostAttackAbAttr", user, target, this.move, hitResult); applyAbAttrs("PostAttackAbAttr", { pokemon: user, opponent: target, move: this.move, hitResult });
// We assume only enemy Pokemon are able to have the EnemyAttackStatusEffectChanceModifier from tokens // We assume only enemy Pokemon are able to have the EnemyAttackStatusEffectChanceModifier from tokens
if (!user.isPlayer() && this.move.is("AttackMove")) { if (!user.isPlayer() && this.move.is("AttackMove")) {

View File

@ -2,8 +2,8 @@ import { globalScene } from "#app/global-scene";
import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type"; import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
import { PokemonPhase } from "./pokemon-phase"; import { PokemonPhase } from "./pokemon-phase";
import type { BattlerIndex } from "#enums/battler-index"; import type { BattlerIndex } from "#enums/battler-index";
import { applyPostSummonAbAttrs } from "#app/data/abilities/apply-ab-attrs";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
export class MoveEndPhase extends PokemonPhase { export class MoveEndPhase extends PokemonPhase {
public readonly phaseName = "MoveEndPhase"; public readonly phaseName = "MoveEndPhase";
@ -30,7 +30,7 @@ export class MoveEndPhase extends PokemonPhase {
globalScene.arena.setIgnoreAbilities(false); globalScene.arena.setIgnoreAbilities(false);
for (const target of this.targets) { for (const target of this.targets) {
if (target) { if (target) {
applyPostSummonAbAttrs("PostSummonRemoveEffectAbAttr", target); applyAbAttrs("PostSummonRemoveEffectAbAttr", { pokemon: target });
} }
} }

View File

@ -1,6 +1,6 @@
import { BattlerIndex } from "#enums/battler-index"; import { BattlerIndex } from "#enums/battler-index";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { applyAbAttrs, applyPostMoveUsedAbAttrs, applyPreAttackAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
import type { DelayedAttackTag } from "#app/data/arena-tag"; import type { DelayedAttackTag } from "#app/data/arena-tag";
import { CommonAnim } from "#enums/move-anims-common"; import { CommonAnim } from "#enums/move-anims-common";
import { CenterOfAttentionTag } from "#app/data/battler-tags"; import { CenterOfAttentionTag } from "#app/data/battler-tags";
@ -228,14 +228,11 @@ export class MovePhase extends BattlePhase {
case StatusEffect.SLEEP: { case StatusEffect.SLEEP: {
applyMoveAttrs("BypassSleepAttr", this.pokemon, null, this.move.getMove()); applyMoveAttrs("BypassSleepAttr", this.pokemon, null, this.move.getMove());
const turnsRemaining = new NumberHolder(this.pokemon.status.sleepTurnsRemaining ?? 0); const turnsRemaining = new NumberHolder(this.pokemon.status.sleepTurnsRemaining ?? 0);
applyAbAttrs( applyAbAttrs("ReduceStatusEffectDurationAbAttr", {
"ReduceStatusEffectDurationAbAttr", pokemon: this.pokemon,
this.pokemon, statusEffect: this.pokemon.status.effect,
null, duration: turnsRemaining,
false, });
this.pokemon.status.effect,
turnsRemaining,
);
this.pokemon.status.sleepTurnsRemaining = turnsRemaining.value; this.pokemon.status.sleepTurnsRemaining = turnsRemaining.value;
healed = this.pokemon.status.sleepTurnsRemaining <= 0; healed = this.pokemon.status.sleepTurnsRemaining <= 0;
activated = !healed && !this.pokemon.getTag(BattlerTagType.BYPASS_SLEEP); activated = !healed && !this.pokemon.getTag(BattlerTagType.BYPASS_SLEEP);
@ -394,7 +391,8 @@ export class MovePhase extends BattlePhase {
*/ */
if (success) { if (success) {
const move = this.move.getMove(); const move = this.move.getMove();
applyPreAttackAbAttrs("PokemonTypeChangeAbAttr", this.pokemon, null, move); // TODO: Investigate whether PokemonTypeChangeAbAttr can drop the "opponent" parameter
applyAbAttrs("PokemonTypeChangeAbAttr", { pokemon: this.pokemon, move, opponent: targets[0] });
globalScene.phaseManager.unshiftNew( globalScene.phaseManager.unshiftNew(
"MoveEffectPhase", "MoveEffectPhase",
this.pokemon.getBattlerIndex(), this.pokemon.getBattlerIndex(),
@ -404,7 +402,11 @@ export class MovePhase extends BattlePhase {
); );
} else { } else {
if ([MoveId.ROAR, MoveId.WHIRLWIND, MoveId.TRICK_OR_TREAT, MoveId.FORESTS_CURSE].includes(this.move.moveId)) { if ([MoveId.ROAR, MoveId.WHIRLWIND, MoveId.TRICK_OR_TREAT, MoveId.FORESTS_CURSE].includes(this.move.moveId)) {
applyPreAttackAbAttrs("PokemonTypeChangeAbAttr", this.pokemon, null, this.move.getMove()); applyAbAttrs("PokemonTypeChangeAbAttr", {
pokemon: this.pokemon,
move: this.move.getMove(),
opponent: targets[0],
});
} }
this.pokemon.pushMoveHistory({ this.pokemon.pushMoveHistory({
@ -436,7 +438,7 @@ export class MovePhase extends BattlePhase {
if (this.move.getMove().hasFlag(MoveFlags.DANCE_MOVE) && !dancerModes.includes(this.useMode)) { if (this.move.getMove().hasFlag(MoveFlags.DANCE_MOVE) && !dancerModes.includes(this.useMode)) {
// TODO: Fix in dancer PR to move to MEP for hit checks // TODO: Fix in dancer PR to move to MEP for hit checks
globalScene.getField(true).forEach(pokemon => { globalScene.getField(true).forEach(pokemon => {
applyPostMoveUsedAbAttrs("PostMoveUsedAbAttr", pokemon, this.move, this.pokemon, this.targets); applyAbAttrs("PostMoveUsedAbAttr", { pokemon, move: this.move, source: this.pokemon, targets: this.targets });
}); });
} }
} }
@ -468,7 +470,11 @@ export class MovePhase extends BattlePhase {
} }
// Protean and Libero apply on the charging turn of charge moves // Protean and Libero apply on the charging turn of charge moves
applyPreAttackAbAttrs("PokemonTypeChangeAbAttr", this.pokemon, null, this.move.getMove()); applyAbAttrs("PokemonTypeChangeAbAttr", {
pokemon: this.pokemon,
move: this.move.getMove(),
opponent: targets[0],
});
globalScene.phaseManager.unshiftNew( globalScene.phaseManager.unshiftNew(
"MoveChargePhase", "MoveChargePhase",
@ -521,7 +527,12 @@ export class MovePhase extends BattlePhase {
.getField(true) .getField(true)
.filter(p => p !== this.pokemon) .filter(p => p !== this.pokemon)
.forEach(p => .forEach(p =>
applyAbAttrs("RedirectMoveAbAttr", p, null, false, this.move.moveId, redirectTarget, this.pokemon), applyAbAttrs("RedirectMoveAbAttr", {
pokemon: p,
moveId: this.move.moveId,
targetIndex: redirectTarget,
sourcePokemon: this.pokemon,
}),
); );
/** `true` if an Ability is responsible for redirecting the move to another target; `false` otherwise */ /** `true` if an Ability is responsible for redirecting the move to another target; `false` otherwise */

View File

@ -14,7 +14,7 @@ export class NewBiomeEncounterPhase extends NextEncounterPhase {
if (pokemon) { if (pokemon) {
pokemon.resetBattleAndWaveData(); pokemon.resetBattleAndWaveData();
if (pokemon.isOnField()) { if (pokemon.isOnField()) {
applyAbAttrs("PostBiomeChangeAbAttr", pokemon, null); applyAbAttrs("PostBiomeChangeAbAttr", { pokemon });
} }
} }
} }

View File

@ -8,7 +8,7 @@ import type Pokemon from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import { PokemonPhase } from "./pokemon-phase"; import { PokemonPhase } from "./pokemon-phase";
import { SpeciesFormChangeStatusEffectTrigger } from "#app/data/pokemon-forms/form-change-triggers"; import { SpeciesFormChangeStatusEffectTrigger } from "#app/data/pokemon-forms/form-change-triggers";
import { applyPostSetStatusAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
import { isNullOrUndefined } from "#app/utils/common"; import { isNullOrUndefined } from "#app/utils/common";
export class ObtainStatusEffectPhase extends PokemonPhase { export class ObtainStatusEffectPhase extends PokemonPhase {
@ -53,7 +53,11 @@ export class ObtainStatusEffectPhase extends PokemonPhase {
globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeStatusEffectTrigger, true); globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeStatusEffectTrigger, true);
// If mold breaker etc was used to set this status, it shouldn't apply to abilities activated afterwards // If mold breaker etc was used to set this status, it shouldn't apply to abilities activated afterwards
globalScene.arena.setIgnoreAbilities(false); globalScene.arena.setIgnoreAbilities(false);
applyPostSetStatusAbAttrs("PostSetStatusAbAttr", pokemon, this.statusEffect, this.sourcePokemon); applyAbAttrs("PostSetStatusAbAttr", {
pokemon,
effect: this.statusEffect,
sourcePokemon: this.sourcePokemon ?? undefined,
});
} }
this.end(); this.end();
}); });

View File

@ -1,4 +1,4 @@
import { applyPostSummonAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
import { PostSummonPhase } from "#app/phases/post-summon-phase"; import { PostSummonPhase } from "#app/phases/post-summon-phase";
import type { BattlerIndex } from "#enums/battler-index"; import type { BattlerIndex } from "#enums/battler-index";
@ -16,7 +16,8 @@ export class PostSummonActivateAbilityPhase extends PostSummonPhase {
} }
start() { start() {
applyPostSummonAbAttrs("PostSummonAbAttr", this.getPokemon(), this.passive, false); // TODO: Check with Dean on whether or not passive must be provided to `this.passive`
applyAbAttrs("PostSummonAbAttr", { pokemon: this.getPokemon(), passive: this.passive });
this.end(); this.end();
} }

View File

@ -28,7 +28,7 @@ export class PostSummonPhase extends PokemonPhase {
const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField();
for (const p of field) { for (const p of field) {
applyAbAttrs("CommanderAbAttr", p, null, false); applyAbAttrs("CommanderAbAttr", { pokemon: p });
} }
this.end(); this.end();

View File

@ -1,6 +1,6 @@
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import type { BattlerIndex } from "#enums/battler-index"; import type { BattlerIndex } from "#enums/battler-index";
import { applyAbAttrs, applyPostDamageAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
import { CommonBattleAnim } from "#app/data/battle-anims"; import { CommonBattleAnim } from "#app/data/battle-anims";
import { CommonAnim } from "#enums/move-anims-common"; import { CommonAnim } from "#enums/move-anims-common";
import { getStatusEffectActivationText } from "#app/data/status-effect"; import { getStatusEffectActivationText } from "#app/data/status-effect";
@ -22,8 +22,8 @@ export class PostTurnStatusEffectPhase extends PokemonPhase {
if (pokemon?.isActive(true) && pokemon.status && pokemon.status.isPostTurn() && !pokemon.switchOutStatus) { if (pokemon?.isActive(true) && pokemon.status && pokemon.status.isPostTurn() && !pokemon.switchOutStatus) {
pokemon.status.incrementTurn(); pokemon.status.incrementTurn();
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelled); applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled });
applyAbAttrs("BlockStatusDamageAbAttr", pokemon, cancelled); applyAbAttrs("BlockStatusDamageAbAttr", { pokemon, cancelled });
if (!cancelled.value) { if (!cancelled.value) {
globalScene.phaseManager.queueMessage( globalScene.phaseManager.queueMessage(
@ -39,14 +39,14 @@ export class PostTurnStatusEffectPhase extends PokemonPhase {
break; break;
case StatusEffect.BURN: case StatusEffect.BURN:
damage.value = Math.max(pokemon.getMaxHp() >> 4, 1); damage.value = Math.max(pokemon.getMaxHp() >> 4, 1);
applyAbAttrs("ReduceBurnDamageAbAttr", pokemon, null, false, damage); applyAbAttrs("ReduceBurnDamageAbAttr", { pokemon, burnDamage: damage });
break; break;
} }
if (damage.value) { if (damage.value) {
// Set preventEndure flag to avoid pokemon surviving thanks to focus band, sturdy, endure ... // Set preventEndure flag to avoid pokemon surviving thanks to focus band, sturdy, endure ...
globalScene.damageNumberHandler.add(this.getPokemon(), pokemon.damage(damage.value, false, true)); globalScene.damageNumberHandler.add(this.getPokemon(), pokemon.damage(damage.value, false, true));
pokemon.updateInfo(); pokemon.updateInfo();
applyPostDamageAbAttrs("PostDamageAbAttr", pokemon, damage.value, pokemon.hasPassive(), false, []); applyAbAttrs("PostDamageAbAttr", { pokemon, damage: damage.value });
} }
new CommonBattleAnim(CommonAnim.POISON + (pokemon.status.effect - 1), pokemon).play(false, () => this.end()); new CommonBattleAnim(CommonAnim.POISON + (pokemon.status.effect - 1), pokemon).play(false, () => this.end());
} else { } else {

View File

@ -181,9 +181,10 @@ export class QuietFormChangePhase extends BattlePhase {
} }
} }
if (this.formChange.trigger instanceof SpeciesFormChangeTeraTrigger) { if (this.formChange.trigger instanceof SpeciesFormChangeTeraTrigger) {
applyAbAttrs("PostTeraFormChangeStatChangeAbAttr", this.pokemon, null); const params = { pokemon: this.pokemon };
applyAbAttrs("ClearWeatherAbAttr", this.pokemon, null); applyAbAttrs("PostTeraFormChangeStatChangeAbAttr", params);
applyAbAttrs("ClearTerrainAbAttr", this.pokemon, null); applyAbAttrs("ClearWeatherAbAttr", params);
applyAbAttrs("ClearTerrainAbAttr", params);
} }
super.end(); super.end();

View File

@ -1,10 +1,6 @@
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import type { BattlerIndex } from "#enums/battler-index"; import type { BattlerIndex } from "#enums/battler-index";
import { import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
applyAbAttrs,
applyPostStatStageChangeAbAttrs,
applyPreStatStageChangeAbAttrs,
} from "#app/data/abilities/apply-ab-attrs";
import { MistTag } from "#app/data/arena-tag"; import { MistTag } from "#app/data/arena-tag";
import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagSide } from "#enums/arena-tag-side";
import type { ArenaTag } from "#app/data/arena-tag"; import type { ArenaTag } from "#app/data/arena-tag";
@ -18,6 +14,10 @@ import { PokemonPhase } from "./pokemon-phase";
import { Stat, type BattleStat, getStatKey, getStatStageChangeDescriptionKey } from "#enums/stat"; import { Stat, type BattleStat, getStatKey, getStatStageChangeDescriptionKey } from "#enums/stat";
import { OctolockTag } from "#app/data/battler-tags"; import { OctolockTag } from "#app/data/battler-tags";
import { ArenaTagType } from "#app/enums/arena-tag-type"; import { ArenaTagType } from "#app/enums/arena-tag-type";
import type {
ConditionalUserFieldProtectStatAbAttrParams,
PreStatStageChangeAbAttrParams,
} from "#app/@types/ability-types";
export type StatStageChangeCallback = ( export type StatStageChangeCallback = (
target: Pokemon | null, target: Pokemon | null,
@ -126,7 +126,7 @@ export class StatStageChangePhase extends PokemonPhase {
const stages = new NumberHolder(this.stages); const stages = new NumberHolder(this.stages);
if (!this.ignoreAbilities) { if (!this.ignoreAbilities) {
applyAbAttrs("StatStageChangeMultiplierAbAttr", pokemon, null, false, stages); applyAbAttrs("StatStageChangeMultiplierAbAttr", { pokemon, numStages: stages });
} }
let simulate = false; let simulate = false;
@ -146,42 +146,38 @@ export class StatStageChangePhase extends PokemonPhase {
} }
if (!cancelled.value && !this.selfTarget && stages.value < 0) { if (!cancelled.value && !this.selfTarget && stages.value < 0) {
applyPreStatStageChangeAbAttrs("ProtectStatAbAttr", pokemon, stat, cancelled, simulate); const abAttrParams: PreStatStageChangeAbAttrParams & ConditionalUserFieldProtectStatAbAttrParams = {
applyPreStatStageChangeAbAttrs(
"ConditionalUserFieldProtectStatAbAttr",
pokemon, pokemon,
stat, stat,
cancelled, cancelled,
simulate, simulated: simulate,
pokemon, target: pokemon,
); stages: this.stages,
};
applyAbAttrs("ProtectStatAbAttr", abAttrParams);
applyAbAttrs("ConditionalUserFieldProtectStatAbAttr", abAttrParams);
// TODO: Consider skipping this call if `cancelled` is false.
const ally = pokemon.getAlly(); const ally = pokemon.getAlly();
if (!isNullOrUndefined(ally)) { if (!isNullOrUndefined(ally)) {
applyPreStatStageChangeAbAttrs( applyAbAttrs("ConditionalUserFieldProtectStatAbAttr", { ...abAttrParams, pokemon: ally });
"ConditionalUserFieldProtectStatAbAttr",
ally,
stat,
cancelled,
simulate,
pokemon,
);
} }
/** Potential stat reflection due to Mirror Armor, does not apply to Octolock end of turn effect */ /** Potential stat reflection due to Mirror Armor, does not apply to Octolock end of turn effect */
if ( if (
opponentPokemon !== undefined && opponentPokemon !== undefined &&
// TODO: investigate whether this is stoping mirror armor from applying to non-octolock
// reasons for stat drops if the user has the Octolock tag
!pokemon.findTag(t => t instanceof OctolockTag) && !pokemon.findTag(t => t instanceof OctolockTag) &&
!this.comingFromMirrorArmorUser !this.comingFromMirrorArmorUser
) { ) {
applyPreStatStageChangeAbAttrs( applyAbAttrs("ReflectStatStageChangeAbAttr", {
"ReflectStatStageChangeAbAttr",
pokemon, pokemon,
stat, stat,
cancelled, cancelled,
simulate, simulated: simulate,
opponentPokemon, source: opponentPokemon,
this.stages, stages: this.stages,
); });
} }
} }
@ -222,17 +218,16 @@ export class StatStageChangePhase extends PokemonPhase {
if (stages.value > 0 && this.canBeCopied) { if (stages.value > 0 && this.canBeCopied) {
for (const opponent of pokemon.getOpponents()) { for (const opponent of pokemon.getOpponents()) {
applyAbAttrs("StatStageChangeCopyAbAttr", opponent, null, false, this.stats, stages.value); applyAbAttrs("StatStageChangeCopyAbAttr", { pokemon: opponent, stats: this.stats, numStages: stages.value });
} }
} }
applyPostStatStageChangeAbAttrs( applyAbAttrs("PostStatStageChangeAbAttr", {
"PostStatStageChangeAbAttr",
pokemon, pokemon,
filteredStats, stats: filteredStats,
this.stages, stages: this.stages,
this.selfTarget, selfTarget: this.selfTarget,
); });
// Look for any other stat change phases; if this is the last one, do White Herb check // Look for any other stat change phases; if this is the last one, do White Herb check
const existingPhase = globalScene.phaseManager.findPhase( const existingPhase = globalScene.phaseManager.findPhase(

View File

@ -10,7 +10,7 @@ import { getPokemonNameWithAffix } from "#app/messages";
import i18next from "i18next"; import i18next from "i18next";
import { PartyMemberPokemonPhase } from "./party-member-pokemon-phase"; import { PartyMemberPokemonPhase } from "./party-member-pokemon-phase";
import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
import { applyPreSummonAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
export class SummonPhase extends PartyMemberPokemonPhase { export class SummonPhase extends PartyMemberPokemonPhase {
@ -27,7 +27,7 @@ export class SummonPhase extends PartyMemberPokemonPhase {
start() { start() {
super.start(); super.start();
applyPreSummonAbAttrs("PreSummonAbAttr", this.getPokemon()); applyAbAttrs("PreSummonAbAttr", { pokemon: this.getPokemon() });
this.preSummon(); this.preSummon();
} }

View File

@ -1,5 +1,5 @@
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { applyPreSummonAbAttrs, applyPreSwitchOutAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
import { allMoves } from "#app/data/data-lists"; import { allMoves } from "#app/data/data-lists";
import { getPokeballTintColor } from "#app/data/pokeball"; import { getPokeballTintColor } from "#app/data/pokeball";
import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms/form-change-triggers"; import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms/form-change-triggers";
@ -124,8 +124,8 @@ export class SwitchSummonPhase extends SummonPhase {
switchedInPokemon.resetSummonData(); switchedInPokemon.resetSummonData();
switchedInPokemon.loadAssets(true); switchedInPokemon.loadAssets(true);
applyPreSummonAbAttrs("PreSummonAbAttr", switchedInPokemon); applyAbAttrs("PreSummonAbAttr", { pokemon: switchedInPokemon });
applyPreSwitchOutAbAttrs("PreSwitchOutAbAttr", this.lastPokemon); applyAbAttrs("PreSwitchOutAbAttr", { pokemon: this.lastPokemon });
if (!switchedInPokemon) { if (!switchedInPokemon) {
this.end(); this.end();
return; return;

View File

@ -1,4 +1,4 @@
import { applyPostTurnAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type"; import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
import { TerrainType } from "#app/data/terrain"; import { TerrainType } from "#app/data/terrain";
import { WeatherType } from "#app/enums/weather-type"; import { WeatherType } from "#app/enums/weather-type";
@ -49,7 +49,7 @@ export class TurnEndPhase extends FieldPhase {
globalScene.applyModifier(EnemyStatusEffectHealChanceModifier, false, pokemon); globalScene.applyModifier(EnemyStatusEffectHealChanceModifier, false, pokemon);
} }
applyPostTurnAbAttrs("PostTurnAbAttr", pokemon); applyAbAttrs("PostTurnAbAttr", { pokemon });
} }
globalScene.applyModifiers(TurnStatusEffectModifier, pokemon.isPlayer(), pokemon); globalScene.applyModifiers(TurnStatusEffectModifier, pokemon.isPlayer(), pokemon);

View File

@ -66,8 +66,12 @@ export class TurnStartPhase extends FieldPhase {
globalScene.getField(true).forEach(p => { globalScene.getField(true).forEach(p => {
const bypassSpeed = new BooleanHolder(false); const bypassSpeed = new BooleanHolder(false);
const canCheckHeldItems = new BooleanHolder(true); const canCheckHeldItems = new BooleanHolder(true);
applyAbAttrs("BypassSpeedChanceAbAttr", p, null, false, bypassSpeed); applyAbAttrs("BypassSpeedChanceAbAttr", { pokemon: p, bypass: bypassSpeed });
applyAbAttrs("PreventBypassSpeedChanceAbAttr", p, null, false, bypassSpeed, canCheckHeldItems); applyAbAttrs("PreventBypassSpeedChanceAbAttr", {
pokemon: p,
bypass: bypassSpeed,
canCheckHeldItems: canCheckHeldItems,
});
if (canCheckHeldItems.value) { if (canCheckHeldItems.value) {
globalScene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p, bypassSpeed); globalScene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p, bypassSpeed);
} }

View File

@ -1,9 +1,5 @@
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
applyPreWeatherEffectAbAttrs,
applyAbAttrs,
applyPostWeatherLapseAbAttrs,
} from "#app/data/abilities/apply-ab-attrs";
import { CommonAnim } from "#enums/move-anims-common"; import { CommonAnim } from "#enums/move-anims-common";
import type { Weather } from "#app/data/weather"; import type { Weather } from "#app/data/weather";
import { getWeatherDamageMessage, getWeatherLapseMessage } from "#app/data/weather"; import { getWeatherDamageMessage, getWeatherLapseMessage } from "#app/data/weather";
@ -41,15 +37,15 @@ export class WeatherEffectPhase extends CommonAnimPhase {
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
this.executeForAll((pokemon: Pokemon) => this.executeForAll((pokemon: Pokemon) =>
applyPreWeatherEffectAbAttrs("SuppressWeatherEffectAbAttr", pokemon, this.weather, cancelled), applyAbAttrs("SuppressWeatherEffectAbAttr", { pokemon, weather: this.weather, cancelled }),
); );
if (!cancelled.value) { if (!cancelled.value) {
const inflictDamage = (pokemon: Pokemon) => { const inflictDamage = (pokemon: Pokemon) => {
const cancelled = new BooleanHolder(false); const cancelled = new BooleanHolder(false);
applyPreWeatherEffectAbAttrs("PreWeatherDamageAbAttr", pokemon, this.weather, cancelled); applyAbAttrs("PreWeatherDamageAbAttr", { pokemon, weather: this.weather, cancelled });
applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelled); applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled });
if ( if (
cancelled.value || cancelled.value ||
@ -80,7 +76,7 @@ export class WeatherEffectPhase extends CommonAnimPhase {
globalScene.ui.showText(getWeatherLapseMessage(this.weather.weatherType) ?? "", null, () => { globalScene.ui.showText(getWeatherLapseMessage(this.weather.weatherType) ?? "", null, () => {
this.executeForAll((pokemon: Pokemon) => { this.executeForAll((pokemon: Pokemon) => {
if (!pokemon.switchOutStatus) { if (!pokemon.switchOutStatus) {
applyPostWeatherLapseAbAttrs("PostWeatherLapseAbAttr", pokemon, this.weather); applyAbAttrs("PostWeatherLapseAbAttr", { pokemon, weather: this.weather });
} }
}); });

View File

@ -5,9 +5,13 @@ import type BattleScene from "#app/battle-scene";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { FixedInt } from "#app/utils/common"; import { FixedInt } from "#app/utils/common";
type TweenManager = typeof Phaser.Tweens.TweenManager.prototype;
/** The set of properties to mutate */
const PROPERTIES = ["delay", "completeDelay", "loopDelay", "duration", "repeatDelay", "hold", "startDelay"];
type FadeInType = typeof FadeIn; type FadeInType = typeof FadeIn;
type FadeOutType = typeof FadeOut; type FadeOutType = typeof FadeOut;
export function initGameSpeed() { export function initGameSpeed() {
const thisArg = this as BattleScene; const thisArg = this as BattleScene;
@ -18,14 +22,44 @@ export function initGameSpeed() {
return thisArg.gameSpeed === 1 ? value : Math.ceil((value /= thisArg.gameSpeed)); return thisArg.gameSpeed === 1 ? value : Math.ceil((value /= thisArg.gameSpeed));
}; };
const originalAddEvent = this.time.addEvent; // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Complexity is necessary here
const mutateProperties = (obj: any, allowArray = false) => {
// We do not mutate Tweens or TweenChain objects themselves.
if (obj instanceof Phaser.Tweens.Tween || obj instanceof Phaser.Tweens.TweenChain) {
return;
}
// If allowArray is true then check if first obj is an array and if so, mutate the tweens inside
if (allowArray && Array.isArray(obj)) {
for (const tween of obj) {
mutateProperties(tween);
}
return;
}
for (const prop of PROPERTIES) {
const objProp = obj[prop];
if (typeof objProp === "number" || objProp instanceof FixedInt) {
obj[prop] = transformValue(objProp);
}
}
// If the object has a 'tweens' property that is an array, then it is a tween chain
// and we need to mutate its properties as well
if (obj.tweens && Array.isArray(obj.tweens)) {
for (const tween of obj.tweens) {
mutateProperties(tween);
}
}
};
const originalAddEvent: typeof Phaser.Time.Clock.prototype.addEvent = this.time.addEvent;
this.time.addEvent = function (config: Phaser.Time.TimerEvent | Phaser.Types.Time.TimerEventConfig) { this.time.addEvent = function (config: Phaser.Time.TimerEvent | Phaser.Types.Time.TimerEventConfig) {
if (!(config instanceof Phaser.Time.TimerEvent) && config.delay) { if (!(config instanceof Phaser.Time.TimerEvent) && config.delay) {
config.delay = transformValue(config.delay); config.delay = transformValue(config.delay);
} }
return originalAddEvent.apply(this, [config]); return originalAddEvent.apply(this, [config]);
}; };
const originalTweensAdd = this.tweens.add; const originalTweensAdd: TweenManager["add"] = this.tweens.add;
this.tweens.add = function ( this.tweens.add = function (
config: config:
| Phaser.Types.Tweens.TweenBuilderConfig | Phaser.Types.Tweens.TweenBuilderConfig
@ -33,71 +67,33 @@ export function initGameSpeed() {
| Phaser.Tweens.Tween | Phaser.Tweens.Tween
| Phaser.Tweens.TweenChain, | Phaser.Tweens.TweenChain,
) { ) {
if (config.loopDelay) { mutateProperties(config);
config.loopDelay = transformValue(config.loopDelay as number);
}
if (!(config instanceof Phaser.Tweens.TweenChain)) {
if (config.duration) {
config.duration = transformValue(config.duration);
}
if (!(config instanceof Phaser.Tweens.Tween)) {
if (config.delay) {
config.delay = transformValue(config.delay as number);
}
if (config.repeatDelay) {
config.repeatDelay = transformValue(config.repeatDelay);
}
if (config.hold) {
config.hold = transformValue(config.hold);
}
}
}
return originalTweensAdd.apply(this, [config]); return originalTweensAdd.apply(this, [config]);
}; } as typeof originalTweensAdd;
const originalTweensChain = this.tweens.chain;
const originalTweensChain: TweenManager["chain"] = this.tweens.chain;
this.tweens.chain = function (config: Phaser.Types.Tweens.TweenChainBuilderConfig): Phaser.Tweens.TweenChain { this.tweens.chain = function (config: Phaser.Types.Tweens.TweenChainBuilderConfig): Phaser.Tweens.TweenChain {
if (config.tweens) { mutateProperties(config);
for (const t of config.tweens) {
if (t.duration) {
t.duration = transformValue(t.duration);
}
if (t.delay) {
t.delay = transformValue(t.delay as number);
}
if (t.repeatDelay) {
t.repeatDelay = transformValue(t.repeatDelay);
}
if (t.loopDelay) {
t.loopDelay = transformValue(t.loopDelay as number);
}
if (t.hold) {
t.hold = transformValue(t.hold);
}
}
}
return originalTweensChain.apply(this, [config]); return originalTweensChain.apply(this, [config]);
}; } as typeof originalTweensChain;
const originalAddCounter = this.tweens.addCounter; const originalAddCounter: TweenManager["addCounter"] = this.tweens.addCounter;
this.tweens.addCounter = function (config: Phaser.Types.Tweens.NumberTweenBuilderConfig) { this.tweens.addCounter = function (config: Phaser.Types.Tweens.NumberTweenBuilderConfig) {
if (config.duration) { mutateProperties(config);
config.duration = transformValue(config.duration);
}
if (config.delay) {
config.delay = transformValue(config.delay);
}
if (config.repeatDelay) {
config.repeatDelay = transformValue(config.repeatDelay);
}
if (config.loopDelay) {
config.loopDelay = transformValue(config.loopDelay as number);
}
if (config.hold) {
config.hold = transformValue(config.hold);
}
return originalAddCounter.apply(this, [config]); return originalAddCounter.apply(this, [config]);
}; } as typeof originalAddCounter;
const originalCreate: TweenManager["create"] = this.tweens.create;
this.tweens.create = function (config: Phaser.Types.Tweens.TweenBuilderConfig) {
mutateProperties(config, true);
return originalCreate.apply(this, [config]);
} as typeof originalCreate;
const originalAddMultiple: TweenManager["addMultiple"] = this.tweens.addMultiple;
this.tweens.addMultiple = function (config: Phaser.Types.Tweens.TweenBuilderConfig[]) {
mutateProperties(config, true);
return originalAddMultiple.apply(this, [config]);
} as typeof originalAddMultiple;
const originalFadeOut = SoundFade.fadeOut; const originalFadeOut = SoundFade.fadeOut;
SoundFade.fadeOut = ((_scene: Phaser.Scene, sound: Phaser.Sound.BaseSound, duration: number, destroy?: boolean) => SoundFade.fadeOut = ((_scene: Phaser.Scene, sound: Phaser.Sound.BaseSound, duration: number, destroy?: boolean) =>

View File

@ -201,19 +201,19 @@ export function formatLargeNumber(count: number, threshold: number): string {
let suffix = ""; let suffix = "";
switch (Math.ceil(ret.length / 3) - 1) { switch (Math.ceil(ret.length / 3) - 1) {
case 1: case 1:
suffix = "K"; suffix = i18next.t("common:abrThousand");
break; break;
case 2: case 2:
suffix = "M"; suffix = i18next.t("common:abrMillion");
break; break;
case 3: case 3:
suffix = "B"; suffix = i18next.t("common:abrBillion");
break; break;
case 4: case 4:
suffix = "T"; suffix = i18next.t("common:abrTrillion");
break; break;
case 5: case 5:
suffix = "q"; suffix = i18next.t("common:abrQuadrillion");
break; break;
default: default:
return "?"; return "?";
@ -227,15 +227,31 @@ export function formatLargeNumber(count: number, threshold: number): string {
} }
// Abbreviations from 10^0 to 10^33 // Abbreviations from 10^0 to 10^33
const AbbreviationsLargeNumber: string[] = ["", "K", "M", "B", "t", "q", "Q", "s", "S", "o", "n", "d"]; function getAbbreviationsLargeNumber(): string[] {
return [
"",
i18next.t("common:abrThousand"),
i18next.t("common:abrMillion"),
i18next.t("common:abrBillion"),
i18next.t("common:abrTrillion"),
i18next.t("common:abrQuadrillion"),
i18next.t("common:abrQuintillion"),
i18next.t("common:abrSextillion"),
i18next.t("common:abrSeptillion"),
i18next.t("common:abrOctillion"),
i18next.t("common:abrNonillion"),
i18next.t("common:abrDecillion"),
];
}
export function formatFancyLargeNumber(number: number, rounded = 3): string { export function formatFancyLargeNumber(number: number, rounded = 3): string {
const abbreviations = getAbbreviationsLargeNumber();
let exponent: number; let exponent: number;
if (number < 1000) { if (number < 1000) {
exponent = 0; exponent = 0;
} else { } else {
const maxExp = AbbreviationsLargeNumber.length - 1; const maxExp = abbreviations.length - 1;
exponent = Math.floor(Math.log(number) / Math.log(1000)); exponent = Math.floor(Math.log(number) / Math.log(1000));
exponent = Math.min(exponent, maxExp); exponent = Math.min(exponent, maxExp);
@ -243,7 +259,7 @@ export function formatFancyLargeNumber(number: number, rounded = 3): string {
number /= Math.pow(1000, exponent); number /= Math.pow(1000, exponent);
} }
return `${(exponent === 0) || number % 1 === 0 ? number : number.toFixed(rounded)}${AbbreviationsLargeNumber[exponent]}`; return `${exponent === 0 || number % 1 === 0 ? number : number.toFixed(rounded)}${abbreviations[exponent]}`;
} }
export function formatMoney(format: MoneyFormat, amount: number) { export function formatMoney(format: MoneyFormat, amount: number) {

View File

@ -1,4 +1,4 @@
import { RepeatBerryNextTurnAbAttr } from "#app/data/abilities/ability"; import { CudChewConsumeBerryAbAttr } from "#app/data/abilities/ability";
import Pokemon from "#app/field/pokemon"; import Pokemon from "#app/field/pokemon";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
@ -196,7 +196,7 @@ describe("Abilities - Cud Chew", () => {
describe("regurgiates berries", () => { describe("regurgiates berries", () => {
it("re-triggers effects on eater without pushing to array", async () => { it("re-triggers effects on eater without pushing to array", async () => {
const apply = vi.spyOn(RepeatBerryNextTurnAbAttr.prototype, "apply"); const apply = vi.spyOn(CudChewConsumeBerryAbAttr.prototype, "apply");
await game.classicMode.startBattle([SpeciesId.FARIGIRAF]); await game.classicMode.startBattle([SpeciesId.FARIGIRAF]);
const farigiraf = game.scene.getPlayerPokemon()!; const farigiraf = game.scene.getPlayerPokemon()!;

View File

@ -91,7 +91,7 @@ describe("Abilities - Gorilla Tactics", () => {
game.move.select(MoveId.METRONOME); game.move.select(MoveId.METRONOME);
await game.phaseInterceptor.to("TurnEndPhase"); await game.phaseInterceptor.to("TurnEndPhase");
// Gorilla Tactics should bypass dancer and instruct // Gorilla Tactics should lock into Metronome, not tackle
expect(darmanitan.isMoveRestricted(MoveId.TACKLE)).toBe(true); expect(darmanitan.isMoveRestricted(MoveId.TACKLE)).toBe(true);
expect(darmanitan.isMoveRestricted(MoveId.METRONOME)).toBe(false); expect(darmanitan.isMoveRestricted(MoveId.METRONOME)).toBe(false);
expect(darmanitan.getLastXMoves(-1)).toEqual([ expect(darmanitan.getLastXMoves(-1)).toEqual([

View File

@ -95,7 +95,7 @@ describe("Abilities - Harvest", () => {
// Give ourselves harvest and disable enemy neut gas, // Give ourselves harvest and disable enemy neut gas,
// but force our roll to fail so we don't accidentally recover anything // but force our roll to fail so we don't accidentally recover anything
vi.spyOn(PostTurnRestoreBerryAbAttr.prototype, "canApplyPostTurn").mockReturnValueOnce(false); vi.spyOn(PostTurnRestoreBerryAbAttr.prototype, "canApply").mockReturnValueOnce(false);
game.override.ability(AbilityId.HARVEST); game.override.ability(AbilityId.HARVEST);
game.move.select(MoveId.GASTRO_ACID); game.move.select(MoveId.GASTRO_ACID);
await game.move.selectEnemyMove(MoveId.NUZZLE); await game.move.selectEnemyMove(MoveId.NUZZLE);

View File

@ -42,7 +42,7 @@ describe("Abilities - Healer", () => {
}); });
it("should not queue a message phase for healing if the ally has fainted", async () => { it("should not queue a message phase for healing if the ally has fainted", async () => {
const abSpy = vi.spyOn(PostTurnResetStatusAbAttr.prototype, "canApplyPostTurn"); const abSpy = vi.spyOn(PostTurnResetStatusAbAttr.prototype, "canApply");
game.override.moveset([MoveId.SPLASH, MoveId.LUNAR_DANCE]); game.override.moveset([MoveId.SPLASH, MoveId.LUNAR_DANCE]);
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]); await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]);

View File

@ -68,7 +68,7 @@ describe("Abilities - Moody", () => {
}); });
it("should only decrease one stat stage by 1 stage if all stat stages are at 6", async () => { it("should only decrease one stat stage by 1 stage if all stat stages are at 6", async () => {
await game.classicMode.startBattle(); await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
const playerPokemon = game.scene.getPlayerPokemon()!; const playerPokemon = game.scene.getPlayerPokemon()!;

View File

@ -178,7 +178,7 @@ describe("Abilities - Neutralizing Gas", () => {
const enemy = game.scene.getEnemyPokemon()!; const enemy = game.scene.getEnemyPokemon()!;
const weatherChangeAttr = enemy.getAbilityAttrs("PostSummonWeatherChangeAbAttr", false)[0]; const weatherChangeAttr = enemy.getAbilityAttrs("PostSummonWeatherChangeAbAttr", false)[0];
vi.spyOn(weatherChangeAttr, "applyPostSummon"); const weatherChangeSpy = vi.spyOn(weatherChangeAttr, "apply");
expect(game.scene.arena.getTag(ArenaTagType.NEUTRALIZING_GAS)).toBeDefined(); expect(game.scene.arena.getTag(ArenaTagType.NEUTRALIZING_GAS)).toBeDefined();
@ -187,6 +187,6 @@ describe("Abilities - Neutralizing Gas", () => {
await game.killPokemon(game.scene.getPlayerPokemon()!); await game.killPokemon(game.scene.getPlayerPokemon()!);
expect(game.scene.arena.getTag(ArenaTagType.NEUTRALIZING_GAS)).toBeUndefined(); expect(game.scene.arena.getTag(ArenaTagType.NEUTRALIZING_GAS)).toBeUndefined();
expect(weatherChangeAttr.applyPostSummon).not.toHaveBeenCalled(); expect(weatherChangeSpy).not.toHaveBeenCalled();
}); });
}); });

View File

@ -1,3 +1,4 @@
import type { StatMultiplierAbAttrParams } from "#app/@types/ability-types";
import { allAbilities } from "#app/data/data-lists"; import { allAbilities } from "#app/data/data-lists";
import { CommandPhase } from "#app/phases/command-phase"; import { CommandPhase } from "#app/phases/command-phase";
import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { MoveEffectPhase } from "#app/phases/move-effect-phase";
@ -46,15 +47,13 @@ describe("Abilities - Sand Veil", () => {
vi.spyOn(leadPokemon[0], "getAbility").mockReturnValue(allAbilities[AbilityId.SAND_VEIL]); vi.spyOn(leadPokemon[0], "getAbility").mockReturnValue(allAbilities[AbilityId.SAND_VEIL]);
const sandVeilAttr = allAbilities[AbilityId.SAND_VEIL].getAttrs("StatMultiplierAbAttr")[0]; const sandVeilAttr = allAbilities[AbilityId.SAND_VEIL].getAttrs("StatMultiplierAbAttr")[0];
vi.spyOn(sandVeilAttr, "applyStatStage").mockImplementation( vi.spyOn(sandVeilAttr, "apply").mockImplementation(({ stat, statVal }: StatMultiplierAbAttrParams) => {
(_pokemon, _passive, _simulated, stat, statValue, _args) => { if (stat === Stat.EVA && game.scene.arena.weather?.weatherType === WeatherType.SANDSTORM) {
if (stat === Stat.EVA && game.scene.arena.weather?.weatherType === WeatherType.SANDSTORM) { statVal.value *= -1; // will make all attacks miss
statValue.value *= -1; // will make all attacks miss return true;
return true; }
} return false;
return false; });
},
);
expect(leadPokemon[0].hasAbility(AbilityId.SAND_VEIL)).toBe(true); expect(leadPokemon[0].hasAbility(AbilityId.SAND_VEIL)).toBe(true);
expect(leadPokemon[1].hasAbility(AbilityId.SAND_VEIL)).toBe(false); expect(leadPokemon[1].hasAbility(AbilityId.SAND_VEIL)).toBe(false);

View File

@ -1,5 +1,5 @@
import { BattlerIndex } from "#enums/battler-index"; import { BattlerIndex } from "#enums/battler-index";
import { applyAbAttrs, applyPreDefendAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import { NumberHolder } from "#app/utils/common"; import { NumberHolder } from "#app/utils/common";
import { AbilityId } from "#enums/ability-id"; import { AbilityId } from "#enums/ability-id";
@ -52,25 +52,16 @@ describe("Abilities - Shield Dust", () => {
expect(move.id).toBe(MoveId.AIR_SLASH); expect(move.id).toBe(MoveId.AIR_SLASH);
const chance = new NumberHolder(move.chance); const chance = new NumberHolder(move.chance);
await applyAbAttrs( applyAbAttrs("MoveEffectChanceMultiplierAbAttr", {
"MoveEffectChanceMultiplierAbAttr", pokemon: phase.getUserPokemon()!,
phase.getUserPokemon()!,
null,
false,
chance, chance,
move, move,
phase.getFirstTarget(), });
false, applyAbAttrs("IgnoreMoveEffectsAbAttr", {
); pokemon: phase.getFirstTarget()!,
await applyPreDefendAbAttrs( move,
"IgnoreMoveEffectsAbAttr",
phase.getFirstTarget()!,
phase.getUserPokemon()!,
null,
null,
false,
chance, chance,
); });
expect(chance.value).toBe(0); expect(chance.value).toBe(0);
}); });

View File

@ -22,10 +22,7 @@ describe("Abilities - Unburden", () => {
*/ */
function getHeldItemCount(pokemon: Pokemon): number { function getHeldItemCount(pokemon: Pokemon): number {
const stackCounts = pokemon.getHeldItems().map(m => m.getStackCount()); const stackCounts = pokemon.getHeldItems().map(m => m.getStackCount());
if (stackCounts.length) { return stackCounts.reduce((a, b) => a + b, 0);
return stackCounts.reduce((a, b) => a + b);
}
return 0;
} }
beforeAll(() => { beforeAll(() => {
@ -277,7 +274,7 @@ describe("Abilities - Unburden", () => {
const initialTreeckoSpeed = treecko.getStat(Stat.SPD); const initialTreeckoSpeed = treecko.getStat(Stat.SPD);
const initialPurrloinSpeed = purrloin.getStat(Stat.SPD); const initialPurrloinSpeed = purrloin.getStat(Stat.SPD);
const unburdenAttr = treecko.getAbilityAttrs("PostItemLostAbAttr")[0]; const unburdenAttr = treecko.getAbilityAttrs("PostItemLostAbAttr")[0];
vi.spyOn(unburdenAttr, "applyPostItemLost"); vi.spyOn(unburdenAttr, "apply");
// Player uses Baton Pass, which also passes the Baton item // Player uses Baton Pass, which also passes the Baton item
game.move.select(MoveId.BATON_PASS); game.move.select(MoveId.BATON_PASS);
@ -288,7 +285,7 @@ describe("Abilities - Unburden", () => {
expect(getHeldItemCount(purrloin)).toBe(1); expect(getHeldItemCount(purrloin)).toBe(1);
expect(treecko.getEffectiveStat(Stat.SPD)).toBe(initialTreeckoSpeed); expect(treecko.getEffectiveStat(Stat.SPD)).toBe(initialTreeckoSpeed);
expect(purrloin.getEffectiveStat(Stat.SPD)).toBe(initialPurrloinSpeed); expect(purrloin.getEffectiveStat(Stat.SPD)).toBe(initialPurrloinSpeed);
expect(unburdenAttr.applyPostItemLost).not.toHaveBeenCalled(); expect(unburdenAttr.apply).not.toHaveBeenCalled();
}); });
it("should not speed up a Pokemon after it loses the ability Unburden", async () => { it("should not speed up a Pokemon after it loses the ability Unburden", async () => {

View File

@ -0,0 +1,79 @@
import type Pokemon from "#app/field/pokemon";
import { MoveId } from "#enums/move-id";
import { AbilityId } from "#enums/ability-id";
import { SpeciesId } from "#enums/species-id";
import { BattleType } from "#enums/battle-type";
import { BattlerTagType } from "#enums/battler-tag-type";
import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { BattlerIndex } from "#enums/battler-index";
describe("Field - Pokemon ID Checks", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.ability(AbilityId.NO_GUARD)
.battleStyle("single")
.battleType(BattleType.TRAINER)
.criticalHits(false)
.enemyLevel(100)
.enemySpecies(SpeciesId.ARCANINE)
.enemyAbility(AbilityId.BALL_FETCH)
.enemyMoveset(MoveId.SPLASH);
});
function onlyUnique<T>(array: T[]): T[] {
return [...new Set<T>(array)];
}
// TODO: We currently generate IDs as a pure random integer; enable once unique UUIDs are added
it.todo("2 Pokemon should not be able to generate with the same ID during 1 encounter", async () => {
game.override.battleType(BattleType.TRAINER); // enemy generates 2 mons
await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.ABRA]);
const ids = (game.scene.getPlayerParty() as Pokemon[]).concat(game.scene.getEnemyParty()).map((p: Pokemon) => p.id);
const uniqueIds = onlyUnique(ids);
expect(ids).toHaveLength(uniqueIds.length);
});
it("should not prevent Battler Tags from triggering if user has PID of 0", async () => {
await game.classicMode.startBattle([SpeciesId.TREECKO, SpeciesId.AERODACTYL]);
const player = game.field.getPlayerPokemon();
const enemy = game.field.getEnemyPokemon();
// Override player pokemon PID to be 0
player.id = 0;
expect(player.getTag(BattlerTagType.DESTINY_BOND)).toBeUndefined();
game.move.use(MoveId.DESTINY_BOND);
game.doSelectPartyPokemon(1);
await game.move.forceEnemyMove(MoveId.FLAME_WHEEL);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to("MoveEndPhase");
const dBondTag = player.getTag(BattlerTagType.DESTINY_BOND)!;
expect(dBondTag).toBeDefined();
expect(dBondTag.sourceId).toBe(0);
expect(dBondTag.getSourcePokemon()).toBe(player);
await game.phaseInterceptor.to("MoveEndPhase");
expect(player.isFainted()).toBe(true);
expect(enemy.isFainted()).toBe(true);
});
});

View File

@ -31,7 +31,7 @@ describe("Spec - Pokemon", () => {
const pkm = game.scene.getPlayerPokemon()!; const pkm = game.scene.getPlayerPokemon()!;
expect(pkm).toBeDefined(); expect(pkm).toBeDefined();
expect(pkm.trySetStatus(undefined)).toBe(true); expect(pkm.trySetStatus(undefined)).toBe(false);
}); });
describe("Add To Party", () => { describe("Add To Party", () => {

View File

@ -140,9 +140,8 @@ describe("Moves - Safeguard", () => {
game.field.mockAbility(player, AbilityId.STATIC); game.field.mockAbility(player, AbilityId.STATIC);
vi.spyOn( vi.spyOn(
allAbilities[AbilityId.STATIC].getAttrs("PostDefendContactApplyStatusEffectAbAttr")[0], allAbilities[AbilityId.STATIC].getAttrs("PostDefendContactApplyStatusEffectAbAttr")[0],
"chance", "canApply",
"get", ).mockReturnValue(true);
).mockReturnValue(100);
game.move.select(MoveId.SPLASH); game.move.select(MoveId.SPLASH);
await game.move.forceEnemyMove(MoveId.SAFEGUARD); await game.move.forceEnemyMove(MoveId.SAFEGUARD);

View File

@ -122,15 +122,20 @@ export default class GameWrapper {
}, },
}; };
// TODO: Replace this with a proper mock of phaser's TweenManager.
this.scene.tweens = { this.scene.tweens = {
add: data => { add: data => {
if (data.onComplete) { // TODO: our mock of `add` should have the same signature as the real one, which returns the tween
data.onComplete(); data.onComplete?.();
}
}, },
getTweensOf: () => [], getTweensOf: () => [],
killTweensOf: () => [], killTweensOf: () => [],
chain: () => null,
chain: data => {
// TODO: our mock of `chain` should have the same signature as the real one, which returns the chain
data?.tweens?.forEach(tween => tween.onComplete?.());
data.onComplete?.();
},
addCounter: data => { addCounter: data => {
if (data.onComplete) { if (data.onComplete) {
data.onComplete(); data.onComplete();

View File

@ -5,7 +5,7 @@ export class MockVideoGameObject implements MockGameObject {
public name: string; public name: string;
public active = true; public active = true;
public play = () => null; public play = () => this;
public stop = () => this; public stop = () => this;
public setOrigin = () => this; public setOrigin = () => this;
public setScale = () => this; public setScale = () => this;