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

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

View File

@ -1,6 +1,19 @@
/** @type {import('dependency-cruiser').IConfiguration} */
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.

View File

@ -68,8 +68,8 @@ Do the reviewers need to do something special in order to test your changes?
- [ ] The PR is self-contained and cannot be split into smaller PRs?
- [ ] 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)?

View File

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

View File

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

View File

@ -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'

View File

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

View File

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

View File

@ -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:

View File

@ -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.

View File

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

View File

@ -23,7 +23,7 @@ When formatted correctly, these comments are shown within VS Code or similar IDE
- Functions also show the comment for each parameter as you type them, making keeping track of arguments inside lengthy functions much more clear.
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`:

View File

@ -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.

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,14 +1,14 @@
import type { AbAttr } from "#app/data/abilities/ability";
import type Move from "#app/data/moves/move";
import type 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];
};

View File

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

View File

@ -67,7 +67,7 @@ import { modifierTypes } from "./data/data-lists";
import { getModifierPoolForType } from "./utils/modifier-utils";
import { 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";
@ -894,9 +894,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(
@ -1256,7 +1266,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 +1471,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 +2753,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 +2793,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;
}
@ -2812,7 +2822,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

View File

@ -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);
}

View File

@ -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 }));
}
}
}

View File

@ -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;
}
@ -763,12 +767,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 +793,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 +811,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 +826,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 +844,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 +916,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 +929,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 +1033,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 +1083,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 +1222,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 +1252,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 +1440,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 +1451,22 @@ export abstract class DamagingTrapTag extends TrappedTag {
}
}
// TODO: Condense all these tags into 1 singular tag with a modified message func
export class BindTag extends DamagingTrapTag {
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 +1478,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 +1518,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 +1574,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 +1593,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 +1705,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 +2284,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 +2311,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 +2349,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 +2371,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 +2706,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 +2976,13 @@ export class SubstituteTag extends BattlerTag {
/** Sets the Substitute's HP and queues an on-add battle animation that initializes the Substitute's sprite. */
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 +3101,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 +3255,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 +3322,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 +3352,7 @@ export class SyrupBombTag extends BattlerTag {
false,
true,
);
return --this.turnCount > 0;
return super.lapse(pokemon, _lapseType);
}
}

View File

@ -35,28 +35,28 @@ export function getBerryPredicate(berryType: BerryType): BerryPredicate {
case BerryType.APICOT:
case BerryType.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(),

View File

@ -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;
};
}
@ -5436,7 +5444,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;
}
@ -6435,9 +6443,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) });
}
}
@ -6476,7 +6484,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;
}
@ -6869,7 +6877,7 @@ export class RandomMovesetMoveAttr extends RandomMoveAttr {
* Whether to consider all moves from the user's party (`true`) or the user's own moveset (`false`);
* default `false`.
*/
private includeParty = false
private includeParty = false,
) {
super(invalidMoves);
}
@ -7970,7 +7978,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 }));

View File

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

View File

@ -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");

View File

@ -20,11 +20,7 @@ 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 { 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 +368,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;
@ -461,8 +457,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;

View File

@ -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";
@ -189,7 +174,7 @@ 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";
/** Base typeclass for damage parameter methods, used for DRY */
type damageParams = {
@ -1364,7 +1349,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 +1400,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 +2036,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 +2185,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 +2247,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 +2257,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 +2277,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 +2350,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 +2389,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 +2433,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 +2457,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 +3402,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 +3446,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 +3468,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 +3616,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 +3763,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 +3781,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 +3802,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 +3870,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/** Doubles damage if the attacker has Tinted Lens and is using a resisted move */
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 +3887,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 +3913,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 +3951,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 +3962,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 +4074,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 +4122,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 +4148,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 +4182,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);
}
@ -4620,7 +4677,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* @param ignoreField Whether any field effects (weather, terrain, etc.) should be considered
*/
canSetStatus(
effect: StatusEffect | undefined,
effect: StatusEffect,
quiet = false,
overrideStatus = false,
sourcePokemon: Pokemon | null = null,
@ -4651,8 +4708,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
// Check if the source Pokemon has an ability that cancels the Poison/Toxic immunity
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;
}
@ -4701,21 +4764,20 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
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 +4808,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 +4869,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 +4929,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 +5475,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 +5535,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;

View File

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

View File

@ -1,4 +1,4 @@
import { applyAbAttrs, applyPreLeaveFieldAbAttrs } from "#app/data/abilities/apply-ab-attrs";
import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
import { Stat } from "#enums/stat";
import { 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);

View File

@ -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) {

View File

@ -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 });
}
}

View File

@ -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);

View File

@ -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();
},
});
}

View File

@ -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) {

View File

@ -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);
});
}

View File

@ -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")) {

View File

@ -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 });
}
}

View File

@ -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";
@ -228,14 +228,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);
@ -394,7 +391,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(),
@ -404,7 +402,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({
@ -436,7 +438,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 });
});
}
}
@ -468,7 +470,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",
@ -521,7 +527,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 */

View File

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

View File

@ -8,7 +8,7 @@ import type Pokemon from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
import { 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();
});

View File

@ -1,4 +1,4 @@
import { applyPostSummonAbAttrs } from "#app/data/abilities/apply-ab-attrs";
import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
import { PostSummonPhase } from "#app/phases/post-summon-phase";
import 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();
}

View File

@ -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();

View File

@ -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 {

View File

@ -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();

View File

@ -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(

View File

@ -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();
}

View File

@ -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;

View File

@ -1,4 +1,4 @@
import { applyPostTurnAbAttrs } from "#app/data/abilities/apply-ab-attrs";
import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs";
import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
import { 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);

View File

@ -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);
}

View File

@ -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 });
}
});

View File

@ -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) =>

View File

@ -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) {

View File

@ -1,4 +1,4 @@
import { RepeatBerryNextTurnAbAttr } from "#app/data/abilities/ability";
import { CudChewConsumeBerryAbAttr } from "#app/data/abilities/ability";
import Pokemon from "#app/field/pokemon";
import { 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()!;

View File

@ -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([

View File

@ -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);

View File

@ -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]);

View File

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

View File

@ -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();
});
});

View File

@ -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);

View File

@ -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);
});

View File

@ -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 () => {

View File

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

View File

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

View File

@ -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);

View File

@ -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();

View File

@ -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;