mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-07-03 23:12:20 +02:00
Merge branch 'beta' into WorkingDiscardFunction
This commit is contained in:
commit
79ddcd4217
@ -1,6 +1,19 @@
|
||||
/** @type {import('dependency-cruiser').IConfiguration} */
|
||||
module.exports = {
|
||||
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",
|
||||
severity: "error",
|
||||
@ -310,7 +323,7 @@ module.exports = {
|
||||
conditionNames: ["import", "require", "node", "default", "types"],
|
||||
/*
|
||||
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
|
||||
the extensions you actually use (e.g. [".js", ".jsx"]). This can speed
|
||||
up module resolution, which is the most expensive step.
|
||||
|
4
.github/pull_request_template.md
vendored
4
.github/pull_request_template.md
vendored
@ -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?
|
||||
- [ ] Have I provided a clear explanation of the changes?
|
||||
- [ ] Have I tested the changes manually?
|
||||
- [ ] Are all unit tests still passing? (`npm run test:silent`)
|
||||
- [ ] Have I created new automated tests (`npm run test:create`) or updated existing tests related to the PR's changes?
|
||||
- [ ] Are all unit tests still passing? (`pnpm test:silent`)
|
||||
- [ ] 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 made sure that any UI change works for both UI themes (default and legacy)?
|
||||
|
||||
|
14
.github/workflows/deploy-beta.yml
vendored
14
.github/workflows/deploy-beta.yml
vendored
@ -18,15 +18,24 @@ jobs:
|
||||
with:
|
||||
submodules: "recursive"
|
||||
ref: ${{ vars.BETA_DEPLOY_BRANCH || 'beta'}}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
run: pnpm i
|
||||
|
||||
- name: Build
|
||||
run: npm run build:beta
|
||||
run: pnpm build:beta
|
||||
env:
|
||||
NODE_ENV: production
|
||||
|
||||
- name: Set up SSH
|
||||
run: |
|
||||
mkdir ~/.ssh
|
||||
@ -34,6 +43,7 @@ jobs:
|
||||
echo "${{ secrets.BETA_SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/*
|
||||
ssh-keyscan -H ${{ secrets.BETA_SSH_HOST }} >> ~/.ssh/known_hosts
|
||||
|
||||
- name: Deploy build on server
|
||||
run: |
|
||||
rsync --del --no-times --checksum -vrm dist/* ${{ secrets.BETA_SSH_USER }}@${{ secrets.BETA_SSH_HOST }}:${{ secrets.BETA_DESTINATION_DIR }}
|
||||
|
15
.github/workflows/deploy.yml
vendored
15
.github/workflows/deploy.yml
vendored
@ -16,15 +16,24 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
run: pnpm i
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
run: pnpm build
|
||||
env:
|
||||
NODE_ENV: production
|
||||
|
||||
- name: Set up SSH
|
||||
if: github.event_name == 'push' && github.ref_name == 'main'
|
||||
run: |
|
||||
@ -33,11 +42,13 @@ jobs:
|
||||
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/*
|
||||
ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts
|
||||
|
||||
- name: Deploy build on server
|
||||
if: github.event_name == 'push' && github.ref_name == 'main'
|
||||
run: |
|
||||
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"
|
||||
|
||||
- name: Purge Cloudflare Cache
|
||||
if: github.event_name == 'push' && github.ref_name == 'main'
|
||||
id: purge-cache
|
||||
|
9
.github/workflows/github-pages.yml
vendored
9
.github/workflows/github-pages.yml
vendored
@ -34,6 +34,11 @@ jobs:
|
||||
sudo apt update
|
||||
sudo apt install -y git openssh-client
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node 22.14.1
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@ -50,13 +55,13 @@ jobs:
|
||||
working-directory: ${{env.api-dir}}
|
||||
run: |
|
||||
cd pokerogue_docs
|
||||
npm ci
|
||||
pnpm i
|
||||
|
||||
- name: Generate Typedoc docs
|
||||
working-directory: ${{env.api-dir}}
|
||||
run: |
|
||||
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
|
||||
if: github.event_name == 'push'
|
||||
|
13
.github/workflows/linting.yml
vendored
13
.github/workflows/linting.yml
vendored
@ -23,17 +23,22 @@ jobs:
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install Node.js dependencies
|
||||
run: npm ci
|
||||
run: pnpm i
|
||||
|
||||
- name: Lint with Biome
|
||||
run: npm run biome-ci
|
||||
run: pnpm biome-ci
|
||||
|
||||
- name: Check dependencies with depcruise
|
||||
run: npm run depcruise
|
||||
run: pnpm depcruise
|
14
.github/workflows/test-shard-template.yml
vendored
14
.github/workflows/test-shard-template.yml
vendored
@ -28,12 +28,20 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: "npm"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install Node.js dependencies
|
||||
run: npm ci
|
||||
run: pnpm i
|
||||
|
||||
- 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' || '' }}
|
||||
|
11
.github/workflows/tests.yml
vendored
11
.github/workflows/tests.yml
vendored
@ -1,16 +1,14 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
# Trigger the workflow on push or pull request,
|
||||
# but only for the main branch
|
||||
push:
|
||||
branches:
|
||||
- main # Trigger on push events to the main branch
|
||||
- beta # Trigger on push events to the beta branch
|
||||
- main
|
||||
- beta
|
||||
pull_request:
|
||||
branches:
|
||||
- main # Trigger on pull request events targeting the main branch
|
||||
- beta # Trigger on pull request events targeting the beta branch
|
||||
- main
|
||||
- beta
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
|
||||
@ -24,6 +22,7 @@ jobs:
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36
|
||||
id: filter
|
||||
with:
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
Thank you for taking the time to contribute, every little bit helps. This project is entirely open-source and unmonetized - community contributions are what keep it alive!
|
||||
|
||||
Please make sure you understand everything relevant to your changes from the [Table of Contents](#-table-of-contents), and absolutely *feel free to reach out reach out in the **#dev-corner** channel on [Discord](https://discord.gg/pokerogue)*. We are here to help and the better you understand what you're working on, the easier it will be for it to find its way into the game.
|
||||
Please make sure you understand everything relevant to your changes from the [Table of Contents](#-table-of-contents), and absolutely *feel free to reach out in the **#dev-corner** channel on [Discord](https://discord.gg/pokerogue)*. We are here to help and the better you understand what you're working on, the easier it will be for it to find its way into the game.
|
||||
|
||||
## 📄 Table of Contents
|
||||
|
||||
@ -24,14 +24,14 @@ If you have the motivation and experience with Typescript/Javascript (or are wil
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- node: >=22.14.0
|
||||
- npm: [how to install](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)
|
||||
- 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)
|
||||
- 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
|
||||
|
||||
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*
|
||||
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
|
||||
|
||||
@ -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.
|
||||
|
||||
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.
|
||||
|
||||
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:
|
||||
- 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.
|
||||
|
@ -27,6 +27,7 @@
|
||||
"!**/.github/**/*",
|
||||
"!**/node_modules/**/*",
|
||||
"!**/.vscode/**/*",
|
||||
"!**/typedoc/**/*",
|
||||
// TODO: lint css and html?
|
||||
"!**/*.css",
|
||||
"!**/*.html",
|
||||
@ -69,7 +70,10 @@
|
||||
},
|
||||
"style": {
|
||||
"useEnumInitializers": "off", // large enums like Moves/Species would make this cumbersome
|
||||
"useBlockStatements": "error",
|
||||
"useBlockStatements": {
|
||||
"level": "error",
|
||||
"fix": "safe"
|
||||
},
|
||||
"useConst": "error",
|
||||
"useImportType": "error",
|
||||
"noNonNullAssertion": "off", // TODO: Turn this on ASAP and fix all non-null assertions in non-test files
|
||||
|
@ -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.
|
||||
|
||||
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
|
||||
For an example of how TSDoc comments work, here are some TSDoc comments taken from `src/data/moves/move.ts`:
|
||||
|
@ -1,14 +1,10 @@
|
||||
# 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.
|
||||
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
|
||||
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
|
||||
|
||||
@ -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.
|
||||
|
||||
### 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
|
||||
If you want Biome to check your files manually, you can run it from the command line like so:
|
||||
|
||||
```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:
|
||||
@ -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.
|
||||
|
||||
[^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.
|
@ -1,8 +1,7 @@
|
||||
pre-commit:
|
||||
parallel: true
|
||||
commands:
|
||||
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
|
||||
skip:
|
||||
- merge
|
||||
|
6132
package-lock.json
generated
6132
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -22,9 +22,9 @@
|
||||
"docs": "typedoc",
|
||||
"depcruise": "depcruise src test",
|
||||
"depcruise:graph": "depcruise src --output-type dot | node dependency-graph.js > dependency-graph.svg",
|
||||
"postinstall": "npx lefthook install && npx lefthook run post-merge",
|
||||
"update-version:patch": "npm version patch --force --no-git-tag-version",
|
||||
"update-version:minor": "npm version minor --force --no-git-tag-version",
|
||||
"postinstall": "lefthook install && lefthook run post-merge",
|
||||
"update-version:patch": "pnpm version patch --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"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -33,6 +33,7 @@
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/node": "^22.13.14",
|
||||
"@vitest/coverage-istanbul": "^3.0.9",
|
||||
"chalk": "^5.4.1",
|
||||
"dependency-cruiser": "^16.3.10",
|
||||
"inquirer": "^12.4.2",
|
||||
"jsdom": "^26.0.0",
|
||||
|
3910
pnpm-lock.yaml
Normal file
3910
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
||||
/**
|
||||
* This script creates a test boilerplate file in the appropriate
|
||||
* directory based on the type selected.
|
||||
* @example npm run test:create
|
||||
* @example pnpm test:create
|
||||
*/
|
||||
|
||||
import chalk from "chalk";
|
||||
|
@ -1,14 +1,14 @@
|
||||
import type { AbAttr } from "#app/data/abilities/ability";
|
||||
import type Move from "#app/data/moves/move";
|
||||
import type Pokemon from "#app/field/pokemon";
|
||||
import type { BattleStat } from "#enums/stat";
|
||||
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 AbAttrApplyFunc<TAttr extends AbAttr> = (attr: TAttr, passive: boolean, ...args: any[]) => void;
|
||||
export type AbAttrSuccessFunc<TAttr extends AbAttr> = (attr: TAttr, passive: boolean, ...args: any[]) => boolean;
|
||||
// biome-ignore lint/correctness/noUnusedImports: Used in a tsdoc comment
|
||||
import type { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
|
||||
|
||||
export type AbAttrCondition = (pokemon: Pokemon) => boolean;
|
||||
export type PokemonAttackCondition = (user: Pokemon | null, target: Pokemon | null, 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 = {
|
||||
[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];
|
||||
};
|
||||
|
34
src/@types/type-helpers.ts
Normal file
34
src/@types/type-helpers.ts
Normal 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];
|
||||
};
|
@ -18,6 +18,7 @@ import {
|
||||
isNullOrUndefined,
|
||||
BooleanHolder,
|
||||
type Constructor,
|
||||
isBetween,
|
||||
} from "#app/utils/common";
|
||||
import { deepMergeSpriteData } from "#app/utils/data";
|
||||
import type { Modifier, ModifierPredicate, TurnHeldItemTransferModifier } from "./modifier/modifier";
|
||||
@ -67,7 +68,7 @@ import { modifierTypes } from "./data/data-lists";
|
||||
import { getModifierPoolForType } from "./utils/modifier-utils";
|
||||
import { ModifierPoolType } from "#enums/modifier-pool-type";
|
||||
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 type { FixedBattleConfig } from "#app/battle";
|
||||
import Battle from "#app/battle";
|
||||
@ -164,10 +165,6 @@ import { PhaseManager } from "./phase-manager";
|
||||
|
||||
const DEBUG_RNG = false;
|
||||
|
||||
const OPP_IVS_OVERRIDE_VALIDATED: number[] = (
|
||||
Array.isArray(Overrides.OPP_IVS_OVERRIDE) ? Overrides.OPP_IVS_OVERRIDE : new Array(6).fill(Overrides.OPP_IVS_OVERRIDE)
|
||||
).map(iv => (Number.isNaN(iv) || iv === null || iv > 31 ? -1 : iv));
|
||||
|
||||
export interface PokeballCounts {
|
||||
[pb: string]: number;
|
||||
}
|
||||
@ -894,9 +891,19 @@ export default class BattleScene extends SceneBase {
|
||||
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 (findInParty(this.getPlayerParty()) || findInParty(this.getEnemyParty())) ?? null;
|
||||
/**
|
||||
* Return the {@linkcode Pokemon} associated with a given ID.
|
||||
* @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(
|
||||
@ -924,9 +931,32 @@ export default class BattleScene extends SceneBase {
|
||||
nature,
|
||||
dataSource,
|
||||
);
|
||||
|
||||
if (postProcess) {
|
||||
postProcess(pokemon);
|
||||
}
|
||||
|
||||
if (Overrides.IVS_OVERRIDE === null) {
|
||||
// do nothing
|
||||
} else if (Array.isArray(Overrides.IVS_OVERRIDE)) {
|
||||
if (Overrides.IVS_OVERRIDE.length !== 6) {
|
||||
throw new Error("The Player IVs override must be an array of length 6 or a number!");
|
||||
}
|
||||
if (Overrides.IVS_OVERRIDE.some(value => !isBetween(value, 0, 31))) {
|
||||
throw new Error("All IVs in the player IV override must be between 0 and 31!");
|
||||
}
|
||||
pokemon.ivs = Overrides.IVS_OVERRIDE;
|
||||
} else {
|
||||
if (!isBetween(Overrides.IVS_OVERRIDE, 0, 31)) {
|
||||
throw new Error("The Player IV override must be a value between 0 and 31!");
|
||||
}
|
||||
pokemon.ivs = new Array(6).fill(Overrides.IVS_OVERRIDE);
|
||||
}
|
||||
|
||||
if (Overrides.NATURE_OVERRIDE !== null) {
|
||||
pokemon.nature = Overrides.NATURE_OVERRIDE;
|
||||
}
|
||||
|
||||
pokemon.init();
|
||||
return pokemon;
|
||||
}
|
||||
@ -971,10 +1001,25 @@ export default class BattleScene extends SceneBase {
|
||||
postProcess(pokemon);
|
||||
}
|
||||
|
||||
for (let i = 0; i < pokemon.ivs.length; i++) {
|
||||
if (OPP_IVS_OVERRIDE_VALIDATED[i] > -1) {
|
||||
pokemon.ivs[i] = OPP_IVS_OVERRIDE_VALIDATED[i];
|
||||
if (Overrides.ENEMY_IVS_OVERRIDE === null) {
|
||||
// do nothing
|
||||
} else if (Array.isArray(Overrides.ENEMY_IVS_OVERRIDE)) {
|
||||
if (Overrides.ENEMY_IVS_OVERRIDE.length !== 6) {
|
||||
throw new Error("The Enemy IVs override must be an array of length 6 or a number!");
|
||||
}
|
||||
if (Overrides.ENEMY_IVS_OVERRIDE.some(value => !isBetween(value, 0, 31))) {
|
||||
throw new Error("All IVs in the enemy IV override must be between 0 and 31!");
|
||||
}
|
||||
pokemon.ivs = Overrides.ENEMY_IVS_OVERRIDE;
|
||||
} else {
|
||||
if (!isBetween(Overrides.ENEMY_IVS_OVERRIDE, 0, 31)) {
|
||||
throw new Error("The Enemy IV override must be a value between 0 and 31!");
|
||||
}
|
||||
pokemon.ivs = new Array(6).fill(Overrides.ENEMY_IVS_OVERRIDE);
|
||||
}
|
||||
|
||||
if (Overrides.ENEMY_NATURE_OVERRIDE !== null) {
|
||||
pokemon.nature = Overrides.ENEMY_NATURE_OVERRIDE;
|
||||
}
|
||||
|
||||
pokemon.init();
|
||||
@ -1256,7 +1301,7 @@ export default class BattleScene extends SceneBase {
|
||||
const doubleChance = new NumberHolder(newWaveIndex % 10 === 0 ? 32 : 8);
|
||||
this.applyModifiers(DoubleBattleChanceBoosterModifier, true, doubleChance);
|
||||
for (const p of playerField) {
|
||||
applyAbAttrs("DoubleBattleChanceAbAttr", p, null, false, doubleChance);
|
||||
applyAbAttrs("DoubleBattleChanceAbAttr", { pokemon: p, chance: doubleChance });
|
||||
}
|
||||
return Math.max(doubleChance.value, 1);
|
||||
}
|
||||
@ -1461,7 +1506,7 @@ export default class BattleScene extends SceneBase {
|
||||
for (const pokemon of this.getPlayerParty()) {
|
||||
pokemon.resetBattleAndWaveData();
|
||||
pokemon.resetTera();
|
||||
applyPostBattleInitAbAttrs("PostBattleInitAbAttr", pokemon);
|
||||
applyAbAttrs("PostBattleInitAbAttr", { pokemon });
|
||||
if (
|
||||
pokemon.hasSpecies(SpeciesId.TERAPAGOS) ||
|
||||
(this.gameMode.isClassic && this.currentBattle.waveIndex > 180 && this.currentBattle.waveIndex <= 190)
|
||||
@ -2743,7 +2788,7 @@ export default class BattleScene extends SceneBase {
|
||||
const cancelled = new BooleanHolder(false);
|
||||
|
||||
if (source && source.isPlayer() !== target.isPlayer()) {
|
||||
applyAbAttrs("BlockItemTheftAbAttr", source, cancelled);
|
||||
applyAbAttrs("BlockItemTheftAbAttr", { pokemon: source, cancelled });
|
||||
}
|
||||
|
||||
if (cancelled.value) {
|
||||
@ -2783,13 +2828,13 @@ export default class BattleScene extends SceneBase {
|
||||
if (target.isPlayer()) {
|
||||
this.addModifier(newItemModifier, ignoreUpdate, playSound, false, instant);
|
||||
if (source && itemLost) {
|
||||
applyPostItemLostAbAttrs("PostItemLostAbAttr", source, false);
|
||||
applyAbAttrs("PostItemLostAbAttr", { pokemon: source });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
this.addEnemyModifier(newItemModifier, ignoreUpdate, instant);
|
||||
if (source && itemLost) {
|
||||
applyPostItemLostAbAttrs("PostItemLostAbAttr", source, false);
|
||||
applyAbAttrs("PostItemLostAbAttr", { pokemon: source });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -2825,7 +2870,7 @@ export default class BattleScene extends SceneBase {
|
||||
const cancelled = new BooleanHolder(false);
|
||||
|
||||
if (source && source.isPlayer() !== target.isPlayer()) {
|
||||
applyAbAttrs("BlockItemTheftAbAttr", source, cancelled);
|
||||
applyAbAttrs("BlockItemTheftAbAttr", { pokemon: source, cancelled });
|
||||
}
|
||||
|
||||
if (cancelled.value) {
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,63 +1,14 @@
|
||||
import type { AbAttrApplyFunc, AbAttrMap, AbAttrString, AbAttrSuccessFunc } from "#app/@types/ability-types";
|
||||
import type Pokemon from "#app/field/pokemon";
|
||||
import type { AbAttrParamMap } from "#app/@types/ability-types";
|
||||
import type { AbAttrBaseParams, AbAttrString, CallableAbAttrString } from "#app/@types/ability-types";
|
||||
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>(
|
||||
pokemon: Pokemon,
|
||||
passive: boolean,
|
||||
attrType: T,
|
||||
applyFunc: AbAttrApplyFunc<AbAttrMap[T]>,
|
||||
successFunc: AbAttrSuccessFunc<AbAttrMap[T]>,
|
||||
args: any[],
|
||||
params: AbAttrParamMap[T],
|
||||
gainedMidTurn = false,
|
||||
simulated = false,
|
||||
messages: string[] = [],
|
||||
) {
|
||||
const { simulated = false, passive = false, pokemon } = params;
|
||||
if (!pokemon?.canApplyAbility(passive) || (passive && pokemon.getPassiveAbility().id === pokemon.getAbility().id)) {
|
||||
return;
|
||||
}
|
||||
@ -75,7 +26,11 @@ function applySingleAbAttrs<T extends AbAttrString>(
|
||||
for (const attr of ability.getAttrs(attrType)) {
|
||||
const condition = attr.getCondition();
|
||||
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;
|
||||
}
|
||||
|
||||
@ -85,15 +40,16 @@ function applySingleAbAttrs<T extends AbAttrString>(
|
||||
globalScene.phaseManager.queueAbilityDisplay(pokemon, passive, true);
|
||||
abShown = true;
|
||||
}
|
||||
const message = attr.getTriggerMessage(pokemon, ability.name, args);
|
||||
|
||||
const message = attr.getTriggerMessage(params as any, ability.name);
|
||||
if (message) {
|
||||
if (!simulated) {
|
||||
globalScene.phaseManager.queueMessage(message);
|
||||
}
|
||||
messages.push(message);
|
||||
}
|
||||
|
||||
applyFunc(attr, passive);
|
||||
// The `as any` cast here uses the same reasoning as above.
|
||||
attr.apply(params as any);
|
||||
|
||||
if (abShown) {
|
||||
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,
|
||||
pokemon: Pokemon | null,
|
||||
applyFunc: AbAttrApplyFunc<AbAttrMap[T]>,
|
||||
successFunc: AbAttrSuccessFunc<AbAttrMap[T]>,
|
||||
args: any[],
|
||||
simulated = false,
|
||||
params: AbAttrParamMap[T],
|
||||
messages: string[] = [],
|
||||
gainedMidTurn = false,
|
||||
) {
|
||||
for (const passive of [false, true]) {
|
||||
if (pokemon) {
|
||||
applySingleAbAttrs(pokemon, passive, attrType, applyFunc, successFunc, args, gainedMidTurn, simulated, messages);
|
||||
globalScene.phaseManager.clearPhaseQueueSplice();
|
||||
}
|
||||
// If the pokemon is not defined, no ability attributes to be applied.
|
||||
// TODO: Evaluate whether this check is even necessary anymore
|
||||
if (!params.pokemon) {
|
||||
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,
|
||||
pokemon: Pokemon,
|
||||
cancelled: BooleanHolder | null,
|
||||
simulated = false,
|
||||
...args: any[]
|
||||
params: AbAttrParamMap[T],
|
||||
messages?: string[],
|
||||
): void {
|
||||
applyAbAttrsInternal<T>(
|
||||
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,
|
||||
);
|
||||
applyAbAttrsInternal(attrType, params, messages);
|
||||
}
|
||||
|
||||
// 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)
|
||||
*
|
||||
* 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 {
|
||||
applySingleAbAttrs(
|
||||
pokemon,
|
||||
passive,
|
||||
"PostSummonAbAttr",
|
||||
(attr, passive) => attr.applyPostSummon(pokemon, passive, simulated, args),
|
||||
(attr, passive) => attr.canApplyPostSummon(pokemon, passive, simulated, args),
|
||||
args,
|
||||
true,
|
||||
simulated,
|
||||
);
|
||||
export function applyOnGainAbAttrs(params: AbAttrBaseParams): void {
|
||||
applySingleAbAttrs("PostSummonAbAttr", params, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
applySingleAbAttrs(
|
||||
pokemon,
|
||||
passive,
|
||||
"PreLeaveFieldAbAttr",
|
||||
(attr, passive) => attr.applyPreLeaveField(pokemon, passive, simulated, [...args, true]),
|
||||
(attr, passive) => attr.canApplyPreLeaveField(pokemon, passive, simulated, [...args, true]),
|
||||
args,
|
||||
true,
|
||||
simulated,
|
||||
);
|
||||
export function applyOnLoseAbAttrs(params: AbAttrBaseParams): void {
|
||||
applySingleAbAttrs("PreLeaveFieldAbAttr", params, true);
|
||||
|
||||
applySingleAbAttrs(
|
||||
pokemon,
|
||||
passive,
|
||||
"IllusionBreakAbAttr",
|
||||
(attr, passive) => attr.apply(pokemon, passive, simulated, null, args),
|
||||
(attr, passive) => attr.canApply(pokemon, passive, simulated, args),
|
||||
args,
|
||||
true,
|
||||
simulated,
|
||||
);
|
||||
applySingleAbAttrs("IllusionBreakAbAttr", params, true);
|
||||
}
|
||||
|
@ -72,10 +72,11 @@ export abstract class ArenaTag {
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
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 {
|
||||
super.onAdd(arena);
|
||||
|
||||
if (this.sourceId) {
|
||||
const source = globalScene.getPokemonById(this.sourceId);
|
||||
|
||||
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");
|
||||
}
|
||||
// We assume `quiet=true` means "just add the bloody tag no questions asked"
|
||||
if (quiet) {
|
||||
return;
|
||||
}
|
||||
|
||||
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) {
|
||||
const bypassed = new BooleanHolder(false);
|
||||
// TODO: Allow this to be simulated
|
||||
applyAbAttrs("InfiltratorAbAttr", attacker, null, false, bypassed);
|
||||
applyAbAttrs("InfiltratorAbAttr", { pokemon: attacker, simulated: false, bypassed });
|
||||
if (bypassed.value) {
|
||||
return false;
|
||||
}
|
||||
@ -202,7 +206,7 @@ export class WeakenMoveScreenTag extends ArenaTag {
|
||||
): boolean {
|
||||
if (this.weakenedCategories.includes(moveCategory)) {
|
||||
const bypassed = new BooleanHolder(false);
|
||||
applyAbAttrs("InfiltratorAbAttr", attacker, null, false, bypassed);
|
||||
applyAbAttrs("InfiltratorAbAttr", { pokemon: attacker, bypassed });
|
||||
if (bypassed.value) {
|
||||
return false;
|
||||
}
|
||||
@ -440,18 +444,18 @@ class MatBlockTag extends ConditionalProtectTag {
|
||||
}
|
||||
|
||||
onAdd(_arena: Arena) {
|
||||
if (this.sourceId) {
|
||||
const source = globalScene.getPokemonById(this.sourceId);
|
||||
if (source) {
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("arenaTag:matBlockOnAdd", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(source),
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
console.warn("Failed to get source for MatBlockTag onAdd");
|
||||
}
|
||||
const source = this.getSourcePokemon();
|
||||
if (!source) {
|
||||
console.warn(`Failed to get source Pokemon for Mat Block message; id: ${this.sourceId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
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 */
|
||||
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(
|
||||
i18next.t("arenaTag:noCritOnRemove", {
|
||||
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.
|
||||
*/
|
||||
class WishTag extends ArenaTag {
|
||||
@ -535,18 +544,20 @@ class WishTag extends ArenaTag {
|
||||
}
|
||||
|
||||
onAdd(_arena: Arena): void {
|
||||
if (this.sourceId) {
|
||||
const user = globalScene.getPokemonById(this.sourceId);
|
||||
if (user) {
|
||||
this.battlerIndex = user.getBattlerIndex();
|
||||
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");
|
||||
}
|
||||
const source = this.getSourcePokemon();
|
||||
if (!source) {
|
||||
console.warn(`Failed to get source Pokemon for WishTag on add message; id: ${this.sourceId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
super.onAdd(_arena);
|
||||
this.healHp = toDmgValue(source.getMaxHp() / 2);
|
||||
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("arenaTag:wishTagOnAdd", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(source),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
onRemove(_arena: Arena): void {
|
||||
@ -741,15 +752,23 @@ class SpikesTag extends ArenaTrapTag {
|
||||
onAdd(arena: Arena, quiet = false): void {
|
||||
super.onAdd(arena);
|
||||
|
||||
const source = this.sourceId ? globalScene.getPokemonById(this.sourceId) : null;
|
||||
if (!quiet && source) {
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("arenaTag:spikesOnAdd", {
|
||||
moveName: this.getMoveName(),
|
||||
opponentDesc: source.getOpponentDescriptor(),
|
||||
}),
|
||||
);
|
||||
// We assume `quiet=true` means "just add the bloody tag no questions asked"
|
||||
if (quiet) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -758,7 +777,7 @@ class SpikesTag extends ArenaTrapTag {
|
||||
}
|
||||
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelled);
|
||||
applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled });
|
||||
if (simulated || cancelled.value) {
|
||||
return !cancelled.value;
|
||||
}
|
||||
@ -794,15 +813,23 @@ class ToxicSpikesTag extends ArenaTrapTag {
|
||||
onAdd(arena: Arena, quiet = false): void {
|
||||
super.onAdd(arena);
|
||||
|
||||
const source = this.sourceId ? globalScene.getPokemonById(this.sourceId) : null;
|
||||
if (!quiet && source) {
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("arenaTag:toxicSpikesOnAdd", {
|
||||
moveName: this.getMoveName(),
|
||||
opponentDesc: source.getOpponentDescriptor(),
|
||||
}),
|
||||
);
|
||||
if (quiet) {
|
||||
// We assume `quiet=true` means "just add the bloody tag no questions asked"
|
||||
return;
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -905,7 +932,11 @@ class StealthRockTag extends ArenaTrapTag {
|
||||
onAdd(arena: Arena, quiet = false): void {
|
||||
super.onAdd(arena);
|
||||
|
||||
const source = this.sourceId ? globalScene.getPokemonById(this.sourceId) : null;
|
||||
if (quiet) {
|
||||
return;
|
||||
}
|
||||
|
||||
const source = this.getSourcePokemon();
|
||||
if (!quiet && source) {
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("arenaTag:stealthRockOnAdd", {
|
||||
@ -946,7 +977,7 @@ class StealthRockTag extends ArenaTrapTag {
|
||||
|
||||
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelled);
|
||||
applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled });
|
||||
if (cancelled.value) {
|
||||
return false;
|
||||
}
|
||||
@ -989,21 +1020,35 @@ class StickyWebTag extends ArenaTrapTag {
|
||||
|
||||
onAdd(arena: Arena, quiet = false): void {
|
||||
super.onAdd(arena);
|
||||
const source = this.sourceId ? globalScene.getPokemonById(this.sourceId) : null;
|
||||
if (!quiet && source) {
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("arenaTag:stickyWebOnAdd", {
|
||||
moveName: this.getMoveName(),
|
||||
opponentDesc: source.getOpponentDescriptor(),
|
||||
}),
|
||||
);
|
||||
|
||||
// We assume `quiet=true` means "just add the bloody tag no questions asked"
|
||||
if (quiet) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 {
|
||||
if (pokemon.isGrounded()) {
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyAbAttrs("ProtectStatAbAttr", pokemon, cancelled);
|
||||
applyAbAttrs("ProtectStatAbAttr", {
|
||||
pokemon,
|
||||
cancelled,
|
||||
stat: Stat.SPD,
|
||||
stages: -1,
|
||||
});
|
||||
|
||||
if (simulated) {
|
||||
return !cancelled.value;
|
||||
@ -1061,14 +1106,20 @@ export class TrickRoomTag extends ArenaTag {
|
||||
}
|
||||
|
||||
onAdd(_arena: Arena): void {
|
||||
const source = this.sourceId ? globalScene.getPokemonById(this.sourceId) : null;
|
||||
if (source) {
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("arenaTag:trickRoomOnAdd", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(source),
|
||||
}),
|
||||
);
|
||||
super.onAdd(_arena);
|
||||
|
||||
const source = this.getSourcePokemon();
|
||||
if (!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 {
|
||||
@ -1115,6 +1166,13 @@ class TailwindTag extends ArenaTag {
|
||||
}
|
||||
|
||||
onAdd(_arena: Arena, quiet = false): void {
|
||||
const source = this.getSourcePokemon();
|
||||
if (!source) {
|
||||
return;
|
||||
}
|
||||
|
||||
super.onAdd(_arena, quiet);
|
||||
|
||||
if (!quiet) {
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t(
|
||||
@ -1123,15 +1181,14 @@ class TailwindTag extends ArenaTag {
|
||||
);
|
||||
}
|
||||
|
||||
const source = globalScene.getPokemonById(this.sourceId!); //TODO: this bang is questionable!
|
||||
const party = (source?.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField()) ?? [];
|
||||
const phaseManager = globalScene.phaseManager;
|
||||
const field = source.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField();
|
||||
|
||||
for (const pokemon of party) {
|
||||
for (const pokemon of field) {
|
||||
// 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)) {
|
||||
pokemon.addTag(BattlerTagType.CHARGED);
|
||||
phaseManager.queueMessage(
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("abilityTriggers:windPowerCharged", {
|
||||
pokemonName: getPokemonNameWithAffix(pokemon),
|
||||
moveName: this.getMoveName(),
|
||||
@ -1142,9 +1199,16 @@ class TailwindTag extends ArenaTag {
|
||||
// Raise attack by one stage if party member has WIND_RIDER ability
|
||||
// TODO: Ability displays should be handled by the ability
|
||||
if (pokemon.hasAbility(AbilityId.WIND_RIDER)) {
|
||||
phaseManager.queueAbilityDisplay(pokemon, false, true);
|
||||
phaseManager.unshiftNew("StatStageChangePhase", pokemon.getBattlerIndex(), true, [Stat.ATK], 1, true);
|
||||
phaseManager.queueAbilityDisplay(pokemon, false, false);
|
||||
globalScene.phaseManager.queueAbilityDisplay(pokemon, false, true);
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"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.
|
||||
* @param arena
|
||||
* Apply the effects of Imprison to all opposing on-field Pokemon.
|
||||
*/
|
||||
override onAdd() {
|
||||
const source = this.getSourcePokemon();
|
||||
if (source) {
|
||||
const party = this.getAffectedPokemon();
|
||||
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),
|
||||
}),
|
||||
);
|
||||
if (!source) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
const party = this.getAffectedPokemon();
|
||||
party?.forEach((p: Pokemon) => {
|
||||
p.removeTag(BattlerTagType.IMPRISON);
|
||||
});
|
||||
party.forEach(p => p.removeTag(BattlerTagType.IMPRISON));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1416,7 +1480,9 @@ export class SuppressAbilitiesTag extends ArenaTag {
|
||||
|
||||
for (const fieldPokemon of globalScene.getField(true)) {
|
||||
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
|
||||
.getField()
|
||||
.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)) {
|
||||
// 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)) {
|
||||
[true, false].forEach(passive => applyOnGainAbAttrs(pokemon, passive));
|
||||
[true, false].forEach(passive => applyOnGainAbAttrs({ pokemon, passive }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -111,7 +111,7 @@ export class BattlerTag {
|
||||
* @returns The source {@linkcode Pokemon}, or `null` if none is found
|
||||
*/
|
||||
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 {
|
||||
const source = globalScene.getPokemonById(this.sourceId!)!;
|
||||
const move = allMoves[this.sourceMove];
|
||||
const source = this.getSourcePokemon();
|
||||
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 isTrapped = pokemon.getTag(TrappedTag);
|
||||
const hasSubstitute = move.hitsSubstitute(source, pokemon);
|
||||
@ -621,7 +625,7 @@ export class FlinchedTag extends BattlerTag {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
}),
|
||||
);
|
||||
applyAbAttrs("FlinchEffectAbAttr", pokemon, null);
|
||||
applyAbAttrs("FlinchEffectAbAttr", { pokemon });
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -669,7 +673,12 @@ export class ConfusedTag extends BattlerTag {
|
||||
}
|
||||
|
||||
canAdd(pokemon: Pokemon): boolean {
|
||||
return globalScene.arena.terrain?.terrainType !== TerrainType.MISTY || !pokemon.isGrounded();
|
||||
const blockedByTerrain = pokemon.isGrounded() && globalScene.arena.terrain?.terrainType === TerrainType.MISTY;
|
||||
if (blockedByTerrain) {
|
||||
pokemon.queueStatusImmuneMessage(false, TerrainType.MISTY);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
onAdd(pokemon: Pokemon): void {
|
||||
@ -763,12 +772,20 @@ export class DestinyBondTag extends BattlerTag {
|
||||
if (lapseType !== BattlerTagLapseType.CUSTOM) {
|
||||
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;
|
||||
}
|
||||
|
||||
if (source?.getAlly() === pokemon) {
|
||||
// Don't kill allies or opposing bosses.
|
||||
if (source.getAlly() === pokemon) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -781,6 +798,7 @@ export class DestinyBondTag extends BattlerTag {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Drag the foe down with the user
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("battlerTags:destinyBondLapse", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(source),
|
||||
@ -798,17 +816,13 @@ export class InfatuatedTag extends BattlerTag {
|
||||
}
|
||||
|
||||
canAdd(pokemon: Pokemon): boolean {
|
||||
if (this.sourceId) {
|
||||
const pkm = globalScene.getPokemonById(this.sourceId);
|
||||
|
||||
if (pkm) {
|
||||
return pokemon.isOppositeGender(pkm);
|
||||
}
|
||||
console.warn("canAdd: this.sourceId is not a valid pokemon id!", this.sourceId);
|
||||
const source = this.getSourcePokemon();
|
||||
if (!source) {
|
||||
console.warn(`Failed to get source Pokemon for InfatuatedTag canAdd; id: ${this.sourceId}`);
|
||||
return false;
|
||||
}
|
||||
console.warn("canAdd: this.sourceId is undefined");
|
||||
return false;
|
||||
|
||||
return pokemon.isOppositeGender(source);
|
||||
}
|
||||
|
||||
onAdd(pokemon: Pokemon): void {
|
||||
@ -817,7 +831,7 @@ export class InfatuatedTag extends BattlerTag {
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("battlerTags:infatuatedOnAdd", {
|
||||
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 +849,36 @@ export class InfatuatedTag extends BattlerTag {
|
||||
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||
const ret = lapseType !== BattlerTagLapseType.CUSTOM || super.lapse(pokemon, lapseType);
|
||||
|
||||
const phaseManager = globalScene.phaseManager;
|
||||
|
||||
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();
|
||||
}
|
||||
if (!ret) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -899,6 +921,12 @@ export class SeedTag extends BattlerTag {
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
globalScene.phaseManager.queueMessage(
|
||||
@ -906,47 +934,51 @@ export class SeedTag extends BattlerTag {
|
||||
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 {
|
||||
const ret = lapseType !== BattlerTagLapseType.CUSTOM || super.lapse(pokemon, lapseType);
|
||||
|
||||
if (ret) {
|
||||
const source = pokemon.getOpponents().find(o => o.getBattlerIndex() === this.sourceIndex);
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!ret) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -1006,7 +1038,7 @@ export class PowderTag extends BattlerTag {
|
||||
globalScene.phaseManager.unshiftNew("CommonAnimPhase", idx, idx, CommonAnim.POWDER);
|
||||
|
||||
const cancelDamage = new BooleanHolder(false);
|
||||
applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelDamage);
|
||||
applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled: cancelDamage });
|
||||
if (!cancelDamage.value) {
|
||||
pokemon.damageAndUpdate(Math.floor(pokemon.getMaxHp() / 4), { result: HitResult.INDIRECT });
|
||||
}
|
||||
@ -1056,7 +1088,7 @@ export class NightmareTag extends BattlerTag {
|
||||
phaseManager.unshiftNew("CommonAnimPhase", pokemon.getBattlerIndex(), undefined, CommonAnim.CURSE); // TODO: Update animation type
|
||||
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelled);
|
||||
applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled });
|
||||
|
||||
if (!cancelled.value) {
|
||||
pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 4), { result: HitResult.INDIRECT });
|
||||
@ -1195,9 +1227,15 @@ export class HelpingHandTag extends BattlerTag {
|
||||
}
|
||||
|
||||
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(
|
||||
i18next.t("battlerTags:helpingHandOnAdd", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(globalScene.getPokemonById(this.sourceId!) ?? undefined), // TODO: is that bang correct?
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(source),
|
||||
pokemonName: getPokemonNameWithAffix(pokemon),
|
||||
}),
|
||||
);
|
||||
@ -1219,9 +1257,7 @@ export class IngrainTag extends TrappedTag {
|
||||
* @returns boolean True if the tag can be added, false otherwise
|
||||
*/
|
||||
canAdd(pokemon: Pokemon): boolean {
|
||||
const isTrapped = pokemon.getTag(BattlerTagType.TRAPPED);
|
||||
|
||||
return !isTrapped;
|
||||
return !pokemon.getTag(BattlerTagType.TRAPPED);
|
||||
}
|
||||
|
||||
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||
@ -1409,7 +1445,7 @@ export abstract class DamagingTrapTag extends TrappedTag {
|
||||
phaseManager.unshiftNew("CommonAnimPhase", pokemon.getBattlerIndex(), undefined, this.commonAnim);
|
||||
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelled);
|
||||
applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled });
|
||||
|
||||
if (!cancelled.value) {
|
||||
pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8), { result: HitResult.INDIRECT });
|
||||
@ -1420,15 +1456,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 {
|
||||
constructor(turnCount: number, sourceId: number) {
|
||||
super(BattlerTagType.BIND, CommonAnim.BIND, turnCount, MoveId.BIND, sourceId);
|
||||
}
|
||||
|
||||
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", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
sourcePokemonName: getPokemonNameWithAffix(globalScene.getPokemonById(this.sourceId!) ?? undefined), // TODO: is that bang correct?
|
||||
sourcePokemonName: getPokemonNameWithAffix(source),
|
||||
moveName: this.getMoveName(),
|
||||
});
|
||||
}
|
||||
@ -1440,9 +1483,16 @@ export class WrapTag extends DamagingTrapTag {
|
||||
}
|
||||
|
||||
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", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
sourcePokemonName: getPokemonNameWithAffix(globalScene.getPokemonById(this.sourceId!) ?? undefined), // TODO: is that bang correct?
|
||||
sourcePokemonName: getPokemonNameWithAffix(source),
|
||||
moveName: this.getMoveName(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1473,8 +1523,14 @@ export class ClampTag extends DamagingTrapTag {
|
||||
}
|
||||
|
||||
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", {
|
||||
sourcePokemonNameWithAffix: getPokemonNameWithAffix(globalScene.getPokemonById(this.sourceId!) ?? undefined), // TODO: is that bang correct?
|
||||
sourcePokemonNameWithAffix: getPokemonNameWithAffix(source),
|
||||
pokemonName: getPokemonNameWithAffix(pokemon),
|
||||
});
|
||||
}
|
||||
@ -1523,9 +1579,15 @@ export class ThunderCageTag extends DamagingTrapTag {
|
||||
}
|
||||
|
||||
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", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
sourcePokemonNameWithAffix: getPokemonNameWithAffix(globalScene.getPokemonById(this.sourceId!) ?? undefined), // TODO: is that bang correct?
|
||||
sourcePokemonNameWithAffix: getPokemonNameWithAffix(source),
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1536,9 +1598,15 @@ export class InfestationTag extends DamagingTrapTag {
|
||||
}
|
||||
|
||||
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", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
sourcePokemonNameWithAffix: getPokemonNameWithAffix(globalScene.getPokemonById(this.sourceId!) ?? undefined), // TODO: is that bang correct?
|
||||
sourcePokemonNameWithAffix: getPokemonNameWithAffix(source),
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1642,7 +1710,7 @@ export class ContactDamageProtectedTag extends ContactProtectedTag {
|
||||
*/
|
||||
override onContact(attacker: Pokemon, user: Pokemon): void {
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyAbAttrs("BlockNonDirectDamageAbAttr", user, cancelled);
|
||||
applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon: user, cancelled });
|
||||
if (!cancelled.value) {
|
||||
attacker.damageAndUpdate(toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio)), {
|
||||
result: HitResult.INDIRECT,
|
||||
@ -2221,14 +2289,19 @@ export class SaltCuredTag extends BattlerTag {
|
||||
}
|
||||
|
||||
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(
|
||||
i18next.t("battlerTags:saltCuredOnAdd", {
|
||||
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 {
|
||||
@ -2243,7 +2316,7 @@ export class SaltCuredTag extends BattlerTag {
|
||||
);
|
||||
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelled);
|
||||
applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled });
|
||||
|
||||
if (!cancelled.value) {
|
||||
const pokemonSteelOrWater = pokemon.isOfType(PokemonType.STEEL) || pokemon.isOfType(PokemonType.WATER);
|
||||
@ -2281,8 +2354,14 @@ export class CursedTag extends BattlerTag {
|
||||
}
|
||||
|
||||
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);
|
||||
this.sourceIndex = globalScene.getPokemonById(this.sourceId!)!.getBattlerIndex(); // TODO: are those bangs correct?
|
||||
this.sourceIndex = source.getBattlerIndex();
|
||||
}
|
||||
|
||||
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||
@ -2297,7 +2376,7 @@ export class CursedTag extends BattlerTag {
|
||||
);
|
||||
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelled);
|
||||
applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled });
|
||||
|
||||
if (!cancelled.value) {
|
||||
pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 4), { result: HitResult.INDIRECT });
|
||||
@ -2632,7 +2711,7 @@ export class GulpMissileTag extends BattlerTag {
|
||||
}
|
||||
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyAbAttrs("BlockNonDirectDamageAbAttr", attacker, cancelled);
|
||||
applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon: attacker, cancelled });
|
||||
|
||||
if (!cancelled.value) {
|
||||
attacker.damageAndUpdate(Math.max(1, Math.floor(attacker.getMaxHp() / 4)), { result: HitResult.INDIRECT });
|
||||
@ -2902,7 +2981,13 @@ export class SubstituteTag extends BattlerTag {
|
||||
|
||||
/** Sets the Substitute's HP and queues an on-add battle animation that initializes the Substitute's sprite. */
|
||||
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;
|
||||
|
||||
// Queue battle animation and message
|
||||
@ -3021,14 +3106,7 @@ export class MysteryEncounterPostSummonTag extends BattlerTag {
|
||||
const ret = super.lapse(pokemon, lapseType);
|
||||
|
||||
if (lapseType === BattlerTagLapseType.CUSTOM) {
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyAbAttrs("ProtectStatAbAttr", pokemon, cancelled);
|
||||
applyAbAttrs("ConditionalUserFieldProtectStatAbAttr", pokemon, cancelled, false, pokemon);
|
||||
if (!cancelled.value) {
|
||||
if (pokemon.mysteryEncounterBattleEffects) {
|
||||
pokemon.mysteryEncounterBattleEffects(pokemon);
|
||||
}
|
||||
}
|
||||
pokemon.mysteryEncounterBattleEffects?.(pokemon);
|
||||
}
|
||||
|
||||
return ret;
|
||||
@ -3182,13 +3260,14 @@ export class ImprisonTag extends MoveRestrictionBattlerTag {
|
||||
*/
|
||||
public override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||
const source = this.getSourcePokemon();
|
||||
if (source) {
|
||||
if (lapseType === BattlerTagLapseType.PRE_MOVE) {
|
||||
return super.lapse(pokemon, lapseType) && source.isActive(true);
|
||||
}
|
||||
return source.isActive(true);
|
||||
if (!source) {
|
||||
console.warn(`Failed to get source Pokemon for ImprisonTag lapse; id: ${this.sourceId}`);
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
if (lapseType === BattlerTagLapseType.PRE_MOVE) {
|
||||
return super.lapse(pokemon, lapseType) && source.isActive(true);
|
||||
}
|
||||
return source.isActive(true);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3248,12 +3327,20 @@ export class SyrupBombTag extends BattlerTag {
|
||||
* Applies the single-stage speed down to the target Pokemon and decrements the tag's turn count
|
||||
* @param pokemon - The target {@linkcode Pokemon}
|
||||
* @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 {
|
||||
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;
|
||||
}
|
||||
|
||||
// Syrup bomb clears immediately if source leaves field/faints
|
||||
if (!source.isActive(true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Custom message in lieu of an animation in mainline
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("battlerTags:syrupBombLapse", {
|
||||
@ -3270,7 +3357,7 @@ export class SyrupBombTag extends BattlerTag {
|
||||
false,
|
||||
true,
|
||||
);
|
||||
return --this.turnCount > 0;
|
||||
return super.lapse(pokemon, _lapseType);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -35,28 +35,28 @@ export function getBerryPredicate(berryType: BerryType): BerryPredicate {
|
||||
case BerryType.APICOT:
|
||||
case BerryType.SALAC:
|
||||
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
|
||||
const stat: BattleStat = berryType - BerryType.ENIGMA;
|
||||
applyAbAttrs("ReduceBerryUseThresholdAbAttr", pokemon, null, false, threshold);
|
||||
return pokemon.getHpRatio() < threshold.value && pokemon.getStatStage(stat) < 6;
|
||||
applyAbAttrs("ReduceBerryUseThresholdAbAttr", { pokemon, hpRatioReq });
|
||||
return pokemon.getHpRatio() < hpRatioReq.value && pokemon.getStatStage(stat) < 6;
|
||||
};
|
||||
case BerryType.LANSAT:
|
||||
return (pokemon: Pokemon) => {
|
||||
const threshold = new NumberHolder(0.25);
|
||||
applyAbAttrs("ReduceBerryUseThresholdAbAttr", pokemon, null, false, threshold);
|
||||
const hpRatioReq = new NumberHolder(0.25);
|
||||
applyAbAttrs("ReduceBerryUseThresholdAbAttr", { pokemon, hpRatioReq });
|
||||
return pokemon.getHpRatio() < 0.25 && !pokemon.getTag(BattlerTagType.CRIT_BOOST);
|
||||
};
|
||||
case BerryType.STARF:
|
||||
return (pokemon: Pokemon) => {
|
||||
const threshold = new NumberHolder(0.25);
|
||||
applyAbAttrs("ReduceBerryUseThresholdAbAttr", pokemon, null, false, threshold);
|
||||
const hpRatioReq = new NumberHolder(0.25);
|
||||
applyAbAttrs("ReduceBerryUseThresholdAbAttr", { pokemon, hpRatioReq });
|
||||
return pokemon.getHpRatio() < 0.25;
|
||||
};
|
||||
case BerryType.LEPPA:
|
||||
return (pokemon: Pokemon) => {
|
||||
const threshold = new NumberHolder(0.25);
|
||||
applyAbAttrs("ReduceBerryUseThresholdAbAttr", pokemon, null, false, threshold);
|
||||
const hpRatioReq = new NumberHolder(0.25);
|
||||
applyAbAttrs("ReduceBerryUseThresholdAbAttr", { pokemon, hpRatioReq });
|
||||
return !!pokemon.getMoveset().find(m => !m.getPpRatio());
|
||||
};
|
||||
}
|
||||
@ -72,7 +72,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc {
|
||||
case BerryType.ENIGMA:
|
||||
{
|
||||
const hpHealed = new NumberHolder(toDmgValue(consumer.getMaxHp() / 4));
|
||||
applyAbAttrs("DoubleBerryEffectAbAttr", consumer, null, false, hpHealed);
|
||||
applyAbAttrs("DoubleBerryEffectAbAttr", { pokemon: consumer, effectValue: hpHealed });
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"PokemonHealPhase",
|
||||
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.
|
||||
const stat: BattleStat = berryType - BerryType.ENIGMA;
|
||||
const statStages = new NumberHolder(1);
|
||||
applyAbAttrs("DoubleBerryEffectAbAttr", consumer, null, false, statStages);
|
||||
applyAbAttrs("DoubleBerryEffectAbAttr", { pokemon: consumer, effectValue: statStages });
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"StatStageChangePhase",
|
||||
consumer.getBattlerIndex(),
|
||||
@ -126,7 +126,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc {
|
||||
{
|
||||
const randStat = randSeedInt(Stat.SPD, Stat.ATK);
|
||||
const stages = new NumberHolder(2);
|
||||
applyAbAttrs("DoubleBerryEffectAbAttr", consumer, null, false, stages);
|
||||
applyAbAttrs("DoubleBerryEffectAbAttr", { pokemon: consumer, effectValue: stages });
|
||||
globalScene.phaseManager.unshiftNew(
|
||||
"StatStageChangePhase",
|
||||
consumer.getBattlerIndex(),
|
||||
|
@ -294,7 +294,7 @@ export class Egg {
|
||||
|
||||
public getEggDescriptor(): string {
|
||||
if (this.isManaphyEgg()) {
|
||||
return "Manaphy";
|
||||
return i18next.t("egg:manaphyTier");
|
||||
}
|
||||
switch (this.tier) {
|
||||
case EggTier.RARE:
|
||||
|
@ -33,11 +33,7 @@ import type { ArenaTrapTag } from "../arena-tag";
|
||||
import { WeakenMoveTypeTag } from "../arena-tag";
|
||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
||||
import {
|
||||
applyAbAttrs,
|
||||
applyPostAttackAbAttrs,
|
||||
applyPostItemLostAbAttrs,
|
||||
applyPreAttackAbAttrs,
|
||||
applyPreDefendAbAttrs
|
||||
applyAbAttrs
|
||||
} from "../abilities/apply-ab-attrs";
|
||||
import { allAbilities, allMoves } from "../data-lists";
|
||||
import {
|
||||
@ -89,9 +85,10 @@ import { MoveEffectTrigger } from "#enums/MoveEffectTrigger";
|
||||
import { MultiHitType } from "#enums/MultiHitType";
|
||||
import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSleepTalkMoves, invalidSketchMoves } from "./invalid-moves";
|
||||
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 { frenzyMissFunc, getMoveTargets } from "./move-utils";
|
||||
import { AbAttrBaseParams, AbAttrParamsWithCancel, PreAttackModifyPowerAbAttrParams } from "../abilities/ability";
|
||||
|
||||
/**
|
||||
* 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);
|
||||
// TODO: Allow this to be simulated
|
||||
applyAbAttrs("InfiltratorAbAttr", user, null, false, bypassed);
|
||||
applyAbAttrs("InfiltratorAbAttr", {pokemon: user, bypassed});
|
||||
|
||||
return !bypassed.value
|
||||
&& !this.hasFlag(MoveFlags.SOUND_BASED)
|
||||
@ -645,7 +642,7 @@ export default abstract class Move implements Localizable {
|
||||
case MoveFlags.IGNORE_ABILITIES:
|
||||
if (user.hasAbilityWithAttr("MoveAbilityBypassAbAttr")) {
|
||||
const abilityEffectsIgnored = new BooleanHolder(false);
|
||||
applyAbAttrs("MoveAbilityBypassAbAttr", user, abilityEffectsIgnored, false, this);
|
||||
applyAbAttrs("MoveAbilityBypassAbAttr", {pokemon: user, cancelled: abilityEffectsIgnored, move: this});
|
||||
if (abilityEffectsIgnored.value) {
|
||||
return true;
|
||||
}
|
||||
@ -762,7 +759,7 @@ export default abstract class Move implements Localizable {
|
||||
const moveAccuracy = new NumberHolder(this.accuracy);
|
||||
|
||||
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) {
|
||||
return moveAccuracy.value;
|
||||
@ -805,17 +802,25 @@ export default abstract class Move implements Localizable {
|
||||
const typeChangeMovePowerMultiplier = new NumberHolder(1);
|
||||
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();
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
if (!isNullOrUndefined(ally)) {
|
||||
applyPreAttackAbAttrs("AllyMoveCategoryPowerBoostAbAttr", ally, target, this, simulated, power);
|
||||
applyAbAttrs("AllyMoveCategoryPowerBoostAbAttr", {...abAttrParams, pokemon: ally});
|
||||
}
|
||||
|
||||
const fieldAuras = new Set(
|
||||
@ -827,11 +832,12 @@ export default abstract class Move implements Localizable {
|
||||
.flat(),
|
||||
);
|
||||
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();
|
||||
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;
|
||||
|
||||
@ -858,7 +864,7 @@ export default abstract class Move implements Localizable {
|
||||
const priority = new NumberHolder(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;
|
||||
}
|
||||
@ -1310,7 +1316,7 @@ export class MoveEffectAttr extends MoveAttr {
|
||||
getMoveChance(user: Pokemon, target: Pokemon, move: Move, selfEffect?: Boolean, showAbility?: Boolean): number {
|
||||
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")) {
|
||||
const userSide = user.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
||||
@ -1318,7 +1324,7 @@ export class MoveEffectAttr extends MoveAttr {
|
||||
}
|
||||
|
||||
if (!selfEffect) {
|
||||
applyPreDefendAbAttrs("IgnoreMoveEffectsAbAttr", target, user, null, null, !showAbility, moveChance);
|
||||
applyAbAttrs("IgnoreMoveEffectsAbAttr", {pokemon: target, move, simulated: !showAbility, chance: moveChance});
|
||||
}
|
||||
return moveChance.value;
|
||||
}
|
||||
@ -1709,8 +1715,9 @@ export class RecoilAttr extends MoveEffectAttr {
|
||||
|
||||
const cancelled = new BooleanHolder(false);
|
||||
if (!this.unblockable) {
|
||||
applyAbAttrs("BlockRecoilDamageAttr", user, cancelled);
|
||||
applyAbAttrs("BlockNonDirectDamageAbAttr", user, cancelled);
|
||||
const abAttrParams: AbAttrParamsWithCancel = {pokemon: user, cancelled};
|
||||
applyAbAttrs("BlockRecoilDamageAttr", abAttrParams);
|
||||
applyAbAttrs("BlockNonDirectDamageAbAttr", abAttrParams);
|
||||
}
|
||||
|
||||
if (cancelled.value) {
|
||||
@ -1843,7 +1850,7 @@ export class HalfSacrificialAttr extends MoveEffectAttr {
|
||||
|
||||
const cancelled = new BooleanHolder(false);
|
||||
// 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) {
|
||||
user.damageAndUpdate(toDmgValue(user.getMaxHp() / 2), { result: HitResult.INDIRECT, ignoreSegments: true });
|
||||
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);
|
||||
|
||||
if (!isNullOrUndefined(targetAlly)) {
|
||||
applyAbAttrs("BlockNonDirectDamageAbAttr", targetAlly, cancelled);
|
||||
applyAbAttrs("BlockNonDirectDamageAbAttr", {pokemon: targetAlly, cancelled});
|
||||
}
|
||||
|
||||
if (cancelled.value || !targetAlly || targetAlly.switchOutStatus) {
|
||||
@ -2414,7 +2421,7 @@ export class MultiHitAttr extends MoveAttr {
|
||||
{
|
||||
const rand = user.randBattleSeedInt(20);
|
||||
const hitValue = new NumberHolder(rand);
|
||||
applyAbAttrs("MaxMultiHitAbAttr", user, null, false, hitValue);
|
||||
applyAbAttrs("MaxMultiHitAbAttr", {pokemon: user, hits: hitValue});
|
||||
if (hitValue.value >= 13) {
|
||||
return 2;
|
||||
} 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))
|
||||
&& 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;
|
||||
}
|
||||
}
|
||||
@ -2574,7 +2581,7 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr {
|
||||
apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
|
||||
const statusToApply: StatusEffect | undefined = user.status?.effect ?? (user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : undefined);
|
||||
|
||||
if (target.status) {
|
||||
if (target.status || !statusToApply) {
|
||||
return false;
|
||||
} else {
|
||||
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 {
|
||||
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
|
||||
// TODO: This should not trigger if the target would faint beforehand
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyAbAttrs("BlockItemTheftAbAttr", target, cancelled);
|
||||
applyAbAttrs("BlockItemTheftAbAttr", {pokemon: target, cancelled});
|
||||
|
||||
if (cancelled.value) {
|
||||
return false;
|
||||
@ -2795,8 +2803,8 @@ export class EatBerryAttr extends MoveEffectAttr {
|
||||
protected eatBerry(consumer: Pokemon, berryOwner: Pokemon = consumer, updateHarvest = consumer === berryOwner) {
|
||||
// consumer eats berry, owner triggers unburden and similar effects
|
||||
getBerryEffectFunc(this.chosenBerry.berryType)(consumer);
|
||||
applyPostItemLostAbAttrs("PostItemLostAbAttr", berryOwner, false);
|
||||
applyAbAttrs("HealFromBerryUseAbAttr", consumer, new BooleanHolder(false));
|
||||
applyAbAttrs("PostItemLostAbAttr", {pokemon: berryOwner});
|
||||
applyAbAttrs("HealFromBerryUseAbAttr", {pokemon: consumer});
|
||||
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 {
|
||||
// check for abilities that block item theft
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyAbAttrs("BlockItemTheftAbAttr", target, cancelled);
|
||||
applyAbAttrs("BlockItemTheftAbAttr", {pokemon: target, cancelled});
|
||||
if (cancelled.value === true) {
|
||||
return false;
|
||||
}
|
||||
@ -2835,7 +2843,7 @@ export class StealEatBerryAttr extends EatBerryAttr {
|
||||
|
||||
// pick a random berry and eat it
|
||||
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 });
|
||||
globalScene.phaseManager.queueMessage(message);
|
||||
this.reduceBerryModifier(target);
|
||||
@ -3026,7 +3034,7 @@ export class OneHitKOAttr extends MoveAttr {
|
||||
getCondition(): MoveConditionFunc {
|
||||
return (user, target, move) => {
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyAbAttrs("BlockOneHitKOAbAttr", target, cancelled);
|
||||
applyAbAttrs("BlockOneHitKOAbAttr", {pokemon: target, cancelled});
|
||||
return !cancelled.value && user.level >= target.level;
|
||||
};
|
||||
}
|
||||
@ -5438,7 +5446,7 @@ export class NoEffectAttr extends MoveAttr {
|
||||
|
||||
const crashDamageFunc = (user: Pokemon, move: Move) => {
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyAbAttrs("BlockNonDirectDamageAbAttr", user, cancelled);
|
||||
applyAbAttrs("BlockNonDirectDamageAbAttr", {pokemon: user, cancelled});
|
||||
if (cancelled.value) {
|
||||
return false;
|
||||
}
|
||||
@ -6437,9 +6445,9 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
||||
}
|
||||
|
||||
getFailedText(_user: Pokemon, target: Pokemon, _move: Move): string | undefined {
|
||||
const blockedByAbility = new BooleanHolder(false);
|
||||
applyAbAttrs("ForceSwitchOutImmunityAbAttr", target, blockedByAbility);
|
||||
if (blockedByAbility.value) {
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyAbAttrs("ForceSwitchOutImmunityAbAttr", {pokemon: target, cancelled});
|
||||
if (cancelled.value) {
|
||||
return i18next.t("moveTriggers:cannotBeSwitchedOut", { pokemonName: getPokemonNameWithAffix(target) });
|
||||
}
|
||||
}
|
||||
@ -6478,7 +6486,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
||||
}
|
||||
|
||||
const blockedByAbility = new BooleanHolder(false);
|
||||
applyAbAttrs("ForceSwitchOutImmunityAbAttr", target, blockedByAbility);
|
||||
applyAbAttrs("ForceSwitchOutImmunityAbAttr", {pokemon: target, cancelled: blockedByAbility});
|
||||
if (blockedByAbility.value) {
|
||||
return false;
|
||||
}
|
||||
@ -6887,12 +6895,12 @@ export class RandomMovesetMoveAttr extends CallMoveAttr {
|
||||
// includeParty will be true for Assist, false for Sleep Talk
|
||||
let allies: Pokemon[];
|
||||
if (this.includeParty) {
|
||||
allies = user.isPlayer() ? globalScene.getPlayerParty().filter(p => p !== user) : globalScene.getEnemyParty().filter(p => p !== user);
|
||||
allies = (user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty()).filter(p => p !== user);
|
||||
} else {
|
||||
allies = [ user ];
|
||||
}
|
||||
const partyMoveset = allies.map(p => p.moveset).flat();
|
||||
const moves = partyMoveset.filter(m => !this.invalidMoves.has(m!.moveId) && !m!.getMove().name.endsWith(" (N)"));
|
||||
const partyMoveset = allies.flatMap(p => p.moveset);
|
||||
const moves = partyMoveset.filter(m => !this.invalidMoves.has(m.moveId) && !m.getMove().name.endsWith(" (N)"));
|
||||
if (moves.length === 0) {
|
||||
return false;
|
||||
}
|
||||
@ -7987,7 +7995,7 @@ const failIfSingleBattle: MoveConditionFunc = (user, target, move) => globalScen
|
||||
|
||||
const failIfDampCondition: MoveConditionFunc = (user, target, move) => {
|
||||
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
|
||||
if (cancelled.value) {
|
||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:cannotUseMove", { pokemonName: getPokemonNameWithAffix(user), moveName: move.name }));
|
||||
|
@ -328,7 +328,7 @@ export const DancingLessonsEncounter: MysteryEncounter = MysteryEncounterBuilder
|
||||
.withOptionPhase(async () => {
|
||||
// Show the Oricorio a dance, and recruit it
|
||||
const encounter = globalScene.currentBattle.mysteryEncounter!;
|
||||
const oricorio = encounter.misc.oricorioData.toPokemon();
|
||||
const oricorio = encounter.misc.oricorioData.toPokemon() as EnemyPokemon;
|
||||
oricorio.passive = true;
|
||||
|
||||
// Ensure the Oricorio's moveset gains the Dance move the player used
|
||||
|
@ -24,7 +24,7 @@ import { PokemonType } from "#enums/pokemon-type";
|
||||
import { BerryType } from "#enums/berry-type";
|
||||
import { Stat } from "#enums/stat";
|
||||
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 { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
|
||||
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
|
||||
pokemon.resetBattleAndWaveData();
|
||||
applyPostBattleInitAbAttrs("PostBattleInitAbAttr", pokemon);
|
||||
applyAbAttrs("PostBattleInitAbAttr", { pokemon });
|
||||
}
|
||||
|
||||
globalScene.phaseManager.unshiftNew("ShowTrainerPhase");
|
||||
|
@ -3,6 +3,7 @@ import type Move from "./moves/move";
|
||||
import { PokemonType } from "#enums/pokemon-type";
|
||||
import type { BattlerIndex } from "#enums/battler-index";
|
||||
import i18next from "i18next";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
|
||||
export enum TerrainType {
|
||||
NONE,
|
||||
@ -96,3 +97,76 @@ export function getTerrainColor(terrainType: TerrainType): [number, number, numb
|
||||
|
||||
return [0, 0, 0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the message associated with a terrain effect starting.
|
||||
* @param terrainType - The {@linkcode TerrainType} starting.
|
||||
* @returns A string containing the appropriate terrain start text.
|
||||
*/
|
||||
export function getTerrainStartMessage(terrainType: TerrainType): string {
|
||||
switch (terrainType) {
|
||||
case TerrainType.MISTY:
|
||||
return i18next.t("terrain:mistyStartMessage");
|
||||
case TerrainType.ELECTRIC:
|
||||
return i18next.t("terrain:electricStartMessage");
|
||||
case TerrainType.GRASSY:
|
||||
return i18next.t("terrain:grassyStartMessage");
|
||||
case TerrainType.PSYCHIC:
|
||||
return i18next.t("terrain:psychicStartMessage");
|
||||
case TerrainType.NONE:
|
||||
default:
|
||||
terrainType satisfies TerrainType.NONE;
|
||||
console.warn(`${terrainType} unexpectedly provided as terrain type to getTerrainStartMessage!`);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the message associated with a terrain effect ceasing to exist.
|
||||
* @param terrainType - The {@linkcode TerrainType} being cleared.
|
||||
* @returns A string containing the appropriate terrain clear text.
|
||||
*/
|
||||
export function getTerrainClearMessage(terrainType: TerrainType): string {
|
||||
switch (terrainType) {
|
||||
case TerrainType.MISTY:
|
||||
return i18next.t("terrain:mistyClearMessage");
|
||||
case TerrainType.ELECTRIC:
|
||||
return i18next.t("terrain:electricClearMessage");
|
||||
case TerrainType.GRASSY:
|
||||
return i18next.t("terrain:grassyClearMessage");
|
||||
case TerrainType.PSYCHIC:
|
||||
return i18next.t("terrain:psychicClearMessage");
|
||||
case TerrainType.NONE:
|
||||
default:
|
||||
terrainType satisfies TerrainType.NONE;
|
||||
console.warn(`${terrainType} unexpectedly provided as terrain type to getTerrainClearMessage!`);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the message associated with a terrain-induced move/effect blockage.
|
||||
* @param pokemon - The {@linkcode Pokemon} being protected.
|
||||
* @param terrainType - The {@linkcode TerrainType} in question
|
||||
* @returns A string containing the appropriate terrain block text.
|
||||
*/
|
||||
export function getTerrainBlockMessage(pokemon: Pokemon, terrainType: TerrainType): string {
|
||||
switch (terrainType) {
|
||||
case TerrainType.MISTY:
|
||||
return i18next.t("terrain:mistyBlockMessage", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
});
|
||||
case TerrainType.ELECTRIC:
|
||||
case TerrainType.GRASSY:
|
||||
case TerrainType.PSYCHIC:
|
||||
return i18next.t("terrain:defaultBlockMessage", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
terrainName: getTerrainName(terrainType),
|
||||
});
|
||||
case TerrainType.NONE:
|
||||
default:
|
||||
terrainType satisfies TerrainType.NONE;
|
||||
console.warn(`${terrainType} unexpectedly provided as terrain type to getTerrainBlockMessage!`);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ import type Pokemon from "../field/pokemon";
|
||||
import { PokemonType } from "#enums/pokemon-type";
|
||||
import type Move from "./moves/move";
|
||||
import { randSeedInt } from "#app/utils/common";
|
||||
import { TerrainType, getTerrainName } from "./terrain";
|
||||
import i18next from "i18next";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import type { Arena } from "#app/field/arena";
|
||||
@ -235,50 +234,6 @@ export function getWeatherBlockMessage(weatherType: WeatherType): string {
|
||||
return i18next.t("weather:defaultEffectMessage");
|
||||
}
|
||||
|
||||
export function getTerrainStartMessage(terrainType: TerrainType): string | null {
|
||||
switch (terrainType) {
|
||||
case TerrainType.MISTY:
|
||||
return i18next.t("terrain:mistyStartMessage");
|
||||
case TerrainType.ELECTRIC:
|
||||
return i18next.t("terrain:electricStartMessage");
|
||||
case TerrainType.GRASSY:
|
||||
return i18next.t("terrain:grassyStartMessage");
|
||||
case TerrainType.PSYCHIC:
|
||||
return i18next.t("terrain:psychicStartMessage");
|
||||
default:
|
||||
console.warn("getTerrainStartMessage not defined. Using default null");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getTerrainClearMessage(terrainType: TerrainType): string | null {
|
||||
switch (terrainType) {
|
||||
case TerrainType.MISTY:
|
||||
return i18next.t("terrain:mistyClearMessage");
|
||||
case TerrainType.ELECTRIC:
|
||||
return i18next.t("terrain:electricClearMessage");
|
||||
case TerrainType.GRASSY:
|
||||
return i18next.t("terrain:grassyClearMessage");
|
||||
case TerrainType.PSYCHIC:
|
||||
return i18next.t("terrain:psychicClearMessage");
|
||||
default:
|
||||
console.warn("getTerrainClearMessage not defined. Using default null");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getTerrainBlockMessage(pokemon: Pokemon, terrainType: TerrainType): string {
|
||||
if (terrainType === TerrainType.MISTY) {
|
||||
return i18next.t("terrain:mistyBlockMessage", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
});
|
||||
}
|
||||
return i18next.t("terrain:defaultBlockMessage", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||
terrainName: getTerrainName(terrainType),
|
||||
});
|
||||
}
|
||||
|
||||
export interface WeatherPoolEntry {
|
||||
weatherType: WeatherType;
|
||||
weight: number;
|
||||
|
@ -5,8 +5,6 @@ import { randSeedInt, NumberHolder, isNullOrUndefined, type Constructor } from "
|
||||
import type PokemonSpecies from "#app/data/pokemon-species";
|
||||
import { getPokemonSpecies } from "#app/utils/pokemon-utils";
|
||||
import {
|
||||
getTerrainClearMessage,
|
||||
getTerrainStartMessage,
|
||||
getWeatherClearMessage,
|
||||
getWeatherStartMessage,
|
||||
getLegendaryWeatherContinuesMessage,
|
||||
@ -19,12 +17,8 @@ import type { ArenaTag } from "#app/data/arena-tag";
|
||||
import { ArenaTrapTag, getArenaTag } from "#app/data/arena-tag";
|
||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
||||
import type { BattlerIndex } from "#enums/battler-index";
|
||||
import { Terrain, TerrainType } from "#app/data/terrain";
|
||||
import {
|
||||
applyAbAttrs,
|
||||
applyPostTerrainChangeAbAttrs,
|
||||
applyPostWeatherChangeAbAttrs,
|
||||
} from "#app/data/abilities/apply-ab-attrs";
|
||||
import { Terrain, TerrainType, getTerrainClearMessage, getTerrainStartMessage } from "#app/data/terrain";
|
||||
import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
|
||||
import type Pokemon from "#app/field/pokemon";
|
||||
import Overrides from "#app/overrides";
|
||||
import { TagAddedEvent, TagRemovedEvent, TerrainChangedEvent, WeatherChangedEvent } from "#app/events/arena";
|
||||
@ -372,7 +366,7 @@ export class Arena {
|
||||
pokemon.findAndRemoveTags(
|
||||
t => "weatherTypes" in t && !(t.weatherTypes as WeatherType[]).find(t => t === weather),
|
||||
);
|
||||
applyPostWeatherChangeAbAttrs("PostWeatherChangeAbAttr", pokemon, weather);
|
||||
applyAbAttrs("PostWeatherChangeAbAttr", { pokemon, weather });
|
||||
});
|
||||
|
||||
return true;
|
||||
@ -449,9 +443,9 @@ export class Arena {
|
||||
CommonAnim.MISTY_TERRAIN + (terrain - 1),
|
||||
);
|
||||
}
|
||||
globalScene.phaseManager.queueMessage(getTerrainStartMessage(terrain)!); // TODO: is this bang correct?
|
||||
globalScene.phaseManager.queueMessage(getTerrainStartMessage(terrain));
|
||||
} else {
|
||||
globalScene.phaseManager.queueMessage(getTerrainClearMessage(oldTerrainType)!); // TODO: is this bang correct?
|
||||
globalScene.phaseManager.queueMessage(getTerrainClearMessage(oldTerrainType));
|
||||
}
|
||||
|
||||
globalScene
|
||||
@ -461,8 +455,8 @@ export class Arena {
|
||||
pokemon.findAndRemoveTags(
|
||||
t => "terrainTypes" in t && !(t.terrainTypes as TerrainType[]).find(t => t === terrain),
|
||||
);
|
||||
applyPostTerrainChangeAbAttrs("PostTerrainChangeAbAttr", pokemon, terrain);
|
||||
applyAbAttrs("TerrainEventTypeChangeAbAttr", pokemon, null, false);
|
||||
applyAbAttrs("PostTerrainChangeAbAttr", { pokemon, terrain });
|
||||
applyAbAttrs("TerrainEventTypeChangeAbAttr", { pokemon });
|
||||
});
|
||||
|
||||
return true;
|
||||
|
@ -108,23 +108,8 @@ import { WeatherType } from "#enums/weather-type";
|
||||
import { NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag";
|
||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
||||
import type { SuppressAbilitiesTag } from "#app/data/arena-tag";
|
||||
import type { Ability } from "#app/data/abilities/ability";
|
||||
import {
|
||||
applyAbAttrs,
|
||||
applyStatMultiplierAbAttrs,
|
||||
applyPreApplyBattlerTagAbAttrs,
|
||||
applyPreAttackAbAttrs,
|
||||
applyPreDefendAbAttrs,
|
||||
applyPreSetStatusAbAttrs,
|
||||
applyFieldStatMultiplierAbAttrs,
|
||||
applyCheckTrappedAbAttrs,
|
||||
applyPostDamageAbAttrs,
|
||||
applyPostItemLostAbAttrs,
|
||||
applyOnGainAbAttrs,
|
||||
applyPreLeaveFieldAbAttrs,
|
||||
applyOnLoseAbAttrs,
|
||||
applyAllyStatMultiplierAbAttrs,
|
||||
} from "#app/data/abilities/apply-ab-attrs";
|
||||
import type { Ability, PreAttackModifyDamageAbAttrParams } from "#app/data/abilities/ability";
|
||||
import { applyAbAttrs, applyOnGainAbAttrs, applyOnLoseAbAttrs } from "#app/data/abilities/apply-ab-attrs";
|
||||
import { allAbilities } from "#app/data/data-lists";
|
||||
import type PokemonData from "#app/system/pokemon-data";
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
@ -184,12 +169,13 @@ import { timedEventManager } from "#app/global-event-manager";
|
||||
import { loadMoveAnimations } from "#app/sprites/pokemon-asset-loader";
|
||||
import { isVirtual, isIgnorePP, MoveUseMode } from "#enums/move-use-mode";
|
||||
import { FieldPosition } from "#enums/field-position";
|
||||
import { LearnMoveSituation } from "#enums/learn-move-situation";
|
||||
import { HitResult } from "#enums/hit-result";
|
||||
import { AiType } from "#enums/ai-type";
|
||||
import type { MoveResult } from "#enums/move-result";
|
||||
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";
|
||||
import { getTerrainBlockMessage } from "#app/data/terrain";
|
||||
import { LearnMoveSituation } from "#enums/learn-move-situation";
|
||||
|
||||
/** Base typeclass for damage parameter methods, used for DRY */
|
||||
type damageParams = {
|
||||
@ -1364,7 +1350,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
applyMoveAttrs("HighCritAttr", source, this, move, critStage);
|
||||
globalScene.applyModifiers(CritBoosterModifier, source.isPlayer(), source, critStage);
|
||||
globalScene.applyModifiers(TempCritBoosterModifier, source.isPlayer(), critStage);
|
||||
applyAbAttrs("BonusCritAbAttr", source, null, false, critStage);
|
||||
applyAbAttrs("BonusCritAbAttr", { pokemon: source, critStage });
|
||||
const critBoostTag = source.getTag(CritBoostTag);
|
||||
if (critBoostTag) {
|
||||
// Dragon cheer only gives +1 crit stage to non-dragon types
|
||||
@ -1415,46 +1401,52 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
simulated = true,
|
||||
ignoreHeldItems = false,
|
||||
): number {
|
||||
const statValue = new NumberHolder(this.getStat(stat, false));
|
||||
const statVal = new NumberHolder(this.getStat(stat, false));
|
||||
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
|
||||
const fieldApplied = new BooleanHolder(false);
|
||||
for (const pokemon of globalScene.getField(true)) {
|
||||
applyFieldStatMultiplierAbAttrs(
|
||||
"FieldMultiplyStatAbAttr",
|
||||
applyAbAttrs("FieldMultiplyStatAbAttr", {
|
||||
pokemon,
|
||||
stat,
|
||||
statValue,
|
||||
this,
|
||||
fieldApplied,
|
||||
statVal,
|
||||
target: this,
|
||||
hasApplied: fieldApplied,
|
||||
simulated,
|
||||
);
|
||||
});
|
||||
if (fieldApplied.value) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
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();
|
||||
if (!isNullOrUndefined(ally)) {
|
||||
applyAllyStatMultiplierAbAttrs(
|
||||
"AllyStatMultiplierAbAttr",
|
||||
ally,
|
||||
applyAbAttrs("AllyStatMultiplierAbAttr", {
|
||||
pokemon: ally,
|
||||
stat,
|
||||
statValue,
|
||||
statVal,
|
||||
simulated,
|
||||
this,
|
||||
move?.hasFlag(MoveFlags.IGNORE_ABILITIES) || ignoreAllyAbility,
|
||||
);
|
||||
// TODO: maybe just don't call this if the move is none?
|
||||
move: move ?? allMoves[MoveId.NONE],
|
||||
ignoreAbility: move?.hasFlag(MoveFlags.IGNORE_ABILITIES) || ignoreAllyAbility,
|
||||
});
|
||||
}
|
||||
|
||||
let ret =
|
||||
statValue.value *
|
||||
statVal.value *
|
||||
this.getStatStageMultiplier(stat, opponent, move, ignoreOppAbility, isCritical, simulated, ignoreHeldItems);
|
||||
|
||||
switch (stat) {
|
||||
@ -2045,20 +2037,20 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
* @param ability New Ability
|
||||
*/
|
||||
public setTempAbility(ability: Ability, passive = false): void {
|
||||
applyOnLoseAbAttrs(this, passive);
|
||||
applyOnLoseAbAttrs({ pokemon: this, passive });
|
||||
if (passive) {
|
||||
this.summonData.passiveAbility = ability.id;
|
||||
} else {
|
||||
this.summonData.ability = ability.id;
|
||||
}
|
||||
applyOnGainAbAttrs(this, passive);
|
||||
applyOnGainAbAttrs({ pokemon: this, passive });
|
||||
}
|
||||
|
||||
/**
|
||||
* Suppresses an ability and calls its onlose attributes
|
||||
*/
|
||||
public suppressAbility() {
|
||||
[true, false].forEach(passive => applyOnLoseAbAttrs(this, passive));
|
||||
[true, false].forEach(passive => applyOnLoseAbAttrs({ pokemon: this, passive }));
|
||||
this.summonData.abilitySuppressed = true;
|
||||
}
|
||||
|
||||
@ -2194,7 +2186,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
const weight = new NumberHolder(this.species.weight - weightRemoved);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
@ -2256,7 +2248,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
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
|
||||
* Afterwards, it filters out Pokemon that have been switched out of the field so trapped abilities/moves do not trigger
|
||||
@ -2265,14 +2258,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
const opposingField = opposingFieldUnfiltered.filter(enemyPkm => enemyPkm.switchOutStatus === false);
|
||||
|
||||
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;
|
||||
return (
|
||||
trappedByAbility.value ||
|
||||
!!this.getTag(TrappedTag) ||
|
||||
!!globalScene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, side)
|
||||
trapped.value || !!this.getTag(TrappedTag) || !!globalScene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, side)
|
||||
);
|
||||
}
|
||||
|
||||
@ -2287,7 +2278,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
const moveTypeHolder = new NumberHolder(move.type);
|
||||
|
||||
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,
|
||||
// then bypass the check for ion deluge and electrify
|
||||
@ -2351,17 +2351,31 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
}
|
||||
|
||||
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) {
|
||||
applyPreDefendAbAttrs("TypeImmunityAbAttr", this, source, move, cancelledHolder, simulated, typeMultiplier);
|
||||
applyAbAttrs("TypeImmunityAbAttr", commonAbAttrParams);
|
||||
|
||||
if (!cancelledHolder.value) {
|
||||
applyPreDefendAbAttrs("MoveImmunityAbAttr", this, source, move, cancelledHolder, simulated, typeMultiplier);
|
||||
applyAbAttrs("MoveImmunityAbAttr", commonAbAttrParams);
|
||||
}
|
||||
|
||||
if (!cancelledHolder.value) {
|
||||
const defendingSidePlayField = this.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField();
|
||||
defendingSidePlayField.forEach(p =>
|
||||
applyPreDefendAbAttrs("FieldPriorityMoveImmunityAbAttr", p, source, move, cancelledHolder),
|
||||
applyAbAttrs("FieldPriorityMoveImmunityAbAttr", {
|
||||
pokemon: p,
|
||||
opponent: source,
|
||||
move,
|
||||
cancelled: cancelledHolder,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -2376,7 +2390,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
|
||||
// Apply Tera Shell's effect to attacks after all immunities are accounted for
|
||||
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)) {
|
||||
@ -2420,16 +2434,22 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
}
|
||||
|
||||
let multiplier = types
|
||||
.map(defType => {
|
||||
const multiplier = new NumberHolder(getTypeDamageMultiplier(moveType, defType));
|
||||
.map(defenderType => {
|
||||
const multiplier = new NumberHolder(getTypeDamageMultiplier(moveType, defenderType));
|
||||
applyChallenges(ChallengeType.TYPE_EFFECTIVENESS, multiplier);
|
||||
if (move) {
|
||||
applyMoveAttrs("VariableMoveTypeChartAttr", null, this, move, multiplier, defType);
|
||||
applyMoveAttrs("VariableMoveTypeChartAttr", null, this, move, multiplier, defenderType);
|
||||
}
|
||||
if (source) {
|
||||
const ignoreImmunity = new BooleanHolder(false);
|
||||
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 (multiplier.value === 0) {
|
||||
@ -2438,7 +2458,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
}
|
||||
|
||||
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) {
|
||||
return 1;
|
||||
}
|
||||
@ -3383,7 +3403,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
}
|
||||
}
|
||||
if (!ignoreOppAbility) {
|
||||
applyAbAttrs("IgnoreOpponentStatStagesAbAttr", opponent, null, simulated, stat, ignoreStatStage);
|
||||
applyAbAttrs("IgnoreOpponentStatStagesAbAttr", {
|
||||
pokemon: opponent,
|
||||
ignored: ignoreStatStage,
|
||||
stat,
|
||||
simulated,
|
||||
});
|
||||
}
|
||||
if (move) {
|
||||
applyMoveAttrs("IgnoreOpponentStatStagesAttr", this, opponent, move, ignoreStatStage);
|
||||
@ -3422,8 +3447,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
const ignoreAccStatStage = new BooleanHolder(false);
|
||||
const ignoreEvaStatStage = new BooleanHolder(false);
|
||||
|
||||
applyAbAttrs("IgnoreOpponentStatStagesAbAttr", target, null, false, Stat.ACC, ignoreAccStatStage);
|
||||
applyAbAttrs("IgnoreOpponentStatStagesAbAttr", this, null, false, Stat.EVA, ignoreEvaStatStage);
|
||||
// TODO: consider refactoring this method to accept `simulated` and then pass simulated to these applyAbAttrs
|
||||
applyAbAttrs("IgnoreOpponentStatStagesAbAttr", { pokemon: target, stat: Stat.ACC, ignored: ignoreAccStatStage });
|
||||
applyAbAttrs("IgnoreOpponentStatStagesAbAttr", { pokemon: this, stat: Stat.EVA, ignored: ignoreEvaStatStage });
|
||||
applyMoveAttrs("IgnoreOpponentStatStagesAttr", this, target, sourceMove, ignoreEvaStatStage);
|
||||
|
||||
globalScene.applyModifiers(TempStatStageBoosterModifier, this.isPlayer(), Stat.ACC, userAccStage);
|
||||
@ -3443,33 +3469,40 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
: 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);
|
||||
applyStatMultiplierAbAttrs("StatMultiplierAbAttr", target, Stat.EVA, evasionMultiplier);
|
||||
applyAbAttrs("StatMultiplierAbAttr", {
|
||||
pokemon: target,
|
||||
stat: Stat.EVA,
|
||||
statVal: evasionMultiplier,
|
||||
move: sourceMove,
|
||||
});
|
||||
|
||||
const ally = this.getAlly();
|
||||
if (!isNullOrUndefined(ally)) {
|
||||
const ignore =
|
||||
this.hasAbilityWithAttr("MoveAbilityBypassAbAttr") || sourceMove.hasFlag(MoveFlags.IGNORE_ABILITIES);
|
||||
applyAllyStatMultiplierAbAttrs(
|
||||
"AllyStatMultiplierAbAttr",
|
||||
ally,
|
||||
Stat.ACC,
|
||||
accuracyMultiplier,
|
||||
false,
|
||||
this,
|
||||
ignore,
|
||||
);
|
||||
applyAllyStatMultiplierAbAttrs(
|
||||
"AllyStatMultiplierAbAttr",
|
||||
ally,
|
||||
Stat.EVA,
|
||||
evasionMultiplier,
|
||||
false,
|
||||
this,
|
||||
ignore,
|
||||
);
|
||||
applyAbAttrs("AllyStatMultiplierAbAttr", {
|
||||
pokemon: ally,
|
||||
stat: Stat.ACC,
|
||||
statVal: accuracyMultiplier,
|
||||
ignoreAbility: ignore,
|
||||
move: sourceMove,
|
||||
});
|
||||
|
||||
applyAbAttrs("AllyStatMultiplierAbAttr", {
|
||||
pokemon: ally,
|
||||
stat: Stat.EVA,
|
||||
statVal: evasionMultiplier,
|
||||
ignoreAbility: ignore,
|
||||
move: sourceMove,
|
||||
});
|
||||
}
|
||||
|
||||
return accuracyMultiplier.value / evasionMultiplier.value;
|
||||
@ -3584,7 +3617,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
applyMoveAttrs("CombinedPledgeStabBoostAttr", source, this, move, stabMultiplier);
|
||||
|
||||
if (!ignoreSourceAbility) {
|
||||
applyAbAttrs("StabBoostAbAttr", source, null, simulated, stabMultiplier);
|
||||
applyAbAttrs("StabBoostAbAttr", { pokemon: source, simulated, multiplier: stabMultiplier });
|
||||
}
|
||||
|
||||
if (source.isTerastallized && sourceTeraType === moveType && moveType !== PokemonType.STELLAR) {
|
||||
@ -3731,16 +3764,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
null,
|
||||
multiStrikeEnhancementMultiplier,
|
||||
);
|
||||
|
||||
if (!ignoreSourceAbility) {
|
||||
applyPreAttackAbAttrs(
|
||||
"AddSecondStrikeAbAttr",
|
||||
source,
|
||||
this,
|
||||
applyAbAttrs("AddSecondStrikeAbAttr", {
|
||||
pokemon: source,
|
||||
move,
|
||||
simulated,
|
||||
null,
|
||||
multiStrikeEnhancementMultiplier,
|
||||
);
|
||||
multiplier: multiStrikeEnhancementMultiplier,
|
||||
});
|
||||
}
|
||||
|
||||
/** Doubles damage if this Pokemon's last move was Glaive Rush */
|
||||
@ -3751,7 +3782,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
|
||||
/** The damage multiplier when the given move critically hits */
|
||||
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]
|
||||
@ -3772,7 +3803,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
) {
|
||||
const burnDamageReductionCancelled = new BooleanHolder(false);
|
||||
if (!ignoreSourceAbility) {
|
||||
applyAbAttrs("BypassBurnDamageReductionAbAttr", source, burnDamageReductionCancelled, simulated);
|
||||
applyAbAttrs("BypassBurnDamageReductionAbAttr", {
|
||||
pokemon: source,
|
||||
cancelled: burnDamageReductionCancelled,
|
||||
simulated,
|
||||
});
|
||||
}
|
||||
if (!burnDamageReductionCancelled.value) {
|
||||
burnMultiplier = 0.5;
|
||||
@ -3836,7 +3871,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
|
||||
/** Doubles damage if the attacker has Tinted Lens and is using a resisted move */
|
||||
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 */
|
||||
@ -3847,14 +3888,25 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
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) */
|
||||
if (!ignoreAbility) {
|
||||
applyPreDefendAbAttrs("ReceivedMoveDamageMultiplierAbAttr", this, source, move, cancelled, simulated, damage);
|
||||
applyAbAttrs("ReceivedMoveDamageMultiplierAbAttr", abAttrParams);
|
||||
|
||||
const ally = this.getAlly();
|
||||
/** Additionally apply friend guard damage reduction if ally has it. */
|
||||
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 +3914,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
applyMoveAttrs("ModifiedDamageAttr", source, this, move, damage);
|
||||
|
||||
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)
|
||||
@ -3900,7 +3952,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
|
||||
const alwaysCrit = new BooleanHolder(false);
|
||||
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 critChance = [24, 8, 2, 1][Phaser.Math.Clamp(this.getCritStage(source, move), 0, 3)];
|
||||
|
||||
@ -3911,7 +3963,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
|
||||
// apply crit block effects from lucky chant & co., overriding previous effects
|
||||
const blockCrit = new BooleanHolder(false);
|
||||
applyAbAttrs("BlockCritAbAttr", this, null, false, blockCrit);
|
||||
applyAbAttrs("BlockCritAbAttr", { pokemon: this, blockCrit });
|
||||
const blockCritTag = globalScene.arena.getTagOnSide(
|
||||
NoCritTag,
|
||||
this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY,
|
||||
@ -4023,7 +4075,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
* Multi-hits are handled in move-effect-phase.ts for PostDamageAbAttr
|
||||
*/
|
||||
if (!source || source.turnData.hitCount <= 1) {
|
||||
applyPostDamageAbAttrs("PostDamageAbAttr", this, damage, this.hasPassive(), false, [], source);
|
||||
applyAbAttrs("PostDamageAbAttr", { pokemon: this, damage, source });
|
||||
}
|
||||
return damage;
|
||||
}
|
||||
@ -4071,11 +4123,17 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
const stubTag = new BattlerTag(tagType, 0, 0);
|
||||
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyPreApplyBattlerTagAbAttrs("BattlerTagImmunityAbAttr", this, stubTag, cancelled, true);
|
||||
applyAbAttrs("BattlerTagImmunityAbAttr", { pokemon: this, tag: stubTag, cancelled, simulated: true });
|
||||
|
||||
const userField = this.getAlliedField();
|
||||
userField.forEach(pokemon =>
|
||||
applyPreApplyBattlerTagAbAttrs("UserFieldBattlerTagImmunityAbAttr", pokemon, stubTag, cancelled, true, this),
|
||||
applyAbAttrs("UserFieldBattlerTagImmunityAbAttr", {
|
||||
pokemon,
|
||||
tag: stubTag,
|
||||
cancelled,
|
||||
simulated: true,
|
||||
target: this,
|
||||
}),
|
||||
);
|
||||
|
||||
return !cancelled.value;
|
||||
@ -4091,13 +4149,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
const newTag = getBattlerTag(tagType, turnCount, sourceMove!, sourceId!); // TODO: are the bangs correct?
|
||||
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyPreApplyBattlerTagAbAttrs("BattlerTagImmunityAbAttr", this, newTag, cancelled);
|
||||
applyAbAttrs("BattlerTagImmunityAbAttr", { pokemon: this, tag: newTag, cancelled });
|
||||
if (cancelled.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const pokemon of this.getAlliedField()) {
|
||||
applyPreApplyBattlerTagAbAttrs("UserFieldBattlerTagImmunityAbAttr", pokemon, newTag, cancelled, false, this);
|
||||
applyAbAttrs("UserFieldBattlerTagImmunityAbAttr", { pokemon, tag: newTag, cancelled, target: this });
|
||||
if (cancelled.value) {
|
||||
return false;
|
||||
}
|
||||
@ -4125,7 +4183,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
getTag<T extends BattlerTag>(tagType: Constructor<T>): T | 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.tagType === tagType);
|
||||
}
|
||||
@ -4597,16 +4655,37 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
);
|
||||
}
|
||||
|
||||
queueImmuneMessage(quiet: boolean, effect?: StatusEffect): void {
|
||||
if (!effect || quiet) {
|
||||
/**
|
||||
* Display an immunity message for a failed status application.
|
||||
* @param quiet - Whether to suppress message and return early
|
||||
* @param reason - The reason for the status application failure -
|
||||
* can be "overlap" (already has same status), "other" (generic fail message)
|
||||
* or a {@linkcode TerrainType} for terrain-based blockages.
|
||||
* Defaults to "other".
|
||||
*/
|
||||
queueStatusImmuneMessage(
|
||||
quiet: boolean,
|
||||
reason: "overlap" | "other" | Exclude<TerrainType, TerrainType.NONE> = "other",
|
||||
): void {
|
||||
if (quiet) {
|
||||
return;
|
||||
}
|
||||
const message =
|
||||
effect && this.status?.effect === effect
|
||||
? getStatusEffectOverlapText(effect ?? StatusEffect.NONE, getPokemonNameWithAffix(this))
|
||||
: i18next.t("abilityTriggers:moveImmunity", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(this),
|
||||
});
|
||||
|
||||
let message: string;
|
||||
if (reason === "overlap") {
|
||||
// "XYZ is already XXX!"
|
||||
message = getStatusEffectOverlapText(this.status?.effect ?? StatusEffect.NONE, getPokemonNameWithAffix(this));
|
||||
} else if (typeof reason === "number") {
|
||||
// "XYZ was protected by the XXX terrain!" /
|
||||
// "XYZ surrounds itself with a protective mist!"
|
||||
message = getTerrainBlockMessage(this, reason);
|
||||
} else {
|
||||
// "It doesn't affect XXX!"
|
||||
message = i18next.t("abilityTriggers:moveImmunity", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(this),
|
||||
});
|
||||
}
|
||||
|
||||
globalScene.phaseManager.queueMessage(message);
|
||||
}
|
||||
|
||||
@ -4620,7 +4699,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
* @param ignoreField Whether any field effects (weather, terrain, etc.) should be considered
|
||||
*/
|
||||
canSetStatus(
|
||||
effect: StatusEffect | undefined,
|
||||
effect: StatusEffect,
|
||||
quiet = false,
|
||||
overrideStatus = false,
|
||||
sourcePokemon: Pokemon | null = null,
|
||||
@ -4628,11 +4707,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
): boolean {
|
||||
if (effect !== StatusEffect.FAINT) {
|
||||
if (overrideStatus ? this.status?.effect === effect : this.status) {
|
||||
this.queueImmuneMessage(quiet, effect);
|
||||
this.queueStatusImmuneMessage(quiet, overrideStatus ? "overlap" : "other"); // having different status displays generic fail message
|
||||
return false;
|
||||
}
|
||||
if (this.isGrounded() && !ignoreField && globalScene.arena.terrain?.terrainType === TerrainType.MISTY) {
|
||||
this.queueImmuneMessage(quiet, effect);
|
||||
this.queueStatusImmuneMessage(quiet, TerrainType.MISTY);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -4651,8 +4730,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
|
||||
// Check if the source Pokemon has an ability that cancels the Poison/Toxic immunity
|
||||
const cancelImmunity = new BooleanHolder(false);
|
||||
// TODO: Determine if we need to pass `quiet` as the value for simulated in this call
|
||||
if (sourcePokemon) {
|
||||
applyAbAttrs("IgnoreTypeStatusEffectImmunityAbAttr", sourcePokemon, cancelImmunity, false, effect, defType);
|
||||
applyAbAttrs("IgnoreTypeStatusEffectImmunityAbAttr", {
|
||||
pokemon: sourcePokemon,
|
||||
cancelled: cancelImmunity,
|
||||
statusEffect: effect,
|
||||
defenderType: defType,
|
||||
});
|
||||
if (cancelImmunity.value) {
|
||||
return false;
|
||||
}
|
||||
@ -4663,7 +4748,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
|
||||
if (this.isOfType(PokemonType.POISON) || this.isOfType(PokemonType.STEEL)) {
|
||||
if (poisonImmunity.includes(true)) {
|
||||
this.queueImmuneMessage(quiet, effect);
|
||||
this.queueStatusImmuneMessage(quiet);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -4671,13 +4756,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
}
|
||||
case StatusEffect.PARALYSIS:
|
||||
if (this.isOfType(PokemonType.ELECTRIC)) {
|
||||
this.queueImmuneMessage(quiet, effect);
|
||||
this.queueStatusImmuneMessage(quiet);
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case StatusEffect.SLEEP:
|
||||
if (this.isGrounded() && globalScene.arena.terrain?.terrainType === TerrainType.ELECTRIC) {
|
||||
this.queueImmuneMessage(quiet, effect);
|
||||
this.queueStatusImmuneMessage(quiet, TerrainType.ELECTRIC);
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
@ -4688,34 +4773,33 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
globalScene?.arena?.weather?.weatherType &&
|
||||
[WeatherType.SUNNY, WeatherType.HARSH_SUN].includes(globalScene.arena.weather.weatherType))
|
||||
) {
|
||||
this.queueImmuneMessage(quiet, effect);
|
||||
this.queueStatusImmuneMessage(quiet);
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case StatusEffect.BURN:
|
||||
if (this.isOfType(PokemonType.FIRE)) {
|
||||
this.queueImmuneMessage(quiet, effect);
|
||||
this.queueStatusImmuneMessage(quiet);
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyPreSetStatusAbAttrs("StatusEffectImmunityAbAttr", this, effect, cancelled, quiet);
|
||||
applyAbAttrs("StatusEffectImmunityAbAttr", { pokemon: this, effect, cancelled, simulated: quiet });
|
||||
if (cancelled.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const pokemon of this.getAlliedField()) {
|
||||
applyPreSetStatusAbAttrs(
|
||||
"UserFieldStatusEffectImmunityAbAttr",
|
||||
applyAbAttrs("UserFieldStatusEffectImmunityAbAttr", {
|
||||
pokemon,
|
||||
effect,
|
||||
cancelled,
|
||||
quiet,
|
||||
this,
|
||||
sourcePokemon,
|
||||
);
|
||||
simulated: quiet,
|
||||
target: this,
|
||||
source: sourcePokemon,
|
||||
});
|
||||
if (cancelled.value) {
|
||||
break;
|
||||
}
|
||||
@ -4746,6 +4830,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
overrideStatus?: boolean,
|
||||
quiet = true,
|
||||
): boolean {
|
||||
if (!effect) {
|
||||
return false;
|
||||
}
|
||||
if (!this.canSetStatus(effect, quiet, overrideStatus, sourcePokemon)) {
|
||||
return false;
|
||||
}
|
||||
@ -4804,7 +4891,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
return true;
|
||||
@ -4865,7 +4951,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
if (globalScene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, defendingSide)) {
|
||||
const bypassed = new BooleanHolder(false);
|
||||
if (attacker) {
|
||||
applyAbAttrs("InfiltratorAbAttr", attacker, null, false, bypassed);
|
||||
applyAbAttrs("InfiltratorAbAttr", { pokemon: attacker, bypassed });
|
||||
}
|
||||
return !bypassed.value;
|
||||
}
|
||||
@ -5411,7 +5497,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
this.hideInfo();
|
||||
}
|
||||
// Trigger abilities that activate upon leaving the field
|
||||
applyPreLeaveFieldAbAttrs("PreLeaveFieldAbAttr", this);
|
||||
applyAbAttrs("PreLeaveFieldAbAttr", { pokemon: this });
|
||||
this.setSwitchOutStatus(true);
|
||||
globalScene.triggerPokemonFormChange(this, SpeciesFormChangeActiveTrigger, true);
|
||||
globalScene.field.remove(this, destroy);
|
||||
@ -5471,7 +5557,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
globalScene.removeModifier(heldItem, this.isEnemy());
|
||||
}
|
||||
if (forBattle) {
|
||||
applyPostItemLostAbAttrs("PostItemLostAbAttr", this, false);
|
||||
applyAbAttrs("PostItemLostAbAttr", { pokemon: this });
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -42,7 +42,7 @@ import type {
|
||||
import { getModifierType } from "#app/utils/modifier-utils";
|
||||
import { Color, ShadowColor } from "#enums/color";
|
||||
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 type { ModifierInstanceMap, ModifierString } from "#app/@types/modifier-types";
|
||||
|
||||
@ -751,7 +751,7 @@ export abstract class PokemonHeldItemModifier extends PersistentModifier {
|
||||
}
|
||||
|
||||
getPokemon(): Pokemon | undefined {
|
||||
return this.pokemonId ? (globalScene.getPokemonById(this.pokemonId) ?? undefined) : undefined;
|
||||
return globalScene.getPokemonById(this.pokemonId) ?? undefined;
|
||||
}
|
||||
|
||||
getScoreMultiplier(): number {
|
||||
@ -1879,7 +1879,7 @@ export class BerryModifier extends PokemonHeldItemModifier {
|
||||
|
||||
// munch the berry and trigger unburden-like effects
|
||||
getBerryEffectFunc(this.berryType)(pokemon);
|
||||
applyPostItemLostAbAttrs("PostItemLostAbAttr", pokemon, false);
|
||||
applyAbAttrs("PostItemLostAbAttr", { pokemon });
|
||||
|
||||
// Update berry eaten trackers for Belch, Harvest, Cud Chew, etc.
|
||||
// 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
|
||||
const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField();
|
||||
for (const p of field) {
|
||||
applyAbAttrs("CommanderAbAttr", p, null, false);
|
||||
applyAbAttrs("CommanderAbAttr", { pokemon: p });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@ -1,18 +1,18 @@
|
||||
import { type PokeballCounts } from "#app/battle-scene";
|
||||
import { EvolutionItem } from "#app/data/balance/pokemon-evolutions";
|
||||
import { Gender } from "#app/data/gender";
|
||||
import { FormChangeItem } from "#enums/form-change-item";
|
||||
import { type ModifierOverride } from "#app/modifier/modifier-type";
|
||||
import { Variant } from "#app/sprites/variant";
|
||||
import { Unlockables } from "#enums/unlockables";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { BattleType } from "#enums/battle-type";
|
||||
import { BerryType } from "#enums/berry-type";
|
||||
import { BiomeId } from "#enums/biome-id";
|
||||
import { EggTier } from "#enums/egg-type";
|
||||
import { FormChangeItem } from "#enums/form-change-item";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
|
||||
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
||||
import { Nature } from "#enums/nature";
|
||||
import { PokeballType } from "#enums/pokeball";
|
||||
import { PokemonType } from "#enums/pokemon-type";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
@ -20,6 +20,7 @@ import { Stat } from "#enums/stat";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { TimeOfDay } from "#enums/time-of-day";
|
||||
import { TrainerType } from "#enums/trainer-type";
|
||||
import { Unlockables } from "#enums/unlockables";
|
||||
import { VariantTier } from "#enums/variant-tier";
|
||||
import { WeatherType } from "#enums/weather-type";
|
||||
|
||||
@ -159,10 +160,20 @@ class DefaultOverrides {
|
||||
readonly MOVESET_OVERRIDE: MoveId | Array<MoveId> = [];
|
||||
readonly SHINY_OVERRIDE: boolean | null = null;
|
||||
readonly VARIANT_OVERRIDE: Variant | null = null;
|
||||
/**
|
||||
* Overrides the IVs of player pokemon. Values must never be outside the range `0` to `31`!
|
||||
* - If set to a number between `0` and `31`, set all IVs of all player pokemon to that number.
|
||||
* - If set to an array, set the IVs of all player pokemon to that array. Array length must be exactly `6`!
|
||||
* - If set to `null`, disable the override.
|
||||
*/
|
||||
readonly IVS_OVERRIDE: number | number[] | null = null;
|
||||
/** Override the nature of all player pokemon to the specified nature. Disabled if `null`. */
|
||||
readonly NATURE_OVERRIDE: Nature | null = null;
|
||||
|
||||
// --------------------------
|
||||
// OPPONENT / ENEMY OVERRIDES
|
||||
// --------------------------
|
||||
// TODO: rename `OPP_` to `ENEMY_`
|
||||
readonly OPP_SPECIES_OVERRIDE: SpeciesId | number = 0;
|
||||
/**
|
||||
* This will make all opponents fused Pokemon
|
||||
@ -181,7 +192,15 @@ class DefaultOverrides {
|
||||
readonly OPP_MOVESET_OVERRIDE: MoveId | Array<MoveId> = [];
|
||||
readonly OPP_SHINY_OVERRIDE: boolean | null = null;
|
||||
readonly OPP_VARIANT_OVERRIDE: Variant | null = null;
|
||||
readonly OPP_IVS_OVERRIDE: number | number[] = [];
|
||||
/**
|
||||
* Overrides the IVs of enemy pokemon. Values must never be outside the range `0` to `31`!
|
||||
* - If set to a number between `0` and `31`, set all IVs of all enemy pokemon to that number.
|
||||
* - If set to an array, set the IVs of all enemy pokemon to that array. Array length must be exactly `6`!
|
||||
* - If set to `null`, disable the override.
|
||||
*/
|
||||
readonly ENEMY_IVS_OVERRIDE: number | number[] | null = null;
|
||||
/** Override the nature of all enemy pokemon to the specified nature. Disabled if `null`. */
|
||||
readonly ENEMY_NATURE_OVERRIDE: Nature | null = null;
|
||||
readonly OPP_FORM_OVERRIDES: Partial<Record<SpeciesId, number>> = {};
|
||||
/**
|
||||
* Override to give the enemy Pokemon a given amount of health segments
|
||||
|
@ -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 { StatusEffect } from "#enums/status-effect";
|
||||
import type { PlayerPokemon, EnemyPokemon } from "#app/field/pokemon";
|
||||
@ -25,10 +25,10 @@ export class AttemptRunPhase extends PokemonPhase {
|
||||
|
||||
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) {
|
||||
enemyField.forEach(enemyPokemon => applyPreLeaveFieldAbAttrs("PreLeaveFieldAbAttr", enemyPokemon));
|
||||
enemyField.forEach(enemyPokemon => applyAbAttrs("PreLeaveFieldAbAttr", { pokemon: enemyPokemon }));
|
||||
|
||||
globalScene.playSound("se/flee");
|
||||
globalScene.phaseManager.queueMessage(i18next.t("battle:runAwaySuccess"), null, true, 500);
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { BattlePhase } from "./battle-phase";
|
||||
|
||||
@ -65,7 +65,7 @@ export class BattleEndPhase extends BattlePhase {
|
||||
}
|
||||
|
||||
for (const pokemon of globalScene.getPokemonAllowedInBattle()) {
|
||||
applyPostBattleAbAttrs("PostBattleAbAttr", pokemon, false, this.isVictory);
|
||||
applyAbAttrs("PostBattleAbAttr", { pokemon, victory: this.isVictory });
|
||||
}
|
||||
|
||||
if (globalScene.currentBattle.moneyScattered) {
|
||||
|
@ -20,7 +20,7 @@ export class BerryPhase extends FieldPhase {
|
||||
|
||||
this.executeForAll(pokemon => {
|
||||
this.eatBerries(pokemon);
|
||||
applyAbAttrs("RepeatBerryNextTurnAbAttr", pokemon, null);
|
||||
applyAbAttrs("CudChewConsumeBerryAbAttr", { pokemon });
|
||||
});
|
||||
|
||||
this.end();
|
||||
@ -42,7 +42,7 @@ export class BerryPhase extends FieldPhase {
|
||||
|
||||
// TODO: If both opponents on field have unnerve, which one displays its message?
|
||||
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) {
|
||||
globalScene.phaseManager.queueMessage(
|
||||
i18next.t("abilityTriggers:preventBerryUse", {
|
||||
@ -70,6 +70,6 @@ export class BerryPhase extends FieldPhase {
|
||||
globalScene.updateModifiers(pokemon.isPlayer());
|
||||
|
||||
// AbilityId.CHEEK_POUCH only works once per round of nom noms
|
||||
applyAbAttrs("HealFromBerryUseAbAttr", pokemon, new BooleanHolder(false));
|
||||
applyAbAttrs("HealFromBerryUseAbAttr", { pokemon });
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { BattlerIndex } from "#enums/battler-index";
|
||||
import { BattleType } from "#enums/battle-type";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
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 { getCharVariantFromDialogue } from "#app/data/dialogue";
|
||||
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)
|
||||
.reverse()
|
||||
.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 (battle.battleType === BattleType.WILD) {
|
||||
for (const pokemon of globalScene.getField()) {
|
||||
applyPreSummonAbAttrs("PreSummonAbAttr", pokemon, []);
|
||||
applyAbAttrs("PreSummonAbAttr", { pokemon });
|
||||
}
|
||||
globalScene.field.add(enemyPokemon);
|
||||
battle.seenEnemyPartyMemberIds.add(enemyPokemon.id);
|
||||
|
@ -23,6 +23,8 @@ export class EvolutionPhase extends Phase {
|
||||
protected pokemon: PlayerPokemon;
|
||||
protected lastLevel: number;
|
||||
|
||||
protected evoChain: Phaser.Tweens.TweenChain | null = null;
|
||||
|
||||
private preEvolvedPokemonName: string;
|
||||
|
||||
private evolution: SpeciesFormEvolution | null;
|
||||
@ -40,13 +42,23 @@ export class EvolutionPhase extends Phase {
|
||||
protected pokemonEvoSprite: 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();
|
||||
|
||||
this.pokemon = pokemon;
|
||||
this.evolution = evolution;
|
||||
this.lastLevel = lastLevel;
|
||||
this.fusionSpeciesEvolved = evolution instanceof FusionSpeciesFormEvolution;
|
||||
this.canCancel = canCancel;
|
||||
}
|
||||
|
||||
validate(): boolean {
|
||||
@ -57,198 +69,227 @@ export class EvolutionPhase extends Phase {
|
||||
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(() => {
|
||||
if (!this.validate()) {
|
||||
return this.end();
|
||||
}
|
||||
this.evolutionBg = globalScene.add
|
||||
.video(0, 0, "evo_bg")
|
||||
.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");
|
||||
this.evolutionBaseBg.setOrigin(0, 0);
|
||||
this.evolutionContainer.add(this.evolutionBaseBg);
|
||||
|
||||
this.evolutionBg = globalScene.add.video(0, 0, "evo_bg").stop();
|
||||
this.evolutionBg.setOrigin(0, 0);
|
||||
this.evolutionBg.setScale(0.4359673025);
|
||||
this.evolutionBg.setVisible(false);
|
||||
this.evolutionContainer.add(this.evolutionBg);
|
||||
|
||||
this.evolutionBgOverlay = globalScene.add.rectangle(
|
||||
0,
|
||||
0,
|
||||
globalScene.game.canvas.width / 6,
|
||||
globalScene.game.canvas.height / 6,
|
||||
0x262626,
|
||||
);
|
||||
this.evolutionBgOverlay.setOrigin(0, 0);
|
||||
this.evolutionBgOverlay.setAlpha(0);
|
||||
this.evolutionContainer.add(this.evolutionBgOverlay);
|
||||
|
||||
const getPokemonSprite = () => {
|
||||
const ret = globalScene.addPokemonSprite(
|
||||
this.pokemon,
|
||||
this.evolutionBaseBg.displayWidth / 2,
|
||||
this.evolutionBaseBg.displayHeight / 2,
|
||||
"pkmn__sub",
|
||||
);
|
||||
ret.setPipeline(globalScene.spritePipeline, {
|
||||
tone: [0.0, 0.0, 0.0, 0.0],
|
||||
ignoreTimeTint: true,
|
||||
});
|
||||
return ret;
|
||||
};
|
||||
|
||||
this.evolutionContainer.add((this.pokemonSprite = getPokemonSprite()));
|
||||
this.evolutionContainer.add((this.pokemonTintSprite = getPokemonSprite()));
|
||||
this.evolutionContainer.add((this.pokemonEvoSprite = getPokemonSprite()));
|
||||
this.evolutionContainer.add((this.pokemonEvoTintSprite = getPokemonSprite()));
|
||||
|
||||
this.pokemonTintSprite.setAlpha(0);
|
||||
this.pokemonTintSprite.setTintFill(0xffffff);
|
||||
this.pokemonEvoSprite.setVisible(false);
|
||||
this.pokemonEvoTintSprite.setVisible(false);
|
||||
this.pokemonEvoTintSprite.setTintFill(0xffffff);
|
||||
|
||||
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, 0);
|
||||
this.evolutionOverlay.setAlpha(0);
|
||||
globalScene.ui.add(this.evolutionOverlay);
|
||||
|
||||
[this.pokemonSprite, this.pokemonTintSprite, this.pokemonEvoSprite, this.pokemonEvoTintSprite].map(sprite => {
|
||||
const spriteKey = this.pokemon.getSpriteKey(true);
|
||||
try {
|
||||
sprite.play(spriteKey);
|
||||
} catch (err: unknown) {
|
||||
console.error(`Failed to play animation for ${spriteKey}`, err);
|
||||
}
|
||||
|
||||
sprite.setPipeline(globalScene.spritePipeline, {
|
||||
tone: [0.0, 0.0, 0.0, 0.0],
|
||||
hasShadow: false,
|
||||
teraColor: getTypeRgb(this.pokemon.getTeraType()),
|
||||
isTerastallized: this.pokemon.isTerastallized,
|
||||
});
|
||||
sprite.setPipelineData("ignoreTimeTint", true);
|
||||
sprite.setPipelineData("spriteKey", this.pokemon.getSpriteKey());
|
||||
sprite.setPipelineData("shiny", this.pokemon.shiny);
|
||||
sprite.setPipelineData("variant", this.pokemon.variant);
|
||||
["spriteColors", "fusionSpriteColors"].map(k => {
|
||||
if (this.pokemon.summonData.speciesForm) {
|
||||
k += "Base";
|
||||
}
|
||||
sprite.pipelineData[k] = this.pokemon.getSprite().pipelineData[k];
|
||||
});
|
||||
if (setPipeline) {
|
||||
sprite.setPipeline(globalScene.spritePipeline, {
|
||||
tone: [0.0, 0.0, 0.0, 0.0],
|
||||
hasShadow: false,
|
||||
teraColor: getTypeRgb(pokemon.getTeraType()),
|
||||
isTerastallized: pokemon.isTerastallized,
|
||||
});
|
||||
}
|
||||
|
||||
sprite
|
||||
.setPipelineData("ignoreTimeTint", true)
|
||||
.setPipelineData("spriteKey", pokemon.getSpriteKey())
|
||||
.setPipelineData("shiny", pokemon.shiny)
|
||||
.setPipelineData("variant", pokemon.variant);
|
||||
|
||||
for (let k of ["spriteColors", "fusionSpriteColors"]) {
|
||||
if (pokemon.summonData.speciesForm) {
|
||||
k += "Base";
|
||||
}
|
||||
sprite.pipelineData[k] = pokemon.getSprite().pipelineData[k];
|
||||
}
|
||||
|
||||
return sprite;
|
||||
}
|
||||
|
||||
private getPokemonSprite(): Phaser.GameObjects.Sprite {
|
||||
const sprite = globalScene.addPokemonSprite(
|
||||
this.pokemon,
|
||||
this.evolutionBaseBg.displayWidth / 2,
|
||||
this.evolutionBaseBg.displayHeight / 2,
|
||||
"pkmn__sub",
|
||||
);
|
||||
sprite.setPipeline(globalScene.spritePipeline, {
|
||||
tone: [0.0, 0.0, 0.0, 0.0],
|
||||
ignoreTimeTint: true,
|
||||
});
|
||||
return sprite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize {@linkcode pokemonSprite}, {@linkcode pokemonTintSprite}, {@linkcode pokemonEvoSprite}, and {@linkcode pokemonEvoTintSprite}
|
||||
* and add them to the {@linkcode evolutionContainer}
|
||||
*/
|
||||
private setupPokemonSprites(): void {
|
||||
this.pokemonSprite = this.configureSprite(this.pokemon, this.getPokemonSprite());
|
||||
this.pokemonTintSprite = this.configureSprite(
|
||||
this.pokemon,
|
||||
this.getPokemonSprite().setAlpha(0).setTintFill(0xffffff),
|
||||
);
|
||||
this.pokemonEvoSprite = this.configureSprite(this.pokemon, this.getPokemonSprite().setVisible(false));
|
||||
this.pokemonEvoTintSprite = this.configureSprite(
|
||||
this.pokemon,
|
||||
this.getPokemonSprite().setVisible(false).setTintFill(0xffffff),
|
||||
);
|
||||
|
||||
this.evolutionContainer.add([
|
||||
this.pokemonSprite,
|
||||
this.pokemonTintSprite,
|
||||
this.pokemonEvoSprite,
|
||||
this.pokemonEvoTintSprite,
|
||||
]);
|
||||
}
|
||||
|
||||
async start() {
|
||||
super.start();
|
||||
await this.setMode();
|
||||
|
||||
if (!this.validate()) {
|
||||
return this.end();
|
||||
}
|
||||
this.setupEvolutionAssets();
|
||||
this.setupPokemonSprites();
|
||||
this.preEvolvedPokemonName = getPokemonNameWithAffix(this.pokemon);
|
||||
this.doEvolution();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the sprites depicting the evolved Pokemon
|
||||
* @param evolvedPokemon - The evolved Pokemon
|
||||
*/
|
||||
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 {
|
||||
globalScene.ui.showText(
|
||||
i18next.t("menu:evolving", { pokemonName: this.preEvolvedPokemonName }),
|
||||
null,
|
||||
() => {
|
||||
this.pokemon.cry();
|
||||
|
||||
this.pokemon.getPossibleEvolution(this.evolution).then(evolvedPokemon => {
|
||||
[this.pokemonEvoSprite, this.pokemonEvoTintSprite].map(sprite => {
|
||||
const spriteKey = evolvedPokemon.getSpriteKey(true);
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
this.updateEvolvedPokemonSprites(evolvedPokemon);
|
||||
this.playEvolutionAnimation(evolvedPokemon);
|
||||
});
|
||||
},
|
||||
1000,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a failed/stopped evolution
|
||||
* @param evolvedPokemon - The evolved Pokemon
|
||||
*/
|
||||
private handleFailedEvolution(evolvedPokemon: Pokemon): void {
|
||||
this.pokemonSprite.setVisible(true);
|
||||
this.pokemonTintSprite.setScale(1);
|
||||
/** Used exclusively by {@linkcode handleFailedEvolution} to fade out the evolution sprites and music */
|
||||
private fadeOutEvolutionAssets(): void {
|
||||
globalScene.tweens.add({
|
||||
targets: [this.evolutionBg, this.pokemonTintSprite, this.pokemonEvoSprite, this.pokemonEvoTintSprite],
|
||||
alpha: 0,
|
||||
@ -257,9 +298,40 @@ export class EvolutionPhase extends Phase {
|
||||
this.evolutionBg.setVisible(false);
|
||||
},
|
||||
});
|
||||
|
||||
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.ui.showText(
|
||||
@ -280,25 +352,7 @@ export class EvolutionPhase extends Phase {
|
||||
evolvedPokemon.destroy();
|
||||
this.end();
|
||||
};
|
||||
globalScene.ui.setOverlayMode(
|
||||
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);
|
||||
},
|
||||
);
|
||||
this.showPauseEvolutionConfirmation(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
|
||||
* @param evolvedPokemon - The evolved Pokemon
|
||||
@ -316,85 +457,15 @@ export class EvolutionPhase extends Phase {
|
||||
this.pokemonEvoSprite.setVisible(true);
|
||||
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, () => {
|
||||
this.evolutionHandler.canCancel = false;
|
||||
this.evolutionHandler.canCancel = this.canCancel;
|
||||
|
||||
this.pokemon.evolve(this.evolution, this.pokemon.species).then(() => {
|
||||
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,
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
this.pokemon.evolve(this.evolution, this.pokemon.species).then(() => this.postEvolve(evolvedPokemon));
|
||||
});
|
||||
}
|
||||
|
||||
doSpiralUpward() {
|
||||
let f = 0;
|
||||
|
||||
globalScene.tweens.addCounter({
|
||||
repeat: 64,
|
||||
duration: getFrameMs(1),
|
||||
@ -430,34 +501,41 @@ export class EvolutionPhase extends Phase {
|
||||
});
|
||||
}
|
||||
|
||||
doCycle(l: number, lastCycle = 15): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
const isLastCycle = l === lastCycle;
|
||||
globalScene.tweens.add({
|
||||
targets: this.pokemonTintSprite,
|
||||
scale: 0.25,
|
||||
/**
|
||||
* Return a tween chain that cycles the evolution sprites
|
||||
*/
|
||||
doCycle(cycles: number, lastCycle = 15, onComplete = () => {}): void {
|
||||
// Make our tween start both at the same time
|
||||
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",
|
||||
duration: 500 / l,
|
||||
yoyo: !isLastCycle,
|
||||
});
|
||||
globalScene.tweens.add({
|
||||
targets: this.pokemonEvoTintSprite,
|
||||
scale: 1,
|
||||
ease: "Cubic.easeInOut",
|
||||
duration: 500 / l,
|
||||
yoyo: !isLastCycle,
|
||||
duration: 500 / i,
|
||||
yoyo: i !== lastCycle,
|
||||
onComplete: () => {
|
||||
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) {
|
||||
this.doCycle(l + 0.5, lastCycle).then(success => resolve(success));
|
||||
} else {
|
||||
this.pokemonTintSprite.setVisible(false);
|
||||
resolve(true);
|
||||
if (i === lastCycle) {
|
||||
this.pokemonEvoTintSprite.setScale(1);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.evoChain = globalScene.tweens.chain({
|
||||
targets: null,
|
||||
tweens,
|
||||
onComplete: () => {
|
||||
this.evoChain = null;
|
||||
onComplete();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,7 @@
|
||||
import type { BattlerIndex } from "#enums/battler-index";
|
||||
import { BattleType } from "#enums/battle-type";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import {
|
||||
applyPostFaintAbAttrs,
|
||||
applyPostKnockOutAbAttrs,
|
||||
applyPostVictoryAbAttrs,
|
||||
} 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 { battleSpecDialogue } from "#app/data/dialogue";
|
||||
import { allMoves } from "#app/data/data-lists";
|
||||
@ -117,29 +113,31 @@ export class FaintPhase extends PokemonPhase {
|
||||
|
||||
pokemon.resetTera();
|
||||
|
||||
// TODO: this can be simplified by just checking whether lastAttack is defined
|
||||
if (pokemon.turnData.attacksReceived?.length) {
|
||||
const lastAttack = pokemon.turnData.attacksReceived[0];
|
||||
applyPostFaintAbAttrs(
|
||||
"PostFaintAbAttr",
|
||||
pokemon,
|
||||
globalScene.getPokemonById(lastAttack.sourceId)!,
|
||||
new PokemonMove(lastAttack.move).getMove(),
|
||||
lastAttack.result,
|
||||
); // TODO: is this bang correct?
|
||||
applyAbAttrs("PostFaintAbAttr", {
|
||||
pokemon: pokemon,
|
||||
// TODO: We should refactor lastAttack's sourceId to forbid null and just use undefined
|
||||
attacker: globalScene.getPokemonById(lastAttack.sourceId) ?? undefined,
|
||||
// TODO: improve the way that we provide the move that knocked out the pokemon...
|
||||
move: new PokemonMove(lastAttack.move).getMove(),
|
||||
hitResult: lastAttack.result,
|
||||
}); // TODO: is this bang correct?
|
||||
} else {
|
||||
//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);
|
||||
for (const p of alivePlayField) {
|
||||
applyPostKnockOutAbAttrs("PostKnockOutAbAttr", p, pokemon);
|
||||
applyAbAttrs("PostKnockOutAbAttr", { pokemon: p, victim: pokemon });
|
||||
}
|
||||
if (pokemon.turnData.attacksReceived?.length) {
|
||||
const defeatSource = this.source;
|
||||
|
||||
if (defeatSource?.isOnField()) {
|
||||
applyPostVictoryAbAttrs("PostVictoryAbAttr", defeatSource);
|
||||
applyAbAttrs("PostVictoryAbAttr", { pokemon: defeatSource });
|
||||
const pvmove = allMoves[pokemon.turnData.attacksReceived[0].move];
|
||||
const pvattrs = pvmove.getAttrs("PostVictoryStatStageChangeAttr");
|
||||
if (pvattrs.length) {
|
||||
|
@ -3,7 +3,7 @@ import { fixedInt } from "#app/utils/common";
|
||||
import { achvs } from "../system/achv";
|
||||
import type { SpeciesFormChange } from "../data/pokemon-forms";
|
||||
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 type PartyUiHandler from "../ui/party-ui-handler";
|
||||
import { getPokemonNameWithAffix } from "../messages";
|
||||
@ -34,146 +34,158 @@ export class FormChangePhase extends EvolutionPhase {
|
||||
return globalScene.ui.setOverlayMode(UiMode.EVOLUTION_SCENE);
|
||||
}
|
||||
|
||||
doEvolution(): void {
|
||||
const preName = getPokemonNameWithAffix(this.pokemon);
|
||||
|
||||
this.pokemon.getPossibleForm(this.formChange).then(transformedPokemon => {
|
||||
[this.pokemonEvoSprite, this.pokemonEvoTintSprite].map(sprite => {
|
||||
const spriteKey = transformedPokemon.getSpriteKey(true);
|
||||
try {
|
||||
sprite.play(spriteKey);
|
||||
} catch (err: unknown) {
|
||||
console.error(`Failed to play animation for ${spriteKey}`, err);
|
||||
/**
|
||||
* Commence the tweens that play after the form change animation finishes
|
||||
* @param transformedPokemon - The Pokemon after the evolution
|
||||
* @param preName - The name of the Pokemon before the evolution
|
||||
*/
|
||||
private postFormChangeTweens(transformedPokemon: Pokemon, preName: string): void {
|
||||
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,
|
||||
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);
|
||||
sprite.setPipelineData("spriteKey", transformedPokemon.getSpriteKey());
|
||||
sprite.setPipelineData("shiny", transformedPokemon.shiny);
|
||||
sprite.setPipelineData("variant", transformedPokemon.variant);
|
||||
["spriteColors", "fusionSpriteColors"].map(k => {
|
||||
if (transformedPokemon.summonData.speciesForm) {
|
||||
k += "Base";
|
||||
}
|
||||
sprite.pipelineData[k] = transformedPokemon.getSprite().pipelineData[k];
|
||||
});
|
||||
});
|
||||
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());
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
alpha: 1,
|
||||
delay: 500,
|
||||
duration: 1500,
|
||||
ease: "Sine.easeOut",
|
||||
// We want the backkground overlay to fade out after it fades in
|
||||
onComplete: () => {
|
||||
globalScene.time.delayedCall(1000, () => {
|
||||
globalScene.tweens.add({
|
||||
targets: this.evolutionBgOverlay,
|
||||
alpha: 0,
|
||||
duration: 250,
|
||||
});
|
||||
this.evolutionBg.setVisible(true);
|
||||
this.evolutionBg.play();
|
||||
globalScene.tweens.add({
|
||||
targets: this.evolutionBgOverlay,
|
||||
alpha: 0,
|
||||
duration: 250,
|
||||
delay: 1000,
|
||||
});
|
||||
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");
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,6 @@
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import {
|
||||
applyExecutedMoveAbAttrs,
|
||||
applyPostAttackAbAttrs,
|
||||
applyPostDamageAbAttrs,
|
||||
applyPostDefendAbAttrs,
|
||||
applyPreAttackAbAttrs,
|
||||
} from "#app/data/abilities/apply-ab-attrs";
|
||||
import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
|
||||
import { ConditionalProtectTag } from "#app/data/arena-tag";
|
||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
||||
import { MoveAnim } from "#app/data/battle-anims";
|
||||
@ -322,7 +316,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
// Assume single target for multi hit
|
||||
applyMoveAttrs("MultiHitAttr", user, this.getFirstTarget() ?? null, move, hitCount);
|
||||
// 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
|
||||
globalScene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, move.id, hitCount);
|
||||
// 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
|
||||
if (this.firstHit) {
|
||||
user.pushMoveHistory(this.moveHistoryEntry);
|
||||
applyExecutedMoveAbAttrs("ExecutedMoveAbAttr", user);
|
||||
applyAbAttrs("ExecutedMoveAbAttr", { pokemon: user });
|
||||
}
|
||||
|
||||
try {
|
||||
@ -439,7 +433,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
* @param hitResult - The {@linkcode HitResult} of the attempted move
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
@ -805,7 +799,9 @@ export class MoveEffectPhase extends PokemonPhase {
|
||||
|
||||
// Multi-hit check for Wimp Out/Emergency Exit
|
||||
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.applyHeldItemFlinchCheck(user, target, dealsDamage);
|
||||
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
|
||||
if (!user.isPlayer() && this.move.is("AttackMove")) {
|
||||
|
@ -2,8 +2,8 @@ import { globalScene } from "#app/global-scene";
|
||||
import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
|
||||
import { PokemonPhase } from "./pokemon-phase";
|
||||
import type { BattlerIndex } from "#enums/battler-index";
|
||||
import { applyPostSummonAbAttrs } from "#app/data/abilities/apply-ab-attrs";
|
||||
import type Pokemon from "#app/field/pokemon";
|
||||
import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
|
||||
|
||||
export class MoveEndPhase extends PokemonPhase {
|
||||
public readonly phaseName = "MoveEndPhase";
|
||||
@ -30,7 +30,7 @@ export class MoveEndPhase extends PokemonPhase {
|
||||
globalScene.arena.setIgnoreAbilities(false);
|
||||
for (const target of this.targets) {
|
||||
if (target) {
|
||||
applyPostSummonAbAttrs("PostSummonRemoveEffectAbAttr", target);
|
||||
applyAbAttrs("PostSummonRemoveEffectAbAttr", { pokemon: target });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
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 { CommonAnim } from "#enums/move-anims-common";
|
||||
import { CenterOfAttentionTag } from "#app/data/battler-tags";
|
||||
@ -11,7 +11,7 @@ import { MoveFlags } from "#enums/MoveFlags";
|
||||
import { SpeciesFormChangePreMoveTrigger } from "#app/data/pokemon-forms/form-change-triggers";
|
||||
import { getStatusEffectActivationText, getStatusEffectHealText } from "#app/data/status-effect";
|
||||
import { PokemonType } from "#enums/pokemon-type";
|
||||
import { getTerrainBlockMessage, getWeatherBlockMessage } from "#app/data/weather";
|
||||
import { getWeatherBlockMessage } from "#app/data/weather";
|
||||
import { MoveUsedEvent } from "#app/events/battle-scene";
|
||||
import type { PokemonMove } from "#app/data/moves/pokemon-move";
|
||||
import type Pokemon from "#app/field/pokemon";
|
||||
@ -26,6 +26,7 @@ import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import i18next from "i18next";
|
||||
import { getTerrainBlockMessage } from "#app/data/terrain";
|
||||
import { isVirtual, isIgnorePP, isReflected, MoveUseMode, isIgnoreStatus } from "#enums/move-use-mode";
|
||||
import { frenzyMissFunc } from "#app/data/moves/move-utils";
|
||||
|
||||
@ -228,14 +229,11 @@ export class MovePhase extends BattlePhase {
|
||||
case StatusEffect.SLEEP: {
|
||||
applyMoveAttrs("BypassSleepAttr", this.pokemon, null, this.move.getMove());
|
||||
const turnsRemaining = new NumberHolder(this.pokemon.status.sleepTurnsRemaining ?? 0);
|
||||
applyAbAttrs(
|
||||
"ReduceStatusEffectDurationAbAttr",
|
||||
this.pokemon,
|
||||
null,
|
||||
false,
|
||||
this.pokemon.status.effect,
|
||||
turnsRemaining,
|
||||
);
|
||||
applyAbAttrs("ReduceStatusEffectDurationAbAttr", {
|
||||
pokemon: this.pokemon,
|
||||
statusEffect: this.pokemon.status.effect,
|
||||
duration: turnsRemaining,
|
||||
});
|
||||
this.pokemon.status.sleepTurnsRemaining = turnsRemaining.value;
|
||||
healed = this.pokemon.status.sleepTurnsRemaining <= 0;
|
||||
activated = !healed && !this.pokemon.getTag(BattlerTagType.BYPASS_SLEEP);
|
||||
@ -396,7 +394,8 @@ export class MovePhase extends BattlePhase {
|
||||
*/
|
||||
if (success) {
|
||||
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(
|
||||
"MoveEffectPhase",
|
||||
this.pokemon.getBattlerIndex(),
|
||||
@ -406,7 +405,11 @@ export class MovePhase extends BattlePhase {
|
||||
);
|
||||
} else {
|
||||
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({
|
||||
@ -438,7 +441,7 @@ export class MovePhase extends BattlePhase {
|
||||
if (this.move.getMove().hasFlag(MoveFlags.DANCE_MOVE) && !dancerModes.includes(this.useMode)) {
|
||||
// TODO: Fix in dancer PR to move to MEP for hit checks
|
||||
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 });
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -470,7 +473,11 @@ export class MovePhase extends BattlePhase {
|
||||
}
|
||||
|
||||
// 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(
|
||||
"MoveChargePhase",
|
||||
@ -523,7 +530,12 @@ export class MovePhase extends BattlePhase {
|
||||
.getField(true)
|
||||
.filter(p => p !== this.pokemon)
|
||||
.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 */
|
||||
|
@ -14,7 +14,7 @@ export class NewBiomeEncounterPhase extends NextEncounterPhase {
|
||||
if (pokemon) {
|
||||
pokemon.resetBattleAndWaveData();
|
||||
if (pokemon.isOnField()) {
|
||||
applyAbAttrs("PostBiomeChangeAbAttr", pokemon, null);
|
||||
applyAbAttrs("PostBiomeChangeAbAttr", { pokemon });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import type Pokemon from "#app/field/pokemon";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import { PokemonPhase } from "./pokemon-phase";
|
||||
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";
|
||||
|
||||
export class ObtainStatusEffectPhase extends PokemonPhase {
|
||||
@ -53,7 +53,11 @@ export class ObtainStatusEffectPhase extends PokemonPhase {
|
||||
globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeStatusEffectTrigger, true);
|
||||
// If mold breaker etc was used to set this status, it shouldn't apply to abilities activated afterwards
|
||||
globalScene.arena.setIgnoreAbilities(false);
|
||||
applyPostSetStatusAbAttrs("PostSetStatusAbAttr", pokemon, this.statusEffect, this.sourcePokemon);
|
||||
applyAbAttrs("PostSetStatusAbAttr", {
|
||||
pokemon,
|
||||
effect: this.statusEffect,
|
||||
sourcePokemon: this.sourcePokemon ?? undefined,
|
||||
});
|
||||
}
|
||||
this.end();
|
||||
});
|
||||
|
@ -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 type { BattlerIndex } from "#enums/battler-index";
|
||||
|
||||
@ -16,7 +16,8 @@ export class PostSummonActivateAbilityPhase extends PostSummonPhase {
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ export class PostSummonPhase extends PokemonPhase {
|
||||
|
||||
const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField();
|
||||
for (const p of field) {
|
||||
applyAbAttrs("CommanderAbAttr", p, null, false);
|
||||
applyAbAttrs("CommanderAbAttr", { pokemon: p });
|
||||
}
|
||||
|
||||
this.end();
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { globalScene } from "#app/global-scene";
|
||||
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 { CommonAnim } from "#enums/move-anims-common";
|
||||
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) {
|
||||
pokemon.status.incrementTurn();
|
||||
const cancelled = new BooleanHolder(false);
|
||||
applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelled);
|
||||
applyAbAttrs("BlockStatusDamageAbAttr", pokemon, cancelled);
|
||||
applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled });
|
||||
applyAbAttrs("BlockStatusDamageAbAttr", { pokemon, cancelled });
|
||||
|
||||
if (!cancelled.value) {
|
||||
globalScene.phaseManager.queueMessage(
|
||||
@ -39,14 +39,14 @@ export class PostTurnStatusEffectPhase extends PokemonPhase {
|
||||
break;
|
||||
case StatusEffect.BURN:
|
||||
damage.value = Math.max(pokemon.getMaxHp() >> 4, 1);
|
||||
applyAbAttrs("ReduceBurnDamageAbAttr", pokemon, null, false, damage);
|
||||
applyAbAttrs("ReduceBurnDamageAbAttr", { pokemon, burnDamage: damage });
|
||||
break;
|
||||
}
|
||||
if (damage.value) {
|
||||
// Set preventEndure flag to avoid pokemon surviving thanks to focus band, sturdy, endure ...
|
||||
globalScene.damageNumberHandler.add(this.getPokemon(), pokemon.damage(damage.value, false, true));
|
||||
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());
|
||||
} else {
|
||||
|
@ -181,9 +181,10 @@ export class QuietFormChangePhase extends BattlePhase {
|
||||
}
|
||||
}
|
||||
if (this.formChange.trigger instanceof SpeciesFormChangeTeraTrigger) {
|
||||
applyAbAttrs("PostTeraFormChangeStatChangeAbAttr", this.pokemon, null);
|
||||
applyAbAttrs("ClearWeatherAbAttr", this.pokemon, null);
|
||||
applyAbAttrs("ClearTerrainAbAttr", this.pokemon, null);
|
||||
const params = { pokemon: this.pokemon };
|
||||
applyAbAttrs("PostTeraFormChangeStatChangeAbAttr", params);
|
||||
applyAbAttrs("ClearWeatherAbAttr", params);
|
||||
applyAbAttrs("ClearTerrainAbAttr", params);
|
||||
}
|
||||
|
||||
super.end();
|
||||
|
@ -1,10 +1,6 @@
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import type { BattlerIndex } from "#enums/battler-index";
|
||||
import {
|
||||
applyAbAttrs,
|
||||
applyPostStatStageChangeAbAttrs,
|
||||
applyPreStatStageChangeAbAttrs,
|
||||
} from "#app/data/abilities/apply-ab-attrs";
|
||||
import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
|
||||
import { MistTag } from "#app/data/arena-tag";
|
||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
||||
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 { OctolockTag } from "#app/data/battler-tags";
|
||||
import { ArenaTagType } from "#app/enums/arena-tag-type";
|
||||
import type {
|
||||
ConditionalUserFieldProtectStatAbAttrParams,
|
||||
PreStatStageChangeAbAttrParams,
|
||||
} from "#app/@types/ability-types";
|
||||
|
||||
export type StatStageChangeCallback = (
|
||||
target: Pokemon | null,
|
||||
@ -126,7 +126,7 @@ export class StatStageChangePhase extends PokemonPhase {
|
||||
const stages = new NumberHolder(this.stages);
|
||||
|
||||
if (!this.ignoreAbilities) {
|
||||
applyAbAttrs("StatStageChangeMultiplierAbAttr", pokemon, null, false, stages);
|
||||
applyAbAttrs("StatStageChangeMultiplierAbAttr", { pokemon, numStages: stages });
|
||||
}
|
||||
|
||||
let simulate = false;
|
||||
@ -146,42 +146,38 @@ export class StatStageChangePhase extends PokemonPhase {
|
||||
}
|
||||
|
||||
if (!cancelled.value && !this.selfTarget && stages.value < 0) {
|
||||
applyPreStatStageChangeAbAttrs("ProtectStatAbAttr", pokemon, stat, cancelled, simulate);
|
||||
applyPreStatStageChangeAbAttrs(
|
||||
"ConditionalUserFieldProtectStatAbAttr",
|
||||
const abAttrParams: PreStatStageChangeAbAttrParams & ConditionalUserFieldProtectStatAbAttrParams = {
|
||||
pokemon,
|
||||
stat,
|
||||
cancelled,
|
||||
simulate,
|
||||
pokemon,
|
||||
);
|
||||
simulated: simulate,
|
||||
target: pokemon,
|
||||
stages: this.stages,
|
||||
};
|
||||
applyAbAttrs("ProtectStatAbAttr", abAttrParams);
|
||||
applyAbAttrs("ConditionalUserFieldProtectStatAbAttr", abAttrParams);
|
||||
// TODO: Consider skipping this call if `cancelled` is false.
|
||||
const ally = pokemon.getAlly();
|
||||
if (!isNullOrUndefined(ally)) {
|
||||
applyPreStatStageChangeAbAttrs(
|
||||
"ConditionalUserFieldProtectStatAbAttr",
|
||||
ally,
|
||||
stat,
|
||||
cancelled,
|
||||
simulate,
|
||||
pokemon,
|
||||
);
|
||||
applyAbAttrs("ConditionalUserFieldProtectStatAbAttr", { ...abAttrParams, pokemon: ally });
|
||||
}
|
||||
|
||||
/** Potential stat reflection due to Mirror Armor, does not apply to Octolock end of turn effect */
|
||||
if (
|
||||
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) &&
|
||||
!this.comingFromMirrorArmorUser
|
||||
) {
|
||||
applyPreStatStageChangeAbAttrs(
|
||||
"ReflectStatStageChangeAbAttr",
|
||||
applyAbAttrs("ReflectStatStageChangeAbAttr", {
|
||||
pokemon,
|
||||
stat,
|
||||
cancelled,
|
||||
simulate,
|
||||
opponentPokemon,
|
||||
this.stages,
|
||||
);
|
||||
simulated: simulate,
|
||||
source: opponentPokemon,
|
||||
stages: this.stages,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -222,17 +218,16 @@ export class StatStageChangePhase extends PokemonPhase {
|
||||
|
||||
if (stages.value > 0 && this.canBeCopied) {
|
||||
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(
|
||||
"PostStatStageChangeAbAttr",
|
||||
applyAbAttrs("PostStatStageChangeAbAttr", {
|
||||
pokemon,
|
||||
filteredStats,
|
||||
this.stages,
|
||||
this.selfTarget,
|
||||
);
|
||||
stats: filteredStats,
|
||||
stages: this.stages,
|
||||
selfTarget: this.selfTarget,
|
||||
});
|
||||
|
||||
// Look for any other stat change phases; if this is the last one, do White Herb check
|
||||
const existingPhase = globalScene.phaseManager.findPhase(
|
||||
|
@ -10,7 +10,7 @@ import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import i18next from "i18next";
|
||||
import { PartyMemberPokemonPhase } from "./party-member-pokemon-phase";
|
||||
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";
|
||||
|
||||
export class SummonPhase extends PartyMemberPokemonPhase {
|
||||
@ -27,7 +27,7 @@ export class SummonPhase extends PartyMemberPokemonPhase {
|
||||
start() {
|
||||
super.start();
|
||||
|
||||
applyPreSummonAbAttrs("PreSummonAbAttr", this.getPokemon());
|
||||
applyAbAttrs("PreSummonAbAttr", { pokemon: this.getPokemon() });
|
||||
this.preSummon();
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { getPokeballTintColor } from "#app/data/pokeball";
|
||||
import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms/form-change-triggers";
|
||||
@ -124,8 +124,8 @@ export class SwitchSummonPhase extends SummonPhase {
|
||||
switchedInPokemon.resetSummonData();
|
||||
switchedInPokemon.loadAssets(true);
|
||||
|
||||
applyPreSummonAbAttrs("PreSummonAbAttr", switchedInPokemon);
|
||||
applyPreSwitchOutAbAttrs("PreSwitchOutAbAttr", this.lastPokemon);
|
||||
applyAbAttrs("PreSummonAbAttr", { pokemon: switchedInPokemon });
|
||||
applyAbAttrs("PreSwitchOutAbAttr", { pokemon: this.lastPokemon });
|
||||
if (!switchedInPokemon) {
|
||||
this.end();
|
||||
return;
|
||||
|
@ -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 { TerrainType } from "#app/data/terrain";
|
||||
import { WeatherType } from "#app/enums/weather-type";
|
||||
@ -49,7 +49,7 @@ export class TurnEndPhase extends FieldPhase {
|
||||
globalScene.applyModifier(EnemyStatusEffectHealChanceModifier, false, pokemon);
|
||||
}
|
||||
|
||||
applyPostTurnAbAttrs("PostTurnAbAttr", pokemon);
|
||||
applyAbAttrs("PostTurnAbAttr", { pokemon });
|
||||
}
|
||||
|
||||
globalScene.applyModifiers(TurnStatusEffectModifier, pokemon.isPlayer(), pokemon);
|
||||
|
@ -66,8 +66,12 @@ export class TurnStartPhase extends FieldPhase {
|
||||
globalScene.getField(true).forEach(p => {
|
||||
const bypassSpeed = new BooleanHolder(false);
|
||||
const canCheckHeldItems = new BooleanHolder(true);
|
||||
applyAbAttrs("BypassSpeedChanceAbAttr", p, null, false, bypassSpeed);
|
||||
applyAbAttrs("PreventBypassSpeedChanceAbAttr", p, null, false, bypassSpeed, canCheckHeldItems);
|
||||
applyAbAttrs("BypassSpeedChanceAbAttr", { pokemon: p, bypass: bypassSpeed });
|
||||
applyAbAttrs("PreventBypassSpeedChanceAbAttr", {
|
||||
pokemon: p,
|
||||
bypass: bypassSpeed,
|
||||
canCheckHeldItems: canCheckHeldItems,
|
||||
});
|
||||
if (canCheckHeldItems.value) {
|
||||
globalScene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p, bypassSpeed);
|
||||
}
|
||||
|
@ -1,9 +1,5 @@
|
||||
import { globalScene } from "#app/global-scene";
|
||||
import {
|
||||
applyPreWeatherEffectAbAttrs,
|
||||
applyAbAttrs,
|
||||
applyPostWeatherLapseAbAttrs,
|
||||
} from "#app/data/abilities/apply-ab-attrs";
|
||||
import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
|
||||
import { CommonAnim } from "#enums/move-anims-common";
|
||||
import type { Weather } 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);
|
||||
|
||||
this.executeForAll((pokemon: Pokemon) =>
|
||||
applyPreWeatherEffectAbAttrs("SuppressWeatherEffectAbAttr", pokemon, this.weather, cancelled),
|
||||
applyAbAttrs("SuppressWeatherEffectAbAttr", { pokemon, weather: this.weather, cancelled }),
|
||||
);
|
||||
|
||||
if (!cancelled.value) {
|
||||
const inflictDamage = (pokemon: Pokemon) => {
|
||||
const cancelled = new BooleanHolder(false);
|
||||
|
||||
applyPreWeatherEffectAbAttrs("PreWeatherDamageAbAttr", pokemon, this.weather, cancelled);
|
||||
applyAbAttrs("BlockNonDirectDamageAbAttr", pokemon, cancelled);
|
||||
applyAbAttrs("PreWeatherDamageAbAttr", { pokemon, weather: this.weather, cancelled });
|
||||
applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled });
|
||||
|
||||
if (
|
||||
cancelled.value ||
|
||||
@ -80,7 +76,7 @@ export class WeatherEffectPhase extends CommonAnimPhase {
|
||||
globalScene.ui.showText(getWeatherLapseMessage(this.weather.weatherType) ?? "", null, () => {
|
||||
this.executeForAll((pokemon: Pokemon) => {
|
||||
if (!pokemon.switchOutStatus) {
|
||||
applyPostWeatherLapseAbAttrs("PostWeatherLapseAbAttr", pokemon, this.weather);
|
||||
applyAbAttrs("PostWeatherLapseAbAttr", { pokemon, weather: this.weather });
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -5,9 +5,13 @@ import type BattleScene from "#app/battle-scene";
|
||||
import { globalScene } from "#app/global-scene";
|
||||
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 FadeOutType = typeof FadeOut;
|
||||
|
||||
export function initGameSpeed() {
|
||||
const thisArg = this as BattleScene;
|
||||
|
||||
@ -18,14 +22,44 @@ export function initGameSpeed() {
|
||||
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) {
|
||||
if (!(config instanceof Phaser.Time.TimerEvent) && config.delay) {
|
||||
config.delay = transformValue(config.delay);
|
||||
}
|
||||
return originalAddEvent.apply(this, [config]);
|
||||
};
|
||||
const originalTweensAdd = this.tweens.add;
|
||||
const originalTweensAdd: TweenManager["add"] = this.tweens.add;
|
||||
|
||||
this.tweens.add = function (
|
||||
config:
|
||||
| Phaser.Types.Tweens.TweenBuilderConfig
|
||||
@ -33,71 +67,33 @@ export function initGameSpeed() {
|
||||
| Phaser.Tweens.Tween
|
||||
| Phaser.Tweens.TweenChain,
|
||||
) {
|
||||
if (config.loopDelay) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
mutateProperties(config);
|
||||
return originalTweensAdd.apply(this, [config]);
|
||||
};
|
||||
const originalTweensChain = this.tweens.chain;
|
||||
} as typeof originalTweensAdd;
|
||||
|
||||
const originalTweensChain: TweenManager["chain"] = this.tweens.chain;
|
||||
this.tweens.chain = function (config: Phaser.Types.Tweens.TweenChainBuilderConfig): Phaser.Tweens.TweenChain {
|
||||
if (config.tweens) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
mutateProperties(config);
|
||||
return originalTweensChain.apply(this, [config]);
|
||||
};
|
||||
const originalAddCounter = this.tweens.addCounter;
|
||||
} as typeof originalTweensChain;
|
||||
const originalAddCounter: TweenManager["addCounter"] = this.tweens.addCounter;
|
||||
|
||||
this.tweens.addCounter = function (config: Phaser.Types.Tweens.NumberTweenBuilderConfig) {
|
||||
if (config.duration) {
|
||||
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);
|
||||
}
|
||||
mutateProperties(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;
|
||||
SoundFade.fadeOut = ((_scene: Phaser.Scene, sound: Phaser.Sound.BaseSound, duration: number, destroy?: boolean) =>
|
||||
|
@ -201,19 +201,19 @@ export function formatLargeNumber(count: number, threshold: number): string {
|
||||
let suffix = "";
|
||||
switch (Math.ceil(ret.length / 3) - 1) {
|
||||
case 1:
|
||||
suffix = "K";
|
||||
suffix = i18next.t("common:abrThousand");
|
||||
break;
|
||||
case 2:
|
||||
suffix = "M";
|
||||
suffix = i18next.t("common:abrMillion");
|
||||
break;
|
||||
case 3:
|
||||
suffix = "B";
|
||||
suffix = i18next.t("common:abrBillion");
|
||||
break;
|
||||
case 4:
|
||||
suffix = "T";
|
||||
suffix = i18next.t("common:abrTrillion");
|
||||
break;
|
||||
case 5:
|
||||
suffix = "q";
|
||||
suffix = i18next.t("common:abrQuadrillion");
|
||||
break;
|
||||
default:
|
||||
return "?";
|
||||
@ -227,15 +227,31 @@ export function formatLargeNumber(count: number, threshold: number): string {
|
||||
}
|
||||
|
||||
// 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 {
|
||||
const abbreviations = getAbbreviationsLargeNumber();
|
||||
let exponent: number;
|
||||
|
||||
if (number < 1000) {
|
||||
exponent = 0;
|
||||
} else {
|
||||
const maxExp = AbbreviationsLargeNumber.length - 1;
|
||||
const maxExp = abbreviations.length - 1;
|
||||
|
||||
exponent = Math.floor(Math.log(number) / Math.log(1000));
|
||||
exponent = Math.min(exponent, maxExp);
|
||||
@ -243,7 +259,7 @@ export function formatFancyLargeNumber(number: number, rounded = 3): string {
|
||||
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) {
|
||||
|
@ -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 { globalScene } from "#app/global-scene";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
@ -196,7 +196,7 @@ describe("Abilities - Cud Chew", () => {
|
||||
|
||||
describe("regurgiates berries", () => {
|
||||
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]);
|
||||
|
||||
const farigiraf = game.scene.getPlayerPokemon()!;
|
||||
|
@ -28,8 +28,10 @@ describe("Abilities - Gorilla Tactics", () => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyMoveset(MoveId.SPLASH)
|
||||
.enemyLevel(30)
|
||||
.moveset([MoveId.SPLASH, MoveId.TACKLE, MoveId.GROWL, MoveId.METRONOME])
|
||||
.ability(AbilityId.GORILLA_TACTICS);
|
||||
@ -42,7 +44,6 @@ describe("Abilities - Gorilla Tactics", () => {
|
||||
const initialAtkStat = darmanitan.getStat(Stat.ATK);
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(darmanitan.getStat(Stat.ATK, false)).toBeCloseTo(initialAtkStat * 1.5);
|
||||
@ -59,7 +60,6 @@ describe("Abilities - Gorilla Tactics", () => {
|
||||
|
||||
// First turn, lock move to Growl
|
||||
game.move.select(MoveId.GROWL);
|
||||
await game.move.forceEnemyMove(MoveId.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
// Second turn, Growl is interrupted by Disable
|
||||
@ -72,7 +72,7 @@ describe("Abilities - Gorilla Tactics", () => {
|
||||
|
||||
// Third turn, Struggle is used
|
||||
game.move.select(MoveId.TACKLE);
|
||||
await game.move.forceEnemyMove(MoveId.SPLASH); //prevent protect from being used by the enemy
|
||||
await game.move.forceEnemyMove(MoveId.SPLASH); // prevent disable from being used by the enemy
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
|
||||
@ -91,7 +91,7 @@ describe("Abilities - Gorilla Tactics", () => {
|
||||
game.move.select(MoveId.METRONOME);
|
||||
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.METRONOME)).toBe(false);
|
||||
expect(darmanitan.getLastXMoves(-1)).toEqual([
|
||||
@ -106,11 +106,13 @@ describe("Abilities - Gorilla Tactics", () => {
|
||||
const darmanitan = game.field.getPlayerPokemon();
|
||||
|
||||
game.move.select(MoveId.TACKLE);
|
||||
await game.move.selectEnemyMove(MoveId.PROTECT);
|
||||
await game.move.forceEnemyMove(MoveId.PROTECT);
|
||||
|
||||
await game.toEndOfTurn();
|
||||
expect(darmanitan.isMoveRestricted(MoveId.SPLASH)).toBe(true);
|
||||
expect(darmanitan.isMoveRestricted(MoveId.TACKLE)).toBe(false);
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
expect(enemy.hp).toBe(enemy.getMaxHp());
|
||||
});
|
||||
|
||||
it("should activate when a move is succesfully executed but misses", async () => {
|
||||
@ -119,7 +121,6 @@ describe("Abilities - Gorilla Tactics", () => {
|
||||
const darmanitan = game.field.getPlayerPokemon();
|
||||
|
||||
game.move.select(MoveId.TACKLE);
|
||||
await game.move.selectEnemyMove(MoveId.SPLASH);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
|
||||
await game.move.forceMiss();
|
||||
await game.toEndOfTurn();
|
||||
|
@ -95,7 +95,7 @@ describe("Abilities - Harvest", () => {
|
||||
|
||||
// Give ourselves harvest and disable enemy neut gas,
|
||||
// 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.move.select(MoveId.GASTRO_ACID);
|
||||
await game.move.selectEnemyMove(MoveId.NUZZLE);
|
||||
|
@ -42,7 +42,7 @@ describe("Abilities - Healer", () => {
|
||||
});
|
||||
|
||||
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]);
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]);
|
||||
|
||||
|
@ -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 () => {
|
||||
await game.classicMode.startBattle();
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
|
@ -178,7 +178,7 @@ describe("Abilities - Neutralizing Gas", () => {
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
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();
|
||||
|
||||
@ -187,6 +187,6 @@ describe("Abilities - Neutralizing Gas", () => {
|
||||
await game.killPokemon(game.scene.getPlayerPokemon()!);
|
||||
|
||||
expect(game.scene.arena.getTag(ArenaTagType.NEUTRALIZING_GAS)).toBeUndefined();
|
||||
expect(weatherChangeAttr.applyPostSummon).not.toHaveBeenCalled();
|
||||
expect(weatherChangeSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
@ -1,3 +1,4 @@
|
||||
import type { StatMultiplierAbAttrParams } from "#app/@types/ability-types";
|
||||
import { allAbilities } from "#app/data/data-lists";
|
||||
import { CommandPhase } from "#app/phases/command-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]);
|
||||
|
||||
const sandVeilAttr = allAbilities[AbilityId.SAND_VEIL].getAttrs("StatMultiplierAbAttr")[0];
|
||||
vi.spyOn(sandVeilAttr, "applyStatStage").mockImplementation(
|
||||
(_pokemon, _passive, _simulated, stat, statValue, _args) => {
|
||||
if (stat === Stat.EVA && game.scene.arena.weather?.weatherType === WeatherType.SANDSTORM) {
|
||||
statValue.value *= -1; // will make all attacks miss
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
);
|
||||
vi.spyOn(sandVeilAttr, "apply").mockImplementation(({ stat, statVal }: StatMultiplierAbAttrParams) => {
|
||||
if (stat === Stat.EVA && game.scene.arena.weather?.weatherType === WeatherType.SANDSTORM) {
|
||||
statVal.value *= -1; // will make all attacks miss
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
expect(leadPokemon[0].hasAbility(AbilityId.SAND_VEIL)).toBe(true);
|
||||
expect(leadPokemon[1].hasAbility(AbilityId.SAND_VEIL)).toBe(false);
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { NumberHolder } from "#app/utils/common";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
@ -52,25 +52,16 @@ describe("Abilities - Shield Dust", () => {
|
||||
expect(move.id).toBe(MoveId.AIR_SLASH);
|
||||
|
||||
const chance = new NumberHolder(move.chance);
|
||||
await applyAbAttrs(
|
||||
"MoveEffectChanceMultiplierAbAttr",
|
||||
phase.getUserPokemon()!,
|
||||
null,
|
||||
false,
|
||||
applyAbAttrs("MoveEffectChanceMultiplierAbAttr", {
|
||||
pokemon: phase.getUserPokemon()!,
|
||||
chance,
|
||||
move,
|
||||
phase.getFirstTarget(),
|
||||
false,
|
||||
);
|
||||
await applyPreDefendAbAttrs(
|
||||
"IgnoreMoveEffectsAbAttr",
|
||||
phase.getFirstTarget()!,
|
||||
phase.getUserPokemon()!,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
});
|
||||
applyAbAttrs("IgnoreMoveEffectsAbAttr", {
|
||||
pokemon: phase.getFirstTarget()!,
|
||||
move,
|
||||
chance,
|
||||
);
|
||||
});
|
||||
expect(chance.value).toBe(0);
|
||||
});
|
||||
|
||||
|
@ -22,10 +22,7 @@ describe("Abilities - Unburden", () => {
|
||||
*/
|
||||
function getHeldItemCount(pokemon: Pokemon): number {
|
||||
const stackCounts = pokemon.getHeldItems().map(m => m.getStackCount());
|
||||
if (stackCounts.length) {
|
||||
return stackCounts.reduce((a, b) => a + b);
|
||||
}
|
||||
return 0;
|
||||
return stackCounts.reduce((a, b) => a + b, 0);
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
@ -277,7 +274,7 @@ describe("Abilities - Unburden", () => {
|
||||
const initialTreeckoSpeed = treecko.getStat(Stat.SPD);
|
||||
const initialPurrloinSpeed = purrloin.getStat(Stat.SPD);
|
||||
const unburdenAttr = treecko.getAbilityAttrs("PostItemLostAbAttr")[0];
|
||||
vi.spyOn(unburdenAttr, "applyPostItemLost");
|
||||
vi.spyOn(unburdenAttr, "apply");
|
||||
|
||||
// Player uses Baton Pass, which also passes the Baton item
|
||||
game.move.select(MoveId.BATON_PASS);
|
||||
@ -288,7 +285,7 @@ describe("Abilities - Unburden", () => {
|
||||
expect(getHeldItemCount(purrloin)).toBe(1);
|
||||
expect(treecko.getEffectiveStat(Stat.SPD)).toBe(initialTreeckoSpeed);
|
||||
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 () => {
|
||||
|
@ -108,7 +108,7 @@ describe("Abilities - Wimp Out", () => {
|
||||
});
|
||||
|
||||
it("Trapping moves do not prevent Wimp Out from activating.", async () => {
|
||||
game.override.enemyMoveset([MoveId.SPIRIT_SHACKLE]).startingLevel(53).enemyLevel(45);
|
||||
game.override.enemyMoveset([MoveId.SPIRIT_SHACKLE]).startingLevel(1).passiveAbility(AbilityId.STURDY);
|
||||
await game.classicMode.startBattle([SpeciesId.WIMPOD, SpeciesId.TYRUNT]);
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
@ -123,7 +123,7 @@ describe("Abilities - Wimp Out", () => {
|
||||
});
|
||||
|
||||
it("If this Ability activates due to being hit by U-turn or Volt Switch, the user of that move will not be switched out.", async () => {
|
||||
game.override.startingLevel(95).enemyMoveset([MoveId.U_TURN]);
|
||||
game.override.startingLevel(1).enemyMoveset([MoveId.U_TURN]).passiveAbility(AbilityId.STURDY);
|
||||
await game.classicMode.startBattle([SpeciesId.WIMPOD, SpeciesId.TYRUNT]);
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
|
79
test/field/pokemon-id-checks.test.ts
Normal file
79
test/field/pokemon-id-checks.test.ts
Normal 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);
|
||||
});
|
||||
});
|
@ -31,7 +31,7 @@ describe("Spec - Pokemon", () => {
|
||||
const pkm = game.scene.getPlayerPokemon()!;
|
||||
expect(pkm).toBeDefined();
|
||||
|
||||
expect(pkm.trySetStatus(undefined)).toBe(true);
|
||||
expect(pkm.trySetStatus(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
describe("Add To Party", () => {
|
||||
|
@ -42,7 +42,7 @@ describe("Moves - Heal Block", () => {
|
||||
const player = game.scene.getPlayerPokemon()!;
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
||||
player.damageAndUpdate(enemy.getMaxHp() - 1);
|
||||
player.damageAndUpdate(player.getMaxHp() - 1);
|
||||
|
||||
game.move.select(MoveId.ABSORB);
|
||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { RandomMoveAttr } from "#app/data/moves/move";
|
||||
import { allMoves } from "#app/data/data-lists";
|
||||
import { RandomMoveAttr } from "#app/data/moves/move";
|
||||
import type Pokemon from "#app/field/pokemon";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import type { TurnMove } from "#app/field/pokemon";
|
||||
import type { MovePhase } from "#app/phases/move-phase";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { MoveUseMode } from "#enums/move-use-mode";
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { MoveUseMode } from "#enums/move-use-mode";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { Stat } from "#enums/stat";
|
||||
import GameManager from "#test/testUtils/gameManager";
|
||||
@ -202,21 +203,32 @@ describe("Moves - Instruct", () => {
|
||||
game.override.battleStyle("double").enemyMoveset(MoveId.SPLASH).enemySpecies(SpeciesId.MAGIKARP).enemyLevel(1);
|
||||
await game.classicMode.startBattle([SpeciesId.HISUI_ELECTRODE, SpeciesId.KOMMO_O]);
|
||||
|
||||
const [electrode, kommo_o] = game.scene.getPlayerField()!;
|
||||
game.move.changeMoveset(electrode, MoveId.CHLOROBLAST);
|
||||
const [electrode, kommo_o] = game.scene.getPlayerField();
|
||||
game.move.changeMoveset(electrode, MoveId.THUNDERBOLT);
|
||||
game.move.changeMoveset(kommo_o, MoveId.INSTRUCT);
|
||||
|
||||
game.move.select(MoveId.CHLOROBLAST, BattlerIndex.PLAYER, BattlerIndex.ENEMY);
|
||||
game.move.select(MoveId.THUNDERBOLT, BattlerIndex.PLAYER, BattlerIndex.ENEMY);
|
||||
game.move.select(MoveId.INSTRUCT, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]);
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
await game.toEndOfTurn();
|
||||
|
||||
// Chloroblast always deals 50% max HP% recoil UNLESS you whiff
|
||||
// due to lack of targets or similar,
|
||||
// so all we have to do is check whether electrode fainted or not.
|
||||
// Naturally, both karps should also be dead as well.
|
||||
expect(electrode.isFainted()).toBe(true);
|
||||
const [karp1, karp2] = game.scene.getEnemyField()!;
|
||||
expect(electrode.getMoveHistory()).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining<TurnMove>({
|
||||
result: MoveResult.SUCCESS,
|
||||
move: MoveId.THUNDERBOLT,
|
||||
targets: [BattlerIndex.ENEMY],
|
||||
useMode: MoveUseMode.NORMAL,
|
||||
}),
|
||||
expect.objectContaining<TurnMove>({
|
||||
result: MoveResult.SUCCESS,
|
||||
move: MoveId.THUNDERBOLT,
|
||||
targets: [BattlerIndex.ENEMY_2],
|
||||
useMode: MoveUseMode.NORMAL,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
const [karp1, karp2] = game.scene.getEnemyField();
|
||||
expect(karp1.isFainted()).toBe(true);
|
||||
expect(karp2.isFainted()).toBe(true);
|
||||
});
|
||||
|
@ -140,9 +140,8 @@ describe("Moves - Safeguard", () => {
|
||||
game.field.mockAbility(player, AbilityId.STATIC);
|
||||
vi.spyOn(
|
||||
allAbilities[AbilityId.STATIC].getAttrs("PostDefendContactApplyStatusEffectAbAttr")[0],
|
||||
"chance",
|
||||
"get",
|
||||
).mockReturnValue(100);
|
||||
"canApply",
|
||||
).mockReturnValue(true);
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
await game.move.forceEnemyMove(MoveId.SAFEGUARD);
|
||||
|
@ -122,15 +122,20 @@ export default class GameWrapper {
|
||||
},
|
||||
};
|
||||
|
||||
// TODO: Replace this with a proper mock of phaser's TweenManager.
|
||||
this.scene.tweens = {
|
||||
add: data => {
|
||||
if (data.onComplete) {
|
||||
data.onComplete();
|
||||
}
|
||||
// TODO: our mock of `add` should have the same signature as the real one, which returns the tween
|
||||
data.onComplete?.();
|
||||
},
|
||||
getTweensOf: () => [],
|
||||
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 => {
|
||||
if (data.onComplete) {
|
||||
data.onComplete();
|
||||
|
@ -1,15 +1,16 @@
|
||||
import { BattleStyle } from "#app/enums/battle-style";
|
||||
import type { SpeciesId } from "#enums/species-id";
|
||||
import { getGameMode } from "#app/game-mode";
|
||||
import { GameModes } from "#enums/game-modes";
|
||||
import overrides from "#app/overrides";
|
||||
import { CommandPhase } from "#app/phases/command-phase";
|
||||
import { EncounterPhase } from "#app/phases/encounter-phase";
|
||||
import { SelectStarterPhase } from "#app/phases/select-starter-phase";
|
||||
import { TurnInitPhase } from "#app/phases/turn-init-phase";
|
||||
import { GameModes } from "#enums/game-modes";
|
||||
import { Nature } from "#enums/nature";
|
||||
import type { SpeciesId } from "#enums/species-id";
|
||||
import { UiMode } from "#enums/ui-mode";
|
||||
import { generateStarter } from "../gameManagerUtils";
|
||||
import { GameManagerHelper } from "./gameManagerHelper";
|
||||
import { generateStarter } from "#test/testUtils/gameManagerUtils";
|
||||
import { GameManagerHelper } from "#test/testUtils/helpers/gameManagerHelper";
|
||||
|
||||
/**
|
||||
* Helper to handle classic-mode specific operations.
|
||||
@ -36,6 +37,12 @@ export class ClassicModeHelper extends GameManagerHelper {
|
||||
if (this.game.override.disableShinies) {
|
||||
this.game.override.shiny(false).enemyShiny(false);
|
||||
}
|
||||
if (this.game.override.normalizeIVs) {
|
||||
this.game.override.playerIVs(31).enemyIVs(31);
|
||||
}
|
||||
if (this.game.override.normalizeNatures) {
|
||||
this.game.override.nature(Nature.HARDY).enemyNature(Nature.HARDY);
|
||||
}
|
||||
|
||||
this.game.onNextPrompt("TitlePhase", UiMode.TITLE, () => {
|
||||
this.game.scene.gameMode = getGameMode(GameModes.CLASSIC);
|
||||
|
@ -1,35 +1,55 @@
|
||||
import type { Variant } from "#app/sprites/variant";
|
||||
/** biome-ignore-start lint/correctness/noUnusedImports: tsdoc imports */
|
||||
import type { NewArenaEvent } from "#app/events/battle-scene";
|
||||
/** biome-ignore-end lint/correctness/noUnusedImports: tsdoc imports */
|
||||
|
||||
import { Weather } from "#app/data/weather";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import type { ModifierOverride } from "#app/modifier/modifier-type";
|
||||
import type { BattleStyle } from "#app/overrides";
|
||||
import type { BattleStyle, RandomTrainerOverride } from "#app/overrides";
|
||||
import Overrides, { defaultOverrides } from "#app/overrides";
|
||||
import type { Unlockables } from "#enums/unlockables";
|
||||
import type { Variant } from "#app/sprites/variant";
|
||||
import { coerceArray, shiftCharCodes } from "#app/utils/common";
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import type { BattleType } from "#enums/battle-type";
|
||||
import { BiomeId } from "#enums/biome-id";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import type { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
|
||||
import type { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
||||
import { Nature } from "#enums/nature";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import type { Unlockables } from "#enums/unlockables";
|
||||
import type { WeatherType } from "#enums/weather-type";
|
||||
import { GameManagerHelper } from "#test/testUtils/helpers/gameManagerHelper";
|
||||
import { expect, vi } from "vitest";
|
||||
import { GameManagerHelper } from "./gameManagerHelper";
|
||||
import { coerceArray, shiftCharCodes } from "#app/utils/common";
|
||||
import type { RandomTrainerOverride } from "#app/overrides";
|
||||
import type { BattleType } from "#enums/battle-type";
|
||||
|
||||
/**
|
||||
* Helper to handle overrides in tests
|
||||
*/
|
||||
export class OverridesHelper extends GameManagerHelper {
|
||||
/** If `true`, removes the starting items from enemies at the start of each test; default `true` */
|
||||
/**
|
||||
* If `true`, removes the starting items from enemies at the start of each test.
|
||||
* @defaultValue `true`
|
||||
*/
|
||||
public removeEnemyStartingItems = true;
|
||||
/** If `true`, sets the shiny overrides to disable shinies at the start of each test; default `true` */
|
||||
/**
|
||||
* If `true`, sets the shiny overrides to disable shinies at the start of each test.
|
||||
* @defaultValue `true`
|
||||
*/
|
||||
public disableShinies = true;
|
||||
/**
|
||||
* If `true`, will set the IV overrides for player and enemy pokemon to `31` at the start of each test.
|
||||
* @defaultValue `true`
|
||||
*/
|
||||
public normalizeIVs = true;
|
||||
/**
|
||||
* If `true`, will set the Nature overrides for player and enemy pokemon to a neutral nature at the start of each test.
|
||||
* @defaultValue `true`
|
||||
*/
|
||||
public normalizeNatures = true;
|
||||
|
||||
/**
|
||||
* Override the starting biome
|
||||
* @warning Any event listeners that are attached to [NewArenaEvent](events\battle-scene.ts) may need to be handled down the line
|
||||
* @warning Any event listeners that are attached to {@linkcode NewArenaEvent} may need to be handled down the line
|
||||
* @param biome - The biome to set
|
||||
*/
|
||||
public startingBiome(biome: BiomeId): this {
|
||||
@ -219,6 +239,80 @@ export class OverridesHelper extends GameManagerHelper {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the IVs of the player pokemon
|
||||
* @param ivs - If set to a number, all IVs are set to the same value. Must be between `0` and `31`!
|
||||
*
|
||||
* If set to an array, that array is applied to the pokemon's IV field as-is.
|
||||
* All values must be between `0` and `31`, and the array must be of exactly length `6`!
|
||||
*
|
||||
* If set to `null`, the override is disabled.
|
||||
* @returns `this`
|
||||
*/
|
||||
public playerIVs(ivs: number | number[] | null): this {
|
||||
this.normalizeIVs = false;
|
||||
vi.spyOn(Overrides, "IVS_OVERRIDE", "get").mockReturnValue(ivs);
|
||||
if (ivs === null) {
|
||||
this.log("Player IVs override disabled!");
|
||||
} else {
|
||||
this.log(`Player IVs set to ${ivs}!`);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the nature of the player's pokemon
|
||||
* @param nature - The nature to set, or `null` to disable the override.
|
||||
* @returns `this`
|
||||
*/
|
||||
public nature(nature: Nature | null): this {
|
||||
this.normalizeNatures = false;
|
||||
vi.spyOn(Overrides, "NATURE_OVERRIDE", "get").mockReturnValue(nature);
|
||||
if (nature === null) {
|
||||
this.log("Player Nature override disabled!");
|
||||
} else {
|
||||
this.log(`Player Nature set to ${Nature[nature]} (=${nature})!`);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the IVs of the enemy pokemon
|
||||
* @param ivs - If set to a number, all IVs are set to the same value. Must be between `0` and `31`!
|
||||
*
|
||||
* If set to an array, that array is applied to the pokemon's IV field as-is.
|
||||
* All values must be between `0` and `31`, and the array must be of exactly length `6`!
|
||||
*
|
||||
* If set to `null`, the override is disabled.
|
||||
* @returns `this`
|
||||
*/
|
||||
public enemyIVs(ivs: number | number[] | null): this {
|
||||
this.normalizeIVs = false;
|
||||
vi.spyOn(Overrides, "ENEMY_IVS_OVERRIDE", "get").mockReturnValue(ivs);
|
||||
if (ivs === null) {
|
||||
this.log("Enemy IVs override disabled!");
|
||||
} else {
|
||||
this.log(`Enemy IVs set to ${ivs}!`);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the nature of the enemy's pokemon
|
||||
* @param nature - The nature to set, or `null` to disable the override.
|
||||
* @returns `this`
|
||||
*/
|
||||
public enemyNature(nature: Nature | null): this {
|
||||
this.normalizeNatures = false;
|
||||
vi.spyOn(Overrides, "ENEMY_NATURE_OVERRIDE", "get").mockReturnValue(nature);
|
||||
if (nature === null) {
|
||||
this.log("Enemy Nature override disabled!");
|
||||
} else {
|
||||
this.log(`Enemy Nature set to ${Nature[nature]} (=${nature})!`);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override each wave to not have standard trainer battles
|
||||
* @returns `this`
|
||||
|
@ -5,7 +5,7 @@ export class MockVideoGameObject implements MockGameObject {
|
||||
public name: string;
|
||||
public active = true;
|
||||
|
||||
public play = () => null;
|
||||
public play = () => this;
|
||||
public stop = () => this;
|
||||
public setOrigin = () => this;
|
||||
public setScale = () => this;
|
||||
|
Loading…
Reference in New Issue
Block a user