Merge remote-tracking branch 'upstream/beta' into test-cleanup

This commit is contained in:
Bertie690 2025-06-24 18:31:54 -04:00
commit 7b7807c3eb
121 changed files with 7572 additions and 13040 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,20 +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
- name: Run ESLint
run: npm run eslint-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:

164
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,164 @@
# Contributing to PokéRogue
Thank you for taking the time to contribute, every little bit helps. This project is entirely open-source and unmonetized - community contributions are what keep it alive!
Please make sure you understand everything relevant to your changes from the [Table of Contents](#-table-of-contents), and absolutely *feel free to reach out reach out in the **#dev-corner** channel on [Discord](https://discord.gg/pokerogue)*. We are here to help and the better you understand what you're working on, the easier it will be for it to find its way into the game.
## 📄 Table of Contents
- [Development Basics](#-development-basics)
- [Environment Setup](#-environment-setup)
- [Getting Started](#-getting-started)
- [Documentation](#-documentation)
- [Testing Your Changes](#-testing-your-changes)
- [Localization](#-localization)
- [Development Save File (Unlock Everything)](#-development-save-file)
## 🛠️ Development Basics
PokéRogue is built with [Typescript](https://www.typescriptlang.org/docs/handbook/intro.html), using the [Phaser](https://github.com/phaserjs/phaser) game framework.
If you have the motivation and experience with Typescript/Javascript (or are willing to learn) you can contribute by forking the repository and making pull requests with contributions.
## 💻 Environment Setup
### Prerequisites
- 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 `pnpm install`
- *if you run into any errors, reach out in the **#dev-corner** channel on Discord*
2. Run `pnpm start:dev` to locally run the project at `localhost:8000`
### Linting
Check out our [in-depth file](./docs/linting.md) on linting and formatting!
## 🚀 Getting Started
A great way to develop an understanding of how the project works is to look at test cases (located in [the `test` folder](./test/)).
Tests show you both how things are supposed to work and the expected "flow" to get from point A to point B in battles.
*This is a big project and you will be confused at times - never be afraid to reach out and ask questions in **#dev-corner***!
### Where to Look
Once you have your feet under you, check out the [Issues](https://github.com/pagefaultgames/pokerogue/issues) page to see how you can help us!
Most issues are bugs and are labeled with their area, such as `Move`, `Ability`, `UI/UX`, etc. There are also priority labels:
- `P0`: Completely gamebreaking (very rare)
- `P1`: Major - Game crash
- `P2`: Minor - Incorrect (but non-crashing) move/ability/interaction
- `P3`: No gameplay impact - typo, minor graphical error, etc.
Also under issues, you can take a look at the [List of Partial / Unimplemented Moves and Abilities](https://github.com/pagefaultgames/pokerogue/issues/3503) and the [Bug Board](https://github.com/orgs/pagefaultgames/projects/3) (the latter is essentially the same as the issues page but easier to work with).
You are free to comment on any issue so that you may be assigned to it and we can avoid multiple people working on the same thing.
## 📚 Documentation
You can find the auto-generated documentation [here](https://pagefaultgames.github.io/pokerogue/main/index.html).
For information on enemy AI, check out the [enemy-ai.md](./docs/enemy-ai.md) file.
For detailed guidelines on documenting your code, refer to the [comments.md](./docs/comments.md) file.
Again, if you have unanswered questions please feel free to ask!
## 🧪 Testing Your Changes
You've just made a change - how can you check if it works? You have two areas to hit:
### 1 - Manual Testing
> This will likely be your first stop. After making a change, you'll want to spin the game up and make sure everything is as you expect. To do this, you will need a way to manipulate the game to produce the situation you're looking to test.
[src/overrides.ts](../src/overrides.ts) contains overrides for most values you'll need to change for testing, controlled through the `overrides` object.
For example, here is how you could test a scenario where the player Pokemon has the ability Drought and the enemy Pokemon has the move Water Gun:
```typescript
const overrides = {
ABILITY_OVERRIDE: AbilityId.DROUGHT,
OPP_MOVESET_OVERRIDE: MoveId.WATER_GUN,
} satisfies Partial<InstanceType<typeof DefaultOverrides>>;
```
Read through `src/overrides.ts` file to find the override that fits your needs - there are a lot of them!
If the situation you're trying to test can't be created using existing overrides (or with the [Dev Save](#-development-save-file)), reach out in **#dev-corner**.
You can get help testing your specific changes, and you might have found a new override that needs to be created!
### 2 - Automatic Testing
> 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 `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 `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.
- Test edge cases. A good strategy is to think of edge cases beforehand and create tests for them using `it.todo`. Once the edge case has been handled, you can remove the `todo` marker.
## 📜 Localization
The project intends for all text to be localized. That is, strings are pulled from translation files using keys (depending on the current language) and *never* hardcoded as a particular language. Note that there is a PDF in a message pinned in **#dev-corner** which gives the following information in greater detail.
### Setting Up and Updating the Locales Submodule
> The locales (translation) files are set up as a git submodule. A project-in-a-project, if you will.
To fetch translations when you first start development in your fork or to update them on your local branch, run:
```bash
git submodule update --progress --init --recursive
```
### How Localizations Work
> This project uses the [i18next](https://www.i18next.com/) library to integrate translations from public/locales
into the source code based on the user's settings or location. The basic process for
fetching translated text is as follows:
1. The source code fetches text by a given key, e.g.
```ts
i18next.t("fileName:keyName", { arg1: "Hello", arg2: "an example", ... })
```
2. The game looks up the key in the corresponding JSON file in the user's
language, e.g.
```ts
// from "en/file-name.json"...
"keyName": "{{arg1}}! This is {{arg2}} of translated text!"
```
If the key doesn't exist for the user's language, the game will default to the
corresponding English key (in the case of LATAM Spanish, it will first default to ES Spanish).
3. The game shows the text to the user:
```ts
"Hello! This is an example of translated text!"
```
### Adding Translated Text
> If your feature involves new or modified text in any form, then you will be modifying the [locales](https://github.com/pagefaultgames/pokerogue-locales) repository. ***Never hardcode new text in any language!***
The workflow is:
1. Make a pull request to the main repository for your new feature.
If this feature requires new text, the text should be integrated into the code with a new i18next key pointing to where you plan to add it into the pokerogue-locales repository.
2. Make another pull request -- this time to the [pokerogue-locales](https://github.com/pagefaultgames/pokerogue-locales)
repository -- adding a new entry to the English locale with text for each key
you added to your main PR. You *only* need to add the English key and value - the translation team will handle the rest.
3. If your feature is pulled from the mainline Pokémon games (e.g. a Move or Ability implementation), add a source link for any added text within the locale PR.
[Poké Corpus](https://abcboy101.github.io/poke-corpus) is a great resource for finding text from the latest mainline games; otherwise, a YouTube video link showing the text in mainline is sufficient.
4. Ping @lugiadrien in **#dev-corner** or the current callout thread to make sure your locales PR is seen.
It'll be merged into the locales repository after any necessary corrections, at which point you can test it in your main PR (after updating locales from remote)
5. The Dev team will approve your main PR, and your changes will be in the beta environment!
## 😈 Development Save File
> Some issues may require you to have unlocks on your save file which go beyond normal overrides. For this reason, the repository contains a [save file](../test/testUtils/saves/everything.psrv) with _everything_ unlocked (even ones not legitimately obtainable, like unimplemented variant shinies).
1. Start the game up locally and navigate to `Menu -> Manage Data -> Import Data`
2. Select [everything.prsv](test/testUtils/saves/everything.prsv) (`test/testUtils/saves/everything.prsv`) and confirm.

View File

@ -4,47 +4,7 @@ PokéRogue is a browser based Pokémon fangame heavily inspired by the roguelite
# Contributing
## 🛠️ Development
If you have the motivation and experience with Typescript/Javascript (or are willing to learn) please feel free to fork the repository and make pull requests with contributions. If you don't know what to work on but want to help, reference the below **To-Do** section or the **#feature-vote** channel in the discord.
### 💻 Environment Setup
#### Prerequisites
- node: 22.14.0
- npm: [how to install](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)
#### Running Locally
1. Clone the repo and in the root directory run `npm install`
- *if you run into any errors, reach out in the **#dev-corner** channel in discord*
2. Run `npm run start:dev` to locally run the project in `localhost:8000`
#### Linting
We're using Biome as our common linter and formatter. It will run automatically during the pre-commit hook but if you would like to manually run it, use the `npm run biome` script. To view the complete rules, check out the [biome.jsonc](./biome.jsonc) file.
### 📚 Documentation
You can find the auto-generated documentation [here](https://pagefaultgames.github.io/pokerogue/main/index.html).
For information on enemy AI, check out the [enemy-ai.md](./docs/enemy-ai.md) file.
For detailed guidelines on documenting your code, refer to the [comments.md](./docs/comments.md) file.
### ❔ FAQ
**How do I test a new _______?**
- In the `src/overrides.ts` file there are overrides for most values you'll need to change for testing
**How do I retrieve the translations?**
- The translations were moved to the [dedicated translation repository](https://github.com/pagefaultgames/pokerogue-locales) and are now applied as a submodule in this project.
- The command to retrieve the translations is `git submodule update --init --recursive`. If you still struggle to get it working, please reach out to #dev-corner channel in Discord.
## 🪧 To Do
Check out [Github Issues](https://github.com/pagefaultgames/pokerogue/issues) to see how can you help us!
See [CONTRIBUTING.md](./CONTRIBUTING.md), this includes instructions on how to set up the game locally.
# 📝 Credits
>

View File

@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
"vcs": {
"enabled": false,
"clientKind": "git",
@ -10,35 +10,47 @@
"enabled": true,
"useEditorconfig": true,
"indentStyle": "space",
"ignore": ["src/enums/*", "src/data/balance/*"],
"includes": ["**", "!**/src/enums/**/*", "!**/src/data/balance/**/*"],
"lineWidth": 120
},
"files": {
"ignoreUnknown": true,
// Adding folders to the ignore list is GREAT for performance because it prevents biome from descending into them
// and having to verify whether each individual file is ignored
"ignore": [
"**/*.d.ts",
"dist/*",
"build/*",
"coverage/*",
"public/*",
".github/*",
"node_modules/*",
".vscode/*",
"*.css", // TODO?
"*.html", // TODO?
// TODO: these files are too big and complex, ignore them until their respective refactors
"src/data/moves/move.ts",
// this file is just too big:
"src/data/balance/tms.ts"
"includes": [
"**",
"!**/*.d.ts",
"!**/dist/**/*",
"!**/build/**/*",
"!**/coverage/**/*",
"!**/public/**/*",
"!**/.github/**/*",
"!**/node_modules/**/*",
"!**/.vscode/**/*",
"!**/typedoc/**/*",
// TODO: lint css and html?
"!**/*.css",
"!**/*.html",
// TODO: enable linting this file
"!**/src/data/moves/move.ts",
// this file is too big
"!**/src/data/balance/tms.ts"
]
},
// While it'd be nice to enable consistent sorting, enabling this causes issues due to circular import resolution order
// TODO: Remove if we ever get down to 0 circular imports
"organizeImports": { "enabled": false },
// TODO: Configure and enable import sorting
"assist": {
"actions": {
"source": {
"organizeImports": {
"level": "off",
"options": {
"groups": []
}
}
}
}
},
"linter": {
"enabled": true,
"rules": {
@ -48,10 +60,15 @@
"noUnusedVariables": "error",
"noSwitchDeclarations": "error",
"noVoidTypeReturn": "error",
"noUnusedImports": "error"
"noUnusedImports": {
"level": "error",
"fix": "safe"
},
"noUnusedFunctionParameters": "error",
"noUnusedLabels": "error",
"noPrivateImports": "error"
},
"style": {
"noVar": "error",
"useEnumInitializers": "off", // large enums like Moves/Species would make this cumbersome
"useBlockStatements": "error",
"useConst": "error",
@ -59,11 +76,31 @@
"noNonNullAssertion": "off", // TODO: Turn this on ASAP and fix all non-null assertions in non-test files
"noParameterAssign": "off",
"useExponentiationOperator": "off", // Too typo-prone and easy to mixup with standard multiplication (* vs **)
"useDefaultParameterLast": "off", // TODO: Fix spots in the codebase where this flag would be triggered, and then enable
"useDefaultParameterLast": {
// TODO: Fix spots in the codebase where this flag would be triggered
// and then set to "error" and re-enable the fixer
"level": "warn",
"fix": "none"
},
"useSingleVarDeclarator": "off",
"useNodejsImportProtocol": "off",
"useTemplate": "off", // string concatenation is faster: https://stackoverflow.com/questions/29055518/are-es6-template-literals-faster-than-string-concatenation
"noNamespaceImport": "error"
"useAsConstAssertion": "error",
"noUnusedTemplateLiteral": "error",
"useNumberNamespace": "error",
"noInferrableTypes": "error",
"noUselessElse": "error",
"noRestrictedTypes": {
"level": "error",
"options": {
"types": {
"integer": {
"message": "This is an alias for 'number' that can provide false impressions of what values can actually be contained in this variable. Use 'number' instead.",
"use": "number"
}
}
}
}
},
"suspicious": {
"noDoubleEquals": "error",
@ -77,45 +114,62 @@
"noImplicitAnyLet": "warn", // TODO: Refactor and make this an error
"noRedeclare": "info", // TODO: Refactor and make this an error
"noGlobalIsNan": "off",
"noAsyncPromiseExecutor": "warn" // TODO: Refactor and make this an error
"noAsyncPromiseExecutor": "warn", // TODO: Refactor and make this an error
"noVar": "error",
"noDocumentCookie": "off" // Firefox has minimal support for the "Cookie Store API"
},
"complexity": {
"noExcessiveCognitiveComplexity": "warn", // TODO: Refactor and make this an error
"noExcessiveCognitiveComplexity": "info", // TODO: Refactor and make this an error
"useLiteralKeys": "off",
"noForEach": "off", // Foreach vs for of is not that simple.
"noUselessSwitchCase": "off", // Explicit > Implicit
"noUselessConstructor": "error",
"noBannedTypes": "warn" // TODO: Refactor and make this an error
"noBannedTypes": "warn", // TODO: Refactor and make this an error
"noThisInStatic": "error",
"noUselessThisAlias": "error",
"noUselessTernary": "error"
},
"performance": {
"noNamespaceImport": "error",
"noDelete": "error"
},
"nursery": {
"noRestrictedTypes": {
"level": "error",
"options": {
"types": {
"integer": {
"message": "This is an alias for 'number' that can provide false impressions of what values can actually be contained in this variable. Use 'number' instead.",
"use": "number"
}
}
}
}
"useAdjacentGetterSetter": "error",
"noConstantBinaryExpression": "error",
"noTsIgnore": "error",
"noAwaitInLoop": "warn",
"useJsonImportAttribute": "off", // "Import attributes are only supported when the '--module' option is set to 'esnext', 'node18', 'nodenext', or 'preserve'. ts(2823)"
"useIndexOf": "error",
"useObjectSpread": "error",
"useNumericSeparators": "off", // TODO: enable?
"useIterableCallbackReturn": "warn", // TODO: refactor and make "error"
"noShadow": "warn" // TODO: refactor and make "error"
}
}
},
"javascript": {
"formatter": { "quoteStyle": "double", "arrowParentheses": "asNeeded" }
"formatter": {
"quoteStyle": "double",
"arrowParentheses": "asNeeded"
},
"parser": {
"jsxEverywhere": false
}
},
"overrides": [
{
"include": ["test/**/*.test.ts"],
"javascript": { "globals": [] },
"includes": ["**/test/**/*.test.ts"],
"linter": {
"rules": {
"performance": {
"noDelete": "off" // TODO: evaluate if this is necessary for the test(s) to function
"noDelete": "off", // TODO: evaluate if this is necessary for the test(s) to function
"noNamespaceImport": "off" // this is required for `vi.spyOn` to work in some tests
},
"style": {
"noNamespaceImport": "off" // this is required for `vi.spyOn` to work in some tests
"noNonNullAssertion": "off"
},
"nursery": {
"noFloatingPromises": "error"
}
}
}
@ -123,7 +177,7 @@
// Overrides to prevent unused import removal inside `overrides.ts` and enums files (for TSDoc linkcodes)
{
"include": ["src/overrides.ts", "src/enums/*"],
"includes": ["**/src/overrides.ts", "**/src/enums/**/*"],
"linter": {
"rules": {
"correctness": {
@ -133,7 +187,7 @@
}
},
{
"include": ["src/overrides.ts"],
"includes": ["**/src/overrides.ts"],
"linter": {
"rules": {
"style": {

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,43 +0,0 @@
/** @ts-check */
import tseslint from "typescript-eslint";
import stylisticTs from "@stylistic/eslint-plugin-ts";
import parser from "@typescript-eslint/parser";
import importX from "eslint-plugin-import-x";
export default tseslint.config(
{
name: "eslint-config",
files: ["src/**/*.{ts,tsx,js,jsx}", "test/**/*.{ts,tsx,js,jsx}"],
ignores: ["dist/*", "build/*", "coverage/*", "public/*", ".github/*", "node_modules/*", ".vscode/*"],
languageOptions: {
parser: parser,
},
plugins: {
"import-x": importX,
"@stylistic/ts": stylisticTs,
"@typescript-eslint": tseslint.plugin,
},
rules: {
"no-undef": "off", // Disables the rule that disallows the use of undeclared variables (TypeScript handles this)
"no-extra-semi": "error", // Disallows unnecessary semicolons for TypeScript-specific syntax
"import-x/extensions": ["error", "never", { json: "always" }], // Enforces no extension for imports unless json
},
},
{
name: "eslint-tests",
files: ["test/**/**.test.ts"],
languageOptions: {
parser: parser,
parserOptions: {
project: ["./tsconfig.json"],
},
},
plugins: {
"@typescript-eslint": tseslint.plugin,
},
rules: {
"@typescript-eslint/no-floating-promises": "error", // Require Promise-like statements to be handled appropriately. - https://typescript-eslint.io/rules/no-floating-promises/
"@typescript-eslint/no-misused-promises": "error", // Disallow Promises in places not designed to handle them. - https://typescript-eslint.io/rules/no-misused-promises/
},
},
);

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

8029
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -22,24 +22,19 @@
"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": {
"@biomejs/biome": "1.9.4",
"@eslint/js": "^9.23.0",
"@biomejs/biome": "2.0.0",
"@hpcc-js/wasm": "^2.22.4",
"@stylistic/eslint-plugin-ts": "^4.1.0",
"@types/jsdom": "^21.1.7",
"@types/node": "^22.13.14",
"@typescript-eslint/eslint-plugin": "^8.28.0",
"@typescript-eslint/parser": "^8.28.0",
"@vitest/coverage-istanbul": "^3.0.9",
"chalk": "^5.4.1",
"dependency-cruiser": "^16.3.10",
"eslint": "^9.23.0",
"eslint-plugin-import-x": "^4.9.4",
"inquirer": "^12.4.2",
"jsdom": "^26.0.0",
"lefthook": "^1.11.5",
@ -47,7 +42,6 @@
"phaser3spectorjs": "^0.0.8",
"typedoc": "^0.28.1",
"typescript": "^5.8.2",
"typescript-eslint": "^8.28.0",
"vite": "^6.3.4",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.9",

3910
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

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";
@ -468,7 +468,7 @@ export default class BattleScene extends SceneBase {
true,
);
//@ts-ignore (the defined types in the package are incromplete...)
//@ts-expect-error (the defined types in the package are incromplete...)
transition.transit({
mode: "blinds",
ease: "Cubic.easeInOut",
@ -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(
@ -1167,7 +1177,7 @@ export default class BattleScene extends SceneBase {
this.field.remove(this.currentBattle.mysteryEncounter?.introVisuals, true);
}
//@ts-ignore - allowing `null` for currentBattle causes a lot of trouble
//@ts-expect-error - allowing `null` for currentBattle causes a lot of trouble
this.currentBattle = null; // TODO: resolve ts-ignore
// Reset RNG after end of game or save & quit.
@ -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) {
@ -3237,7 +3247,7 @@ export default class BattleScene extends SceneBase {
(!this.gameData.achvUnlocks.hasOwnProperty(achv.id) || Overrides.ACHIEVEMENTS_REUNLOCK_OVERRIDE) &&
achv.validate(args)
) {
this.gameData.achvUnlocks[achv.id] = new Date().getTime();
this.gameData.achvUnlocks[achv.id] = Date.now();
this.ui.achvBar.showAchv(achv);
if (vouchers.hasOwnProperty(achv.id)) {
this.validateVoucher(vouchers[achv.id]);
@ -3250,7 +3260,7 @@ export default class BattleScene extends SceneBase {
validateVoucher(voucher: Voucher, args?: unknown[]): boolean {
if (!this.gameData.voucherUnlocks.hasOwnProperty(voucher.id) && voucher.validate(args)) {
this.gameData.voucherUnlocks[voucher.id] = new Date().getTime();
this.gameData.voucherUnlocks[voucher.id] = Date.now();
this.ui.achvBar.showAchv(voucher);
this.gameData.voucherCounts[voucher.voucherType]++;
return true;

View File

@ -178,7 +178,7 @@ export default class Battle {
)
.map(i => {
const ret = i as PokemonHeldItemModifier;
//@ts-ignore - this is awful to fix/change
//@ts-expect-error - this is awful to fix/change
ret.pokemonId = null;
return ret;
}),

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

@ -595,13 +595,13 @@ function parseEggMoves(content: string): void {
const cols = line.split(",").slice(0, 5);
const moveNames = allMoves.map(m => m.name.replace(/ \([A-Z]\)$/, "").toLowerCase());
const enumSpeciesName = cols[0].toUpperCase().replace(/[ -]/g, "_");
const species = speciesValues[speciesNames.findIndex(s => s === enumSpeciesName)];
const species = speciesValues[speciesNames.indexOf(enumSpeciesName)];
const eggMoves: MoveId[] = [];
for (let m = 0; m < 4; m++) {
const moveName = cols[m + 1].trim();
const moveIndex = moveName !== "N/A" ? moveNames.findIndex(mn => mn === moveName.toLowerCase()) : -1;
const moveIndex = moveName !== "N/A" ? moveNames.indexOf(moveName.toLowerCase()) : -1;
eggMoves.push(moveIndex > -1 ? moveIndex as MoveId : MoveId.NONE);
if (moveIndex === -1) {

View File

@ -650,8 +650,8 @@ export const pokemonEvolutions: PokemonEvolutions = {
new SpeciesEvolution(SpeciesId.KIRLIA, 20, null, null)
],
[SpeciesId.KIRLIA]: [
new SpeciesEvolution(SpeciesId.GARDEVOIR, 30, null, {key: EvoCondKey.GENDER, gender: Gender.FEMALE}),
new SpeciesEvolution(SpeciesId.GALLADE, 30, null, {key: EvoCondKey.GENDER, gender: Gender.MALE})
new SpeciesEvolution(SpeciesId.GARDEVOIR, 30, null, null),
new SpeciesEvolution(SpeciesId.GALLADE, 1, EvolutionItem.DAWN_STONE, {key: EvoCondKey.GENDER, gender: Gender.MALE})
],
[SpeciesId.SURSKIT]: [
new SpeciesEvolution(SpeciesId.MASQUERAIN, 22, null, null)
@ -739,8 +739,8 @@ export const pokemonEvolutions: PokemonEvolutions = {
new SpeciesEvolution(SpeciesId.DUSCLOPS, 37, null, null)
],
[SpeciesId.SNORUNT]: [
new SpeciesEvolution(SpeciesId.GLALIE, 42, null, {key: EvoCondKey.GENDER, gender: Gender.MALE}),
new SpeciesEvolution(SpeciesId.FROSLASS, 42, null, {key: EvoCondKey.GENDER, gender: Gender.FEMALE})
new SpeciesEvolution(SpeciesId.GLALIE, 42, null, null),
new SpeciesEvolution(SpeciesId.FROSLASS, 1, EvolutionItem.DAWN_STONE, {key: EvoCondKey.GENDER, gender: Gender.FEMALE})
],
[SpeciesId.SPHEAL]: [
new SpeciesEvolution(SpeciesId.SEALEO, 32, null, null)

View File

@ -346,7 +346,7 @@ abstract class AnimTimedBgEvent extends AnimTimedEvent {
}
class AnimTimedUpdateBgEvent extends AnimTimedBgEvent {
// biome-ignore lint/correctness/noUnusedVariables: seems intentional
// biome-ignore lint/correctness/noUnusedFunctionParameters: seems intentional
execute(moveAnim: MoveAnim, priority?: number): number {
const tweenProps = {};
if (this.bgX !== undefined) {
@ -359,15 +359,11 @@ class AnimTimedUpdateBgEvent extends AnimTimedBgEvent {
tweenProps["alpha"] = (this.opacity || 0) / 255;
}
if (Object.keys(tweenProps).length) {
globalScene.tweens.add(
Object.assign(
{
targets: moveAnim.bgSprite,
duration: getFrameMs(this.duration * 3),
},
tweenProps,
),
);
globalScene.tweens.add({
targets: moveAnim.bgSprite,
duration: getFrameMs(this.duration * 3),
...tweenProps,
});
}
return this.duration * 2;
}
@ -423,7 +419,7 @@ export function initCommonAnims(): Promise<void> {
const commonAnimId = commonAnimIds[ca];
commonAnimFetches.push(
globalScene
.cachedFetch(`./battle-anims/common-${commonAnimNames[ca].toLowerCase().replace(/\_/g, "-")}.json`)
.cachedFetch(`./battle-anims/common-${commonAnimNames[ca].toLowerCase().replace(/_/g, "-")}.json`)
.then(response => response.json())
.then(cas => commonAnims.set(commonAnimId, new AnimConfig(cas))),
);
@ -535,7 +531,7 @@ export async function initEncounterAnims(encounterAnim: EncounterAnim | Encounte
}
encounterAnimFetches.push(
globalScene
.cachedFetch(`./battle-anims/encounter-${encounterAnimNames[anim].toLowerCase().replace(/\_/g, "-")}.json`)
.cachedFetch(`./battle-anims/encounter-${encounterAnimNames[anim].toLowerCase().replace(/_/g, "-")}.json`)
.then(response => response.json())
.then(cas => encounterAnims.set(anim, new AnimConfig(cas))),
);
@ -559,7 +555,7 @@ export function initMoveChargeAnim(chargeAnim: ChargeAnim): Promise<void> {
} else {
chargeAnims.set(chargeAnim, null);
globalScene
.cachedFetch(`./battle-anims/${ChargeAnim[chargeAnim].toLowerCase().replace(/\_/g, "-")}.json`)
.cachedFetch(`./battle-anims/${ChargeAnim[chargeAnim].toLowerCase().replace(/_/g, "-")}.json`)
.then(response => response.json())
.then(ca => {
if (Array.isArray(ca)) {
@ -1405,15 +1401,15 @@ export class EncounterBattleAnim extends BattleAnim {
export async function populateAnims() {
const commonAnimNames = getEnumKeys(CommonAnim).map(k => k.toLowerCase());
const commonAnimMatchNames = commonAnimNames.map(k => k.replace(/\_/g, ""));
const commonAnimMatchNames = commonAnimNames.map(k => k.replace(/_/g, ""));
const commonAnimIds = getEnumValues(CommonAnim) as CommonAnim[];
const chargeAnimNames = getEnumKeys(ChargeAnim).map(k => k.toLowerCase());
const chargeAnimMatchNames = chargeAnimNames.map(k => k.replace(/\_/g, " "));
const chargeAnimMatchNames = chargeAnimNames.map(k => k.replace(/_/g, " "));
const chargeAnimIds = getEnumValues(ChargeAnim) as ChargeAnim[];
const commonNamePattern = /name: (?:Common:)?(Opp )?(.*)/;
const moveNameToId = {};
for (const move of getEnumValues(MoveId).slice(1)) {
const moveName = MoveId[move].toUpperCase().replace(/\_/g, "");
const moveName = MoveId[move].toUpperCase().replace(/_/g, "");
moveNameToId[moveName] = move;
}
@ -1469,7 +1465,7 @@ export async function populateAnims() {
const frameData = framesData[fd];
const focusFramesData = frameData.split(" - - ");
for (let tf = 0; tf < focusFramesData.length; tf++) {
const values = focusFramesData[tf].replace(/ {6}\- /g, "").split("\n");
const values = focusFramesData[tf].replace(/ {6}- /g, "").split("\n");
const targetFrame = new AnimFrame(
Number.parseFloat(values[0]),
Number.parseFloat(values[1]),
@ -1516,7 +1512,7 @@ export async function populateAnims() {
.replace(/[a-z]+: ! '', /gi, "")
.replace(/name: (.*?),/, 'name: "$1",')
.replace(
/flashColor: !ruby\/object:Color { alpha: ([\d\.]+), blue: ([\d\.]+), green: ([\d\.]+), red: ([\d\.]+)}/,
/flashColor: !ruby\/object:Color { alpha: ([\d.]+), blue: ([\d.]+), green: ([\d.]+), red: ([\d.]+)}/,
"flashRed: $4, flashGreen: $3, flashBlue: $2, flashAlpha: $1",
);
const frameIndex = Number.parseInt(/frame: (\d+)/.exec(timingData)![1]); // TODO: is the bang correct?
@ -1641,12 +1637,12 @@ export async function populateAnims() {
let props: string[];
for (let p = 0; p < propSets.length; p++) {
props = propSets[p];
// @ts-ignore TODO
// @ts-expect-error TODO
const ai = props.indexOf(a.key);
if (ai === -1) {
continue;
}
// @ts-ignore TODO
// @ts-expect-error TODO
const bi = props.indexOf(b.key);
return ai < bi ? -1 : ai > bi ? 1 : 0;

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

@ -175,7 +175,7 @@ export class Egg {
this._sourceType = eggOptions?.sourceType ?? undefined;
this._hatchWaves = eggOptions?.hatchWaves ?? this.getEggTierDefaultHatchWaves();
this._timestamp = eggOptions?.timestamp ?? new Date().getTime();
this._timestamp = eggOptions?.timestamp ?? Date.now();
// First roll shiny and variant so we can filter if species with an variant exist
this._isShiny = eggOptions?.isShiny ?? (Overrides.EGG_SHINY_OVERRIDE || this.rollShiny());
@ -255,7 +255,7 @@ export class Egg {
// Sets the hidden ability if a hidden ability exists and
// the override is set or the egg hits the chance
let abilityIndex: number | undefined = undefined;
let abilityIndex: number | undefined;
const sameSpeciesEggHACheck =
this._sourceType === EggSourceType.SAME_SPECIES_EGG && !randSeedInt(SAME_SPECIES_EGG_HA_RATE);
const gachaEggHACheck = !(this._sourceType === EggSourceType.SAME_SPECIES_EGG) && !randSeedInt(GACHA_EGG_HA_RATE);
@ -524,7 +524,7 @@ export class Egg {
/**
* Rolls whether the egg is shiny or not.
* @returns `true` if the egg is shiny
**/
*/
private rollShiny(): boolean {
let shinyChance = GACHA_DEFAULT_SHINY_RATE;
switch (this._sourceType) {

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,10 +85,15 @@ 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}.
* Conventionally returns `true` for success and `false` for failure.
*/
type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean;
export type UserMoveConditionFunc = (user: Pokemon, move: Move) => boolean;
@ -343,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)
@ -641,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;
}
@ -758,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;
@ -801,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(
@ -823,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;
@ -854,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;
}
@ -1306,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;
@ -1314,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;
}
@ -1390,18 +1400,31 @@ export class BeakBlastHeaderAttr extends AddBattlerTagHeaderAttr {
}
}
/**
* Attribute to display a message before a move is executed.
*/
export class PreMoveMessageAttr extends MoveAttr {
private message: string | ((user: Pokemon, target: Pokemon, move: Move) => string);
/** The message to display or a function returning one */
private message: string | ((user: Pokemon, target: Pokemon, move: Move) => string | undefined);
/**
* Create a new {@linkcode PreMoveMessageAttr} to display a message before move execution.
* @param message - The message to display before move use, either as a string or a function producing one.
* @remarks
* If {@linkcode message} evaluates to an empty string (`''`), no message will be displayed
* (though the move will still succeed).
*/
constructor(message: string | ((user: Pokemon, target: Pokemon, move: Move) => string)) {
super();
this.message = message;
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const message = typeof this.message === "string"
? this.message as string
: this.message(user, target, move);
apply(user: Pokemon, target: Pokemon, move: Move, _args: any[]): boolean {
const message = typeof this.message === "function"
? this.message(user, target, move)
: this.message;
// TODO: Consider changing if/when MoveAttr `apply` return values become significant
if (message) {
globalScene.phaseManager.queueMessage(message, 500);
return true;
@ -1692,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) {
@ -1826,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
@ -2025,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) {
@ -2397,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) {
@ -2505,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;
}
}
@ -2557,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);
@ -2573,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;
}
}
@ -2661,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;
@ -2778,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);
}
}
@ -2804,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;
}
@ -2818,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);
@ -3009,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;
};
}
@ -5421,7 +5446,7 @@ export class NoEffectAttr extends MoveAttr {
const crashDamageFunc = (user: Pokemon, move: Move) => {
const cancelled = new BooleanHolder(false);
applyAbAttrs("BlockNonDirectDamageAbAttr", user, cancelled);
applyAbAttrs("BlockNonDirectDamageAbAttr", {pokemon: user, cancelled});
if (cancelled.value) {
return false;
}
@ -6420,9 +6445,9 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
}
getFailedText(_user: Pokemon, target: Pokemon, _move: Move): string | undefined {
const blockedByAbility = new BooleanHolder(false);
applyAbAttrs("ForceSwitchOutImmunityAbAttr", target, blockedByAbility);
if (blockedByAbility.value) {
const cancelled = new BooleanHolder(false);
applyAbAttrs("ForceSwitchOutImmunityAbAttr", {pokemon: target, cancelled});
if (cancelled.value) {
return i18next.t("moveTriggers:cannotBeSwitchedOut", { pokemonName: getPokemonNameWithAffix(target) });
}
}
@ -6461,7 +6486,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
}
const blockedByAbility = new BooleanHolder(false);
applyAbAttrs("ForceSwitchOutImmunityAbAttr", target, blockedByAbility);
applyAbAttrs("ForceSwitchOutImmunityAbAttr", {pokemon: target, cancelled: blockedByAbility});
if (blockedByAbility.value) {
return false;
}
@ -6870,12 +6895,12 @@ export class RandomMovesetMoveAttr extends CallMoveAttr {
// includeParty will be true for Assist, false for Sleep Talk
let allies: Pokemon[];
if (this.includeParty) {
allies = user.isPlayer() ? globalScene.getPlayerParty().filter(p => p !== user) : globalScene.getEnemyParty().filter(p => p !== user);
allies = (user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty()).filter(p => p !== user);
} else {
allies = [ user ];
}
const partyMoveset = allies.map(p => p.moveset).flat();
const moves = partyMoveset.filter(m => !this.invalidMoves.has(m!.moveId) && !m!.getMove().name.endsWith(" (N)"));
const partyMoveset = allies.flatMap(p => p.moveset);
const moves = partyMoveset.filter(m => !this.invalidMoves.has(m.moveId) && !m.getMove().name.endsWith(" (N)"));
if (moves.length === 0) {
return false;
}
@ -7970,7 +7995,7 @@ const failIfSingleBattle: MoveConditionFunc = (user, target, move) => globalScen
const failIfDampCondition: MoveConditionFunc = (user, target, move) => {
const cancelled = new BooleanHolder(false);
globalScene.getField(true).map(p=>applyAbAttrs("FieldPreventExplosiveMovesAbAttr", p, cancelled));
globalScene.getField(true).map(p=>applyAbAttrs("FieldPreventExplosiveMovesAbAttr", {pokemon: p, cancelled}));
// Queue a message if an ability prevented usage of the move
if (cancelled.value) {
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:cannotUseMove", { pokemonName: getPokemonNameWithAffix(user), moveName: move.name }));
@ -11299,7 +11324,11 @@ export function initMoves() {
.attr(ForceSwitchOutAttr, true, SwitchType.SHED_TAIL)
.condition(failIfLastInPartyCondition),
new SelfStatusMove(MoveId.CHILLY_RECEPTION, PokemonType.ICE, -1, 10, -1, 0, 9)
.attr(PreMoveMessageAttr, (user, move) => i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(user) }))
.attr(PreMoveMessageAttr, (user, _target, _move) =>
// Don't display text if current move phase is follow up (ie move called indirectly)
isVirtual((globalScene.phaseManager.getCurrentPhase() as MovePhase).useMode)
? ""
: i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(user) }))
.attr(ChillyReceptionAttr, true),
new SelfStatusMove(MoveId.TIDY_UP, PokemonType.NORMAL, -1, 10, -1, 0, 9)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPD ], 1, true)

View File

@ -16,7 +16,7 @@ import type Move from "./move";
* @see {@linkcode getMovePp} - returns amount of PP a move currently has.
* @see {@linkcode getPpRatio} - returns the current PP amount / max PP amount.
* @see {@linkcode getName} - returns name of {@linkcode Move}.
**/
*/
export class PokemonMove {
public moveId: MoveId;
public ppUsed: number;

View File

@ -135,7 +135,7 @@ export const ClowningAroundEncounter: MysteryEncounter = MysteryEncounterBuilder
);
clownConfig.setPartyTemplates(clownPartyTemplate);
clownConfig.setDoubleOnly();
// @ts-ignore
// @ts-expect-error
clownConfig.partyTemplateFunc = null; // Overrides party template func if it exists
// Generate random ability for Blacephalon from pool

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

@ -92,7 +92,7 @@ export const MysteriousChallengersEncounter: MysteryEncounter = MysteryEncounter
const brutalConfig = trainerConfigs[brutalTrainerType].clone();
brutalConfig.title = trainerConfigs[brutalTrainerType].title;
brutalConfig.setPartyTemplates(e4Template);
// @ts-ignore
// @ts-expect-error
brutalConfig.partyTemplateFunc = null; // Overrides gym leader party template func
female = false;
if (brutalConfig.hasGenders) {

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

@ -1226,7 +1226,7 @@ export function calculateMEAggregateStats(baseSpawnWeight: number) {
);
for (const value of meanEncountersPerRunPerBiomeSorted) {
stats += value[0] + "avg valid floors " + meanMEFloorsPerRunPerBiome.get(value[0]) + ", avg MEs ${value[1]},\n";
stats += value[0] + "avg valid floors " + meanMEFloorsPerRunPerBiome.get(value[0]) + `, avg MEs ${value[1]},\n`;
}
console.log(stats);

View File

@ -751,7 +751,7 @@ export async function catchPokemon(
UiMode.POKEDEX_PAGE,
pokemon.species,
pokemon.formIndex,
attributes,
[attributes],
null,
() => {
globalScene.ui.setMode(UiMode.MESSAGE).then(() => {

View File

@ -96,8 +96,8 @@ export function getPokemonSpeciesForm(species: SpeciesId, formIndex: number): Po
}
export function getFusedSpeciesName(speciesAName: string, speciesBName: string): string {
const fragAPattern = /([a-z]{2}.*?[aeiou(?:y$)\-\']+)(.*?)$/i;
const fragBPattern = /([a-z]{2}.*?[aeiou(?:y$)\-\'])(.*?)$/i;
const fragAPattern = /([a-z]{2}.*?[aeiou(?:y$)\-']+)(.*?)$/i;
const fragBPattern = /([a-z]{2}.*?[aeiou(?:y$)\-'])(.*?)$/i;
const [speciesAPrefixMatch, speciesBPrefixMatch] = [speciesAName, speciesBName].map(n => /^(?:[^ ]+) /.exec(n));
const [speciesAPrefix, speciesBPrefix] = [speciesAPrefixMatch, speciesBPrefixMatch].map(m => (m ? m[0] : ""));
@ -134,7 +134,7 @@ export function getFusedSpeciesName(speciesAName: string, speciesBName: string):
if (fragBMatch) {
const lastCharA = fragA.slice(fragA.length - 1);
const prevCharB = fragBMatch[1].slice(fragBMatch.length - 1);
fragB = (/[\-']/.test(prevCharB) ? prevCharB : "") + fragBMatch[2] || prevCharB;
fragB = (/[-']/.test(prevCharB) ? prevCharB : "") + fragBMatch[2] || prevCharB;
if (lastCharA === fragB[0]) {
if (/[aiu]/.test(lastCharA)) {
fragB = fragB.slice(1);
@ -379,7 +379,7 @@ export abstract class PokemonSpeciesForm {
}
getSpriteAtlasPath(female: boolean, formIndex?: number, shiny?: boolean, variant?: number, back?: boolean): string {
const spriteId = this.getSpriteId(female, formIndex, shiny, variant, back).replace(/\_{2}/g, "/");
const spriteId = this.getSpriteId(female, formIndex, shiny, variant, back).replace(/_{2}/g, "/");
return `${/_[1-3]$/.test(spriteId) ? "variant/" : ""}${spriteId}`;
}
@ -478,8 +478,8 @@ export abstract class PokemonSpeciesForm {
case SpeciesId.DUDUNSPARCE:
break;
case SpeciesId.ZACIAN:
// biome-ignore lint/suspicious/noFallthroughSwitchClause: Intentionally falls through
case SpeciesId.ZAMAZENTA:
// biome-ignore lint/suspicious/noFallthroughSwitchClause: Falls through
if (formSpriteKey.startsWith("behemoth")) {
formSpriteKey = "crowned";
}
@ -569,7 +569,7 @@ export abstract class PokemonSpeciesForm {
const rootSpeciesId = this.getRootSpeciesId();
for (const moveId of moveset) {
if (speciesEggMoves.hasOwnProperty(rootSpeciesId)) {
const eggMoveIndex = speciesEggMoves[rootSpeciesId].findIndex(m => m === moveId);
const eggMoveIndex = speciesEggMoves[rootSpeciesId].indexOf(moveId);
if (eggMoveIndex > -1 && eggMoves & (1 << eggMoveIndex)) {
continue;
}

View File

@ -290,7 +290,7 @@ export class TrainerConfig {
* @param {string} [nameFemale] The name of the female trainer. If 'Ivy', a localized name will be assigned.
* @param {TrainerType | string} [femaleEncounterBgm] The encounter BGM for the female trainer, which can be a TrainerType or a string.
* @returns {TrainerConfig} The updated TrainerConfig instance.
**/
*/
setHasGenders(nameFemale?: string, femaleEncounterBgm?: TrainerType | string): TrainerConfig {
// If the female name is 'Ivy' (the rival), assign a localized name.
if (nameFemale === "Ivy") {
@ -335,7 +335,7 @@ export class TrainerConfig {
if (doubleEncounterBgm) {
this.doubleEncounterBgm =
typeof doubleEncounterBgm === "number"
? TrainerType[doubleEncounterBgm].toString().replace(/\_/g, " ").toLowerCase()
? TrainerType[doubleEncounterBgm].toString().replace(/_/g, " ").toLowerCase()
: doubleEncounterBgm;
}
return this;
@ -540,7 +540,7 @@ export class TrainerConfig {
* @param {SpeciesId | SpeciesId[]} signatureSpecies The signature species for the evil team leader.
* @param specialtyType The specialty Type of the admin, if they have one
* @returns {TrainerConfig} The updated TrainerConfig instance.
* **/
*/
initForEvilTeamAdmin(
title: string,
poolName: EvilTeam,
@ -581,7 +581,7 @@ export class TrainerConfig {
* Initializes the trainer configuration for a Stat Trainer, as part of the Trainer's Test Mystery Encounter.
* @param _isMale Whether the stat trainer is Male or Female (for localization of the title).
* @returns {TrainerConfig} The updated TrainerConfig instance.
**/
*/
initForStatTrainer(_isMale = false): TrainerConfig {
if (!getIsInitialized()) {
initI18n();
@ -608,7 +608,7 @@ export class TrainerConfig {
* @param {PokemonType} specialtyType The specialty type for the evil team Leader.
* @param boolean Whether or not this is the rematch fight
* @returns {TrainerConfig} The updated TrainerConfig instance.
* **/
*/
initForEvilTeamLeader(
title: string,
signatureSpecies: (SpeciesId | SpeciesId[])[],
@ -651,7 +651,7 @@ export class TrainerConfig {
* @param ignoreMinTeraWave Whether the Gym Leader always uses Tera (true), or only Teras after {@linkcode GYM_LEADER_TERA_WAVE} (false). Defaults to false.
* @param teraSlot Optional, sets the party member in this slot to Terastallize. Wraps based on party size.
* @returns {TrainerConfig} The updated TrainerConfig instance.
* **/
*/
initForGymLeader(
signatureSpecies: (SpeciesId | SpeciesId[])[],
isMale: boolean,
@ -709,7 +709,7 @@ export class TrainerConfig {
* @param specialtyType - The specialty type for the Elite Four member.
* @param teraSlot - Optional, sets the party member in this slot to Terastallize.
* @returns The updated TrainerConfig instance.
**/
*/
initForEliteFour(
signatureSpecies: (SpeciesId | SpeciesId[])[],
isMale: boolean,
@ -765,7 +765,7 @@ export class TrainerConfig {
* @param {SpeciesId | SpeciesId[]} signatureSpecies The signature species for the Champion.
* @param isMale Whether the Champion is Male or Female (for localization of the title).
* @returns {TrainerConfig} The updated TrainerConfig instance.
**/
*/
initForChampion(isMale: boolean): TrainerConfig {
// Check if the internationalization (i18n) system is initialized.
if (!getIsInitialized()) {
@ -815,7 +815,7 @@ export class TrainerConfig {
* @param {TrainerSlot} trainerSlot - The slot to determine which title to use. Defaults to TrainerSlot.NONE.
* @param {TrainerVariant} variant - The variant of the trainer to determine the specific title.
* @returns {string} - The title of the trainer.
**/
*/
getTitle(trainerSlot: TrainerSlot = TrainerSlot.NONE, variant: TrainerVariant): string {
const ret = this.name;

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 = {
@ -898,12 +883,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
getSpriteAtlasPath(ignoreOverride?: boolean): string {
const spriteId = this.getSpriteId(ignoreOverride).replace(/\_{2}/g, "/");
const spriteId = this.getSpriteId(ignoreOverride).replace(/_{2}/g, "/");
return `${/_[1-3]$/.test(spriteId) ? "variant/" : ""}${spriteId}`;
}
getBattleSpriteAtlasPath(back?: boolean, ignoreOverride?: boolean): string {
const spriteId = this.getBattleSpriteId(back, ignoreOverride).replace(/\_{2}/g, "/");
const spriteId = this.getBattleSpriteId(back, ignoreOverride).replace(/_{2}/g, "/");
return `${/_[1-3]$/.test(spriteId) ? "variant/" : ""}${spriteId}`;
}
@ -977,7 +962,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
getFusionBattleSpriteAtlasPath(back?: boolean, ignoreOverride?: boolean): string {
return this.getFusionBattleSpriteId(back, ignoreOverride).replace(/\_{2}/g, "/");
return this.getFusionBattleSpriteId(back, ignoreOverride).replace(/_{2}/g, "/");
}
getIconAtlasKey(ignoreOverride = false, useIllusion = true): string {
@ -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;
}
@ -2498,14 +2517,39 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
defScore *=
1 / Math.max(this.getAttackTypeEffectiveness(enemyTypes[1], opponent, false, false, undefined, true), 0.25);
}
atkScore *= 1.25; //give more value for the pokemon's typing
const moveset = this.moveset;
let moveAtkScoreLength = 0;
for (const move of moveset) {
if (move.getMove().category === MoveCategory.SPECIAL || move.getMove().category === MoveCategory.PHYSICAL) {
atkScore += opponent.getAttackTypeEffectiveness(move.getMove().type, this, false, true, undefined, true);
moveAtkScoreLength++;
}
}
atkScore = atkScore / (moveAtkScoreLength + 1); //calculate the median for the attack score
/**
* Based on this Pokemon's HP ratio compared to that of the opponent.
* This ratio is multiplied by 1.5 if this Pokemon outspeeds the opponent;
* however, the final ratio cannot be higher than 1.
*/
let hpDiffRatio = this.getHpRatio() + (1 - opponent.getHpRatio());
if (outspeed) {
hpDiffRatio = Math.min(hpDiffRatio * 1.5, 1);
const hpRatio = this.getHpRatio();
const oppHpRatio = opponent.getHpRatio();
const isDying = hpRatio <= 0.2;
let hpDiffRatio = hpRatio + (1 - oppHpRatio);
if (isDying && this.isActive(true)) {
//It might be a sacrifice candidate if hp under 20%
const badMatchup = atkScore < 1.5 && defScore < 1.5;
if (!outspeed && badMatchup) {
//It might not be a worthy sacrifice if it doesn't outspeed or doesn't do enough damage
hpDiffRatio *= 0.85;
} else {
hpDiffRatio = Math.min(1 - hpRatio + (outspeed ? 0.2 : 0.1), 1);
}
} else if (outspeed) {
hpDiffRatio = Math.min(hpDiffRatio * 1.25, 1);
} else if (hpRatio > 0.2 && hpRatio <= 0.4) {
//Might be considered to be switched because it's not in low enough health
hpDiffRatio = Math.min(hpDiffRatio * 0.5, 1);
}
return (atkScore + defScore) * hpDiffRatio;
}
@ -2880,7 +2924,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
);
};
let fusionOverride: PokemonSpecies | undefined = undefined;
let fusionOverride: PokemonSpecies | undefined;
if (forStarter && this.isPlayer() && Overrides.STARTER_FUSION_SPECIES_OVERRIDE) {
fusionOverride = getPokemonSpecies(Overrides.STARTER_FUSION_SPECIES_OVERRIDE);
@ -3358,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);
@ -3397,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);
@ -3418,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;
@ -3559,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) {
@ -3706,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 */
@ -3726,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]
@ -3747,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;
@ -3811,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 */
@ -3822,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,
});
}
}
@ -3837,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)
@ -3875,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)];
@ -3886,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,
@ -3998,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;
}
@ -4046,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;
@ -4066,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;
}
@ -4100,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);
}
@ -4373,9 +4455,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
scene.time.delayedCall(fixedInt(Math.ceil(duration * 0.4)), () => {
try {
SoundFade.fadeOut(scene, cry, fixedInt(Math.ceil(duration * 0.2)));
fusionCry = this.getFusionSpeciesForm(undefined, true).cry(
Object.assign({ seek: Math.max(fusionCry.totalDuration * 0.4, 0) }, soundConfig),
);
fusionCry = this.getFusionSpeciesForm(undefined, true).cry({
seek: Math.max(fusionCry.totalDuration * 0.4, 0),
...soundConfig,
});
SoundFade.fadeIn(
scene,
fusionCry,
@ -4517,13 +4600,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
if (i === transitionIndex && fusionCryKey) {
SoundFade.fadeOut(globalScene, cry, fixedInt(Math.ceil((duration / rate) * 0.2)));
fusionCry = globalScene.playSound(
fusionCryKey,
Object.assign({
seek: Math.max(fusionCry.totalDuration * 0.4, 0),
rate: rate,
}),
);
fusionCry = globalScene.playSound(fusionCryKey, {
seek: Math.max(fusionCry.totalDuration * 0.4, 0),
rate: rate,
});
SoundFade.fadeIn(
globalScene,
fusionCry,
@ -4597,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,
@ -4628,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;
}
@ -4678,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;
}
@ -4723,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;
}
@ -4781,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;
@ -4842,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;
}
@ -5316,10 +5403,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
for (let sc = 0; sc < spriteColors.length; sc++) {
const delta = Math.min(...paletteDeltas[sc]);
const paletteIndex = Math.min(
paletteDeltas[sc].findIndex(pd => pd === delta),
fusionPalette.length - 1,
);
const paletteIndex = Math.min(paletteDeltas[sc].indexOf(delta), fusionPalette.length - 1);
if (delta < 255) {
const ratio = easeFunc(delta / 255);
const color = [0, 0, 0, fusionSpriteColors[sc][3]];
@ -5391,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);
@ -5451,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

@ -158,7 +158,7 @@ export default class Trainer extends Phaser.GameObjects.Container {
* @param {TrainerSlot} trainerSlot - The slot to determine which name to use. Defaults to TrainerSlot.NONE.
* @param {boolean} includeTitle - Whether to include the title in the returned name. Defaults to false.
* @returns {string} - The formatted name of the trainer.
**/
*/
getName(trainerSlot: TrainerSlot = TrainerSlot.NONE, includeTitle = false): string {
// Get the base title based on the trainer slot and variant.
let name = this.config.getTitle(trainerSlot, this.variant);

View File

@ -70,20 +70,20 @@ const repeatInputDelayMillis = 250;
* providing a unified interface for all input-related interactions.
*/
export class InputsController {
private gamepads: Array<Phaser.Input.Gamepad.Gamepad> = new Array();
private gamepads: Array<Phaser.Input.Gamepad.Gamepad> = [];
public events: Phaser.Events.EventEmitter;
private buttonLock: Button[] = new Array();
private buttonLock: Button[] = [];
private interactions: Map<Button, Map<string, boolean>> = new Map();
private configs: Map<string, InterfaceConfig> = new Map();
public gamepadSupport = true;
public selectedDevice;
private disconnectedGamepads: Array<string> = new Array();
private disconnectedGamepads: Array<string> = [];
public lastSource = "keyboard";
private inputInterval: NodeJS.Timeout[] = new Array();
private inputInterval: NodeJS.Timeout[] = [];
private touchControls: TouchControl;
public moveTouchControlsHandler: MoveTouchControlsHandler;

View File

@ -1585,7 +1585,9 @@ class EvolutionItemModifierTypeGenerator extends ModifierTypeGenerator {
pokemonEvolutions.hasOwnProperty(p.species.speciesId) &&
(!p.pauseEvolutions ||
p.species.speciesId === SpeciesId.SLOWPOKE ||
p.species.speciesId === SpeciesId.EEVEE),
p.species.speciesId === SpeciesId.EEVEE ||
p.species.speciesId === SpeciesId.KIRLIA ||
p.species.speciesId === SpeciesId.SNORUNT),
)
.flatMap(p => {
const evolutions = pokemonEvolutions[p.species.speciesId];
@ -1599,7 +1601,9 @@ class EvolutionItemModifierTypeGenerator extends ModifierTypeGenerator {
pokemonEvolutions.hasOwnProperty(p.fusionSpecies.speciesId) &&
(!p.pauseEvolutions ||
p.fusionSpecies.speciesId === SpeciesId.SLOWPOKE ||
p.fusionSpecies.speciesId === SpeciesId.EEVEE),
p.fusionSpecies.speciesId === SpeciesId.EEVEE ||
p.fusionSpecies.speciesId === SpeciesId.KIRLIA ||
p.fusionSpecies.speciesId === SpeciesId.SNORUNT),
)
.flatMap(p => {
const evolutions = pokemonEvolutions[p.fusionSpecies!.speciesId];

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

@ -100,7 +100,7 @@ import { UnlockPhase } from "#app/phases/unlock-phase";
import { VictoryPhase } from "#app/phases/victory-phase";
import { WeatherEffectPhase } from "#app/phases/weather-effect-phase";
/**
/*
* Manager for phases used by battle scene.
*
* *This file must not be imported or used directly. The manager is exclusively used by the battle scene and is not intended for external use.*

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);
@ -38,14 +38,11 @@ export class AttemptRunPhase extends PokemonPhase {
alpha: 0,
duration: 250,
ease: "Sine.easeIn",
onComplete: () =>
// biome-ignore lint/complexity/noForEach: TODO
enemyField.forEach(enemyPokemon => enemyPokemon.destroy()),
onComplete: () => enemyField.forEach(enemyPokemon => enemyPokemon.destroy()),
});
globalScene.clearEnemyHeldItemModifiers();
// biome-ignore lint/complexity/noForEach: TODO
enemyField.forEach(enemyPokemon => {
enemyPokemon.hideInfo().then(() => enemyPokemon.destroy());
enemyPokemon.hp = 0;

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

@ -299,7 +299,7 @@ export class GameOverPhase extends BattlePhase {
battleType: globalScene.currentBattle.battleType,
trainer: globalScene.currentBattle.trainer ? new TrainerData(globalScene.currentBattle.trainer) : null,
gameVersion: globalScene.game.config.gameVersion,
timestamp: new Date().getTime(),
timestamp: Date.now(),
challenges: globalScene.gameMode.challenges.map(c => new ChallengeData(c)),
mysteryEncounterType: globalScene.currentBattle.mysteryEncounter?.encounterType ?? -1,
mysteryEncounterSaveData: globalScene.mysteryEncounterSaveData,

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);
}
@ -696,12 +690,9 @@ export class MoveEffectPhase extends PokemonPhase {
* @param target - The {@linkcode Pokemon} to be removed
*/
protected removeTarget(target: Pokemon): void {
const targetIndex = this.targets.findIndex(ind => ind === target.getBattlerIndex());
const targetIndex = this.targets.indexOf(target.getBattlerIndex());
if (targetIndex !== -1) {
this.targets.splice(
this.targets.findIndex(ind => ind === target.getBattlerIndex()),
1,
);
this.targets.splice(this.targets.indexOf(target.getBattlerIndex()), 1);
}
}
@ -808,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 });
}
}
}
@ -1002,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);
@ -396,7 +393,8 @@ export class MovePhase extends BattlePhase {
*/
if (success) {
const move = this.move.getMove();
applyPreAttackAbAttrs("PokemonTypeChangeAbAttr", this.pokemon, null, move);
// TODO: Investigate whether PokemonTypeChangeAbAttr can drop the "opponent" parameter
applyAbAttrs("PokemonTypeChangeAbAttr", { pokemon: this.pokemon, move, opponent: targets[0] });
globalScene.phaseManager.unshiftNew(
"MoveEffectPhase",
this.pokemon.getBattlerIndex(),
@ -406,7 +404,11 @@ export class MovePhase extends BattlePhase {
);
} else {
if ([MoveId.ROAR, MoveId.WHIRLWIND, MoveId.TRICK_OR_TREAT, MoveId.FORESTS_CURSE].includes(this.move.moveId)) {
applyPreAttackAbAttrs("PokemonTypeChangeAbAttr", this.pokemon, null, this.move.getMove());
applyAbAttrs("PokemonTypeChangeAbAttr", {
pokemon: this.pokemon,
move: this.move.getMove(),
opponent: targets[0],
});
}
this.pokemon.pushMoveHistory({
@ -438,7 +440,7 @@ export class MovePhase extends BattlePhase {
if (this.move.getMove().hasFlag(MoveFlags.DANCE_MOVE) && !dancerModes.includes(this.useMode)) {
// TODO: Fix in dancer PR to move to MEP for hit checks
globalScene.getField(true).forEach(pokemon => {
applyPostMoveUsedAbAttrs("PostMoveUsedAbAttr", pokemon, this.move, this.pokemon, this.targets);
applyAbAttrs("PostMoveUsedAbAttr", { pokemon, move: this.move, source: this.pokemon, targets: this.targets });
});
}
}
@ -470,7 +472,11 @@ export class MovePhase extends BattlePhase {
}
// Protean and Libero apply on the charging turn of charge moves
applyPreAttackAbAttrs("PokemonTypeChangeAbAttr", this.pokemon, null, this.move.getMove());
applyAbAttrs("PokemonTypeChangeAbAttr", {
pokemon: this.pokemon,
move: this.move.getMove(),
opponent: targets[0],
});
globalScene.phaseManager.unshiftNew(
"MoveChargePhase",
@ -523,7 +529,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 */
@ -668,6 +679,9 @@ export class MovePhase extends BattlePhase {
}),
500,
);
// Moves with pre-use messages (Magnitude, Chilly Reception, Fickle Beam, etc.) always display their messages even on failure
// TODO: This assumes single target for message funcs - is this sustainable?
applyMoveAttrs("PreMoveMessageAttr", this.pokemon, this.pokemon.getOpponents(false)[0], this.move.getMove());
}

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";
@ -44,7 +44,7 @@ export class SwitchSummonPhase extends SummonPhase {
preSummon(): void {
if (!this.player) {
if (this.slotIndex === -1) {
//@ts-ignore
//@ts-expect-error
this.slotIndex = globalScene.currentBattle.trainer?.getNextSummonIndex(
!this.fieldIndex ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER,
); // TODO: what would be the default trainer-slot fallback?
@ -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

@ -33,7 +33,7 @@ export abstract class ApiBase {
* @param dataType The data-type of the {@linkcode bodyData}.
*/
protected async doPost<D = undefined>(path: string, bodyData?: D, dataType: DataType = "json") {
let body: string | undefined = undefined;
let body: string | undefined;
const headers: HeadersInit = {};
if (bodyData) {

View File

@ -9,7 +9,7 @@ import type BattleScene from "#app/battle-scene";
// Regex patterns
/** Regex matching double underscores */
const DUNDER_REGEX = /\_{2}/g;
const DUNDER_REGEX = /_{2}/g;
/**
* Calculate the sprite ID from a pokemon form.

View File

@ -1,6 +1,6 @@
import { expSpriteKeys } from "#app/sprites/sprite-keys";
const expKeyRegex = /^pkmn__?(back__)?(shiny__)?(female__)?(\d+)(\-.*?)?(?:_[1-3])?$/;
const expKeyRegex = /^pkmn__?(back__)?(shiny__)?(female__)?(\d+)(-.*?)?(?:_[1-3])?$/;
export function hasExpSprite(key: string): boolean {
const keyMatch = expKeyRegex.exec(key);

View File

@ -39,7 +39,7 @@ import { setSettingGamepad, SettingGamepad, settingGamepadDefaults } from "#app/
import type { SettingKeyboard } from "#app/system/settings/settings-keyboard";
import { setSettingKeyboard } from "#app/system/settings/settings-keyboard";
import { TagAddedEvent, TerrainChangedEvent, WeatherChangedEvent } from "#app/events/arena";
// biome-ignore lint/style/noNamespaceImport: Something weird is going on here and I don't want to touch it
// biome-ignore lint/performance/noNamespaceImport: Something weird is going on here and I don't want to touch it
import * as Modifier from "#app/modifier/modifier";
import { StatusEffect } from "#enums/status-effect";
import ChallengeData from "#app/system/challenge-data";
@ -300,7 +300,7 @@ export class GameData {
voucherCounts: this.voucherCounts,
eggs: this.eggs.map(e => new EggData(e)),
gameVersion: globalScene.game.config.gameVersion,
timestamp: new Date().getTime(),
timestamp: Date.now(),
eggPity: this.eggPity.slice(0),
unlockPity: this.unlockPity.slice(0),
};
@ -930,7 +930,7 @@ export class GameData {
? new TrainerData(globalScene.currentBattle.trainer)
: null,
gameVersion: globalScene.game.config.gameVersion,
timestamp: new Date().getTime(),
timestamp: Date.now(),
challenges: globalScene.gameMode.challenges.map(c => new ChallengeData(c)),
mysteryEncounterType: globalScene.currentBattle.mysteryEncounter?.encounterType ?? -1,
mysteryEncounterSaveData: globalScene.mysteryEncounterSaveData,
@ -939,7 +939,7 @@ export class GameData {
}
getSession(slotId: number): Promise<SessionSaveData | null> {
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: <explanation>
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO: fix this
return new Promise(async (resolve, reject) => {
if (slotId < 0) {
return resolve(null);
@ -980,7 +980,7 @@ export class GameData {
}
loadSession(slotId: number, sessionData?: SessionSaveData): Promise<boolean> {
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: <explanation>
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO: fix this
return new Promise(async (resolve, reject) => {
try {
const initSessionFromData = async (sessionData: SessionSaveData) => {
@ -1610,7 +1610,7 @@ export class GameData {
}
}
this.defaultDexData = Object.assign({}, data);
this.defaultDexData = { ...data };
this.dexData = data;
}

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

@ -1,3 +1,5 @@
/** biome-ignore-all lint/performance/noNamespaceImport: Convenience */
import type { SessionSaveMigrator } from "#app/@types/SessionSaveMigrator";
import type { SettingsSaveMigrator } from "#app/@types/SettingsSaveMigrator";
import type { SystemSaveMigrator } from "#app/@types/SystemSaveMigrator";
@ -48,23 +50,18 @@ export const settingsMigrators: Readonly<SettingsSaveMigrator[]> = [settingsMigr
// import * as vA_B_C from "./versions/vA_B_C";
// --- v1.0.4 (and below) PATCHES --- //
// biome-ignore lint/style/noNamespaceImport: Convenience (TODO: make this a file-wide ignore when Biome supports those)
import * as v1_0_4 from "./versions/v1_0_4";
// --- v1.7.0 PATCHES --- //
// biome-ignore lint/style/noNamespaceImport: Convenience
import * as v1_7_0 from "./versions/v1_7_0";
// --- v1.8.3 PATCHES --- //
// biome-ignore lint/style/noNamespaceImport: Convenience
import * as v1_8_3 from "./versions/v1_8_3";
// --- v1.9.0 PATCHES --- //
// biome-ignore lint/style/noNamespaceImport: Convenience
import * as v1_9_0 from "./versions/v1_9_0";
// --- v1.10.0 PATCHES --- //
// biome-ignore lint/style/noNamespaceImport: Convenience
import * as v1_10_0 from "./versions/v1_10_0";
/** Current game version */

View File

@ -6,8 +6,8 @@ const repeatInputDelayMillis = 250;
export default class TouchControl {
events: EventEmitter;
private buttonLock: string[] = new Array();
private inputInterval: NodeJS.Timeout[] = new Array();
private buttonLock: string[] = [];
private inputInterval: NodeJS.Timeout[] = [];
/** Whether touch controls are disabled */
private disabled = false;
/** Whether the last touch event has finished before disabling */
@ -42,7 +42,7 @@ export default class TouchControl {
document.querySelectorAll(".apad-button").forEach(element => this.preventElementZoom(element as HTMLElement));
// Select all elements with the 'data-key' attribute and bind keys to them
for (const button of document.querySelectorAll("[data-key]")) {
// @ts-ignore - Bind the key to the button using the dataset key
// @ts-expect-error - Bind the key to the button using the dataset key
this.bindKey(button, button.dataset.key);
}
}
@ -208,7 +208,7 @@ export function isMobile(): boolean {
/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(
a,
) ||
/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(
/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene|gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk\/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i.test(
a.substr(0, 4),
)
) {

View File

@ -176,12 +176,12 @@ export class UiInputs {
return;
}
switch (globalScene.ui?.getMode()) {
// biome-ignore lint/suspicious/noFallthroughSwitchClause: falls through to show menu overlay
case UiMode.MESSAGE: {
const messageHandler = globalScene.ui.getHandler<MessageUiHandler>();
if (!messageHandler.pendingPrompt || messageHandler.isTextAnimationInProgress()) {
return;
}
// biome-ignore lint/suspicious/noFallthroughSwitchClause: falls through to show menu overlay
}
case UiMode.TITLE:
case UiMode.COMMAND:

View File

@ -52,7 +52,7 @@ export default class BattleFlyout extends Phaser.GameObjects.Container {
/** The array of {@linkcode Phaser.GameObjects.Text} objects which are drawn on the flyout */
private flyoutText: Phaser.GameObjects.Text[] = new Array(4);
/** The array of {@linkcode MoveInfo} used to track moves for the {@linkcode Pokemon} linked to the flyout */
private moveInfo: MoveInfo[] = new Array();
private moveInfo: MoveInfo[] = [];
/** Current state of the flyout's visibility */
public flyoutVisible = false;

View File

@ -41,24 +41,15 @@ export class DailyRunScoreboard extends Phaser.GameObjects.Container {
this.setup();
}
/**
* Sets the updating state and updates button states accordingly.
* If value is true (updating), disables the buttons; if false, enables the buttons.
* @param {boolean} value - The new updating state.
*/
set isUpdating(value) {
/** When set to `true`, disables the buttons; when set to `false`, enables the buttons. */
get isUpdating(): boolean {
return this._isUpdating;
}
set isUpdating(value: boolean) {
this._isUpdating = value;
this.setButtonsState(!value);
}
/**
* Gets the current updating state.
* @returns {boolean} - The current updating state.
*/
get isUpdating() {
return this._isUpdating;
}
setup() {
const titleWindow = addWindow(0, 0, 114, 18, false, false, undefined, undefined, WindowVariant.THIN);
this.add(titleWindow);

View File

@ -625,7 +625,7 @@ export default class EggGachaUiHandler extends MessageUiHandler {
const infoContainer = this.gachaInfoContainers[gachaType];
switch (gachaType as GachaType) {
case GachaType.LEGENDARY: {
const species = getPokemonSpecies(getLegendaryGachaSpeciesForTimestamp(new Date().getTime()));
const species = getPokemonSpecies(getLegendaryGachaSpeciesForTimestamp(Date.now()));
const pokemonIcon = infoContainer.getAt(1) as Phaser.GameObjects.Sprite;
pokemonIcon.setTexture(species.getIconAtlasKey(), species.getIconId(false));
break;

View File

@ -591,9 +591,9 @@ export default class MysteryEncounterUiHandler extends UiHandler {
// Auto-color options green/blue for good/bad by looking for (+)/(-)
if (text) {
const primaryStyleString = [...text.match(new RegExp(/\[color=[^\[]*\]\[shadow=[^\[]*\]/i))!][0];
const primaryStyleString = [...text.match(new RegExp(/\[color=[^[]*\]\[shadow=[^[]*\]/i))!][0];
text = text.replace(
/(\(\+\)[^\(\[]*)/gi,
/(\(\+\)[^([]*)/gi,
substring =>
"[/color][/shadow]" +
getBBCodeFrag(substring, TextStyle.SUMMARY_GREEN) +
@ -601,7 +601,7 @@ export default class MysteryEncounterUiHandler extends UiHandler {
primaryStyleString,
);
text = text.replace(
/(\(\-\)[^\(\[]*)/gi,
/(\(-\)[^([]*)/gi,
substring =>
"[/color][/shadow]" +
getBBCodeFrag(substring, TextStyle.SUMMARY_BLUE) +

View File

@ -2057,7 +2057,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
}
let newSpecies: PokemonSpecies;
if (this.filteredIndices) {
const index = this.filteredIndices.findIndex(id => id === this.species.speciesId);
const index = this.filteredIndices.indexOf(this.species.speciesId);
const newIndex = index <= 0 ? this.filteredIndices.length - 1 : index - 1;
newSpecies = getPokemonSpecies(this.filteredIndices[newIndex]);
} else {
@ -2096,7 +2096,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
}
let newSpecies: PokemonSpecies;
if (this.filteredIndices) {
const index = this.filteredIndices.findIndex(id => id === this.species.speciesId);
const index = this.filteredIndices.indexOf(this.species.speciesId);
const newIndex = index >= this.filteredIndices.length - 1 ? 0 : index + 1;
newSpecies = getPokemonSpecies(this.filteredIndices[newIndex]);
} else {
@ -2321,7 +2321,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
this.showStats();
} else {
this.statsContainer.setVisible(false);
//@ts-ignore
//@ts-expect-error
this.statsContainer.updateIvs(null); // TODO: resolve ts-ignore. what. how? huh?
}
}
@ -2786,7 +2786,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
this.statsMode = false;
this.statsContainer.setVisible(false);
this.pokemonSprite.setVisible(true);
//@ts-ignore
//@ts-expect-error
this.statsContainer.updateIvs(null); // TODO: resolve ts-ignore. !?!?
}
}

View File

@ -1389,7 +1389,7 @@ export default class PokedexUiHandler extends MessageUiHandler {
const fitsMoves = fitsMove1 && fitsMove2;
if (fitsEggMove1 && !fitsLevelMove1) {
const em1 = eggMoves.findIndex(name => name === selectedMove1);
const em1 = eggMoves.indexOf(selectedMove1);
if ((starterData.eggMoves & (1 << em1)) === 0) {
data.eggMove1 = false;
} else {
@ -1399,7 +1399,7 @@ export default class PokedexUiHandler extends MessageUiHandler {
data.tmMove1 = true;
}
if (fitsEggMove2 && !fitsLevelMove2) {
const em2 = eggMoves.findIndex(name => name === selectedMove2);
const em2 = eggMoves.indexOf(selectedMove2);
if ((starterData.eggMoves & (1 << em2)) === 0) {
data.eggMove2 = false;
} else {

View File

@ -19,7 +19,7 @@ import { PokemonType } from "#enums/pokemon-type";
import { TypeColor, TypeShadow } from "#app/enums/color";
import { getNatureStatMultiplier, getNatureName } from "../data/nature";
import { getVariantTint } from "#app/sprites/variant";
// biome-ignore lint/style/noNamespaceImport: See `src/system/game-data.ts`
// biome-ignore lint/performance/noNamespaceImport: See `src/system/game-data.ts`
import * as Modifier from "#app/modifier/modifier";
import type { SpeciesId } from "#enums/species-id";
import { PlayerGender } from "#enums/player-gender";

View File

@ -2,7 +2,7 @@ import i18next from "i18next";
import { globalScene } from "#app/global-scene";
import { Button } from "#enums/buttons";
import { GameMode } from "../game-mode";
// biome-ignore lint/style/noNamespaceImport: See `src/system/game-data.ts`
// biome-ignore lint/performance/noNamespaceImport: See `src/system/game-data.ts`
import * as Modifier from "#app/modifier/modifier";
import type { SessionSaveData } from "../system/game-data";
import type PokemonData from "../system/pokemon-data";

View File

@ -209,7 +209,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
settingFiltered.forEach((setting, s) => {
// Convert the setting key from format 'Key_Name' to 'Key name' for display.
const settingName = setting.replace(/\_/g, " ");
const settingName = setting.replace(/_/g, " ");
// Create and add a text object for the setting name to the scene.
const isLock = this.settingBlacklisted.includes(this.setting[setting]);

View File

@ -16,7 +16,7 @@ export class NavigationManager {
private static instance: NavigationManager;
public modes: UiMode[];
public selectedMode: UiMode = UiMode.SETTINGS;
public navigationMenus: NavigationMenu[] = new Array<NavigationMenu>();
public navigationMenus: NavigationMenu[] = [];
public labels: string[];
/**
@ -105,7 +105,7 @@ export class NavigationManager {
export default class NavigationMenu extends Phaser.GameObjects.Container {
private navigationIcons: InputsIcons;
protected headerTitles: Phaser.GameObjects.Text[] = new Array<Phaser.GameObjects.Text>();
protected headerTitles: Phaser.GameObjects.Text[] = [];
/**
* Creates an instance of NavigationMenu.

View File

@ -2822,7 +2822,6 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
iconElement: GameObjects.Sprite,
controlLabel: GameObjects.Text,
): void {
// biome-ignore lint/suspicious/noImplicitAnyLet: TODO
let iconPath: string;
// touch controls cannot be rebound as is, and are just emulating a keyboard event.
// Additionally, since keyboard controls can be rebound (and will be displayed when they are), we need to have special handling for the touch controls
@ -2856,7 +2855,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
} else {
iconPath = globalScene.inputController?.getIconForLatestInputRecorded(iconSetting);
}
// @ts-ignore: TODO can iconPath actually be undefined?
// @ts-expect-error: TODO can iconPath actually be undefined?
iconElement.setTexture(gamepadType, iconPath);
iconElement.setPosition(this.instructionRowX, this.instructionRowY);
controlLabel.setPosition(this.instructionRowX + this.instructionRowTextOffset, this.instructionRowY);
@ -3481,7 +3480,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.showStats();
} else {
this.statsContainer.setVisible(false);
//@ts-ignore
//@ts-expect-error
this.statsContainer.updateIvs(null); // TODO: resolve ts-ignore. what. how? huh?
}
}
@ -4489,7 +4488,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.statsMode = false;
this.statsContainer.setVisible(false);
this.pokemonSprite.setVisible(!!this.speciesStarterDexEntry?.caughtAttr);
//@ts-ignore
//@ts-expect-error
this.statsContainer.updateIvs(null); // TODO: resolve ts-ignore. !?!?
this.teraIcon.setVisible(globalScene.gameData.achvUnlocks.hasOwnProperty(achvs.TERASTALLIZE.id));
const props = globalScene.gameData.getSpeciesDexAttrProps(

View File

@ -117,7 +117,7 @@ export default class SummaryUiHandler extends UiHandler {
private pokemon: PlayerPokemon | null;
private playerParty: boolean;
/**This is set to false when checking the summary of a freshly caught Pokemon as it is not part of a player's party yet but still needs to display its items**/
/**This is set to false when checking the summary of a freshly caught Pokemon as it is not part of a player's party yet but still needs to display its items*/
private newMove: Move | null;
private moveSelectFunction: Function | null;
private transitioning: boolean;

View File

@ -300,7 +300,7 @@ export function getTextWithColors(
): string {
// Apply primary styling before anything else
let text = getBBCodeFrag(content, primaryStyle, uiTheme) + "[/color][/shadow]";
const primaryStyleString = [...text.match(new RegExp(/\[color=[^\[]*\]\[shadow=[^\[]*\]/i))!][0];
const primaryStyleString = [...text.match(new RegExp(/\[color=[^[]*\]\[shadow=[^[]*\]/i))!][0];
/* For money text displayed in game windows, we can't use the default {@linkcode TextStyle.MONEY}
* or it will look wrong in legacy mode because of the different window background color
@ -320,7 +320,7 @@ export function getTextWithColors(
});
// Remove extra style block at the end
return text.replace(/\[color=[^\[]*\]\[shadow=[^\[]*\]\[\/color\]\[\/shadow\]/gi, "");
return text.replace(/\[color=[^[]*\]\[shadow=[^[]*\]\[\/color\]\[\/shadow\]/gi, "");
}
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: This is a giant switch which is the best option.

View File

@ -10,7 +10,7 @@ export const MissingTextureKey = "__MISSING";
export function toReadableString(str: string): string {
return str
.replace(/\_/g, " ")
.replace(/_/g, " ")
.split(" ")
.map(s => `${s.slice(0, 1)}${s.slice(1).toLowerCase()}`)
.join(" ");
@ -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) {
@ -583,7 +599,7 @@ export function isBetween(num: number, min: number, max: number): boolean {
* @param move the move for which the animation filename is needed
*/
export function animationFileName(move: MoveId): string {
return MoveId[move].toLowerCase().replace(/\_/g, "-");
return MoveId[move].toLowerCase().replace(/_/g, "-");
}
/**

View File

@ -2,7 +2,7 @@ import { isBeta } from "./utility-vars";
export function setCookie(cName: string, cValue: string): void {
const expiration = new Date();
expiration.setTime(new Date().getTime() + 3600000 * 24 * 30 * 3 /*7*/);
expiration.setTime(Date.now() + 3600000 * 24 * 30 * 3 /*7*/);
document.cookie = `${cName}=${cValue};Secure;SameSite=Strict;Domain=${window.location.hostname};Path=/;Expires=${expiration.toUTCString()}`;
}

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

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