diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000000..e94afd0a9be --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,61 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node +{ + "name": "Node.js & TypeScript", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm", + "features": { + "ghcr.io/devcontainers/features/github-cli:1": { + "installDirectlyFromGitHubRelease": true, + "version": "latest" + }, + "ghcr.io/devcontainers-extra/features/pnpm:2": { + "version": "latest" + } + }, + "customizations": { + "vscode": { + "settings": { + // # Formatter configs + "editor.defaultFormatter": "biomejs.biome", + "editor.tabSize": 2, + "editor.insertSpaces": true, + "editor.codeActionsOnSave": { + "source.addMissingImports.ts": "always", + "source.removeUnusedImports": "always", + "source.fixAll.biome": "always", + "source.organizeImports.biome": "always" + }, + "biome.suggestInstallingGlobally": false, + + // # JS/TS setting overrides + "javascript.preferences.importModuleSpecifier": "non-relative", + "javascript.preferences.importModuleSpecifierEnding": "index", + "javascript.preferGoToSourceDefinition": true, + "javascript.updateImportsOnFileMove.enabled": "always", + + "typescript.preferences.importModuleSpecifier": "non-relative", + "typescript.preferences.importModuleSpecifierEnding": "index", + "typescript.preferGoToSourceDefinition": true, + "typescript.updateImportsOnFileMove.enabled": "always", + + "typescript.tsserver.experimental.enableProjectDiagnostics": true, + + // # Miscellaneous + "npm.packageManager": "pnpm", + "npm.scriptRunner": "pnpm", + "vitest.cliArguments": "--no-isolate" + }, + "extensions": [ + "biomejs.biome", + "YoavBls.pretty-ts-errors", + "vitest.explorer", + "adpyke.codesnap", // Bind to a hotkey (ctrl+\, etc) for best results + "aaron-bond.better-comments", + "MuTsunTsai.jsdoc-link" + ] + } + }, + "postCreateCommand": "pnpm install", + "forwardPorts": [8000] +} diff --git a/.gitignore b/.gitignore index 00df0002e01..299767e742a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,9 +12,10 @@ dist dist-ssr *.local -# Editor directories and files -.vscode +# Editor directories and files (excluding `extensions.json` for devcontainer) *.code-workspace +.vscode/* +!.vscode/extensions.json .idea .DS_Store *.suo diff --git a/.ls-lint.yml b/.ls-lint.yml index 22f08f72938..0086367367b 100644 --- a/.ls-lint.yml +++ b/.ls-lint.yml @@ -26,3 +26,4 @@ ignore: - .git - public - dist + - .devcontainer diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000000..81abc8df2c0 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,13 @@ +{ + "recommendations": [ + "biomejs.biome", + "YoavBls.pretty-ts-errors", + "vitest.explorer", + + // This stuff isn't mandatory - it's just nice to have :) + + "adpyke.codesnap", // Bind to a hotkey (ctrl+\, etc) for best results + "aaron-bond.better-comments", + "MuTsunTsai.jsdoc-link" + ] +} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2d56b868cff..04ab7ff4faa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ Thank you for taking the time to contribute, every little bit helps. This project is entirely open-source and unmonetized - community contributions are what keep it alive! -Please make sure you understand everything relevant to your changes from the [Table of Contents](#-table-of-contents), and absolutely *feel free to reach out in the **#dev-corner** channel on [Discord](https://discord.gg/pokerogue)*. +Please make sure you understand everything relevant to your changes from the [Table of Contents](#-table-of-contents), and absolutely *feel free to reach out in the **#dev-corner** channel on [Discord](https://discord.gg/pokerogue)*. We are here to help and the better you understand what you're working on, the easier it will be for it to find its way into the game. ## 📄 Table of Contents @@ -16,19 +16,36 @@ We are here to help and the better you understand what you're working on, the ea ## 🛠️ 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. +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. +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 +### Codespaces/Devcontainer Environment -- 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) +Arguably the easiest way to get started is by using the prepared development environment. + +We have a `.devcontainer/devcontainer.json` file, meaning we are compatible with: + +- [![Open in GitHub Codespaces][codespaces-badge]][codespaces-link], or +- the [Visual Studio Code Remote - Containers][devcontainer-ext] extension. + +This Linux environment comes with all required dependencies needed to start working on the project. + +[codespaces-badge]: +[codespaces-link]: +[devcontainer-ext]: + +### Local Development + +#### 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) | [manage with volta.sh](https://volta.sh/) - 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/) - The repository [forked](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) and [cloned](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository) locally on your device -### Running Locally +#### Running Locally 1. Run `pnpm install` from the repository root - *if you run into any errors, reach out in the **#dev-corner** channel on Discord* @@ -36,7 +53,7 @@ If you have the motivation and experience with Typescript/Javascript (or are wil ## 🚀 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/)). +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***! @@ -50,7 +67,7 @@ Most issues are bugs and are labeled with their area, such as `Move`, `Ability`, - `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). +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, so take your pick. 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. @@ -58,7 +75,7 @@ You are free to comment on any issue so that you may be assigned to it and we ca You can find the auto-generated documentation [here](https://pagefaultgames.github.io/pokerogue/main/index.html). -Additionally, the [docs folder](./docs) contains a variety of in-depth documents and guides useful for aspiring contributors. +Additionally, the [docs folder](./docs) contains a variety of in-depth documents and guides useful for aspiring contributors. \ Notable topics include: - [Commenting your code](./docs/comments.md) - [Linting & Formatting](./docs/linting.md) @@ -81,22 +98,22 @@ For example, here is how you could test a scenario where the player Pokemon has ```typescript const overrides = { ABILITY_OVERRIDE: AbilityId.DROUGHT, - OPP_MOVESET_OVERRIDE: MoveId.WATER_GUN, + ENEMY_MOVESET_OVERRIDE: MoveId.WATER_GUN, } satisfies Partial>; ``` 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**. +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`. +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. +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. @@ -107,4 +124,4 @@ Most non-trivial changes (*especially bug fixes*) should come along with new tes > 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/test-utils/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/test-utils/saves/everything.prsv) (`test/test-utils/saves/everything.prsv`) and confirm. +2. Select [everything.prsv](test/test-utils/saves/everything.prsv) (`test/test-utils/saves/everything.prsv`) and confirm. \ No newline at end of file diff --git a/CREDITS.md b/CREDITS.md index bca89c816ca..6bd73d72901 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -24,9 +24,10 @@ - Pokémon Sword/Shield - Pokémon Legends: Arceus - Pokémon Scarlet/Violet -- Firel (Custom Graveyard, Ice Cave, Laboratory, Metropolis, Plains, Power Plant, Seabed, Space, and Volcano biome music) +- Firel (Custom Graveyard, Ice Cave, Laboratory, Metropolis, Plains, Power Plant, Seabed, Space, Volcano, and Desert biome music) - Lmz (Custom Ancient Ruins, Jungle, and Lake biome music) -- Andr06 (Custom Forest, Slum and Sea biome music) +- Andr06 (Custom Forest, Slum, Sea, and Fairy Cave biome music) +- Leavannite (Custom Wasteland biome music) - _tresnoir - unveiler diff --git a/biome.jsonc b/biome.jsonc index d2f7c711dc9..a63ce0ee07d 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -36,7 +36,6 @@ "!**/src/data/balance/tms.ts" ] }, - "assist": { "actions": { "source": { diff --git a/docs/localization.md b/docs/localization.md index 0fe950a361d..c325aaf55a9 100644 --- a/docs/localization.md +++ b/docs/localization.md @@ -90,9 +90,13 @@ If this feature requires new text, the text should be integrated into the code w - For any feature pulled from the mainline Pokémon games (e.g. a Move or Ability implementation), it's best practice to include a source link for any added text. [Poké Corpus](https://abcboy101.github.io/poke-corpus/) is a great resource for finding text from the mainline games; otherwise, a video/picture showing the text being displayed should suffice. - You should also [notify the current Head of Translation](#notifying-translation) to ensure a fast response. -3. At this point, you may begin [testing locales integration in your main PR](#documenting-locales-changes). -4. The Translation Team will approve the locale PR (after corrections, if necessary), then merge it into `pokerogue-locales`. -5. The Dev Team will approve your main PR for your feature, then merge it into PokéRogue's beta environment. +3. Your locales should use the following format: + - File names should be in `kebab-case`. Example: `trainer-names.json` + - Key names should be in `camelCase`. Example: `aceTrainer` + - If you make use of i18next's inbuilt [context support](https://www.i18next.com/translation-function/context), you need to use `snake_case` for the context key. Example: `aceTrainer_male` +4. At this point, you may begin [testing locales integration in your main PR](#documenting-locales-changes). +5. The Translation Team will approve the locale PR (after corrections, if necessary), then merge it into `pokerogue-locales`. +6. The Dev Team will approve your main PR for your feature, then merge it into PokéRogue's beta environment. [^2]: For those wondering, the reason for choosing English specifically is due to it being the master language set in Pontoon (the program used by the Translation Team to perform locale updates). If a key is present in any language _except_ the master language, it won't appear anywhere else in the translation tool, rendering missing English keys quite a hassle. diff --git a/package.json b/package.json index d3494da677c..d3ea890c005 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,6 @@ "test:silent": "vitest run --silent='passed-only' --no-isolate", "test:create": "node scripts/create-test/create-test.js", "typecheck": "tsc --noEmit", - "eslint": "eslint --fix .", - "eslint-ci": "eslint .", "biome": "biome check --write --changed --no-errors-on-unmatched --diagnostic-level=error", "biome-ci": "biome ci --diagnostic-level=error --reporter=github --no-errors-on-unmatched", "docs": "typedoc", @@ -29,6 +27,7 @@ "devDependencies": { "@biomejs/biome": "2.0.0", "@ls-lint/ls-lint": "2.3.1", + "@types/crypto-js": "^4.2.0", "@types/jsdom": "^21.1.7", "@types/node": "^22.16.5", "@vitest/coverage-istanbul": "^3.2.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 900be6fd76e..c3b58a60f48 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,6 +48,9 @@ importers: '@ls-lint/ls-lint': specifier: 2.3.1 version: 2.3.1 + '@types/crypto-js': + specifier: ^4.2.0 + version: 4.2.2 '@types/jsdom': specifier: ^21.1.7 version: 21.1.7 @@ -718,6 +721,9 @@ packages: '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/crypto-js@4.2.2': + resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -2525,6 +2531,8 @@ snapshots: '@types/cookie@0.6.0': {} + '@types/crypto-js@4.2.2': {} + '@types/deep-eql@4.0.2': {} '@types/estree@1.0.8': {} diff --git a/public/audio/bgm/desert.mp3 b/public/audio/bgm/desert.mp3 index febbacc0100..10938f814fe 100644 Binary files a/public/audio/bgm/desert.mp3 and b/public/audio/bgm/desert.mp3 differ diff --git a/public/audio/bgm/fairy_cave.mp3 b/public/audio/bgm/fairy_cave.mp3 index 4e1c9ea0eb4..32cc3dbaa41 100644 Binary files a/public/audio/bgm/fairy_cave.mp3 and b/public/audio/bgm/fairy_cave.mp3 differ diff --git a/public/audio/bgm/wasteland.mp3 b/public/audio/bgm/wasteland.mp3 index 646cbd89a1f..ee096af99ac 100644 Binary files a/public/audio/bgm/wasteland.mp3 and b/public/audio/bgm/wasteland.mp3 differ diff --git a/public/images/items/ampharosite.png b/public/images/items/ampharosite.png index 154f7ea5609..3297b4a90b8 100644 Binary files a/public/images/items/ampharosite.png and b/public/images/items/ampharosite.png differ diff --git a/public/images/items/amulet_coin.png b/public/images/items/amulet_coin.png index 727562836c4..8c86df496e4 100644 Binary files a/public/images/items/amulet_coin.png and b/public/images/items/amulet_coin.png differ diff --git a/public/images/items/blunder_policy.png b/public/images/items/blunder_policy.png index 8104d00c40c..ba7b12ebaab 100644 Binary files a/public/images/items/blunder_policy.png and b/public/images/items/blunder_policy.png differ diff --git a/public/images/items/bug_memory.png b/public/images/items/bug_memory.png index a71637ceb5a..fce22e69081 100644 Binary files a/public/images/items/bug_memory.png and b/public/images/items/bug_memory.png differ diff --git a/public/images/items/catching_charm.png b/public/images/items/catching_charm.png index 4699b8c3d85..8fd89791206 100644 Binary files a/public/images/items/catching_charm.png and b/public/images/items/catching_charm.png differ diff --git a/public/images/items/diancite.png b/public/images/items/diancite.png index fbb0267fb59..3c871b3158b 100644 Binary files a/public/images/items/diancite.png and b/public/images/items/diancite.png differ diff --git a/public/images/items/galarica_cuff.png b/public/images/items/galarica_cuff.png index 4706279c43d..223be91acee 100644 Binary files a/public/images/items/galarica_cuff.png and b/public/images/items/galarica_cuff.png differ diff --git a/public/images/items/gardevoirite.png b/public/images/items/gardevoirite.png index 9180b369df4..5f5d75f7ca5 100644 Binary files a/public/images/items/gardevoirite.png and b/public/images/items/gardevoirite.png differ diff --git a/public/images/items/gb.png b/public/images/items/gb.png index bcc88593fad..3a47a5cd3ce 100644 Binary files a/public/images/items/gb.png and b/public/images/items/gb.png differ diff --git a/public/images/items/hp_up.png b/public/images/items/hp_up.png index b9284383978..ddbca745436 100644 Binary files a/public/images/items/hp_up.png and b/public/images/items/hp_up.png differ diff --git a/public/images/items/leftovers.png b/public/images/items/leftovers.png index c06eaf073d9..43200bfe559 100644 Binary files a/public/images/items/leftovers.png and b/public/images/items/leftovers.png differ diff --git a/public/images/items/magnet.png b/public/images/items/magnet.png index f8668e72abd..13a602d7140 100644 Binary files a/public/images/items/magnet.png and b/public/images/items/magnet.png differ diff --git a/public/images/items/max_mushrooms.png b/public/images/items/max_mushrooms.png index 10ff9f5a72f..e1b066b0267 100644 Binary files a/public/images/items/max_mushrooms.png and b/public/images/items/max_mushrooms.png differ diff --git a/public/images/items/mega_bracelet.png b/public/images/items/mega_bracelet.png index 674c59a8f5a..5dde0be94ac 100644 Binary files a/public/images/items/mega_bracelet.png and b/public/images/items/mega_bracelet.png differ diff --git a/public/images/items/mint_atk.png b/public/images/items/mint_atk.png index f42e0cccfc0..0af6364f09c 100644 Binary files a/public/images/items/mint_atk.png and b/public/images/items/mint_atk.png differ diff --git a/public/images/items/n_solarizer.png b/public/images/items/n_solarizer.png index 787d2b49bc4..6060fe4e5fb 100644 Binary files a/public/images/items/n_solarizer.png and b/public/images/items/n_solarizer.png differ diff --git a/public/images/items/poison_barb.png b/public/images/items/poison_barb.png index b85f7d3b777..0454c5b5e78 100644 Binary files a/public/images/items/poison_barb.png and b/public/images/items/poison_barb.png differ diff --git a/public/images/items/power_herb.png b/public/images/items/power_herb.png index 477e32e3271..9f877f62396 100644 Binary files a/public/images/items/power_herb.png and b/public/images/items/power_herb.png differ diff --git a/public/images/items/rayquazite.png b/public/images/items/rayquazite.png index 6617924c434..80f467522fd 100644 Binary files a/public/images/items/rayquazite.png and b/public/images/items/rayquazite.png differ diff --git a/public/images/items/ribbon_gen4.png b/public/images/items/ribbon_gen4.png index ad4cb139a7e..a7765d1de50 100644 Binary files a/public/images/items/ribbon_gen4.png and b/public/images/items/ribbon_gen4.png differ diff --git a/public/images/items/ribbon_gen5.png b/public/images/items/ribbon_gen5.png index a4f99ffb7f2..00b84aab978 100644 Binary files a/public/images/items/ribbon_gen5.png and b/public/images/items/ribbon_gen5.png differ diff --git a/public/images/items/sceptilite.png b/public/images/items/sceptilite.png index 4b255b1ff6d..4411c172826 100644 Binary files a/public/images/items/sceptilite.png and b/public/images/items/sceptilite.png differ diff --git a/public/images/items/scroll_of_waters.png b/public/images/items/scroll_of_waters.png index 9195a26abe1..adacf66398f 100644 Binary files a/public/images/items/scroll_of_waters.png and b/public/images/items/scroll_of_waters.png differ diff --git a/public/images/items/soothe_bell.png b/public/images/items/soothe_bell.png index d21da03f663..8ca868108d3 100644 Binary files a/public/images/items/soothe_bell.png and b/public/images/items/soothe_bell.png differ diff --git a/public/images/items/sweet_apple.png b/public/images/items/sweet_apple.png index de183d1c644..591961604f8 100644 Binary files a/public/images/items/sweet_apple.png and b/public/images/items/sweet_apple.png differ diff --git a/public/images/items/tm_ice.png b/public/images/items/tm_ice.png index eece8623654..26354211f6d 100644 Binary files a/public/images/items/tm_ice.png and b/public/images/items/tm_ice.png differ diff --git a/public/images/items/tm_normal.png b/public/images/items/tm_normal.png index cede4cb96e2..8bc5e6b6a24 100644 Binary files a/public/images/items/tm_normal.png and b/public/images/items/tm_normal.png differ diff --git a/public/images/items/toxic_orb.png b/public/images/items/toxic_orb.png index 8b8a1026f92..f18a55c43dc 100644 Binary files a/public/images/items/toxic_orb.png and b/public/images/items/toxic_orb.png differ diff --git a/public/images/items/unknown.png b/public/images/items/unknown.png index 17697d0d640..7a1dc347093 100644 Binary files a/public/images/items/unknown.png and b/public/images/items/unknown.png differ diff --git a/public/images/items/white_herb.png b/public/images/items/white_herb.png index 0729602fd15..9e6aa77eeff 100644 Binary files a/public/images/items/white_herb.png and b/public/images/items/white_herb.png differ diff --git a/public/images/items/wl_antidote.png b/public/images/items/wl_antidote.png index 18a74292dcf..3a60ed639a7 100644 Binary files a/public/images/items/wl_antidote.png and b/public/images/items/wl_antidote.png differ diff --git a/public/images/items/wl_awakening.png b/public/images/items/wl_awakening.png index 4fa59e2d10d..27bbd49253a 100644 Binary files a/public/images/items/wl_awakening.png and b/public/images/items/wl_awakening.png differ diff --git a/public/images/items/wl_burn_heal.png b/public/images/items/wl_burn_heal.png index 98fa6c11766..3bbf5298a1d 100644 Binary files a/public/images/items/wl_burn_heal.png and b/public/images/items/wl_burn_heal.png differ diff --git a/public/images/items/wl_custom_thief.png b/public/images/items/wl_custom_thief.png index aa6f9e0acc7..8d54f18e739 100644 Binary files a/public/images/items/wl_custom_thief.png and b/public/images/items/wl_custom_thief.png differ diff --git a/public/images/items/wl_elixir.png b/public/images/items/wl_elixir.png index e9b7b48ab87..8146091e46b 100644 Binary files a/public/images/items/wl_elixir.png and b/public/images/items/wl_elixir.png differ diff --git a/public/images/items/wl_ether.png b/public/images/items/wl_ether.png index 457f2bf0649..03be51f7532 100644 Binary files a/public/images/items/wl_ether.png and b/public/images/items/wl_ether.png differ diff --git a/public/images/items/wl_full_heal.png b/public/images/items/wl_full_heal.png index 361d432781a..2f0d58e201d 100644 Binary files a/public/images/items/wl_full_heal.png and b/public/images/items/wl_full_heal.png differ diff --git a/public/images/items/wl_hyper_potion.png b/public/images/items/wl_hyper_potion.png index 016c257a1eb..7d6028eb784 100644 Binary files a/public/images/items/wl_hyper_potion.png and b/public/images/items/wl_hyper_potion.png differ diff --git a/public/images/items/wl_ice_heal.png b/public/images/items/wl_ice_heal.png index 619d30d9ca9..3d94ca63a3b 100644 Binary files a/public/images/items/wl_ice_heal.png and b/public/images/items/wl_ice_heal.png differ diff --git a/public/images/items/wl_max_elixir.png b/public/images/items/wl_max_elixir.png index 4df3eaff10a..40ed8baa441 100644 Binary files a/public/images/items/wl_max_elixir.png and b/public/images/items/wl_max_elixir.png differ diff --git a/public/images/items/wl_max_ether.png b/public/images/items/wl_max_ether.png index 0e174f9eba4..eeaad41fa05 100644 Binary files a/public/images/items/wl_max_ether.png and b/public/images/items/wl_max_ether.png differ diff --git a/public/images/items/wl_max_potion.png b/public/images/items/wl_max_potion.png index 2f334795489..4c37c175e38 100644 Binary files a/public/images/items/wl_max_potion.png and b/public/images/items/wl_max_potion.png differ diff --git a/public/images/items/wl_paralyze_heal.png b/public/images/items/wl_paralyze_heal.png index ba9c172fa17..35d29b54452 100644 Binary files a/public/images/items/wl_paralyze_heal.png and b/public/images/items/wl_paralyze_heal.png differ diff --git a/public/images/items/wl_potion.png b/public/images/items/wl_potion.png index a70bba17b2f..cce66644ed9 100644 Binary files a/public/images/items/wl_potion.png and b/public/images/items/wl_potion.png differ diff --git a/public/images/items/wl_super_potion.png b/public/images/items/wl_super_potion.png index 5bf5dd8fc89..62177ddbb26 100644 Binary files a/public/images/items/wl_super_potion.png and b/public/images/items/wl_super_potion.png differ diff --git a/public/images/logo_fake.png b/public/images/logo_fake.png index 9fdb8724025..42d1aeca7a5 100755 Binary files a/public/images/logo_fake.png and b/public/images/logo_fake.png differ diff --git a/public/images/pokeball/gb_opening.png b/public/images/pokeball/gb_opening.png index 075aafeb460..4122b2ffbc8 100644 Binary files a/public/images/pokeball/gb_opening.png and b/public/images/pokeball/gb_opening.png differ diff --git a/public/images/pokeball/pb_open.png b/public/images/pokeball/pb_open.png index 0fdfa7b98fa..bfb8e592742 100644 Binary files a/public/images/pokeball/pb_open.png and b/public/images/pokeball/pb_open.png differ diff --git a/public/images/pokeball/pb_opening.png b/public/images/pokeball/pb_opening.png index 1a7136f186d..415d0f92f1f 100644 Binary files a/public/images/pokeball/pb_opening.png and b/public/images/pokeball/pb_opening.png differ diff --git a/public/images/pokeball/rb_open.png b/public/images/pokeball/rb_open.png index 3fe5f6e81c7..db65009898c 100644 Binary files a/public/images/pokeball/rb_open.png and b/public/images/pokeball/rb_open.png differ diff --git a/public/images/pokeball/ub_open.png b/public/images/pokeball/ub_open.png index 29e5ddac92f..c15a51329b6 100644 Binary files a/public/images/pokeball/ub_open.png and b/public/images/pokeball/ub_open.png differ diff --git a/public/images/pokeball/ub_opening.png b/public/images/pokeball/ub_opening.png index 0696c666d95..09f73970f3c 100644 Binary files a/public/images/pokeball/ub_opening.png and b/public/images/pokeball/ub_opening.png differ diff --git a/public/images/statuses_tl.png b/public/images/statuses_tl.png index 9f24c6a0810..cdec7a29e8b 100644 Binary files a/public/images/statuses_tl.png and b/public/images/statuses_tl.png differ diff --git a/public/images/trainer/aether_grunt_m.png b/public/images/trainer/aether_grunt_m.png index c61970f4c98..08ab681d2fc 100644 Binary files a/public/images/trainer/aether_grunt_m.png and b/public/images/trainer/aether_grunt_m.png differ diff --git a/public/images/trainer/allister.png b/public/images/trainer/allister.png index c29781660d1..7007a7365a9 100644 Binary files a/public/images/trainer/allister.png and b/public/images/trainer/allister.png differ diff --git a/public/images/trainer/aqua_grunt_m.png b/public/images/trainer/aqua_grunt_m.png index bddb6e9fa3c..771c88c9170 100644 Binary files a/public/images/trainer/aqua_grunt_m.png and b/public/images/trainer/aqua_grunt_m.png differ diff --git a/public/images/trainer/archie.png b/public/images/trainer/archie.png index 7b047322bfb..e684d847882 100644 Binary files a/public/images/trainer/archie.png and b/public/images/trainer/archie.png differ diff --git a/public/images/trainer/expert_pokemon_breeder.png b/public/images/trainer/expert_pokemon_breeder.png index 0c53da6f2dc..83e9bb99816 100644 Binary files a/public/images/trainer/expert_pokemon_breeder.png and b/public/images/trainer/expert_pokemon_breeder.png differ diff --git a/public/images/trainer/katy.png b/public/images/trainer/katy.png index 534abca0397..009293a8fc6 100644 Binary files a/public/images/trainer/katy.png and b/public/images/trainer/katy.png differ diff --git a/public/images/trainer/rocket_boss_giovanni_1.png b/public/images/trainer/rocket_boss_giovanni_1.png index 8adab2d0575..d7c2e0fe590 100644 Binary files a/public/images/trainer/rocket_boss_giovanni_1.png and b/public/images/trainer/rocket_boss_giovanni_1.png differ diff --git a/public/images/ui/boolean_sel.png b/public/images/ui/boolean_sel.png index 7f1c86fbb4d..020708801e0 100644 Binary files a/public/images/ui/boolean_sel.png and b/public/images/ui/boolean_sel.png differ diff --git a/public/images/ui/champion_ribbon.png b/public/images/ui/champion_ribbon.png index d70aa92daaf..a19bb01279b 100644 Binary files a/public/images/ui/champion_ribbon.png and b/public/images/ui/champion_ribbon.png differ diff --git a/public/images/ui/champion_ribbon_bronze.png b/public/images/ui/champion_ribbon_bronze.png index 8d92b6e0ab1..d30431fa015 100644 Binary files a/public/images/ui/champion_ribbon_bronze.png and b/public/images/ui/champion_ribbon_bronze.png differ diff --git a/public/images/ui/champion_ribbon_diamond.png b/public/images/ui/champion_ribbon_diamond.png index 9effbe3669c..0d469640529 100644 Binary files a/public/images/ui/champion_ribbon_diamond.png and b/public/images/ui/champion_ribbon_diamond.png differ diff --git a/public/images/ui/champion_ribbon_emerald.png b/public/images/ui/champion_ribbon_emerald.png new file mode 100644 index 00000000000..29a9503059d Binary files /dev/null and b/public/images/ui/champion_ribbon_emerald.png differ diff --git a/public/images/ui/champion_ribbon_silver.png b/public/images/ui/champion_ribbon_silver.png index c4dcd8d7be0..9cf7c40552f 100644 Binary files a/public/images/ui/champion_ribbon_silver.png and b/public/images/ui/champion_ribbon_silver.png differ diff --git a/public/images/ui/cursor.png b/public/images/ui/cursor.png index 0e2f9d98a16..961746a63db 100644 Binary files a/public/images/ui/cursor.png and b/public/images/ui/cursor.png differ diff --git a/public/images/ui/dawn_icon_fg.png b/public/images/ui/dawn_icon_fg.png index c64f0578073..30d793b509d 100644 Binary files a/public/images/ui/dawn_icon_fg.png and b/public/images/ui/dawn_icon_fg.png differ diff --git a/public/images/ui/day_icon_fg.png b/public/images/ui/day_icon_fg.png index a480e4d274d..24d470a70bc 100644 Binary files a/public/images/ui/day_icon_fg.png and b/public/images/ui/day_icon_fg.png differ diff --git a/public/images/ui/friendship_overlay.png b/public/images/ui/friendship_overlay.png index b6957ded371..8a7c2e55c38 100644 Binary files a/public/images/ui/friendship_overlay.png and b/public/images/ui/friendship_overlay.png differ diff --git a/public/images/ui/hall_of_fame_blue.png b/public/images/ui/hall_of_fame_blue.png index 2b799070cb1..4ef4e0bdeae 100644 Binary files a/public/images/ui/hall_of_fame_blue.png and b/public/images/ui/hall_of_fame_blue.png differ diff --git a/public/images/ui/hall_of_fame_red.png b/public/images/ui/hall_of_fame_red.png index df999ae631f..a7685d6cb71 100644 Binary files a/public/images/ui/hall_of_fame_red.png and b/public/images/ui/hall_of_fame_red.png differ diff --git a/public/images/ui/icon_egg_move.png b/public/images/ui/icon_egg_move.png index 3b79a6682d0..1356f6077b6 100644 Binary files a/public/images/ui/icon_egg_move.png and b/public/images/ui/icon_egg_move.png differ diff --git a/public/images/ui/legacy/champion_ribbon.png b/public/images/ui/legacy/champion_ribbon.png index d70aa92daaf..a19bb01279b 100644 Binary files a/public/images/ui/legacy/champion_ribbon.png and b/public/images/ui/legacy/champion_ribbon.png differ diff --git a/public/images/ui/legacy/champion_ribbon_bronze.png b/public/images/ui/legacy/champion_ribbon_bronze.png index 8d92b6e0ab1..d30431fa015 100644 Binary files a/public/images/ui/legacy/champion_ribbon_bronze.png and b/public/images/ui/legacy/champion_ribbon_bronze.png differ diff --git a/public/images/ui/legacy/champion_ribbon_diamond.png b/public/images/ui/legacy/champion_ribbon_diamond.png index 9effbe3669c..0d469640529 100644 Binary files a/public/images/ui/legacy/champion_ribbon_diamond.png and b/public/images/ui/legacy/champion_ribbon_diamond.png differ diff --git a/public/images/ui/legacy/champion_ribbon_emerald.png b/public/images/ui/legacy/champion_ribbon_emerald.png new file mode 100644 index 00000000000..29a9503059d Binary files /dev/null and b/public/images/ui/legacy/champion_ribbon_emerald.png differ diff --git a/public/images/ui/legacy/champion_ribbon_silver.png b/public/images/ui/legacy/champion_ribbon_silver.png index c4dcd8d7be0..9cf7c40552f 100644 Binary files a/public/images/ui/legacy/champion_ribbon_silver.png and b/public/images/ui/legacy/champion_ribbon_silver.png differ diff --git a/public/images/ui/legacy/cursor.png b/public/images/ui/legacy/cursor.png index deadae321e3..a2c271eebb2 100644 Binary files a/public/images/ui/legacy/cursor.png and b/public/images/ui/legacy/cursor.png differ diff --git a/public/images/ui/legacy/day_icon_fg.png b/public/images/ui/legacy/day_icon_fg.png index 79872f95202..a010c3ec9c4 100644 Binary files a/public/images/ui/legacy/day_icon_fg.png and b/public/images/ui/legacy/day_icon_fg.png differ diff --git a/public/images/ui/legacy/discord.png b/public/images/ui/legacy/discord.png index 1bb254b1594..e9acf219d3c 100644 Binary files a/public/images/ui/legacy/discord.png and b/public/images/ui/legacy/discord.png differ diff --git a/public/images/ui/legacy/friendship_overlay.png b/public/images/ui/legacy/friendship_overlay.png index b6957ded371..8a7c2e55c38 100644 Binary files a/public/images/ui/legacy/friendship_overlay.png and b/public/images/ui/legacy/friendship_overlay.png differ diff --git a/public/images/ui/legacy/hall_of_fame_blue.png b/public/images/ui/legacy/hall_of_fame_blue.png index 2b799070cb1..4ef4e0bdeae 100644 Binary files a/public/images/ui/legacy/hall_of_fame_blue.png and b/public/images/ui/legacy/hall_of_fame_blue.png differ diff --git a/public/images/ui/legacy/hall_of_fame_red.png b/public/images/ui/legacy/hall_of_fame_red.png index df999ae631f..a7685d6cb71 100644 Binary files a/public/images/ui/legacy/hall_of_fame_red.png and b/public/images/ui/legacy/hall_of_fame_red.png differ diff --git a/public/images/ui/legacy/icon_egg_move.png b/public/images/ui/legacy/icon_egg_move.png index 3b79a6682d0..1356f6077b6 100644 Binary files a/public/images/ui/legacy/icon_egg_move.png and b/public/images/ui/legacy/icon_egg_move.png differ diff --git a/public/images/ui/legacy/link_icon.png b/public/images/ui/legacy/link_icon.png index 1e20ab56ec7..faf3d64e752 100644 Binary files a/public/images/ui/legacy/link_icon.png and b/public/images/ui/legacy/link_icon.png differ diff --git a/public/images/ui/legacy/namebox.png b/public/images/ui/legacy/namebox.png index 5eea1504d77..766a07bdc2a 100644 Binary files a/public/images/ui/legacy/namebox.png and b/public/images/ui/legacy/namebox.png differ diff --git a/public/images/ui/legacy/overlay_hp.png b/public/images/ui/legacy/overlay_hp.png index 75607c199f5..56f9b50b734 100644 Binary files a/public/images/ui/legacy/overlay_hp.png and b/public/images/ui/legacy/overlay_hp.png differ diff --git a/public/images/ui/legacy/overlay_lv_alt.png b/public/images/ui/legacy/overlay_lv_alt.png index f7a76955e7b..92ec684bf75 100644 Binary files a/public/images/ui/legacy/overlay_lv_alt.png and b/public/images/ui/legacy/overlay_lv_alt.png differ diff --git a/public/images/ui/legacy/party_bg.png b/public/images/ui/legacy/party_bg.png index cdade735b4a..707db79f1e2 100644 Binary files a/public/images/ui/legacy/party_bg.png and b/public/images/ui/legacy/party_bg.png differ diff --git a/public/images/ui/legacy/party_bg_double.png b/public/images/ui/legacy/party_bg_double.png index 8e00bd6c293..3694e3edf3c 100644 Binary files a/public/images/ui/legacy/party_bg_double.png and b/public/images/ui/legacy/party_bg_double.png differ diff --git a/public/images/ui/legacy/party_bg_double_manage.png b/public/images/ui/legacy/party_bg_double_manage.png index 2bf2d63c315..d3954fc14ff 100644 Binary files a/public/images/ui/legacy/party_bg_double_manage.png and b/public/images/ui/legacy/party_bg_double_manage.png differ diff --git a/public/images/ui/legacy/party_slot_hp_overlay.png b/public/images/ui/legacy/party_slot_hp_overlay.png index 859adab148c..4f79d34c702 100644 Binary files a/public/images/ui/legacy/party_slot_hp_overlay.png and b/public/images/ui/legacy/party_slot_hp_overlay.png differ diff --git a/public/images/ui/legacy/party_slot_main_short.json b/public/images/ui/legacy/party_slot_main_short.json new file mode 100644 index 00000000000..d738d524a5b --- /dev/null +++ b/public/images/ui/legacy/party_slot_main_short.json @@ -0,0 +1,146 @@ +{ + "textures": [ + { + "image": "party_slot_main_short.png", + "format": "RGBA8888", + "size": { + "w": 110, + "h": 294 + }, + "scale": 1, + "frames": [ + { + "filename": "party_slot_main_short", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 110, + "h": 41 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 110, + "h": 41 + }, + "frame": { + "x": 0, + "y": 0, + "w": 110, + "h": 41 + } + }, + { + "filename": "party_slot_main_short_sel", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 110, + "h": 41 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 110, + "h": 41 + }, + "frame": { + "x": 0, + "y": 41, + "w": 110, + "h": 41 + } + }, + { + "filename": "party_slot_main_short_fnt", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 110, + "h": 41 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 110, + "h": 41 + }, + "frame": { + "x": 0, + "y": 82, + "w": 110, + "h": 41 + } + }, + { + "filename": "party_slot_main_short_fnt_sel", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 110, + "h": 41 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 110, + "h": 41 + }, + "frame": { + "x": 0, + "y": 123, + "w": 110, + "h": 41 + } + }, + { + "filename": "party_slot_main_short_swap", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 110, + "h": 41 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 110, + "h": 41 + }, + "frame": { + "x": 0, + "y": 164, + "w": 110, + "h": 41 + } + }, + { + "filename": "party_slot_main_short_swap_sel", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 110, + "h": 41 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 110, + "h": 41 + }, + "frame": { + "x": 0, + "y": 205, + "w": 110, + "h": 41 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:29685f2f538901cf5bf7f0ed2ea867c3:a080ea6c8cccd1e03244214053e79796:565f7afc5ca419b6ba8dbce51ea30818$" + } +} diff --git a/public/images/ui/legacy/party_slot_main_short.png b/public/images/ui/legacy/party_slot_main_short.png new file mode 100644 index 00000000000..54af35bd69c Binary files /dev/null and b/public/images/ui/legacy/party_slot_main_short.png differ diff --git a/public/images/ui/legacy/party_slot_overlay_lv_alt.png b/public/images/ui/legacy/party_slot_overlay_lv_alt.png index 387dfec6b22..7aa9a590a13 100644 Binary files a/public/images/ui/legacy/party_slot_overlay_lv_alt.png and b/public/images/ui/legacy/party_slot_overlay_lv_alt.png differ diff --git a/public/images/ui/legacy/passive_bg.png b/public/images/ui/legacy/passive_bg.png index 4a3fa1ec72b..8f89bfc29a2 100644 Binary files a/public/images/ui/legacy/passive_bg.png and b/public/images/ui/legacy/passive_bg.png differ diff --git a/public/images/ui/legacy/pb_tray_overlay_enemy.png b/public/images/ui/legacy/pb_tray_overlay_enemy.png index 86ac85f946f..70940db1c58 100644 Binary files a/public/images/ui/legacy/pb_tray_overlay_enemy.png and b/public/images/ui/legacy/pb_tray_overlay_enemy.png differ diff --git a/public/images/ui/legacy/pbinfo_enemy_type.png b/public/images/ui/legacy/pbinfo_enemy_type.png index 27cb481e861..8d3b956bb98 100644 Binary files a/public/images/ui/legacy/pbinfo_enemy_type.png and b/public/images/ui/legacy/pbinfo_enemy_type.png differ diff --git a/public/images/ui/legacy/pbinfo_enemy_type1.png b/public/images/ui/legacy/pbinfo_enemy_type1.png index df44b5bad5d..9e547234391 100644 Binary files a/public/images/ui/legacy/pbinfo_enemy_type1.png and b/public/images/ui/legacy/pbinfo_enemy_type1.png differ diff --git a/public/images/ui/legacy/pbinfo_enemy_type2.png b/public/images/ui/legacy/pbinfo_enemy_type2.png index df44b5bad5d..9e547234391 100644 Binary files a/public/images/ui/legacy/pbinfo_enemy_type2.png and b/public/images/ui/legacy/pbinfo_enemy_type2.png differ diff --git a/public/images/ui/legacy/pbinfo_player_type1.png b/public/images/ui/legacy/pbinfo_player_type1.png index df44b5bad5d..9e547234391 100644 Binary files a/public/images/ui/legacy/pbinfo_player_type1.png and b/public/images/ui/legacy/pbinfo_player_type1.png differ diff --git a/public/images/ui/legacy/pbinfo_player_type2.png b/public/images/ui/legacy/pbinfo_player_type2.png index df44b5bad5d..9e547234391 100644 Binary files a/public/images/ui/legacy/pbinfo_player_type2.png and b/public/images/ui/legacy/pbinfo_player_type2.png differ diff --git a/public/images/ui/legacy/pokedex_summary_bg.png b/public/images/ui/legacy/pokedex_summary_bg.png index 7323f99a913..0f8fed843c9 100644 Binary files a/public/images/ui/legacy/pokedex_summary_bg.png and b/public/images/ui/legacy/pokedex_summary_bg.png differ diff --git a/public/images/ui/legacy/prompt.png b/public/images/ui/legacy/prompt.png index d70362d1958..7781688f0ee 100644 Binary files a/public/images/ui/legacy/prompt.png and b/public/images/ui/legacy/prompt.png differ diff --git a/public/images/ui/legacy/saving_icon.png b/public/images/ui/legacy/saving_icon.png index 772fa91fb8c..02509d0e40c 100644 Binary files a/public/images/ui/legacy/saving_icon.png and b/public/images/ui/legacy/saving_icon.png differ diff --git a/public/images/ui/legacy/shiny.png b/public/images/ui/legacy/shiny.png index 362f8f48ff1..bc2a6c0d916 100644 Binary files a/public/images/ui/legacy/shiny.png and b/public/images/ui/legacy/shiny.png differ diff --git a/public/images/ui/legacy/summary_bg.png b/public/images/ui/legacy/summary_bg.png index 261b4974fb7..668a7438bf0 100644 Binary files a/public/images/ui/legacy/summary_bg.png and b/public/images/ui/legacy/summary_bg.png differ diff --git a/public/images/ui/legacy/summary_moves_cursor.png b/public/images/ui/legacy/summary_moves_cursor.png index 7375cedcdf6..6a17bccdffd 100644 Binary files a/public/images/ui/legacy/summary_moves_cursor.png and b/public/images/ui/legacy/summary_moves_cursor.png differ diff --git a/public/images/ui/legacy/summary_profile.png b/public/images/ui/legacy/summary_profile.png index 0b5b2ffec05..d8cfebb000e 100644 Binary files a/public/images/ui/legacy/summary_profile.png and b/public/images/ui/legacy/summary_profile.png differ diff --git a/public/images/ui/legacy/summary_profile_ability.png b/public/images/ui/legacy/summary_profile_ability.png index a803323564b..f839a6249f8 100644 Binary files a/public/images/ui/legacy/summary_profile_ability.png and b/public/images/ui/legacy/summary_profile_ability.png differ diff --git a/public/images/ui/legacy/summary_stats.png b/public/images/ui/legacy/summary_stats.png index 660fbee6df6..1da361f6157 100644 Binary files a/public/images/ui/legacy/summary_stats.png and b/public/images/ui/legacy/summary_stats.png differ diff --git a/public/images/ui/link_icon.png b/public/images/ui/link_icon.png index 1e20ab56ec7..faf3d64e752 100644 Binary files a/public/images/ui/link_icon.png and b/public/images/ui/link_icon.png differ diff --git a/public/images/ui/night_icon_fg.png b/public/images/ui/night_icon_fg.png index ef184e24435..9c173eeaa83 100644 Binary files a/public/images/ui/night_icon_fg.png and b/public/images/ui/night_icon_fg.png differ diff --git a/public/images/ui/night_icon_mg.png b/public/images/ui/night_icon_mg.png index f808e50b2c7..94b3529030c 100644 Binary files a/public/images/ui/night_icon_mg.png and b/public/images/ui/night_icon_mg.png differ diff --git a/public/images/ui/overlay_hp.png b/public/images/ui/overlay_hp.png index 9612ed8c4c2..8654174a2f1 100644 Binary files a/public/images/ui/overlay_hp.png and b/public/images/ui/overlay_hp.png differ diff --git a/public/images/ui/overlay_hp_boss.png b/public/images/ui/overlay_hp_boss.png index 5a011709e0c..215c3ff1158 100644 Binary files a/public/images/ui/overlay_hp_boss.png and b/public/images/ui/overlay_hp_boss.png differ diff --git a/public/images/ui/overlay_lv.png b/public/images/ui/overlay_lv.png index ee3f439f7be..6ced2da2660 100644 Binary files a/public/images/ui/overlay_lv.png and b/public/images/ui/overlay_lv.png differ diff --git a/public/images/ui/party_bg_double.png b/public/images/ui/party_bg_double.png index c4d381d828c..61cacc222d4 100644 Binary files a/public/images/ui/party_bg_double.png and b/public/images/ui/party_bg_double.png differ diff --git a/public/images/ui/party_bg_double_manage.png b/public/images/ui/party_bg_double_manage.png index e85413b5fb5..f1561422867 100644 Binary files a/public/images/ui/party_bg_double_manage.png and b/public/images/ui/party_bg_double_manage.png differ diff --git a/public/images/ui/party_slot_hp_bar.png b/public/images/ui/party_slot_hp_bar.png index 5381fe0a0b6..181bd04c0ea 100644 Binary files a/public/images/ui/party_slot_hp_bar.png and b/public/images/ui/party_slot_hp_bar.png differ diff --git a/public/images/ui/party_slot_main_short.json b/public/images/ui/party_slot_main_short.json new file mode 100644 index 00000000000..d738d524a5b --- /dev/null +++ b/public/images/ui/party_slot_main_short.json @@ -0,0 +1,146 @@ +{ + "textures": [ + { + "image": "party_slot_main_short.png", + "format": "RGBA8888", + "size": { + "w": 110, + "h": 294 + }, + "scale": 1, + "frames": [ + { + "filename": "party_slot_main_short", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 110, + "h": 41 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 110, + "h": 41 + }, + "frame": { + "x": 0, + "y": 0, + "w": 110, + "h": 41 + } + }, + { + "filename": "party_slot_main_short_sel", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 110, + "h": 41 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 110, + "h": 41 + }, + "frame": { + "x": 0, + "y": 41, + "w": 110, + "h": 41 + } + }, + { + "filename": "party_slot_main_short_fnt", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 110, + "h": 41 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 110, + "h": 41 + }, + "frame": { + "x": 0, + "y": 82, + "w": 110, + "h": 41 + } + }, + { + "filename": "party_slot_main_short_fnt_sel", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 110, + "h": 41 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 110, + "h": 41 + }, + "frame": { + "x": 0, + "y": 123, + "w": 110, + "h": 41 + } + }, + { + "filename": "party_slot_main_short_swap", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 110, + "h": 41 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 110, + "h": 41 + }, + "frame": { + "x": 0, + "y": 164, + "w": 110, + "h": 41 + } + }, + { + "filename": "party_slot_main_short_swap_sel", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 110, + "h": 41 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 110, + "h": 41 + }, + "frame": { + "x": 0, + "y": 205, + "w": 110, + "h": 41 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:29685f2f538901cf5bf7f0ed2ea867c3:a080ea6c8cccd1e03244214053e79796:565f7afc5ca419b6ba8dbce51ea30818$" + } +} diff --git a/public/images/ui/party_slot_main_short.png b/public/images/ui/party_slot_main_short.png new file mode 100644 index 00000000000..4a4ef9ae937 Binary files /dev/null and b/public/images/ui/party_slot_main_short.png differ diff --git a/public/images/ui/party_slot_overlay_lv.png b/public/images/ui/party_slot_overlay_lv.png index 3d7dafd6737..122d3f7151c 100644 Binary files a/public/images/ui/party_slot_overlay_lv.png and b/public/images/ui/party_slot_overlay_lv.png differ diff --git a/public/images/ui/passive_bg.png b/public/images/ui/passive_bg.png index 4a3fa1ec72b..8f89bfc29a2 100644 Binary files a/public/images/ui/passive_bg.png and b/public/images/ui/passive_bg.png differ diff --git a/public/images/ui/pbinfo_enemy_type1.png b/public/images/ui/pbinfo_enemy_type1.png index be28e1d2c65..bb0c37430ad 100644 Binary files a/public/images/ui/pbinfo_enemy_type1.png and b/public/images/ui/pbinfo_enemy_type1.png differ diff --git a/public/images/ui/pbinfo_player_type2.png b/public/images/ui/pbinfo_player_type2.png index be28e1d2c65..bb0c37430ad 100644 Binary files a/public/images/ui/pbinfo_player_type2.png and b/public/images/ui/pbinfo_player_type2.png differ diff --git a/public/images/ui/prompt.png b/public/images/ui/prompt.png index c045c5e91fe..06e1bce26e6 100644 Binary files a/public/images/ui/prompt.png and b/public/images/ui/prompt.png differ diff --git a/public/images/ui/saving_icon.png b/public/images/ui/saving_icon.png index 772fa91fb8c..02509d0e40c 100644 Binary files a/public/images/ui/saving_icon.png and b/public/images/ui/saving_icon.png differ diff --git a/public/images/ui/select_gen_cursor.png b/public/images/ui/select_gen_cursor.png index 7f1c86fbb4d..020708801e0 100644 Binary files a/public/images/ui/select_gen_cursor.png and b/public/images/ui/select_gen_cursor.png differ diff --git a/public/images/ui/select_gen_cursor_highlight.png b/public/images/ui/select_gen_cursor_highlight.png index cecc543ae56..76607a6cd62 100644 Binary files a/public/images/ui/select_gen_cursor_highlight.png and b/public/images/ui/select_gen_cursor_highlight.png differ diff --git a/public/images/ui/shiny.png b/public/images/ui/shiny.png index 362f8f48ff1..bc2a6c0d916 100644 Binary files a/public/images/ui/shiny.png and b/public/images/ui/shiny.png differ diff --git a/public/images/ui/summary_profile_ability.png b/public/images/ui/summary_profile_ability.png index a803323564b..f839a6249f8 100644 Binary files a/public/images/ui/summary_profile_ability.png and b/public/images/ui/summary_profile_ability.png differ diff --git a/public/locales b/public/locales index ab2716d5440..813e5a34739 160000 --- a/public/locales +++ b/public/locales @@ -1 +1 @@ -Subproject commit ab2716d5440c25f73986664aa3f3131821c3c392 +Subproject commit 813e5a34739100efd5936bc8a63301dfe451ff8d diff --git a/src/@types/arena-tags.ts b/src/@types/arena-tags.ts index afcc8a0f924..bac9e815c31 100644 --- a/src/@types/arena-tags.ts +++ b/src/@types/arena-tags.ts @@ -2,7 +2,7 @@ import type { ArenaTagTypeMap } from "#data/arena-tag"; import type { ArenaTagType } from "#enums/arena-tag-type"; /** Subset of {@linkcode ArenaTagType}s that apply some negative effect to pokemon that switch in ({@link https://bulbapedia.bulbagarden.net/wiki/List_of_moves_that_cause_entry_hazards#List_of_traps | entry hazards} and Imprison. */ -export type ArenaTrapTagType = +export type EntryHazardTagType = | ArenaTagType.STICKY_WEB | ArenaTagType.SPIKES | ArenaTagType.TOXIC_SPIKES diff --git a/src/@types/dex-data.ts b/src/@types/dex-data.ts index 88cc16886bd..005e8034b18 100644 --- a/src/@types/dex-data.ts +++ b/src/@types/dex-data.ts @@ -1,3 +1,5 @@ +import type { RibbonData } from "#system/ribbons/ribbon-data"; + export interface DexData { [key: number]: DexEntry; } @@ -10,4 +12,5 @@ export interface DexEntry { caughtCount: number; hatchedCount: number; ivs: number[]; + ribbons: RibbonData; } diff --git a/src/@types/helpers/type-helpers.ts b/src/@types/helpers/type-helpers.ts index 7ad20b88956..0be391aa3c4 100644 --- a/src/@types/helpers/type-helpers.ts +++ b/src/@types/helpers/type-helpers.ts @@ -103,3 +103,12 @@ export type CoerceNullPropertiesToUndefined = { * @typeParam T - The type to render partial */ export type AtLeastOne = Partial & ObjectValues<{ [K in keyof T]: Pick, K> }>; + +/** Type helper that adds a brand to a type, used for nominal typing. + * + * @remarks + * Brands should be either a string or unique symbol. This prevents overlap with other types. + */ +export declare class Brander { + private __brand: B; +} diff --git a/src/@types/move-types.ts b/src/@types/move-types.ts index ff44c665e48..1def61f1329 100644 --- a/src/@types/move-types.ts +++ b/src/@types/move-types.ts @@ -1,13 +1,24 @@ +import type { Pokemon } from "#field/pokemon"; import type { AttackMove, ChargingAttackMove, ChargingSelfStatusMove, + Move, MoveAttr, MoveAttrConstructorMap, SelfStatusMove, StatusMove, } from "#moves/move"; +/** + * A generic function producing a message during a Move's execution. + * @param user - The {@linkcode Pokemon} using the move + * @param target - The {@linkcode Pokemon} targeted by the move + * @param move - The {@linkcode Move} being used + * @returns a string + */ +export type MoveMessageFunc = (user: Pokemon, target: Pokemon, move: Move) => string; + export type MoveAttrFilter = (attr: MoveAttr) => boolean; export type * from "#moves/move"; diff --git a/src/account.ts b/src/account.ts index b01691ce940..c97721889ae 100644 --- a/src/account.ts +++ b/src/account.ts @@ -17,45 +17,42 @@ export function initLoggedInUser(): void { }; } -export function updateUserInfo(): Promise<[boolean, number]> { - return new Promise<[boolean, number]>(resolve => { - if (bypassLogin) { - loggedInUser = { - username: "Guest", - lastSessionSlot: -1, - discordId: "", - googleId: "", - hasAdminRole: false, - }; - let lastSessionSlot = -1; - for (let s = 0; s < 5; s++) { - if (localStorage.getItem(`sessionData${s ? s : ""}_${loggedInUser.username}`)) { - lastSessionSlot = s; - break; - } +export async function updateUserInfo(): Promise<[boolean, number]> { + if (bypassLogin) { + loggedInUser = { + username: "Guest", + lastSessionSlot: -1, + discordId: "", + googleId: "", + hasAdminRole: false, + }; + let lastSessionSlot = -1; + for (let s = 0; s < 5; s++) { + if (localStorage.getItem(`sessionData${s ? s : ""}_${loggedInUser.username}`)) { + lastSessionSlot = s; + break; } - loggedInUser.lastSessionSlot = lastSessionSlot; - // Migrate old data from before the username was appended - ["data", "sessionData", "sessionData1", "sessionData2", "sessionData3", "sessionData4"].map(d => { - const lsItem = localStorage.getItem(d); - if (lsItem && !!loggedInUser?.username) { - const lsUserItem = localStorage.getItem(`${d}_${loggedInUser.username}`); - if (lsUserItem) { - localStorage.setItem(`${d}_${loggedInUser.username}_bak`, lsUserItem); - } - localStorage.setItem(`${d}_${loggedInUser.username}`, lsItem); - localStorage.removeItem(d); - } - }); - return resolve([true, 200]); } - pokerogueApi.account.getInfo().then(([accountInfo, status]) => { - if (!accountInfo) { - resolve([false, status]); - return; + loggedInUser.lastSessionSlot = lastSessionSlot; + // Migrate old data from before the username was appended + ["data", "sessionData", "sessionData1", "sessionData2", "sessionData3", "sessionData4"].forEach(d => { + const lsItem = localStorage.getItem(d); + if (lsItem && !!loggedInUser?.username) { + const lsUserItem = localStorage.getItem(`${d}_${loggedInUser.username}`); + if (lsUserItem) { + localStorage.setItem(`${d}_${loggedInUser.username}_bak`, lsUserItem); + } + localStorage.setItem(`${d}_${loggedInUser.username}`, lsItem); + localStorage.removeItem(d); } - loggedInUser = accountInfo; - resolve([true, 200]); }); - }); + return [true, 200]; + } + + const [accountInfo, status] = await pokerogueApi.account.getInfo(); + if (!accountInfo) { + return [false, status]; + } + loggedInUser = accountInfo; + return [true, 200]; } diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 271cde1aaa9..4d3f190c02a 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -27,13 +27,7 @@ import { UiInputs } from "#app/ui-inputs"; import { biomeDepths, getBiomeName } from "#balance/biomes"; import { pokemonPrevolutions } from "#balance/pokemon-evolutions"; import { FRIENDSHIP_GAIN_FROM_BATTLE } from "#balance/starters"; -import { - initCommonAnims, - initMoveAnim, - loadCommonAnimAssets, - loadMoveAnimAssets, - populateAnims, -} from "#data/battle-anims"; +import { initCommonAnims, initMoveAnim, loadCommonAnimAssets, loadMoveAnimAssets } from "#data/battle-anims"; import { allAbilities, allMoves, allSpecies, modifierTypes } from "#data/data-lists"; import { battleSpecDialogue } from "#data/dialogue"; import type { SpeciesFormChangeTrigger } from "#data/form-change-triggers"; @@ -104,6 +98,7 @@ import { getLuckString, getLuckTextTint, getPartyLuckValue, + type ModifierType, PokemonHeldItemModifierType, } from "#modifiers/modifier-type"; import { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; @@ -387,7 +382,6 @@ export class BattleScene extends SceneBase { const defaultMoves = [MoveId.TACKLE, MoveId.TAIL_WHIP, MoveId.FOCUS_ENERGY, MoveId.STRUGGLE]; await Promise.all([ - populateAnims(), this.initVariantData(), initCommonAnims().then(() => loadCommonAnimAssets(true)), Promise.all(defaultMoves.map(m => initMoveAnim(m))).then(() => loadMoveAnimAssets(defaultMoves, true)), @@ -943,17 +937,17 @@ export class BattleScene extends SceneBase { dataSource?: PokemonData, postProcess?: (enemyPokemon: EnemyPokemon) => void, ): EnemyPokemon { - if (Overrides.OPP_LEVEL_OVERRIDE > 0) { - level = Overrides.OPP_LEVEL_OVERRIDE; + if (Overrides.ENEMY_LEVEL_OVERRIDE > 0) { + level = Overrides.ENEMY_LEVEL_OVERRIDE; } - if (Overrides.OPP_SPECIES_OVERRIDE) { - species = getPokemonSpecies(Overrides.OPP_SPECIES_OVERRIDE); + if (Overrides.ENEMY_SPECIES_OVERRIDE) { + species = getPokemonSpecies(Overrides.ENEMY_SPECIES_OVERRIDE); // The fact that a Pokemon is a boss or not can change based on its Species and level boss = this.getEncounterBossSegments(this.currentBattle.waveIndex, level, species) > 1; } const pokemon = new EnemyPokemon(species, level, trainerSlot, boss, shinyLock, dataSource); - if (Overrides.OPP_FUSION_OVERRIDE) { + if (Overrides.ENEMY_FUSION_OVERRIDE) { pokemon.generateFusionSpecies(); } @@ -1203,7 +1197,9 @@ export class BattleScene extends SceneBase { this.updateScoreText(); this.scoreText.setVisible(false); - [this.luckLabelText, this.luckText].map(t => t.setVisible(false)); + [this.luckLabelText, this.luckText].forEach(t => { + t.setVisible(false); + }); this.newArena(Overrides.STARTING_BIOME_OVERRIDE || BiomeId.TOWN); @@ -1237,8 +1233,7 @@ export class BattleScene extends SceneBase { Object.values(mp) .flat() .map(mt => mt.modifierType) - .filter(mt => "localize" in mt) - .map(lpb => lpb as unknown as Localizable), + .filter((mt): mt is ModifierType & Localizable => "localize" in mt && typeof mt.localize === "function"), ), ]; for (const item of localizable) { @@ -1513,8 +1508,8 @@ export class BattleScene extends SceneBase { return this.currentBattle; } - newArena(biome: BiomeId, playerFaints?: number): Arena { - this.arena = new Arena(biome, BiomeId[biome].toLowerCase(), playerFaints); + newArena(biome: BiomeId, playerFaints = 0): Arena { + this.arena = new Arena(biome, playerFaints); this.eventTarget.dispatchEvent(new NewArenaEvent()); this.arenaBg.pipelineData = { @@ -1764,10 +1759,10 @@ export class BattleScene extends SceneBase { } getEncounterBossSegments(waveIndex: number, level: number, species?: PokemonSpecies, forceBoss = false): number { - if (Overrides.OPP_HEALTH_SEGMENTS_OVERRIDE > 1) { - return Overrides.OPP_HEALTH_SEGMENTS_OVERRIDE; + if (Overrides.ENEMY_HEALTH_SEGMENTS_OVERRIDE > 1) { + return Overrides.ENEMY_HEALTH_SEGMENTS_OVERRIDE; } - if (Overrides.OPP_HEALTH_SEGMENTS_OVERRIDE === 1) { + if (Overrides.ENEMY_HEALTH_SEGMENTS_OVERRIDE === 1) { // The rest of the code expects to be returned 0 and not 1 if the enemy is not a boss return 0; } @@ -2711,7 +2706,9 @@ export class BattleScene extends SceneBase { } } - this.party.map(p => p.updateInfo(instant)); + this.party.forEach(p => { + p.updateInfo(instant); + }); } else { const args = [this]; if (modifier.shouldApply(...args)) { diff --git a/src/constants.ts b/src/constants.ts index 6f9f4a6d2fb..17cf08aa7e2 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -101,3 +101,9 @@ export const ANTI_VARIANCE_WEIGHT_MODIFIER = 15; * Default: `10000` (0.01%) */ export const FAKE_TITLE_LOGO_CHANCE = 10000; + +/** + * The ceiling on friendship amount that can be reached through the use of rare candies. + * Using rare candies will never increase friendship beyond this value. + */ +export const RARE_CANDY_FRIENDSHIP_CAP = 200; diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index f5144f3bca8..422d6fdb24e 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -8,7 +8,7 @@ import type { MoveEndPhase } from "#phases/move-end-phase"; import { applyAbAttrs } from "#abilities/apply-ab-attrs"; import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; -import type { ArenaTrapTag, SuppressAbilitiesTag } from "#data/arena-tag"; +import type { EntryHazardTag, SuppressAbilitiesTag } from "#data/arena-tag"; import type { BattlerTag } from "#data/battler-tags"; import { GroundedTag } from "#data/battler-tags"; import { getBerryEffectFunc } from "#data/berry"; @@ -77,6 +77,7 @@ import { randSeedItem, toDmgValue, } from "#utils/common"; +import { toCamelCase } from "#utils/strings"; import i18next from "i18next"; export class Ability implements Localizable { @@ -112,13 +113,9 @@ export class Ability implements Localizable { } localize(): void { - const i18nKey = AbilityId[this.id] - .split("_") - .filter(f => f) - .map((f, i) => (i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase())) - .join("") as string; + const i18nKey = toCamelCase(AbilityId[this.id]); - this.name = this.id ? `${i18next.t(`ability:${i18nKey}.name`) as string}${this.nameAppend}` : ""; + this.name = this.id ? `${i18next.t(`ability:${i18nKey}.name`)}${this.nameAppend}` : ""; this.description = this.id ? (i18next.t(`ability:${i18nKey}.description`) as string) : ""; } @@ -1119,7 +1116,7 @@ export class PostDefendApplyArenaTrapTagAbAttr extends PostDefendAbAttr { } override canApply({ pokemon, opponent: attacker, move }: PostMoveInteractionAbAttrParams): boolean { - const tag = globalScene.arena.getTag(this.arenaTagType) as ArenaTrapTag; + const tag = globalScene.arena.getTag(this.arenaTagType) as EntryHazardTag; return ( this.condition(pokemon, attacker, move) && (!globalScene.arena.getTag(this.arenaTagType) || tag.layers < tag.maxLayers) @@ -1190,7 +1187,7 @@ export class PostDefendTypeChangeAbAttr extends PostDefendAbAttr { return i18next.t("abilityTriggers:postDefendTypeChange", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName, - typeName: i18next.t(`pokemonInfo:Type.${PokemonType[this.type]}`), + typeName: i18next.t(`pokemonInfo:type.${toCamelCase(PokemonType[this.type])}`), }); } } @@ -1241,7 +1238,7 @@ export class PostDefendContactApplyStatusEffectAbAttr extends PostDefendAbAttr { // TODO: Probably want to check against simulated here const effect = this.effects.length === 1 ? this.effects[0] : this.effects[pokemon.randBattleSeedInt(this.effects.length)]; - attacker.trySetStatus(effect, true, pokemon); + attacker.trySetStatus(effect, pokemon); } } @@ -1673,6 +1670,7 @@ export class MoveTypeChangeAbAttr extends PreAttackAbAttr { constructor( private newType: PokemonType, private powerMultiplier: number, + // TODO: all moves with this attr solely check the move being used... private condition?: PokemonAttackCondition, ) { super(false); @@ -1754,7 +1752,7 @@ export class PokemonTypeChangeAbAttr extends PreAttackAbAttr { getTriggerMessage({ pokemon }: AugmentMoveInteractionAbAttrParams, _abilityName: string): string { return i18next.t("abilityTriggers:pokemonTypeChange", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - moveType: i18next.t(`pokemonInfo:Type.${PokemonType[this.moveType]}`), + moveType: i18next.t(`pokemonInfo:type.${toCamelCase(PokemonType[this.moveType])}`), }); } } @@ -2233,7 +2231,7 @@ export class PostAttackApplyStatusEffectAbAttr extends PostAttackAbAttr { apply({ pokemon, opponent }: PostMoveInteractionAbAttrParams): void { const effect = this.effects.length === 1 ? this.effects[0] : this.effects[pokemon.randBattleSeedInt(this.effects.length)]; - opponent.trySetStatus(effect, true, pokemon); + opponent.trySetStatus(effect, pokemon); } } @@ -2388,7 +2386,7 @@ export class SynchronizeStatusAbAttr extends PostSetStatusAbAttr { */ override apply({ simulated, effect, sourcePokemon, pokemon }: PostSetStatusAbAttrParams): void { if (!simulated && sourcePokemon) { - sourcePokemon.trySetStatus(effect, true, pokemon); + sourcePokemon.trySetStatus(effect, pokemon); } } } @@ -3664,7 +3662,8 @@ export class PreSetStatusEffectImmunityAbAttr extends PreSetStatusAbAttr { protected immuneEffects: StatusEffect[]; /** - * @param immuneEffects - The status effects to which the Pokémon is immune. + * @param immuneEffects - An array of {@linkcode StatusEffect}s to prevent application. + * If none are provided, will block **all** status effects regardless of type. */ constructor(...immuneEffects: StatusEffect[]) { super(); @@ -3673,7 +3672,7 @@ export class PreSetStatusEffectImmunityAbAttr extends PreSetStatusAbAttr { } override canApply({ effect }: PreSetStatusAbAttrParams): boolean { - return (effect !== StatusEffect.FAINT && this.immuneEffects.length < 1) || this.immuneEffects.includes(effect); + return (this.immuneEffects.length === 0 && effect !== StatusEffect.FAINT) || this.immuneEffects.includes(effect); } /** @@ -3725,6 +3724,11 @@ export interface UserFieldStatusEffectImmunityAbAttrParams extends AbAttrBasePar */ export class UserFieldStatusEffectImmunityAbAttr extends AbAttr { protected immuneEffects: StatusEffect[]; + + /** + * @param immuneEffects - An array of {@linkcode StatusEffect}s to prevent application. + * If none are provided, will block **all** status effects regardless of type. + */ constructor(...immuneEffects: StatusEffect[]) { super(); @@ -3733,7 +3737,7 @@ export class UserFieldStatusEffectImmunityAbAttr extends AbAttr { override canApply({ effect, cancelled }: UserFieldStatusEffectImmunityAbAttrParams): boolean { return ( - (!cancelled.value && effect !== StatusEffect.FAINT && this.immuneEffects.length < 1) || + (!cancelled.value && this.immuneEffects.length === 0 && effect !== StatusEffect.FAINT) || this.immuneEffects.includes(effect) ); } @@ -3759,6 +3763,10 @@ export class ConditionalUserFieldStatusEffectImmunityAbAttr extends UserFieldSta */ private condition: (target: Pokemon, source: Pokemon | null) => boolean; + /** + * @param immuneEffects - An array of {@linkcode StatusEffect}s to prevent application. + * If none are provided, will block **all** status effects regardless of type. + */ constructor(condition: (target: Pokemon, source: Pokemon | null) => boolean, ...immuneEffects: StatusEffect[]) { super(...immuneEffects); @@ -6236,7 +6244,9 @@ export class TerrainEventTypeChangeAbAttr extends PostSummonAbAttr { if (currentTerrain === TerrainType.NONE) { return i18next.t("abilityTriggers:pokemonTypeChangeRevert", { pokemonNameWithAffix }); } - const moveType = i18next.t(`pokemonInfo:Type.${PokemonType[this.determineTypeChange(pokemon, currentTerrain)[0]]}`); + const moveType = i18next.t( + `pokemonInfo:type.${toCamelCase(PokemonType[this.determineTypeChange(pokemon, currentTerrain)[0]])}`, + ); return i18next.t("abilityTriggers:pokemonTypeChange", { pokemonNameWithAffix, moveType }); } } @@ -7490,8 +7500,7 @@ export function initAbilities() { .unsuppressable() .bypassFaint(), new Ability(AbilityId.CORROSION, 7) - .attr(IgnoreTypeStatusEffectImmunityAbAttr, [ StatusEffect.POISON, StatusEffect.TOXIC ], [ PokemonType.STEEL, PokemonType.POISON ]) - .edgeCase(), // Should poison itself with toxic orb. + .attr(IgnoreTypeStatusEffectImmunityAbAttr, [ StatusEffect.POISON, StatusEffect.TOXIC ], [ PokemonType.STEEL, PokemonType.POISON ]), new Ability(AbilityId.COMATOSE, 7) .attr(StatusEffectImmunityAbAttr, ...getNonVolatileStatusEffects()) .attr(BattlerTagImmunityAbAttr, BattlerTagType.DROWSY) diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index 15c2cde1d58..ab8b9f7990f 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -24,11 +24,11 @@ import type { Pokemon } from "#field/pokemon"; import type { ArenaScreenTagType, ArenaTagTypeData, - ArenaTrapTagType, + EntryHazardTagType, SerializableArenaTagType, } from "#types/arena-tags"; import type { Mutable } from "#types/type-helpers"; -import { BooleanHolder, NumberHolder, toDmgValue } from "#utils/common"; +import { BooleanHolder, type NumberHolder, toDmgValue } from "#utils/common"; import i18next from "i18next"; /** @@ -725,42 +725,79 @@ export class IonDelugeTag extends ArenaTag { } /** - * Abstract class to implement arena traps. + * Abstract class to implement [entry hazards](https://bulbapedia.bulbagarden.net/wiki/List_of_moves_that_cause_entry_hazards). + * These persistent tags remain on-field across turns and apply effects to any {@linkcode Pokemon} switching in. \ + * Uniquely, adding a tag multiple times may stack multiple "layers" of the effect, increasing its severity. */ -export abstract class ArenaTrapTag extends SerializableArenaTag { - abstract readonly tagType: ArenaTrapTagType; - public layers: number; - public maxLayers: number; - +export abstract class EntryHazardTag extends SerializableArenaTag { + public declare abstract readonly tagType: EntryHazardTagType; /** - * Creates a new instance of the ArenaTrapTag class. - * - * @param tagType - The type of the arena tag. - * @param sourceMove - The move that created the tag. - * @param sourceId - The ID of the source of the tag. - * @param side - The side (player or enemy) the tag affects. - * @param maxLayers - The maximum amount of layers this tag can have. + * The current number of layers this tag has. + * Starts at 1 and increases each time the trap is laid. */ - constructor(sourceMove: MoveId, sourceId: number | undefined, side: ArenaTagSide, maxLayers: number) { - super(0, sourceMove, sourceId, side); - - this.layers = 1; - this.maxLayers = maxLayers; + public layers = 1; + /** The maximum number of layers this tag can have. */ + public abstract get maxLayers(): number; + /** Whether this tag should only affect grounded targets; default `true` */ + protected get groundedOnly(): boolean { + return true; } - onOverlap(arena: Arena, _source: Pokemon | null): void { - if (this.layers < this.maxLayers) { - this.layers++; + constructor(sourceMove: MoveId, sourceId: number | undefined, side: ArenaTagSide) { + super(0, sourceMove, sourceId, side); + } - this.onAdd(arena); + // TODO: Add a `canAdd` field to arena tags to remove need for callers to check layer counts + + /** + * Display text when this tag is added to the field. + * @param _arena - The {@linkcode Arena} at the time of adding this tag + * @param quiet - Whether to suppress messages during tag creation; default `false` + */ + override onAdd(_arena: Arena, quiet = false): void { + // Here, `quiet=true` means "just add the tag, no questions asked" + if (quiet) { + return; } + + const source = this.getSourcePokemon(); + if (!source) { + console.warn( + "Failed to get source Pokemon for AernaTrapTag on add message!" + + `\nTag type: ${this.tagType}` + + `\nPID: ${this.sourceId}`, + ); + return; + } + + globalScene.phaseManager.queueMessage(this.getAddMessage(source)); } /** - * Activates the hazard effect onto a Pokemon when it enters the field - * @param _arena the {@linkcode Arena} containing this tag - * @param simulated if `true`, only checks if the hazard would activate. - * @param pokemon the {@linkcode Pokemon} triggering this hazard + * Return the text to be displayed upon adding a new layer to this trap. + * @param source - The {@linkcode Pokemon} having created this tag + * @returns The localized message to be displayed on screen. + */ + protected abstract getAddMessage(source: Pokemon): string; + + /** + * Add a new layer to this tag upon overlap, triggering the tag's normal {@linkcode onAdd} effects upon doing so. + * @param arena - The {@linkcode arena} at the time of adding the tag + */ + override onOverlap(arena: Arena): void { + if (this.layers >= this.maxLayers) { + return; + } + this.layers++; + + this.onAdd(arena); + } + + /** + * Activate the hazard effect onto a Pokemon when it enters the field. + * @param _arena - The {@linkcode Arena} at the time of tag activation + * @param simulated - Whether to suppress activation effects during execution + * @param pokemon - The {@linkcode Pokemon} triggering this hazard * @returns `true` if this hazard affects the given Pokemon; `false` otherwise. */ override apply(_arena: Arena, simulated: boolean, pokemon: Pokemon): boolean { @@ -768,12 +805,21 @@ export abstract class ArenaTrapTag extends SerializableArenaTag { return false; } + if (this.groundedOnly && !pokemon.isGrounded()) { + return false; + } + return this.activateTrap(pokemon, simulated); } - activateTrap(_pokemon: Pokemon, _simulated: boolean): boolean { - return false; - } + /** + * Activate this trap's effects when a Pokemon switches into it. + * @param _pokemon - The {@linkcode Pokemon} + * @param _simulated - Whether the activation is simulated + * @returns Whether the trap activation succeeded + * @todo Do we need the return value? nothing uses it + */ + protected abstract activateTrap(_pokemon: Pokemon, _simulated: boolean): boolean; getMatchupScoreMultiplier(pokemon: Pokemon): number { return pokemon.isGrounded() @@ -781,141 +827,186 @@ export abstract class ArenaTrapTag extends SerializableArenaTag { : Phaser.Math.Linear(0, 1 / Math.pow(2, this.layers), Math.min(pokemon.getHpRatio(), 0.5) * 2); } - public loadTag(source: BaseArenaTag & Pick): void { + public loadTag(source: BaseArenaTag & Pick): void { super.loadTag(source); this.layers = source.layers; - this.maxLayers = source.maxLayers; } } +/** + * Abstract class to implement damaging entry hazards. + * Currently used for {@linkcode SpikesTag} and {@linkcode StealthRockTag}. + */ +abstract class DamagingTrapTag extends EntryHazardTag { + override activateTrap(pokemon: Pokemon, simulated: boolean): boolean { + // Check for magic guard immunity + const cancelled = new BooleanHolder(false); + applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled }); + if (cancelled.value) { + return false; + } + + if (simulated) { + return true; + } + + // Damage the target and trigger a message + const damageHpRatio = this.getDamageHpRatio(pokemon); + const damage = toDmgValue(pokemon.getMaxHp() * damageHpRatio); + + globalScene.phaseManager.queueMessage(this.getTriggerMessage(pokemon)); + pokemon.damageAndUpdate(damage, { result: HitResult.INDIRECT }); + pokemon.turnData.damageTaken += damage; + return true; + } + + /** + * Return the text to be displayed when this tag deals damage. + * @param _pokemon - The {@linkcode Pokemon} switching in + * @returns The localized trigger message to be displayed on-screen. + */ + protected abstract getTriggerMessage(_pokemon: Pokemon): string; + + /** + * Return the amount of damage this tag should deal to the given Pokemon, relative to its maximum HP. + * @param _pokemon - The {@linkcode Pokemon} switching in + * @returns The percentage of max HP to deal upon activation. + */ + protected abstract getDamageHpRatio(_pokemon: Pokemon): number; +} + /** * Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Spikes_(move) Spikes}. * Applies up to 3 layers of Spikes, dealing 1/8th, 1/6th, or 1/4th of the the Pokémon's HP * in damage for 1, 2, or 3 layers of Spikes respectively if they are summoned into this trap. */ -class SpikesTag extends ArenaTrapTag { +class SpikesTag extends DamagingTrapTag { public readonly tagType = ArenaTagType.SPIKES; + override get maxLayers() { + return 3 as const; + } + constructor(sourceId: number | undefined, side: ArenaTagSide) { - super(MoveId.SPIKES, sourceId, side, 3); + super(MoveId.SPIKES, sourceId, side); } - onAdd(arena: Arena, quiet = false): void { - super.onAdd(arena); - - // 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(), - }), - ); + protected override getAddMessage(source: Pokemon): string { + return i18next.t("arenaTag:spikesOnAdd", { + moveName: this.getMoveName(), + opponentDesc: source.getOpponentDescriptor(), + }); } - override activateTrap(pokemon: Pokemon, simulated: boolean): boolean { - if (!pokemon.isGrounded()) { - return false; - } + protected override getTriggerMessage(pokemon: Pokemon): string { + return i18next.t("arenaTag:spikesActivateTrap", { + pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), + }); + } - const cancelled = new BooleanHolder(false); - applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled }); - if (simulated || cancelled.value) { - return !cancelled.value; - } - - const damageHpRatio = 1 / (10 - 2 * this.layers); - const damage = toDmgValue(pokemon.getMaxHp() * damageHpRatio); - - globalScene.phaseManager.queueMessage( - i18next.t("arenaTag:spikesActivateTrap", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - }), - ); - pokemon.damageAndUpdate(damage, { result: HitResult.INDIRECT }); - pokemon.turnData.damageTaken += damage; - return true; + protected override getDamageHpRatio(_pokemon: Pokemon): number { + // 1/8 for 1 layer, 1/6 for 2, 1/4 for 3 + return 1 / (10 - 2 * this.layers); } } /** - * Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Toxic_Spikes_(move) Toxic Spikes}. - * Applies up to 2 layers of Toxic Spikes, poisoning or badly poisoning any Pokémon who is - * summoned into this trap if 1 or 2 layers of Toxic Spikes respectively are up. Poison-type - * Pokémon summoned into this trap remove it entirely. + * Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Stealth_Rock_(move) | Stealth Rock}. + * Applies up to 1 layer of Stealth Rocks, dealing percentage-based damage to any Pokémon + * who is summoned into the trap based on the Rock type's type effectiveness. */ -class ToxicSpikesTag extends ArenaTrapTag { - #neutralized: boolean; - public readonly tagType = ArenaTagType.TOXIC_SPIKES; +class StealthRockTag extends DamagingTrapTag { + public readonly tagType = ArenaTagType.STEALTH_ROCK; + public override get maxLayers() { + return 1 as const; + } + protected override get groundedOnly() { + return false; + } constructor(sourceId: number | undefined, side: ArenaTagSide) { - super(MoveId.TOXIC_SPIKES, sourceId, side, 2); - this.#neutralized = false; + super(MoveId.STEALTH_ROCK, sourceId, side); } - onAdd(arena: Arena, quiet = false): void { - super.onAdd(arena); - - 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(), - }), - ); + protected override getAddMessage(source: Pokemon): string { + return i18next.t("arenaTag:stealthRockOnAdd", { + opponentDesc: source.getOpponentDescriptor(), + }); } - onRemove(arena: Arena): void { + protected override getTriggerMessage(pokemon: Pokemon): string { + return i18next.t("arenaTag:stealthRockActivateTrap", { + pokemonName: getPokemonNameWithAffix(pokemon), + }); + } + + protected override getDamageHpRatio(pokemon: Pokemon): number { + const effectiveness = pokemon.getAttackTypeEffectiveness(PokemonType.ROCK, undefined, true); + return 0.125 * effectiveness; + } + + getMatchupScoreMultiplier(pokemon: Pokemon): number { + const damageHpRatio = this.getDamageHpRatio(pokemon); + return Phaser.Math.Linear(super.getMatchupScoreMultiplier(pokemon), 1, 1 - Math.pow(damageHpRatio, damageHpRatio)); + } +} + +/** + * Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Toxic_Spikes_(move) | Toxic Spikes}. + * Applies up to 2 layers of Toxic Spikes, poisoning or badly poisoning any Pokémon switched in + * based on the current layer count. \ + * Poison-type Pokémon will remove it entirely upon switch-in. + */ +class ToxicSpikesTag extends EntryHazardTag { + /** + * Whether the tag is currently in the process of being neutralized by a Poison-type. + * @defaultValue `false` + */ + #neutralized = false; + public readonly tagType = ArenaTagType.TOXIC_SPIKES; + override get maxLayers() { + return 2 as const; + } + + constructor(sourceId: number | undefined, side: ArenaTagSide) { + super(MoveId.TOXIC_SPIKES, sourceId, side); + } + + protected override getAddMessage(source: Pokemon): string { + return i18next.t("arenaTag:toxicSpikesOnAdd", { + moveName: this.getMoveName(), + opponentDesc: source.getOpponentDescriptor(), + }); + } + + // Override remove function to only display text when not neutralized + override onRemove(arena: Arena): void { if (!this.#neutralized) { super.onRemove(arena); } } override activateTrap(pokemon: Pokemon, simulated: boolean): boolean { - if (pokemon.isGrounded()) { - if (simulated) { - return true; - } - if (pokemon.isOfType(PokemonType.POISON)) { - this.#neutralized = true; - if (globalScene.arena.removeTag(this.tagType)) { - globalScene.phaseManager.queueMessage( - i18next.t("arenaTag:toxicSpikesActivateTrapPoison", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - moveName: this.getMoveName(), - }), - ); - return true; - } - } else if (!pokemon.status) { - const toxic = this.layers > 1; - if ( - pokemon.trySetStatus(!toxic ? StatusEffect.POISON : StatusEffect.TOXIC, true, null, 0, this.getMoveName()) - ) { - return true; - } - } + if (simulated) { + return true; } - return false; + if (pokemon.isOfType(PokemonType.POISON)) { + // Neutralize the tag and remove it from the field. + // Message cannot be moved to `onRemove` as that requires a reference to the neutralizing pokemon + this.#neutralized = true; + globalScene.arena.removeTagOnSide(this.tagType, this.side); + globalScene.phaseManager.queueMessage( + i18next.t("arenaTag:toxicSpikesActivateTrapPoison", { + pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), + moveName: this.getMoveName(), + }), + ); + return true; + } + + // Attempt to poison the target, suppressing any status effect messages + const effect = this.layers === 1 ? StatusEffect.POISON : StatusEffect.TOXIC; + return pokemon.trySetStatus(effect, null, 0, this.getMoveName(), false, true); } getMatchupScoreMultiplier(pokemon: Pokemon): number { @@ -930,71 +1021,37 @@ class ToxicSpikesTag extends ArenaTrapTag { } /** - * Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Stealth_Rock_(move) Stealth Rock}. - * Applies up to 1 layer of Stealth Rocks, dealing percentage-based damage to any Pokémon - * who is summoned into the trap, based on the Rock type's type effectiveness. + * Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Sticky_Web_(move) | Sticky Web}. + * Applies a single-layer trap that lowers the Speed of all grounded Pokémon switching in. */ -class StealthRockTag extends ArenaTrapTag { - public readonly tagType = ArenaTagType.STEALTH_ROCK; +class StickyWebTag extends EntryHazardTag { + public readonly tagType = ArenaTagType.STICKY_WEB; + public override get maxLayers() { + return 1 as const; + } + constructor(sourceId: number | undefined, side: ArenaTagSide) { - super(MoveId.STEALTH_ROCK, sourceId, side, 1); + super(MoveId.STICKY_WEB, sourceId, side); } - onAdd(arena: Arena, quiet = false): void { - super.onAdd(arena); - - if (quiet) { - return; - } - - const source = this.getSourcePokemon(); - if (!quiet && source) { - globalScene.phaseManager.queueMessage( - i18next.t("arenaTag:stealthRockOnAdd", { - opponentDesc: source.getOpponentDescriptor(), - }), - ); - } - } - - getDamageHpRatio(pokemon: Pokemon): number { - const effectiveness = pokemon.getAttackTypeEffectiveness(PokemonType.ROCK, undefined, true); - - let damageHpRatio = 0; - - switch (effectiveness) { - case 0: - damageHpRatio = 0; - break; - case 0.25: - damageHpRatio = 0.03125; - break; - case 0.5: - damageHpRatio = 0.0625; - break; - case 1: - damageHpRatio = 0.125; - break; - case 2: - damageHpRatio = 0.25; - break; - case 4: - damageHpRatio = 0.5; - break; - } - - return damageHpRatio; + protected override getAddMessage(source: Pokemon): string { + return i18next.t("arenaTag:stickyWebOnAdd", { + moveName: this.getMoveName(), + opponentDesc: source.getOpponentDescriptor(), + }); } override activateTrap(pokemon: Pokemon, simulated: boolean): boolean { const cancelled = new BooleanHolder(false); - applyAbAttrs("BlockNonDirectDamageAbAttr", { pokemon, cancelled }); - if (cancelled.value) { - return false; - } + // TODO: Does this need to pass `simulated` as a parameter? + applyAbAttrs("ProtectStatAbAttr", { + pokemon, + cancelled, + stat: Stat.SPD, + stages: -1, + }); - const damageHpRatio = this.getDamageHpRatio(pokemon); - if (!damageHpRatio) { + if (cancelled.value) { return false; } @@ -1002,95 +1059,96 @@ class StealthRockTag extends ArenaTrapTag { return true; } - const damage = toDmgValue(pokemon.getMaxHp() * damageHpRatio); globalScene.phaseManager.queueMessage( - i18next.t("arenaTag:stealthRockActivateTrap", { - pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), + i18next.t("arenaTag:stickyWebActivateTrap", { + pokemonName: pokemon.getNameToRender(), }), ); - pokemon.damageAndUpdate(damage, { result: HitResult.INDIRECT }); - pokemon.turnData.damageTaken += damage; - return true; - } - getMatchupScoreMultiplier(pokemon: Pokemon): number { - const damageHpRatio = this.getDamageHpRatio(pokemon); - return Phaser.Math.Linear(super.getMatchupScoreMultiplier(pokemon), 1, 1 - Math.pow(damageHpRatio, damageHpRatio)); + globalScene.phaseManager.unshiftNew( + "StatStageChangePhase", + pokemon.getBattlerIndex(), + false, + [Stat.SPD], + -1, + true, + false, + true, + null, + false, + true, + ); + return true; } } /** - * Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Sticky_Web_(move) Sticky Web}. - * Applies up to 1 layer of Sticky Web, which lowers the Speed by one stage - * to any Pokémon who is summoned into this trap. + * This arena tag facilitates the application of the move Imprison + * Imprison remains in effect as long as the source Pokemon is active and present on the field. + * Imprison will apply to any opposing Pokemon that switch onto the field as well. */ -class StickyWebTag extends ArenaTrapTag { - public readonly tagType = ArenaTagType.STICKY_WEB; +class ImprisonTag extends EntryHazardTag { + public readonly tagType = ArenaTagType.IMPRISON; + public override get maxLayers() { + return 1 as const; + } + constructor(sourceId: number | undefined, side: ArenaTagSide) { - super(MoveId.STICKY_WEB, sourceId, side, 1); + super(MoveId.IMPRISON, sourceId, side); } - onAdd(arena: Arena, quiet = false): void { - super.onAdd(arena); + /** + * Apply the effects of Imprison to all opposing on-field Pokemon. + */ + override onAdd(_arena: Arena, quiet = false) { + super.onAdd(_arena, quiet); - // We assume `quiet=true` means "just add the bloody tag no questions asked" - if (quiet) { - return; - } + const party = this.getAffectedPokemon(); + party.forEach(p => { + if (p.isAllowedInBattle()) { + p.addTag(BattlerTagType.IMPRISON, 1, MoveId.IMPRISON, this.sourceId); + } + }); + } + protected override getAddMessage(source: Pokemon): string { + return i18next.t("battlerTags:imprisonOnAdd", { + pokemonNameWithAffix: getPokemonNameWithAffix(source), + }); + } + + /** + * Checks if the source Pokemon is still active on the field + * @param _arena + * @returns `true` if the source of the tag is still active on the field | `false` if not + */ + override lapse(): boolean { 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(), - }), - ); + return !!source?.isActive(true); } - override activateTrap(pokemon: Pokemon, simulated: boolean): boolean { - if (pokemon.isGrounded()) { - const cancelled = new BooleanHolder(false); - applyAbAttrs("ProtectStatAbAttr", { - pokemon, - cancelled, - stat: Stat.SPD, - stages: -1, - }); - - if (simulated) { - return !cancelled.value; - } - - if (!cancelled.value) { - globalScene.phaseManager.queueMessage( - i18next.t("arenaTag:stickyWebActivateTrap", { - pokemonName: pokemon.getNameToRender(), - }), - ); - const stages = new NumberHolder(-1); - globalScene.phaseManager.unshiftNew( - "StatStageChangePhase", - pokemon.getBattlerIndex(), - false, - [Stat.SPD], - stages.value, - true, - false, - true, - null, - false, - true, - ); - return true; - } + /** + * This applies the effects of Imprison to any opposing Pokemon that switch into the field while the source Pokemon is still active + * @param {Pokemon} pokemon the Pokemon Imprison is applied to + * @returns `true` + */ + override activateTrap(pokemon: Pokemon): boolean { + const source = this.getSourcePokemon(); + if (source?.isActive(true) && pokemon.isAllowedInBattle()) { + pokemon.addTag(BattlerTagType.IMPRISON, 1, MoveId.IMPRISON, this.sourceId); } + return true; + } - return false; + /** + * When the arena tag is removed, it also attempts to remove any related Battler Tags if they haven't already been removed from the affected Pokemon + * @param arena + */ + override onRemove(): void { + const party = this.getAffectedPokemon(); + party.forEach(p => { + p.removeTag(BattlerTagType.IMPRISON); + }); } } @@ -1287,75 +1345,6 @@ class NoneTag extends ArenaTag { } } -/** - * This arena tag facilitates the application of the move Imprison - * Imprison remains in effect as long as the source Pokemon is active and present on the field. - * Imprison will apply to any opposing Pokemon that switch onto the field as well. - */ -class ImprisonTag extends ArenaTrapTag { - public readonly tagType = ArenaTagType.IMPRISON; - constructor(sourceId: number | undefined, side: ArenaTagSide) { - super(MoveId.IMPRISON, sourceId, side, 1); - } - - /** - * Apply the effects of Imprison to all opposing on-field Pokemon. - */ - override onAdd() { - const source = this.getSourcePokemon(); - 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), - }), - ); - } - - /** - * Checks if the source Pokemon is still active on the field - * @param _arena - * @returns `true` if the source of the tag is still active on the field | `false` if not - */ - override lapse(): boolean { - const source = this.getSourcePokemon(); - return !!source?.isActive(true); - } - - /** - * This applies the effects of Imprison to any opposing Pokemon that switch into the field while the source Pokemon is still active - * @param {Pokemon} pokemon the Pokemon Imprison is applied to - * @returns `true` - */ - override activateTrap(pokemon: Pokemon): boolean { - const source = this.getSourcePokemon(); - if (source?.isActive(true) && pokemon.isAllowedInBattle()) { - pokemon.addTag(BattlerTagType.IMPRISON, 1, MoveId.IMPRISON, this.sourceId); - } - return true; - } - - /** - * When the arena tag is removed, it also attempts to remove any related Battler Tags if they haven't already been removed from the affected Pokemon - * @param arena - */ - override onRemove(): void { - const party = this.getAffectedPokemon(); - party.forEach(p => { - p.removeTag(BattlerTagType.IMPRISON); - }); - } -} - /** * Arena Tag implementing the "sea of fire" effect from the combination * of {@link https://bulbapedia.bulbagarden.net/wiki/Fire_Pledge_(move) | Fire Pledge} diff --git a/src/data/balance/biomes.ts b/src/data/balance/biomes.ts index d8297636393..c0c57cb15fb 100644 --- a/src/data/balance/biomes.ts +++ b/src/data/balance/biomes.ts @@ -7,6 +7,7 @@ import { TimeOfDay } from "#enums/time-of-day"; import { TrainerType } from "#enums/trainer-type"; import { randSeedInt } from "#utils/common"; import { getEnumValues } from "#utils/enums"; +import { toCamelCase } from "#utils/strings"; import i18next from "i18next"; export function getBiomeName(biome: BiomeId | -1) { @@ -15,13 +16,13 @@ export function getBiomeName(biome: BiomeId | -1) { } switch (biome) { case BiomeId.GRASS: - return i18next.t("biome:GRASS"); + return i18next.t("biome:grass"); case BiomeId.RUINS: - return i18next.t("biome:RUINS"); + return i18next.t("biome:ruins"); case BiomeId.END: - return i18next.t("biome:END"); + return i18next.t("biome:end"); default: - return i18next.t(`biome:${BiomeId[biome].toUpperCase()}`); + return i18next.t(`biome:${toCamelCase(BiomeId[biome])}`); } } diff --git a/src/data/balance/pokemon-evolutions.ts b/src/data/balance/pokemon-evolutions.ts index ab535682e86..a09c1e38eff 100644 --- a/src/data/balance/pokemon-evolutions.ts +++ b/src/data/balance/pokemon-evolutions.ts @@ -15,6 +15,7 @@ import type { Pokemon } from "#field/pokemon"; import type { SpeciesStatBoosterItem, SpeciesStatBoosterModifierType } from "#modifiers/modifier-type"; import { coerceArray, isNullOrUndefined, randSeedInt } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; +import { toCamelCase } from "#utils/strings"; import i18next from "i18next"; export enum SpeciesWildEvolutionDelay { @@ -133,11 +134,11 @@ export class SpeciesEvolutionCondition { case EvoCondKey.FRIENDSHIP: return i18next.t("pokemonEvolutions:friendship"); case EvoCondKey.TIME: - return i18next.t(`pokemonEvolutions:timeOfDay.${TimeOfDay[cond.time[cond.time.length - 1]]}`); // For Day and Night evos, the key we want goes last + return i18next.t(`pokemonEvolutions:timeOfDay.${toCamelCase(TimeOfDay[cond.time[cond.time.length - 1]])}`); // For Day and Night evos, the key we want goes last case EvoCondKey.MOVE_TYPE: - return i18next.t("pokemonEvolutions:moveType", {type: i18next.t(`pokemonInfo:Type.${PokemonType[cond.pkmnType]}`)}); + return i18next.t("pokemonEvolutions:moveType", {type: i18next.t(`pokemonInfo:type.${toCamelCase(PokemonType[cond.pkmnType])}`)}); case EvoCondKey.PARTY_TYPE: - return i18next.t("pokemonEvolutions:partyType", {type: i18next.t(`pokemonInfo:Type.${PokemonType[cond.pkmnType]}`)}); + return i18next.t("pokemonEvolutions:partyType", {type: i18next.t(`pokemonInfo:type.${toCamelCase(PokemonType[cond.pkmnType])}`)}); case EvoCondKey.GENDER: return i18next.t("pokemonEvolutions:gender", {gender: getGenderSymbol(cond.gender)}); case EvoCondKey.MOVE: @@ -156,7 +157,7 @@ export class SpeciesEvolutionCondition { case EvoCondKey.SPECIES_CAUGHT: return i18next.t("pokemonEvolutions:caught", {species: getPokemonSpecies(cond.speciesCaught).name}); case EvoCondKey.HELD_ITEM: - return i18next.t(`pokemonEvolutions:heldItem.${cond.itemKey}`); + return i18next.t(`pokemonEvolutions:heldItem.${toCamelCase(cond.itemKey)}`); } }).filter(s => !isNullOrUndefined(s)); // Filter out stringless conditions return this.desc; @@ -245,7 +246,7 @@ export class SpeciesFormEvolution { } if (this.item) { const itemDescription = i18next.t(`modifierType:EvolutionItem.${EvolutionItem[this.item].toUpperCase()}`); - const rarity = this.item > 50 ? i18next.t("pokemonEvolutions:ULTRA") : i18next.t("pokemonEvolutions:GREAT"); + const rarity = this.item > 50 ? i18next.t("pokemonEvolutions:ultra") : i18next.t("pokemonEvolutions:great"); strings.push(i18next.t("pokemonEvolutions:using", {item: itemDescription, tier: rarity})); } if (this.condition) { @@ -1866,17 +1867,16 @@ interface PokemonPrevolutions { export const pokemonPrevolutions: PokemonPrevolutions = {}; export function initPokemonPrevolutions(): void { - const megaFormKeys = [ SpeciesFormKey.MEGA, "", SpeciesFormKey.MEGA_X, "", SpeciesFormKey.MEGA_Y ].map(sfk => sfk as string); - const prevolutionKeys = Object.keys(pokemonEvolutions); - prevolutionKeys.forEach(pk => { - const evolutions = pokemonEvolutions[pk]; + // TODO: Why do we have empty strings in our array? + const megaFormKeys = [ SpeciesFormKey.MEGA, "", SpeciesFormKey.MEGA_X, "", SpeciesFormKey.MEGA_Y ]; + for (const [pk, evolutions] of Object.entries(pokemonEvolutions)) { for (const ev of evolutions) { if (ev.evoFormKey && megaFormKeys.indexOf(ev.evoFormKey) > -1) { continue; } pokemonPrevolutions[ev.speciesId] = Number.parseInt(pk) as SpeciesId; } - }); + } } diff --git a/src/data/balance/pokemon-species.ts b/src/data/balance/pokemon-species.ts index 13269308958..c6c17986257 100644 --- a/src/data/balance/pokemon-species.ts +++ b/src/data/balance/pokemon-species.ts @@ -1505,9 +1505,9 @@ export function initSpecies() { new PokemonForm("Hero of Many Battles", "hero-of-many-battles", PokemonType.FIGHTING, null, 2.9, 210, AbilityId.DAUNTLESS_SHIELD, AbilityId.NONE, AbilityId.NONE, 660, 92, 120, 115, 80, 115, 138, 10, 0, 335, false, "", true), new PokemonForm("Crowned", "crowned", PokemonType.FIGHTING, PokemonType.STEEL, 2.9, 785, AbilityId.DAUNTLESS_SHIELD, AbilityId.NONE, AbilityId.NONE, 700, 92, 120, 140, 80, 140, 128, 10, 0, 360) ), - new PokemonSpecies(SpeciesId.ETERNATUS, 8, false, true, false, "Gigantic Pokémon", PokemonType.POISON, PokemonType.DRAGON, 20, 950, AbilityId.PRESSURE, AbilityId.NONE, AbilityId.NONE, 690, 140, 85, 95, 145, 95, 130, 255, 0, 345, GrowthRate.SLOW, null, false, true, - new PokemonForm("Normal", "", PokemonType.POISON, PokemonType.DRAGON, 20, 950, AbilityId.PRESSURE, AbilityId.NONE, AbilityId.NONE, 690, 140, 85, 95, 145, 95, 130, 255, 0, 345, false, null, true), - new PokemonForm("E-Max", "eternamax", PokemonType.POISON, PokemonType.DRAGON, 100, 999.9, AbilityId.PRESSURE, AbilityId.NONE, AbilityId.NONE, 1125, 255, 115, 250, 125, 250, 130, 255, 0, 345) + new PokemonSpecies(SpeciesId.ETERNATUS, 8, false, true, false, "Gigantic Pokémon", PokemonType.POISON, PokemonType.DRAGON, 20, 950, AbilityId.PRESSURE, AbilityId.NONE, AbilityId.NONE, 690, 140, 85, 95, 145, 95, 130, 45, 0, 345, GrowthRate.SLOW, null, false, true, + new PokemonForm("Normal", "", PokemonType.POISON, PokemonType.DRAGON, 20, 950, AbilityId.PRESSURE, AbilityId.NONE, AbilityId.NONE, 690, 140, 85, 95, 145, 95, 130, 45, 0, 345, false, null, true), + new PokemonForm("E-Max", "eternamax", PokemonType.POISON, PokemonType.DRAGON, 100, 999.9, AbilityId.PRESSURE, AbilityId.NONE, AbilityId.NONE, 1125, 255, 115, 250, 125, 250, 130, 45, 0, 345) ), new PokemonSpecies(SpeciesId.KUBFU, 8, true, false, false, "Wushu Pokémon", PokemonType.FIGHTING, null, 0.6, 12, AbilityId.INNER_FOCUS, AbilityId.NONE, AbilityId.NONE, 385, 60, 90, 60, 53, 50, 72, 3, 50, 77, GrowthRate.SLOW, 87.5, false), new PokemonSpecies(SpeciesId.URSHIFU, 8, true, false, false, "Wushu Pokémon", PokemonType.FIGHTING, PokemonType.DARK, 1.9, 105, AbilityId.UNSEEN_FIST, AbilityId.NONE, AbilityId.NONE, 550, 100, 130, 100, 63, 60, 97, 3, 50, 275, GrowthRate.SLOW, 87.5, false, true, diff --git a/src/data/balance/tms.ts b/src/data/balance/tms.ts index e194dc4040c..bd7cf401ad1 100644 --- a/src/data/balance/tms.ts +++ b/src/data/balance/tms.ts @@ -45736,6 +45736,285 @@ export const tmSpecies: TmSpecies = { SpeciesId.HISUI_ARCANINE, SpeciesId.HISUI_AVALUGG, ], + [MoveId.SHOCK_WAVE]: [ + SpeciesId.RATTATA, + SpeciesId.RATICATE, + SpeciesId.PIKACHU, + SpeciesId.RAICHU, + SpeciesId.NIDORAN_F, + SpeciesId.NIDORINA, + SpeciesId.NIDOQUEEN, + SpeciesId.NIDORAN_M, + SpeciesId.NIDORINO, + SpeciesId.NIDOKING, + SpeciesId.CLEFAIRY, + SpeciesId.CLEFABLE, + SpeciesId.JIGGLYPUFF, + SpeciesId.WIGGLYTUFF, + SpeciesId.MEOWTH, + SpeciesId.PERSIAN, + SpeciesId.ABRA, + SpeciesId.KADABRA, + SpeciesId.ALAKAZAM, + SpeciesId.MAGNEMITE, + SpeciesId.MAGNETON, + SpeciesId.GRIMER, + SpeciesId.MUK, + SpeciesId.VOLTORB, + SpeciesId.ELECTRODE, + SpeciesId.LICKITUNG, + SpeciesId.KOFFING, + SpeciesId.WEEZING, + SpeciesId.RHYHORN, + SpeciesId.RHYDON, + SpeciesId.CHANSEY, + SpeciesId.TANGELA, + SpeciesId.KANGASKHAN, + SpeciesId.MR_MIME, + SpeciesId.ELECTABUZZ, + SpeciesId.TAUROS, + SpeciesId.LAPRAS, + SpeciesId.JOLTEON, + SpeciesId.PORYGON, + SpeciesId.SNORLAX, + SpeciesId.ZAPDOS, + SpeciesId.DRATINI, + SpeciesId.DRAGONAIR, + SpeciesId.DRAGONITE, + SpeciesId.MEWTWO, + SpeciesId.MEW, + SpeciesId.SENTRET, + SpeciesId.FURRET, + SpeciesId.CHINCHOU, + SpeciesId.LANTURN, + SpeciesId.PICHU, + SpeciesId.CLEFFA, + SpeciesId.IGGLYBUFF, + SpeciesId.TOGEPI, + SpeciesId.TOGETIC, + SpeciesId.MAREEP, + SpeciesId.FLAAFFY, + SpeciesId.AMPHAROS, + SpeciesId.AIPOM, + SpeciesId.MISDREAVUS, + SpeciesId.GIRAFARIG, + SpeciesId.DUNSPARCE, + SpeciesId.SNUBBULL, + SpeciesId.GRANBULL, + SpeciesId.QWILFISH, + SpeciesId.PORYGON2, + SpeciesId.STANTLER, + SpeciesId.ELEKID, + SpeciesId.MILTANK, + SpeciesId.BLISSEY, + SpeciesId.RAIKOU, + SpeciesId.TYRANITAR, + SpeciesId.LUGIA, + SpeciesId.HO_OH, + SpeciesId.CELEBI, + SpeciesId.ZIGZAGOON, + SpeciesId.LINOONE, + SpeciesId.WINGULL, + SpeciesId.PELIPPER, + SpeciesId.RALTS, + SpeciesId.KIRLIA, + SpeciesId.GARDEVOIR, + SpeciesId.SLAKOTH, + SpeciesId.VIGOROTH, + SpeciesId.SLAKING, + SpeciesId.WHISMUR, + SpeciesId.LOUDRED, + SpeciesId.EXPLOUD, + SpeciesId.NOSEPASS, + SpeciesId.SKITTY, + SpeciesId.DELCATTY, + SpeciesId.SABLEYE, + SpeciesId.ARON, + SpeciesId.LAIRON, + SpeciesId.AGGRON, + SpeciesId.ELECTRIKE, + SpeciesId.MANECTRIC, + SpeciesId.PLUSLE, + SpeciesId.MINUN, + SpeciesId.VOLBEAT, + SpeciesId.ILLUMISE, + SpeciesId.GULPIN, + SpeciesId.SWALOT, + SpeciesId.SPOINK, + SpeciesId.GRUMPIG, + SpeciesId.SPINDA, + SpeciesId.ZANGOOSE, + SpeciesId.CASTFORM, + SpeciesId.KECLEON, + SpeciesId.SHUPPET, + SpeciesId.BANETTE, + SpeciesId.CHIMECHO, + SpeciesId.ABSOL, + SpeciesId.REGIROCK, + SpeciesId.REGICE, + SpeciesId.REGISTEEL, + SpeciesId.LATIAS, + SpeciesId.LATIOS, + SpeciesId.KYOGRE, + SpeciesId.GROUDON, + SpeciesId.RAYQUAZA, + SpeciesId.JIRACHI, + SpeciesId.DEOXYS, + SpeciesId.BIDOOF, + SpeciesId.BIBAREL, + SpeciesId.SHINX, + SpeciesId.LUXIO, + SpeciesId.LUXRAY, + SpeciesId.CRANIDOS, + SpeciesId.RAMPARDOS, + SpeciesId.SHIELDON, + SpeciesId.BASTIODON, + SpeciesId.PACHIRISU, + SpeciesId.AMBIPOM, + SpeciesId.DRIFLOON, + SpeciesId.DRIFBLIM, + SpeciesId.BUNEARY, + SpeciesId.LOPUNNY, + SpeciesId.MISMAGIUS, + SpeciesId.GLAMEOW, + SpeciesId.PURUGLY, + SpeciesId.CHINGLING, + SpeciesId.MIME_JR, + SpeciesId.HAPPINY, + SpeciesId.SPIRITOMB, + SpeciesId.MUNCHLAX, + SpeciesId.MAGNEZONE, + SpeciesId.LICKILICKY, + SpeciesId.RHYPERIOR, + SpeciesId.TANGROWTH, + SpeciesId.ELECTIVIRE, + SpeciesId.TOGEKISS, + SpeciesId.PORYGON_Z, + SpeciesId.GALLADE, + SpeciesId.PROBOPASS, + SpeciesId.FROSLASS, + SpeciesId.ROTOM, + SpeciesId.UXIE, + SpeciesId.MESPRIT, + SpeciesId.AZELF, + SpeciesId.DIALGA, + SpeciesId.PALKIA, + SpeciesId.REGIGIGAS, + SpeciesId.GIRATINA, + SpeciesId.DARKRAI, + SpeciesId.ARCEUS, + SpeciesId.VICTINI, + SpeciesId.PATRAT, + SpeciesId.WATCHOG, + SpeciesId.LILLIPUP, + SpeciesId.HERDIER, + SpeciesId.STOUTLAND, + SpeciesId.MUNNA, + SpeciesId.MUSHARNA, + SpeciesId.BLITZLE, + SpeciesId.ZEBSTRIKA, + SpeciesId.WOOBAT, + SpeciesId.SWOOBAT, + SpeciesId.SIGILYPH, + SpeciesId.YAMASK, + SpeciesId.COFAGRIGUS, + SpeciesId.MINCCINO, + SpeciesId.CINCCINO, + SpeciesId.GOTHITA, + SpeciesId.GOTHORITA, + SpeciesId.GOTHITELLE, + SpeciesId.SOLOSIS, + SpeciesId.DUOSION, + SpeciesId.REUNICLUS, + SpeciesId.EMOLGA, + SpeciesId.FRILLISH, + SpeciesId.JELLICENT, + SpeciesId.JOLTIK, + SpeciesId.GALVANTULA, + SpeciesId.KLINK, + SpeciesId.KLANG, + SpeciesId.KLINKLANG, + SpeciesId.EELEKTRIK, + SpeciesId.EELEKTROSS, + SpeciesId.ELGYEM, + SpeciesId.BEHEEYEM, + SpeciesId.LITWICK, + SpeciesId.LAMPENT, + SpeciesId.CHANDELURE, + SpeciesId.AXEW, + SpeciesId.FRAXURE, + SpeciesId.HAXORUS, + SpeciesId.STUNFISK, + SpeciesId.DRUDDIGON, + SpeciesId.GOLETT, + SpeciesId.GOLURK, + SpeciesId.DEINO, + SpeciesId.ZWEILOUS, + SpeciesId.HYDREIGON, + SpeciesId.THUNDURUS, + SpeciesId.ZEKROM, + SpeciesId.MELOETTA, + SpeciesId.GENESECT, + SpeciesId.BRAIXEN, + SpeciesId.DELPHOX, + SpeciesId.ESPURR, + SpeciesId.MEOWSTIC, + SpeciesId.HONEDGE, + SpeciesId.DOUBLADE, + SpeciesId.AEGISLASH, + SpeciesId.SKRELP, + SpeciesId.DRAGALGE, + SpeciesId.HELIOPTILE, + SpeciesId.HELIOLISK, + SpeciesId.DEDENNE, + SpeciesId.GOOMY, + SpeciesId.SLIGGOO, + SpeciesId.GOODRA, + SpeciesId.ZYGARDE, + SpeciesId.HOOPA, + SpeciesId.YUNGOOS, + SpeciesId.GUMSHOOS, + SpeciesId.GRUBBIN, + SpeciesId.CHARJABUG, + SpeciesId.VIKAVOLT, + SpeciesId.PASSIMIAN, + SpeciesId.TURTONATOR, + SpeciesId.TOGEDEMARU, + SpeciesId.DRAMPA, + SpeciesId.KOMMO_O, + SpeciesId.TAPU_KOKO, + SpeciesId.SOLGALEO, + SpeciesId.LUNALA, + SpeciesId.PHEROMOSA, + SpeciesId.XURKITREE, + SpeciesId.CELESTEELA, + SpeciesId.GUZZLORD, + SpeciesId.NECROZMA, + SpeciesId.MAGEARNA, + SpeciesId.NAGANADEL, + SpeciesId.ZERAORA, + SpeciesId.TOXTRICITY, + SpeciesId.MR_RIME, + SpeciesId.REGIELEKI, + SpeciesId.WYRDEER, + SpeciesId.FARIGIRAF, + SpeciesId.DUDUNSPARCE, + SpeciesId.MIRAIDON, + SpeciesId.RAGING_BOLT, + SpeciesId.ALOLA_RATTATA, + SpeciesId.ALOLA_RATICATE, + SpeciesId.ALOLA_RAICHU, + SpeciesId.ALOLA_MEOWTH, + SpeciesId.ALOLA_PERSIAN, + SpeciesId.ALOLA_GRAVELER, + SpeciesId.ALOLA_GOLEM, + SpeciesId.ALOLA_GRIMER, + SpeciesId.ALOLA_MUK, + SpeciesId.GALAR_WEEZING, + SpeciesId.GALAR_MR_MIME, + SpeciesId.HISUI_SLIGGOO, + SpeciesId.HISUI_GOODRA, + ], [MoveId.WATER_PULSE]: [ SpeciesId.SQUIRTLE, SpeciesId.WARTORTLE, @@ -68747,6 +69026,7 @@ export const tmPoolTiers: TmPoolTiers = { [MoveId.LEAF_BLADE]: ModifierTier.ULTRA, [MoveId.DRAGON_DANCE]: ModifierTier.GREAT, [MoveId.ROCK_BLAST]: ModifierTier.GREAT, + [MoveId.SHOCK_WAVE]: ModifierTier.GREAT, [MoveId.WATER_PULSE]: ModifierTier.GREAT, [MoveId.ROOST]: ModifierTier.GREAT, [MoveId.GRAVITY]: ModifierTier.COMMON, diff --git a/src/data/battle-anims.ts b/src/data/battle-anims.ts index 55a3cc4e916..aa4951f3263 100644 --- a/src/data/battle-anims.ts +++ b/src/data/battle-anims.ts @@ -404,22 +404,18 @@ export const chargeAnims = new Map(); export const encounterAnims = new Map(); -export function initCommonAnims(): Promise { - return new Promise(resolve => { - const commonAnimNames = getEnumKeys(CommonAnim); - const commonAnimIds = getEnumValues(CommonAnim); - const commonAnimFetches: Promise>[] = []; - for (let ca = 0; ca < commonAnimIds.length; ca++) { - const commonAnimId = commonAnimIds[ca]; - commonAnimFetches.push( - globalScene - .cachedFetch(`./battle-anims/common-${toKebabCase(commonAnimNames[ca])}.json`) - .then(response => response.json()) - .then(cas => commonAnims.set(commonAnimId, new AnimConfig(cas))), - ); - } - Promise.allSettled(commonAnimFetches).then(() => resolve()); - }); +export async function initCommonAnims(): Promise { + const commonAnimFetches: Promise>[] = []; + for (const commonAnimName of getEnumKeys(CommonAnim)) { + const commonAnimId = CommonAnim[commonAnimName]; + commonAnimFetches.push( + globalScene + .cachedFetch(`./battle-anims/common-${toKebabCase(commonAnimName)}.json`) + .then(response => response.json()) + .then(cas => commonAnims.set(commonAnimId, new AnimConfig(cas))), + ); + } + await Promise.allSettled(commonAnimFetches); } export function initMoveAnim(move: MoveId): Promise { @@ -1396,279 +1392,3 @@ export class EncounterBattleAnim extends BattleAnim { return this.oppAnim; } } - -export async function populateAnims() { - const commonAnimNames = getEnumKeys(CommonAnim).map(k => k.toLowerCase()); - const commonAnimMatchNames = commonAnimNames.map(k => k.replace(/_/g, "")); - const commonAnimIds = getEnumValues(CommonAnim); - const chargeAnimNames = getEnumKeys(ChargeAnim).map(k => k.toLowerCase()); - const chargeAnimMatchNames = chargeAnimNames.map(k => k.replace(/_/g, " ")); - const chargeAnimIds = getEnumValues(ChargeAnim); - const commonNamePattern = /name: (?:Common:)?(Opp )?(.*)/; - const moveNameToId = {}; - // Exclude MoveId.NONE; - for (const move of getEnumValues(MoveId).slice(1)) { - // KARATE_CHOP => KARATECHOP - const moveName = MoveId[move].toUpperCase().replace(/_/g, ""); - moveNameToId[moveName] = move; - } - - const seNames: string[] = []; //(await fs.readdir('./public/audio/se/battle_anims/')).map(se => se.toString()); - - const animsData: any[] = []; //battleAnimRawData.split('!ruby/array:PBAnimation').slice(1); // TODO: add a proper type - for (let a = 0; a < animsData.length; a++) { - const fields = animsData[a].split("@").slice(1); - - const nameField = fields.find(f => f.startsWith("name: ")); - - let isOppMove: boolean | undefined; - let commonAnimId: CommonAnim | undefined; - let chargeAnimId: ChargeAnim | undefined; - if (!nameField.startsWith("name: Move:") && !(isOppMove = nameField.startsWith("name: OppMove:"))) { - const nameMatch = commonNamePattern.exec(nameField)!; // TODO: is this bang correct? - const name = nameMatch[2].toLowerCase(); - if (commonAnimMatchNames.indexOf(name) > -1) { - commonAnimId = commonAnimIds[commonAnimMatchNames.indexOf(name)]; - } else if (chargeAnimMatchNames.indexOf(name) > -1) { - isOppMove = nameField.startsWith("name: Opp "); - chargeAnimId = chargeAnimIds[chargeAnimMatchNames.indexOf(name)]; - } - } - const nameIndex = nameField.indexOf(":", 5) + 1; - const animName = nameField.slice(nameIndex, nameField.indexOf("\n", nameIndex)); - if (!moveNameToId.hasOwnProperty(animName) && !commonAnimId && !chargeAnimId) { - continue; - } - const anim = commonAnimId || chargeAnimId ? new AnimConfig() : new AnimConfig(); - if (anim instanceof AnimConfig) { - (anim as AnimConfig).id = moveNameToId[animName]; - } - if (commonAnimId) { - commonAnims.set(commonAnimId, anim); - } else if (chargeAnimId) { - chargeAnims.set(chargeAnimId, !isOppMove ? anim : [chargeAnims.get(chargeAnimId) as AnimConfig, anim]); - } else { - moveAnims.set( - moveNameToId[animName], - !isOppMove ? (anim as AnimConfig) : [moveAnims.get(moveNameToId[animName]) as AnimConfig, anim as AnimConfig], - ); - } - for (let f = 0; f < fields.length; f++) { - const field = fields[f]; - const fieldName = field.slice(0, field.indexOf(":")); - const fieldData = field.slice(fieldName.length + 1, field.lastIndexOf("\n")).trim(); - switch (fieldName) { - case "array": { - const framesData = fieldData.split(" - - - ").slice(1); - for (let fd = 0; fd < framesData.length; fd++) { - anim.frames.push([]); - 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 targetFrame = new AnimFrame( - Number.parseFloat(values[0]), - Number.parseFloat(values[1]), - Number.parseFloat(values[2]), - Number.parseFloat(values[11]), - Number.parseFloat(values[3]), - Number.parseInt(values[4]) === 1, - Number.parseInt(values[6]) === 1, - Number.parseInt(values[5]), - Number.parseInt(values[7]), - Number.parseInt(values[8]), - Number.parseInt(values[12]), - Number.parseInt(values[13]), - Number.parseInt(values[14]), - Number.parseInt(values[15]), - Number.parseInt(values[16]), - Number.parseInt(values[17]), - Number.parseInt(values[18]), - Number.parseInt(values[19]), - Number.parseInt(values[21]), - Number.parseInt(values[22]), - Number.parseInt(values[23]), - Number.parseInt(values[24]), - Number.parseInt(values[20]) === 1, - Number.parseInt(values[25]), - Number.parseInt(values[26]) as AnimFocus, - ); - anim.frames[fd].push(targetFrame); - } - } - break; - } - case "graphic": { - const graphic = fieldData !== "''" ? fieldData : ""; - anim.graphic = graphic.indexOf(".") > -1 ? graphic.slice(0, fieldData.indexOf(".")) : graphic; - break; - } - case "timing": { - const timingEntries = fieldData.split("- !ruby/object:PBAnimTiming ").slice(1); - for (let t = 0; t < timingEntries.length; t++) { - const timingData = timingEntries[t] - .replace(/\n/g, " ") - .replace(/[ ]{2,}/g, " ") - .replace(/[a-z]+: ! '', /gi, "") - .replace(/name: (.*?),/, 'name: "$1",') - .replace( - /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? - let resourceName = /name: "(.*?)"/.exec(timingData)![1].replace("''", ""); // TODO: is the bang correct? - const timingType = Number.parseInt(/timingType: (\d)/.exec(timingData)![1]); // TODO: is the bang correct? - let timedEvent: AnimTimedEvent | undefined; - switch (timingType) { - case 0: - if (resourceName && resourceName.indexOf(".") === -1) { - let ext: string | undefined; - ["wav", "mp3", "m4a"].every(e => { - if (seNames.indexOf(`${resourceName}.${e}`) > -1) { - ext = e; - return false; - } - return true; - }); - if (!ext) { - ext = ".wav"; - } - resourceName += `.${ext}`; - } - timedEvent = new AnimTimedSoundEvent(frameIndex, resourceName); - break; - case 1: - timedEvent = new AnimTimedAddBgEvent(frameIndex, resourceName.slice(0, resourceName.indexOf("."))); - break; - case 2: - timedEvent = new AnimTimedUpdateBgEvent(frameIndex, resourceName.slice(0, resourceName.indexOf("."))); - break; - } - if (!timedEvent) { - continue; - } - const propPattern = /([a-z]+): (.*?)(?:,|\})/gi; - let propMatch: RegExpExecArray; - while ((propMatch = propPattern.exec(timingData)!)) { - // TODO: is this bang correct? - const prop = propMatch[1]; - let value: any = propMatch[2]; - switch (prop) { - case "bgX": - case "bgY": - value = Number.parseFloat(value); - break; - case "volume": - case "pitch": - case "opacity": - case "colorRed": - case "colorGreen": - case "colorBlue": - case "colorAlpha": - case "duration": - case "flashScope": - case "flashRed": - case "flashGreen": - case "flashBlue": - case "flashAlpha": - case "flashDuration": - value = Number.parseInt(value); - break; - } - if (timedEvent.hasOwnProperty(prop)) { - timedEvent[prop] = value; - } - } - if (!anim.frameTimedEvents.has(frameIndex)) { - anim.frameTimedEvents.set(frameIndex, []); - } - anim.frameTimedEvents.get(frameIndex)!.push(timedEvent); // TODO: is this bang correct? - } - break; - } - case "position": - anim.position = Number.parseInt(fieldData); - break; - case "hue": - anim.hue = Number.parseInt(fieldData); - break; - } - } - } - - // biome-ignore lint/correctness/noUnusedVariables: used in commented code - const animReplacer = (k, v) => { - if (k === "id" && !v) { - return undefined; - } - if (v instanceof Map) { - return Object.fromEntries(v); - } - if (v instanceof AnimTimedEvent) { - v["eventType"] = v.getEventType(); - } - return v; - }; - - const animConfigProps = ["id", "graphic", "frames", "frameTimedEvents", "position", "hue"]; - const animFrameProps = [ - "x", - "y", - "zoomX", - "zoomY", - "angle", - "mirror", - "visible", - "blendType", - "target", - "graphicFrame", - "opacity", - "color", - "tone", - "flash", - "locked", - "priority", - "focus", - ]; - const propSets = [animConfigProps, animFrameProps]; - - // biome-ignore lint/correctness/noUnusedVariables: used in commented code - const animComparator = (a: Element, b: Element) => { - let props: string[]; - for (let p = 0; p < propSets.length; p++) { - props = propSets[p]; - // @ts-expect-error TODO - const ai = props.indexOf(a.key); - if (ai === -1) { - continue; - } - // @ts-expect-error TODO - const bi = props.indexOf(b.key); - - return ai < bi ? -1 : ai > bi ? 1 : 0; - } - - return 0; - }; - - /*for (let ma of moveAnims.keys()) { - const data = moveAnims.get(ma); - (async () => { - await fs.writeFile(`../public/battle-anims/${Moves[ma].toLowerCase().replace(/\_/g, '-')}.json`, stringify(data, { replacer: animReplacer, cmp: animComparator, space: ' ' })); - })(); - } - - for (let ca of chargeAnims.keys()) { - const data = chargeAnims.get(ca); - (async () => { - await fs.writeFile(`../public/battle-anims/${chargeAnimNames[chargeAnimIds.indexOf(ca)].replace(/\_/g, '-')}.json`, stringify(data, { replacer: animReplacer, cmp: animComparator, space: ' ' })); - })(); - } - - for (let cma of commonAnims.keys()) { - const data = commonAnims.get(cma); - (async () => { - await fs.writeFile(`../public/battle-anims/common-${commonAnimNames[commonAnimIds.indexOf(cma)].replace(/\_/g, '-')}.json`, stringify(data, { replacer: animReplacer, cmp: animComparator, space: ' ' })); - })(); - }*/ -} diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index aa98e723767..904ab6ea060 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -52,6 +52,7 @@ import type { } from "#types/battler-tags"; import type { Mutable } from "#types/type-helpers"; import { BooleanHolder, coerceArray, getFrameMs, isNullOrUndefined, NumberHolder, toDmgValue } from "#utils/common"; +import { toCamelCase } from "#utils/strings"; /** * @module @@ -566,7 +567,7 @@ export class BeakBlastChargingTag extends BattlerTag { target: pokemon, }) ) { - phaseData.attacker.trySetStatus(StatusEffect.BURN, true, pokemon); + phaseData.attacker.trySetStatus(StatusEffect.BURN, pokemon); } return true; } @@ -1543,6 +1544,7 @@ export class DrowsyTag extends SerializableBattlerTag { if (!super.lapse(pokemon, lapseType)) { // TODO: Safeguard should not prevent yawn from setting sleep after tag use pokemon.trySetStatus(StatusEffect.SLEEP, true); + pokemon.trySetStatus(StatusEffect.SLEEP); return false; } @@ -1892,7 +1894,7 @@ export class ContactSetStatusProtectedTag extends DamageProtectedTag { * @param user - The pokemon that is being attacked and has the tag */ override onContact(attacker: Pokemon, user: Pokemon): void { - attacker.trySetStatus(this.#statusEffect, true, user); + attacker.trySetStatus(this.#statusEffect, user); } } @@ -2338,7 +2340,7 @@ export class TypeBoostTag extends SerializableBattlerTag { globalScene.phaseManager.queueMessage( i18next.t("abilityTriggers:typeImmunityPowerBoost", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - typeName: i18next.t(`pokemonInfo:Type.${PokemonType[this.boostedType]}`), + typeName: i18next.t(`pokemonInfo:type.${toCamelCase(PokemonType[this.boostedType])}`), }), ); } @@ -2836,7 +2838,7 @@ export class GulpMissileTag extends SerializableBattlerTag { if (this.tagType === BattlerTagType.GULP_MISSILE_ARROKUDA) { globalScene.phaseManager.unshiftNew("StatStageChangePhase", attacker.getBattlerIndex(), false, [Stat.DEF], -1); } else { - attacker.trySetStatus(StatusEffect.PARALYSIS, true, pokemon); + attacker.trySetStatus(StatusEffect.PARALYSIS, pokemon); } } return false; diff --git a/src/data/berry.ts b/src/data/berry.ts index 61235b75e21..762423799fe 100644 --- a/src/data/berry.ts +++ b/src/data/berry.ts @@ -11,11 +11,11 @@ import { NumberHolder, randSeedInt, toDmgValue } from "#utils/common"; import i18next from "i18next"; export function getBerryName(berryType: BerryType): string { - return i18next.t(`berry:${BerryType[berryType]}.name`); + return i18next.t(`berry:${BerryType[berryType].toLowerCase()}.name`); } export function getBerryEffectDescription(berryType: BerryType): string { - return i18next.t(`berry:${BerryType[berryType]}.effect`); + return i18next.t(`berry:${BerryType[berryType].toLowerCase()}.effect`); } export type BerryPredicate = (pokemon: Pokemon) => boolean; diff --git a/src/data/challenge.ts b/src/data/challenge.ts index 724d1f302da..98b7878c42b 100644 --- a/src/data/challenge.ts +++ b/src/data/challenge.ts @@ -20,6 +20,7 @@ import { Trainer } from "#field/trainer"; import type { ModifierTypeOption } from "#modifiers/modifier-type"; import { PokemonMove } from "#moves/pokemon-move"; import type { DexAttrProps, GameData } from "#system/game-data"; +import { RibbonData, type RibbonFlag } from "#system/ribbons/ribbon-data"; import { type BooleanHolder, isBetween, type NumberHolder, randSeedItem } from "#utils/common"; import { deepCopy } from "#utils/data"; import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils"; @@ -42,6 +43,15 @@ export abstract class Challenge { public conditions: ChallengeCondition[]; + /** + * The Ribbon awarded on challenge completion, or 0 if the challenge has no ribbon or is not enabled + * + * @defaultValue 0 + */ + public get ribbonAwarded(): RibbonFlag { + return 0n as RibbonFlag; + } + /** * @param id {@link Challenges} The enum value for the challenge */ @@ -423,6 +433,12 @@ type ChallengeCondition = (data: GameData) => boolean; * Implements a mono generation challenge. */ export class SingleGenerationChallenge extends Challenge { + public override get ribbonAwarded(): RibbonFlag { + // NOTE: This logic will not work for the eventual mono gen 10 ribbon, as + // as its flag will not be in sequence with the other mono gen ribbons. + return this.value ? ((RibbonData.MONO_GEN_1 << (BigInt(this.value) - 1n)) as RibbonFlag) : 0n; + } + constructor() { super(Challenges.SINGLE_GENERATION, 9); } @@ -658,10 +674,10 @@ export class SingleGenerationChallenge extends Challenge { getDescription(overrideValue: number = this.value): string { if (overrideValue === 0) { - return i18next.t("challenges:singleGeneration.desc_default"); + return i18next.t("challenges:singleGeneration.descDefault"); } return i18next.t("challenges:singleGeneration.desc", { - gen: i18next.t(`challenges:singleGeneration.gen_${overrideValue}`), + gen: i18next.t(`challenges:singleGeneration.gen.${overrideValue}`), }); } @@ -686,6 +702,12 @@ interface monotypeOverride { * Implements a mono type challenge. */ export class SingleTypeChallenge extends Challenge { + public override get ribbonAwarded(): RibbonFlag { + // `this.value` represents the 1-based index of pokemon type + // `RibbonData.MONO_NORMAL` starts the flag position for the types, + // and we shift it by 1 for the specific type. + return this.value ? ((RibbonData.MONO_NORMAL << (BigInt(this.value) - 1n)) as RibbonFlag) : 0n; + } private static TYPE_OVERRIDES: monotypeOverride[] = [ { species: SpeciesId.CASTFORM, type: PokemonType.NORMAL, fusion: false }, ]; @@ -734,9 +756,9 @@ export class SingleTypeChallenge extends Challenge { } getDescription(overrideValue: number = this.value): string { - const type = i18next.t(`pokemonInfo:Type.${PokemonType[overrideValue - 1]}`); + const type = i18next.t(`pokemonInfo:type.${toCamelCase(PokemonType[overrideValue - 1])}`); const typeColor = `[color=${TypeColor[PokemonType[overrideValue - 1]]}][shadow=${TypeShadow[PokemonType[this.value - 1]]}]${type}[/shadow][/color]`; - const defaultDesc = i18next.t("challenges:singleType.desc_default"); + const defaultDesc = i18next.t("challenges:singleType.descDefault"); const typeDesc = i18next.t("challenges:singleType.desc", { type: typeColor, }); @@ -755,6 +777,9 @@ export class SingleTypeChallenge extends Challenge { * Implements a fresh start challenge. */ export class FreshStartChallenge extends Challenge { + public override get ribbonAwarded(): RibbonFlag { + return this.value ? RibbonData.FRESH_START : 0n; + } constructor() { super(Challenges.FRESH_START, 2); } @@ -828,6 +853,9 @@ export class FreshStartChallenge extends Challenge { * Implements an inverse battle challenge. */ export class InverseBattleChallenge extends Challenge { + public override get ribbonAwarded(): RibbonFlag { + return this.value ? RibbonData.INVERSE : 0n; + } constructor() { super(Challenges.INVERSE_BATTLE, 1); } @@ -861,6 +889,9 @@ export class InverseBattleChallenge extends Challenge { * Implements a flip stat challenge. */ export class FlipStatChallenge extends Challenge { + public override get ribbonAwarded(): RibbonFlag { + return this.value ? RibbonData.FLIP_STATS : 0n; + } constructor() { super(Challenges.FLIP_STAT, 1); } @@ -941,6 +972,9 @@ export class LowerStarterPointsChallenge extends Challenge { * Implements a No Support challenge */ export class LimitedSupportChallenge extends Challenge { + public override get ribbonAwarded(): RibbonFlag { + return this.value ? ((RibbonData.NO_HEAL << (BigInt(this.value) - 1n)) as RibbonFlag) : 0n; + } constructor() { super(Challenges.LIMITED_SUPPORT, 3); } @@ -973,6 +1007,9 @@ export class LimitedSupportChallenge extends Challenge { * Implements a Limited Catch challenge */ export class LimitedCatchChallenge extends Challenge { + public override get ribbonAwarded(): RibbonFlag { + return this.value ? RibbonData.LIMITED_CATCH : 0n; + } constructor() { super(Challenges.LIMITED_CATCH, 1); } @@ -997,6 +1034,9 @@ export class LimitedCatchChallenge extends Challenge { * Implements a Permanent Faint challenge */ export class HardcoreChallenge extends Challenge { + public override get ribbonAwarded(): RibbonFlag { + return this.value ? RibbonData.HARDCORE : 0n; + } constructor() { super(Challenges.HARDCORE, 1); } diff --git a/src/data/dialogue.ts b/src/data/dialogue.ts index 361d005e83b..1d672397777 100644 --- a/src/data/dialogue.ts +++ b/src/data/dialogue.ts @@ -85,20 +85,16 @@ export const trainerTypeDialogue: TrainerTypeDialogue = { }, { encounter: [ - "dialogue:breeder_female.encounter.1", - "dialogue:breeder_female.encounter.2", - "dialogue:breeder_female.encounter.3", + "dialogue:breederFemale.encounter.1", + "dialogue:breederFemale.encounter.2", + "dialogue:breederFemale.encounter.3", ], victory: [ - "dialogue:breeder_female.victory.1", - "dialogue:breeder_female.victory.2", - "dialogue:breeder_female.victory.3", - ], - defeat: [ - "dialogue:breeder_female.defeat.1", - "dialogue:breeder_female.defeat.2", - "dialogue:breeder_female.defeat.3", + "dialogue:breederFemale.victory.1", + "dialogue:breederFemale.victory.2", + "dialogue:breederFemale.victory.3", ], + defeat: ["dialogue:breederFemale.defeat.1", "dialogue:breederFemale.defeat.2", "dialogue:breederFemale.defeat.3"], }, ], [TrainerType.FISHERMAN]: [ @@ -108,14 +104,14 @@ export const trainerTypeDialogue: TrainerTypeDialogue = { }, { encounter: [ - "dialogue:fisherman_female.encounter.1", - "dialogue:fisherman_female.encounter.2", - "dialogue:fisherman_female.encounter.3", + "dialogue:fishermanFemale.encounter.1", + "dialogue:fishermanFemale.encounter.2", + "dialogue:fishermanFemale.encounter.3", ], victory: [ - "dialogue:fisherman_female.victory.1", - "dialogue:fisherman_female.victory.2", - "dialogue:fisherman_female.victory.3", + "dialogue:fishermanFemale.victory.1", + "dialogue:fishermanFemale.victory.2", + "dialogue:fishermanFemale.victory.3", ], }, ], @@ -144,29 +140,29 @@ export const trainerTypeDialogue: TrainerTypeDialogue = { [TrainerType.ACE_TRAINER]: [ { encounter: [ - "dialogue:ace_trainer.encounter.1", - "dialogue:ace_trainer.encounter.2", - "dialogue:ace_trainer.encounter.3", - "dialogue:ace_trainer.encounter.4", + "dialogue:aceTrainer.encounter.1", + "dialogue:aceTrainer.encounter.2", + "dialogue:aceTrainer.encounter.3", + "dialogue:aceTrainer.encounter.4", ], victory: [ - "dialogue:ace_trainer.victory.1", - "dialogue:ace_trainer.victory.2", - "dialogue:ace_trainer.victory.3", - "dialogue:ace_trainer.victory.4", + "dialogue:aceTrainer.victory.1", + "dialogue:aceTrainer.victory.2", + "dialogue:aceTrainer.victory.3", + "dialogue:aceTrainer.victory.4", ], defeat: [ - "dialogue:ace_trainer.defeat.1", - "dialogue:ace_trainer.defeat.2", - "dialogue:ace_trainer.defeat.3", - "dialogue:ace_trainer.defeat.4", + "dialogue:aceTrainer.defeat.1", + "dialogue:aceTrainer.defeat.2", + "dialogue:aceTrainer.defeat.3", + "dialogue:aceTrainer.defeat.4", ], }, ], [TrainerType.PARASOL_LADY]: [ { - encounter: ["dialogue:parasol_lady.encounter.1"], - victory: ["dialogue:parasol_lady.victory.1"], + encounter: ["dialogue:parasolLady.encounter.1"], + victory: ["dialogue:parasolLady.victory.1"], }, ], [TrainerType.TWINS]: [ @@ -184,13 +180,13 @@ export const trainerTypeDialogue: TrainerTypeDialogue = { ], [TrainerType.BLACK_BELT]: [ { - encounter: ["dialogue:black_belt.encounter.1", "dialogue:black_belt.encounter.2"], - victory: ["dialogue:black_belt.victory.1", "dialogue:black_belt.victory.2"], + encounter: ["dialogue:blackBelt.encounter.1", "dialogue:blackBelt.encounter.2"], + victory: ["dialogue:blackBelt.victory.1", "dialogue:blackBelt.victory.2"], }, //BATTLE GIRL { - encounter: ["dialogue:battle_girl.encounter.1"], - victory: ["dialogue:battle_girl.victory.1"], + encounter: ["dialogue:battleGirl.encounter.1"], + victory: ["dialogue:battleGirl.victory.1"], }, ], [TrainerType.HIKER]: [ @@ -214,8 +210,8 @@ export const trainerTypeDialogue: TrainerTypeDialogue = { ], [TrainerType.SCHOOL_KID]: [ { - encounter: ["dialogue:school_kid.encounter.1", "dialogue:school_kid.encounter.2"], - victory: ["dialogue:school_kid.victory.1", "dialogue:school_kid.victory.2"], + encounter: ["dialogue:schoolKid.encounter.1", "dialogue:schoolKid.encounter.2"], + victory: ["dialogue:schoolKid.victory.1", "dialogue:schoolKid.victory.2"], }, ], [TrainerType.ARTIST]: [ @@ -236,31 +232,31 @@ export const trainerTypeDialogue: TrainerTypeDialogue = { victory: ["dialogue:worker.victory.1"], }, { - encounter: ["dialogue:worker_female.encounter.1"], - victory: ["dialogue:worker_female.victory.1"], - defeat: ["dialogue:worker_female.defeat.1"], + encounter: ["dialogue:workerFemale.encounter.1"], + victory: ["dialogue:workerFemale.victory.1"], + defeat: ["dialogue:workerFemale.defeat.1"], }, { - encounter: ["dialogue:worker_double.encounter.1"], - victory: ["dialogue:worker_double.victory.1"], + encounter: ["dialogue:workerDouble.encounter.1"], + victory: ["dialogue:workerDouble.victory.1"], }, ], // Defeat dialogue in the language .JSONS exist as translated or placeholders; (en, fr, it, es, de, ja, ko, zh_cn, zh_tw, pt_br) [TrainerType.SNOW_WORKER]: [ { - encounter: ["dialogue:snow_worker.encounter.1"], - victory: ["dialogue:snow_worker.victory.1"], + encounter: ["dialogue:snowWorker.encounter.1"], + victory: ["dialogue:snowWorker.victory.1"], }, { - encounter: ["dialogue:snow_worker_double.encounter.1"], - victory: ["dialogue:snow_worker_double.victory.1"], + encounter: ["dialogue:snowWorkerDouble.encounter.1"], + victory: ["dialogue:snowWorkerDouble.victory.1"], }, ], [TrainerType.HEX_MANIAC]: [ { - encounter: ["dialogue:hex_maniac.encounter.1", "dialogue:hex_maniac.encounter.2"], - victory: ["dialogue:hex_maniac.victory.1", "dialogue:hex_maniac.victory.2"], - defeat: ["dialogue:hex_maniac.defeat.1", "dialogue:hex_maniac.defeat.2"], + encounter: ["dialogue:hexManiac.encounter.1", "dialogue:hexManiac.encounter.2"], + victory: ["dialogue:hexManiac.victory.1", "dialogue:hexManiac.victory.2"], + defeat: ["dialogue:hexManiac.defeat.1", "dialogue:hexManiac.defeat.2"], }, ], [TrainerType.PSYCHIC]: [ @@ -320,15 +316,11 @@ export const trainerTypeDialogue: TrainerTypeDialogue = { }, { encounter: [ - "dialogue:clerk_female.encounter.1", - "dialogue:clerk_female.encounter.2", - "dialogue:clerk_female.encounter.3", - ], - victory: [ - "dialogue:clerk_female.victory.1", - "dialogue:clerk_female.victory.2", - "dialogue:clerk_female.victory.3", + "dialogue:clerkFemale.encounter.1", + "dialogue:clerkFemale.encounter.2", + "dialogue:clerkFemale.encounter.3", ], + victory: ["dialogue:clerkFemale.victory.1", "dialogue:clerkFemale.victory.2", "dialogue:clerkFemale.victory.3"], }, ], [TrainerType.HOOLIGANS]: [ @@ -371,14 +363,14 @@ export const trainerTypeDialogue: TrainerTypeDialogue = { }, { encounter: [ - "dialogue:pokefan_female.encounter.1", - "dialogue:pokefan_female.encounter.2", - "dialogue:pokefan_female.encounter.3", + "dialogue:pokefanFemale.encounter.1", + "dialogue:pokefanFemale.encounter.2", + "dialogue:pokefanFemale.encounter.3", ], victory: [ - "dialogue:pokefan_female.victory.1", - "dialogue:pokefan_female.victory.2", - "dialogue:pokefan_female.victory.3", + "dialogue:pokefanFemale.victory.1", + "dialogue:pokefanFemale.victory.2", + "dialogue:pokefanFemale.victory.3", ], }, ], @@ -389,52 +381,52 @@ export const trainerTypeDialogue: TrainerTypeDialogue = { }, { encounter: [ - "dialogue:rich_female.encounter.1", - "dialogue:rich_female.encounter.2", - "dialogue:rich_female.encounter.3", + "dialogue:richFemale.encounter.1", + "dialogue:richFemale.encounter.2", + "dialogue:richFemale.encounter.3", ], - victory: ["dialogue:rich_female.victory.1", "dialogue:rich_female.victory.2", "dialogue:rich_female.victory.3"], + victory: ["dialogue:richFemale.victory.1", "dialogue:richFemale.victory.2", "dialogue:richFemale.victory.3"], }, ], [TrainerType.RICH_KID]: [ { - encounter: ["dialogue:rich_kid.encounter.1", "dialogue:rich_kid.encounter.2", "dialogue:rich_kid.encounter.3"], + encounter: ["dialogue:richKid.encounter.1", "dialogue:richKid.encounter.2", "dialogue:richKid.encounter.3"], victory: [ - "dialogue:rich_kid.victory.1", - "dialogue:rich_kid.victory.2", - "dialogue:rich_kid.victory.3", - "dialogue:rich_kid.victory.4", + "dialogue:richKid.victory.1", + "dialogue:richKid.victory.2", + "dialogue:richKid.victory.3", + "dialogue:richKid.victory.4", ], }, { encounter: [ - "dialogue:rich_kid_female.encounter.1", - "dialogue:rich_kid_female.encounter.2", - "dialogue:rich_kid_female.encounter.3", + "dialogue:richKidFemale.encounter.1", + "dialogue:richKidFemale.encounter.2", + "dialogue:richKidFemale.encounter.3", ], victory: [ - "dialogue:rich_kid_female.victory.1", - "dialogue:rich_kid_female.victory.2", - "dialogue:rich_kid_female.victory.3", - "dialogue:rich_kid_female.victory.4", + "dialogue:richKidFemale.victory.1", + "dialogue:richKidFemale.victory.2", + "dialogue:richKidFemale.victory.3", + "dialogue:richKidFemale.victory.4", ], }, ], [TrainerType.ROCKET_GRUNT]: [ { encounter: [ - "dialogue:rocket_grunt.encounter.1", - "dialogue:rocket_grunt.encounter.2", - "dialogue:rocket_grunt.encounter.3", - "dialogue:rocket_grunt.encounter.4", - "dialogue:rocket_grunt.encounter.5", + "dialogue:rocketGrunt.encounter.1", + "dialogue:rocketGrunt.encounter.2", + "dialogue:rocketGrunt.encounter.3", + "dialogue:rocketGrunt.encounter.4", + "dialogue:rocketGrunt.encounter.5", ], victory: [ - "dialogue:rocket_grunt.victory.1", - "dialogue:rocket_grunt.victory.2", - "dialogue:rocket_grunt.victory.3", - "dialogue:rocket_grunt.victory.4", - "dialogue:rocket_grunt.victory.5", + "dialogue:rocketGrunt.victory.1", + "dialogue:rocketGrunt.victory.2", + "dialogue:rocketGrunt.victory.3", + "dialogue:rocketGrunt.victory.4", + "dialogue:rocketGrunt.victory.5", ], }, ], @@ -465,18 +457,18 @@ export const trainerTypeDialogue: TrainerTypeDialogue = { [TrainerType.MAGMA_GRUNT]: [ { encounter: [ - "dialogue:magma_grunt.encounter.1", - "dialogue:magma_grunt.encounter.2", - "dialogue:magma_grunt.encounter.3", - "dialogue:magma_grunt.encounter.4", - "dialogue:magma_grunt.encounter.5", + "dialogue:magmaGrunt.encounter.1", + "dialogue:magmaGrunt.encounter.2", + "dialogue:magmaGrunt.encounter.3", + "dialogue:magmaGrunt.encounter.4", + "dialogue:magmaGrunt.encounter.5", ], victory: [ - "dialogue:magma_grunt.victory.1", - "dialogue:magma_grunt.victory.2", - "dialogue:magma_grunt.victory.3", - "dialogue:magma_grunt.victory.4", - "dialogue:magma_grunt.victory.5", + "dialogue:magmaGrunt.victory.1", + "dialogue:magmaGrunt.victory.2", + "dialogue:magmaGrunt.victory.3", + "dialogue:magmaGrunt.victory.4", + "dialogue:magmaGrunt.victory.5", ], }, ], @@ -495,18 +487,18 @@ export const trainerTypeDialogue: TrainerTypeDialogue = { [TrainerType.AQUA_GRUNT]: [ { encounter: [ - "dialogue:aqua_grunt.encounter.1", - "dialogue:aqua_grunt.encounter.2", - "dialogue:aqua_grunt.encounter.3", - "dialogue:aqua_grunt.encounter.4", - "dialogue:aqua_grunt.encounter.5", + "dialogue:aquaGrunt.encounter.1", + "dialogue:aquaGrunt.encounter.2", + "dialogue:aquaGrunt.encounter.3", + "dialogue:aquaGrunt.encounter.4", + "dialogue:aquaGrunt.encounter.5", ], victory: [ - "dialogue:aqua_grunt.victory.1", - "dialogue:aqua_grunt.victory.2", - "dialogue:aqua_grunt.victory.3", - "dialogue:aqua_grunt.victory.4", - "dialogue:aqua_grunt.victory.5", + "dialogue:aquaGrunt.victory.1", + "dialogue:aquaGrunt.victory.2", + "dialogue:aquaGrunt.victory.3", + "dialogue:aquaGrunt.victory.4", + "dialogue:aquaGrunt.victory.5", ], }, ], @@ -525,18 +517,18 @@ export const trainerTypeDialogue: TrainerTypeDialogue = { [TrainerType.GALACTIC_GRUNT]: [ { encounter: [ - "dialogue:galactic_grunt.encounter.1", - "dialogue:galactic_grunt.encounter.2", - "dialogue:galactic_grunt.encounter.3", - "dialogue:galactic_grunt.encounter.4", - "dialogue:galactic_grunt.encounter.5", + "dialogue:galacticGrunt.encounter.1", + "dialogue:galacticGrunt.encounter.2", + "dialogue:galacticGrunt.encounter.3", + "dialogue:galacticGrunt.encounter.4", + "dialogue:galacticGrunt.encounter.5", ], victory: [ - "dialogue:galactic_grunt.victory.1", - "dialogue:galactic_grunt.victory.2", - "dialogue:galactic_grunt.victory.3", - "dialogue:galactic_grunt.victory.4", - "dialogue:galactic_grunt.victory.5", + "dialogue:galacticGrunt.victory.1", + "dialogue:galacticGrunt.victory.2", + "dialogue:galacticGrunt.victory.3", + "dialogue:galacticGrunt.victory.4", + "dialogue:galacticGrunt.victory.5", ], }, ], @@ -561,18 +553,18 @@ export const trainerTypeDialogue: TrainerTypeDialogue = { [TrainerType.PLASMA_GRUNT]: [ { encounter: [ - "dialogue:plasma_grunt.encounter.1", - "dialogue:plasma_grunt.encounter.2", - "dialogue:plasma_grunt.encounter.3", - "dialogue:plasma_grunt.encounter.4", - "dialogue:plasma_grunt.encounter.5", + "dialogue:plasmaGrunt.encounter.1", + "dialogue:plasmaGrunt.encounter.2", + "dialogue:plasmaGrunt.encounter.3", + "dialogue:plasmaGrunt.encounter.4", + "dialogue:plasmaGrunt.encounter.5", ], victory: [ - "dialogue:plasma_grunt.victory.1", - "dialogue:plasma_grunt.victory.2", - "dialogue:plasma_grunt.victory.3", - "dialogue:plasma_grunt.victory.4", - "dialogue:plasma_grunt.victory.5", + "dialogue:plasmaGrunt.victory.1", + "dialogue:plasmaGrunt.victory.2", + "dialogue:plasmaGrunt.victory.3", + "dialogue:plasmaGrunt.victory.4", + "dialogue:plasmaGrunt.victory.5", ], }, ], @@ -591,18 +583,18 @@ export const trainerTypeDialogue: TrainerTypeDialogue = { [TrainerType.FLARE_GRUNT]: [ { encounter: [ - "dialogue:flare_grunt.encounter.1", - "dialogue:flare_grunt.encounter.2", - "dialogue:flare_grunt.encounter.3", - "dialogue:flare_grunt.encounter.4", - "dialogue:flare_grunt.encounter.5", + "dialogue:flareGrunt.encounter.1", + "dialogue:flareGrunt.encounter.2", + "dialogue:flareGrunt.encounter.3", + "dialogue:flareGrunt.encounter.4", + "dialogue:flareGrunt.encounter.5", ], victory: [ - "dialogue:flare_grunt.victory.1", - "dialogue:flare_grunt.victory.2", - "dialogue:flare_grunt.victory.3", - "dialogue:flare_grunt.victory.4", - "dialogue:flare_grunt.victory.5", + "dialogue:flareGrunt.victory.1", + "dialogue:flareGrunt.victory.2", + "dialogue:flareGrunt.victory.3", + "dialogue:flareGrunt.victory.4", + "dialogue:flareGrunt.victory.5", ], }, ], @@ -621,18 +613,18 @@ export const trainerTypeDialogue: TrainerTypeDialogue = { [TrainerType.AETHER_GRUNT]: [ { encounter: [ - "dialogue:aether_grunt.encounter.1", - "dialogue:aether_grunt.encounter.2", - "dialogue:aether_grunt.encounter.3", - "dialogue:aether_grunt.encounter.4", - "dialogue:aether_grunt.encounter.5", + "dialogue:aetherGrunt.encounter.1", + "dialogue:aetherGrunt.encounter.2", + "dialogue:aetherGrunt.encounter.3", + "dialogue:aetherGrunt.encounter.4", + "dialogue:aetherGrunt.encounter.5", ], victory: [ - "dialogue:aether_grunt.victory.1", - "dialogue:aether_grunt.victory.2", - "dialogue:aether_grunt.victory.3", - "dialogue:aether_grunt.victory.4", - "dialogue:aether_grunt.victory.5", + "dialogue:aetherGrunt.victory.1", + "dialogue:aetherGrunt.victory.2", + "dialogue:aetherGrunt.victory.3", + "dialogue:aetherGrunt.victory.4", + "dialogue:aetherGrunt.victory.5", ], }, ], @@ -645,18 +637,18 @@ export const trainerTypeDialogue: TrainerTypeDialogue = { [TrainerType.SKULL_GRUNT]: [ { encounter: [ - "dialogue:skull_grunt.encounter.1", - "dialogue:skull_grunt.encounter.2", - "dialogue:skull_grunt.encounter.3", - "dialogue:skull_grunt.encounter.4", - "dialogue:skull_grunt.encounter.5", + "dialogue:skullGrunt.encounter.1", + "dialogue:skullGrunt.encounter.2", + "dialogue:skullGrunt.encounter.3", + "dialogue:skullGrunt.encounter.4", + "dialogue:skullGrunt.encounter.5", ], victory: [ - "dialogue:skull_grunt.victory.1", - "dialogue:skull_grunt.victory.2", - "dialogue:skull_grunt.victory.3", - "dialogue:skull_grunt.victory.4", - "dialogue:skull_grunt.victory.5", + "dialogue:skullGrunt.victory.1", + "dialogue:skullGrunt.victory.2", + "dialogue:skullGrunt.victory.3", + "dialogue:skullGrunt.victory.4", + "dialogue:skullGrunt.victory.5", ], }, ], @@ -669,18 +661,18 @@ export const trainerTypeDialogue: TrainerTypeDialogue = { [TrainerType.MACRO_GRUNT]: [ { encounter: [ - "dialogue:macro_grunt.encounter.1", - "dialogue:macro_grunt.encounter.2", - "dialogue:macro_grunt.encounter.3", - "dialogue:macro_grunt.encounter.4", - "dialogue:macro_grunt.encounter.5", + "dialogue:macroGrunt.encounter.1", + "dialogue:macroGrunt.encounter.2", + "dialogue:macroGrunt.encounter.3", + "dialogue:macroGrunt.encounter.4", + "dialogue:macroGrunt.encounter.5", ], victory: [ - "dialogue:macro_grunt.victory.1", - "dialogue:macro_grunt.victory.2", - "dialogue:macro_grunt.victory.3", - "dialogue:macro_grunt.victory.4", - "dialogue:macro_grunt.victory.5", + "dialogue:macroGrunt.victory.1", + "dialogue:macroGrunt.victory.2", + "dialogue:macroGrunt.victory.3", + "dialogue:macroGrunt.victory.4", + "dialogue:macroGrunt.victory.5", ], }, ], @@ -693,18 +685,18 @@ export const trainerTypeDialogue: TrainerTypeDialogue = { [TrainerType.STAR_GRUNT]: [ { encounter: [ - "dialogue:star_grunt.encounter.1", - "dialogue:star_grunt.encounter.2", - "dialogue:star_grunt.encounter.3", - "dialogue:star_grunt.encounter.4", - "dialogue:star_grunt.encounter.5", + "dialogue:starGrunt.encounter.1", + "dialogue:starGrunt.encounter.2", + "dialogue:starGrunt.encounter.3", + "dialogue:starGrunt.encounter.4", + "dialogue:starGrunt.encounter.5", ], victory: [ - "dialogue:star_grunt.victory.1", - "dialogue:star_grunt.victory.2", - "dialogue:star_grunt.victory.3", - "dialogue:star_grunt.victory.4", - "dialogue:star_grunt.victory.5", + "dialogue:starGrunt.victory.1", + "dialogue:starGrunt.victory.2", + "dialogue:starGrunt.victory.3", + "dialogue:starGrunt.victory.4", + "dialogue:starGrunt.victory.5", ], }, ], @@ -740,207 +732,207 @@ export const trainerTypeDialogue: TrainerTypeDialogue = { ], [TrainerType.ROCKET_BOSS_GIOVANNI_1]: [ { - encounter: ["dialogue:rocket_boss_giovanni_1.encounter.1"], - victory: ["dialogue:rocket_boss_giovanni_1.victory.1"], - defeat: ["dialogue:rocket_boss_giovanni_1.defeat.1"], + encounter: ["dialogue:rocketBossGiovanni1.encounter.1"], + victory: ["dialogue:rocketBossGiovanni1.victory.1"], + defeat: ["dialogue:rocketBossGiovanni1.defeat.1"], }, ], [TrainerType.ROCKET_BOSS_GIOVANNI_2]: [ { - encounter: ["dialogue:rocket_boss_giovanni_2.encounter.1"], - victory: ["dialogue:rocket_boss_giovanni_2.victory.1"], - defeat: ["dialogue:rocket_boss_giovanni_2.defeat.1"], + encounter: ["dialogue:rocketBossGiovanni2.encounter.1"], + victory: ["dialogue:rocketBossGiovanni2.victory.1"], + defeat: ["dialogue:rocketBossGiovanni2.defeat.1"], }, ], [TrainerType.MAXIE]: [ { - encounter: ["dialogue:magma_boss_maxie_1.encounter.1"], - victory: ["dialogue:magma_boss_maxie_1.victory.1"], - defeat: ["dialogue:magma_boss_maxie_1.defeat.1"], + encounter: ["dialogue:magmaBossMaxie1.encounter.1"], + victory: ["dialogue:magmaBossMaxie1.victory.1"], + defeat: ["dialogue:magmaBossMaxie1.defeat.1"], }, ], [TrainerType.MAXIE_2]: [ { - encounter: ["dialogue:magma_boss_maxie_2.encounter.1"], - victory: ["dialogue:magma_boss_maxie_2.victory.1"], - defeat: ["dialogue:magma_boss_maxie_2.defeat.1"], + encounter: ["dialogue:magmaBossMaxie2.encounter.1"], + victory: ["dialogue:magmaBossMaxie2.victory.1"], + defeat: ["dialogue:magmaBossMaxie2.defeat.1"], }, ], [TrainerType.ARCHIE]: [ { - encounter: ["dialogue:aqua_boss_archie_1.encounter.1"], - victory: ["dialogue:aqua_boss_archie_1.victory.1"], - defeat: ["dialogue:aqua_boss_archie_1.defeat.1"], + encounter: ["dialogue:aquaBossArchie1.encounter.1"], + victory: ["dialogue:aquaBossArchie1.victory.1"], + defeat: ["dialogue:aquaBossArchie1.defeat.1"], }, ], [TrainerType.ARCHIE_2]: [ { - encounter: ["dialogue:aqua_boss_archie_2.encounter.1"], - victory: ["dialogue:aqua_boss_archie_2.victory.1"], - defeat: ["dialogue:aqua_boss_archie_2.defeat.1"], + encounter: ["dialogue:aquaBossArchie2.encounter.1"], + victory: ["dialogue:aquaBossArchie2.victory.1"], + defeat: ["dialogue:aquaBossArchie2.defeat.1"], }, ], [TrainerType.CYRUS]: [ { - encounter: ["dialogue:galactic_boss_cyrus_1.encounter.1"], - victory: ["dialogue:galactic_boss_cyrus_1.victory.1"], - defeat: ["dialogue:galactic_boss_cyrus_1.defeat.1"], + encounter: ["dialogue:galacticBossCyrus1.encounter.1"], + victory: ["dialogue:galacticBossCyrus1.victory.1"], + defeat: ["dialogue:galacticBossCyrus1.defeat.1"], }, ], [TrainerType.CYRUS_2]: [ { - encounter: ["dialogue:galactic_boss_cyrus_2.encounter.1"], - victory: ["dialogue:galactic_boss_cyrus_2.victory.1"], - defeat: ["dialogue:galactic_boss_cyrus_2.defeat.1"], + encounter: ["dialogue:galacticBossCyrus2.encounter.1"], + victory: ["dialogue:galacticBossCyrus2.victory.1"], + defeat: ["dialogue:galacticBossCyrus2.defeat.1"], }, ], [TrainerType.GHETSIS]: [ { - encounter: ["dialogue:plasma_boss_ghetsis_1.encounter.1"], - victory: ["dialogue:plasma_boss_ghetsis_1.victory.1"], - defeat: ["dialogue:plasma_boss_ghetsis_1.defeat.1"], + encounter: ["dialogue:plasmaBossGhetsis1.encounter.1"], + victory: ["dialogue:plasmaBossGhetsis1.victory.1"], + defeat: ["dialogue:plasmaBossGhetsis1.defeat.1"], }, ], [TrainerType.GHETSIS_2]: [ { - encounter: ["dialogue:plasma_boss_ghetsis_2.encounter.1"], - victory: ["dialogue:plasma_boss_ghetsis_2.victory.1"], - defeat: ["dialogue:plasma_boss_ghetsis_2.defeat.1"], + encounter: ["dialogue:plasmaBossGhetsis2.encounter.1"], + victory: ["dialogue:plasmaBossGhetsis2.victory.1"], + defeat: ["dialogue:plasmaBossGhetsis2.defeat.1"], }, ], [TrainerType.LYSANDRE]: [ { - encounter: ["dialogue:flare_boss_lysandre_1.encounter.1"], - victory: ["dialogue:flare_boss_lysandre_1.victory.1"], - defeat: ["dialogue:flare_boss_lysandre_1.defeat.1"], + encounter: ["dialogue:flareBossLysandre1.encounter.1"], + victory: ["dialogue:flareBossLysandre1.victory.1"], + defeat: ["dialogue:flareBossLysandre1.defeat.1"], }, ], [TrainerType.LYSANDRE_2]: [ { - encounter: ["dialogue:flare_boss_lysandre_2.encounter.1"], - victory: ["dialogue:flare_boss_lysandre_2.victory.1"], - defeat: ["dialogue:flare_boss_lysandre_2.defeat.1"], + encounter: ["dialogue:flareBossLysandre2.encounter.1"], + victory: ["dialogue:flareBossLysandre2.victory.1"], + defeat: ["dialogue:flareBossLysandre2.defeat.1"], }, ], [TrainerType.LUSAMINE]: [ { - encounter: ["dialogue:aether_boss_lusamine_1.encounter.1"], - victory: ["dialogue:aether_boss_lusamine_1.victory.1"], - defeat: ["dialogue:aether_boss_lusamine_1.defeat.1"], + encounter: ["dialogue:aetherBossLusamine1.encounter.1"], + victory: ["dialogue:aetherBossLusamine1.victory.1"], + defeat: ["dialogue:aetherBossLusamine1.defeat.1"], }, ], [TrainerType.LUSAMINE_2]: [ { - encounter: ["dialogue:aether_boss_lusamine_2.encounter.1"], - victory: ["dialogue:aether_boss_lusamine_2.victory.1"], - defeat: ["dialogue:aether_boss_lusamine_2.defeat.1"], + encounter: ["dialogue:aetherBossLusamine2.encounter.1"], + victory: ["dialogue:aetherBossLusamine2.victory.1"], + defeat: ["dialogue:aetherBossLusamine2.defeat.1"], }, ], [TrainerType.GUZMA]: [ { - encounter: ["dialogue:skull_boss_guzma_1.encounter.1"], - victory: ["dialogue:skull_boss_guzma_1.victory.1"], - defeat: ["dialogue:skull_boss_guzma_1.defeat.1"], + encounter: ["dialogue:skullBossGuzma1.encounter.1"], + victory: ["dialogue:skullBossGuzma1.victory.1"], + defeat: ["dialogue:skullBossGuzma1.defeat.1"], }, ], [TrainerType.GUZMA_2]: [ { - encounter: ["dialogue:skull_boss_guzma_2.encounter.1"], - victory: ["dialogue:skull_boss_guzma_2.victory.1"], - defeat: ["dialogue:skull_boss_guzma_2.defeat.1"], + encounter: ["dialogue:skullBossGuzma2.encounter.1"], + victory: ["dialogue:skullBossGuzma2.victory.1"], + defeat: ["dialogue:skullBossGuzma2.defeat.1"], }, ], [TrainerType.ROSE]: [ { - encounter: ["dialogue:macro_boss_rose_1.encounter.1"], - victory: ["dialogue:macro_boss_rose_1.victory.1"], - defeat: ["dialogue:macro_boss_rose_1.defeat.1"], + encounter: ["dialogue:macroBossRose1.encounter.1"], + victory: ["dialogue:macroBossRose1.victory.1"], + defeat: ["dialogue:macroBossRose1.defeat.1"], }, ], [TrainerType.ROSE_2]: [ { - encounter: ["dialogue:macro_boss_rose_2.encounter.1"], - victory: ["dialogue:macro_boss_rose_2.victory.1"], - defeat: ["dialogue:macro_boss_rose_2.defeat.1"], + encounter: ["dialogue:macroBossRose2.encounter.1"], + victory: ["dialogue:macroBossRose2.victory.1"], + defeat: ["dialogue:macroBossRose2.defeat.1"], }, ], [TrainerType.PENNY]: [ { - encounter: ["dialogue:star_boss_penny_1.encounter.1"], - victory: ["dialogue:star_boss_penny_1.victory.1"], - defeat: ["dialogue:star_boss_penny_1.defeat.1"], + encounter: ["dialogue:starBossPenny1.encounter.1"], + victory: ["dialogue:starBossPenny1.victory.1"], + defeat: ["dialogue:starBossPenny1.defeat.1"], }, ], [TrainerType.PENNY_2]: [ { - encounter: ["dialogue:star_boss_penny_2.encounter.1"], - victory: ["dialogue:star_boss_penny_2.victory.1"], - defeat: ["dialogue:star_boss_penny_2.defeat.1"], + encounter: ["dialogue:starBossPenny2.encounter.1"], + victory: ["dialogue:starBossPenny2.victory.1"], + defeat: ["dialogue:starBossPenny2.defeat.1"], }, ], [TrainerType.BUCK]: [ { - encounter: ["dialogue:stat_trainer_buck.encounter.1", "dialogue:stat_trainer_buck.encounter.2"], - victory: ["dialogue:stat_trainer_buck.victory.1", "dialogue:stat_trainer_buck.victory.2"], - defeat: ["dialogue:stat_trainer_buck.defeat.1", "dialogue:stat_trainer_buck.defeat.2"], + encounter: ["dialogue:statTrainerBuck.encounter.1", "dialogue:statTrainerBuck.encounter.2"], + victory: ["dialogue:statTrainerBuck.victory.1", "dialogue:statTrainerBuck.victory.2"], + defeat: ["dialogue:statTrainerBuck.defeat.1", "dialogue:statTrainerBuck.defeat.2"], }, ], [TrainerType.CHERYL]: [ { - encounter: ["dialogue:stat_trainer_cheryl.encounter.1", "dialogue:stat_trainer_cheryl.encounter.2"], - victory: ["dialogue:stat_trainer_cheryl.victory.1", "dialogue:stat_trainer_cheryl.victory.2"], - defeat: ["dialogue:stat_trainer_cheryl.defeat.1", "dialogue:stat_trainer_cheryl.defeat.2"], + encounter: ["dialogue:statTrainerCheryl.encounter.1", "dialogue:statTrainerCheryl.encounter.2"], + victory: ["dialogue:statTrainerCheryl.victory.1", "dialogue:statTrainerCheryl.victory.2"], + defeat: ["dialogue:statTrainerCheryl.defeat.1", "dialogue:statTrainerCheryl.defeat.2"], }, ], [TrainerType.MARLEY]: [ { - encounter: ["dialogue:stat_trainer_marley.encounter.1", "dialogue:stat_trainer_marley.encounter.2"], - victory: ["dialogue:stat_trainer_marley.victory.1", "dialogue:stat_trainer_marley.victory.2"], - defeat: ["dialogue:stat_trainer_marley.defeat.1", "dialogue:stat_trainer_marley.defeat.2"], + encounter: ["dialogue:statTrainerMarley.encounter.1", "dialogue:statTrainerMarley.encounter.2"], + victory: ["dialogue:statTrainerMarley.victory.1", "dialogue:statTrainerMarley.victory.2"], + defeat: ["dialogue:statTrainerMarley.defeat.1", "dialogue:statTrainerMarley.defeat.2"], }, ], [TrainerType.MIRA]: [ { - encounter: ["dialogue:stat_trainer_mira.encounter.1", "dialogue:stat_trainer_mira.encounter.2"], - victory: ["dialogue:stat_trainer_mira.victory.1", "dialogue:stat_trainer_mira.victory.2"], - defeat: ["dialogue:stat_trainer_mira.defeat.1", "dialogue:stat_trainer_mira.defeat.2"], + encounter: ["dialogue:statTrainerMira.encounter.1", "dialogue:statTrainerMira.encounter.2"], + victory: ["dialogue:statTrainerMira.victory.1", "dialogue:statTrainerMira.victory.2"], + defeat: ["dialogue:statTrainerMira.defeat.1", "dialogue:statTrainerMira.defeat.2"], }, ], [TrainerType.RILEY]: [ { - encounter: ["dialogue:stat_trainer_riley.encounter.1", "dialogue:stat_trainer_riley.encounter.2"], - victory: ["dialogue:stat_trainer_riley.victory.1", "dialogue:stat_trainer_riley.victory.2"], - defeat: ["dialogue:stat_trainer_riley.defeat.1", "dialogue:stat_trainer_riley.defeat.2"], + encounter: ["dialogue:statTrainerRiley.encounter.1", "dialogue:statTrainerRiley.encounter.2"], + victory: ["dialogue:statTrainerRiley.victory.1", "dialogue:statTrainerRiley.victory.2"], + defeat: ["dialogue:statTrainerRiley.defeat.1", "dialogue:statTrainerRiley.defeat.2"], }, ], [TrainerType.VICTOR]: [ { - encounter: ["dialogue:winstrates_victor.encounter.1"], - victory: ["dialogue:winstrates_victor.victory.1"], + encounter: ["dialogue:winstratesVictor.encounter.1"], + victory: ["dialogue:winstratesVictor.victory.1"], }, ], [TrainerType.VICTORIA]: [ { - encounter: ["dialogue:winstrates_victoria.encounter.1"], - victory: ["dialogue:winstrates_victoria.victory.1"], + encounter: ["dialogue:winstratesVictoria.encounter.1"], + victory: ["dialogue:winstratesVictoria.victory.1"], }, ], [TrainerType.VIVI]: [ { - encounter: ["dialogue:winstrates_vivi.encounter.1"], - victory: ["dialogue:winstrates_vivi.victory.1"], + encounter: ["dialogue:winstratesVivi.encounter.1"], + victory: ["dialogue:winstratesVivi.victory.1"], }, ], [TrainerType.VICKY]: [ { - encounter: ["dialogue:winstrates_vicky.encounter.1"], - victory: ["dialogue:winstrates_vicky.victory.1"], + encounter: ["dialogue:winstratesVicky.encounter.1"], + victory: ["dialogue:winstratesVicky.victory.1"], }, ], [TrainerType.VITO]: [ { - encounter: ["dialogue:winstrates_vito.encounter.1"], - victory: ["dialogue:winstrates_vito.victory.1"], + encounter: ["dialogue:winstratesVito.encounter.1"], + victory: ["dialogue:winstratesVito.victory.1"], }, ], [TrainerType.BROCK]: { @@ -954,9 +946,9 @@ export const trainerTypeDialogue: TrainerTypeDialogue = { defeat: ["dialogue:misty.defeat.1", "dialogue:misty.defeat.2", "dialogue:misty.defeat.3"], }, [TrainerType.LT_SURGE]: { - encounter: ["dialogue:lt_surge.encounter.1", "dialogue:lt_surge.encounter.2", "dialogue:lt_surge.encounter.3"], - victory: ["dialogue:lt_surge.victory.1", "dialogue:lt_surge.victory.2", "dialogue:lt_surge.victory.3"], - defeat: ["dialogue:lt_surge.defeat.1", "dialogue:lt_surge.defeat.2", "dialogue:lt_surge.defeat.3"], + encounter: ["dialogue:ltSurge.encounter.1", "dialogue:ltSurge.encounter.2", "dialogue:ltSurge.encounter.3"], + victory: ["dialogue:ltSurge.victory.1", "dialogue:ltSurge.victory.2", "dialogue:ltSurge.victory.3"], + defeat: ["dialogue:ltSurge.defeat.1", "dialogue:ltSurge.defeat.2", "dialogue:ltSurge.defeat.3"], }, [TrainerType.ERIKA]: { encounter: [ @@ -1055,12 +1047,12 @@ export const trainerTypeDialogue: TrainerTypeDialogue = { }, [TrainerType.CRASHER_WAKE]: { encounter: [ - "dialogue:crasher_wake.encounter.1", - "dialogue:crasher_wake.encounter.2", - "dialogue:crasher_wake.encounter.3", + "dialogue:crasherWake.encounter.1", + "dialogue:crasherWake.encounter.2", + "dialogue:crasherWake.encounter.3", ], - victory: ["dialogue:crasher_wake.victory.1", "dialogue:crasher_wake.victory.2", "dialogue:crasher_wake.victory.3"], - defeat: ["dialogue:crasher_wake.defeat.1", "dialogue:crasher_wake.defeat.2", "dialogue:crasher_wake.defeat.3"], + victory: ["dialogue:crasherWake.victory.1", "dialogue:crasherWake.victory.2", "dialogue:crasherWake.victory.3"], + defeat: ["dialogue:crasherWake.defeat.1", "dialogue:crasherWake.defeat.2", "dialogue:crasherWake.defeat.3"], }, [TrainerType.FALKNER]: { encounter: ["dialogue:falkner.encounter.1", "dialogue:falkner.encounter.2", "dialogue:falkner.encounter.3"], @@ -1354,9 +1346,9 @@ export const trainerTypeDialogue: TrainerTypeDialogue = { defeat: ["dialogue:acerola.defeat.1"], }, [TrainerType.LARRY_ELITE]: { - encounter: ["dialogue:larry_elite.encounter.1"], - victory: ["dialogue:larry_elite.victory.1"], - defeat: ["dialogue:larry_elite.defeat.1"], + encounter: ["dialogue:larryElite.encounter.1"], + victory: ["dialogue:larryElite.victory.1"], + defeat: ["dialogue:larryElite.defeat.1"], }, [TrainerType.LANCE]: { encounter: ["dialogue:lance.encounter.1", "dialogue:lance.encounter.2"], @@ -1414,9 +1406,9 @@ export const trainerTypeDialogue: TrainerTypeDialogue = { defeat: ["dialogue:jasmine.defeat.1"], }, [TrainerType.LANCE_CHAMPION]: { - encounter: ["dialogue:lance_champion.encounter.1"], - victory: ["dialogue:lance_champion.victory.1"], - defeat: ["dialogue:lance_champion.defeat.1"], + encounter: ["dialogue:lanceChampion.encounter.1"], + victory: ["dialogue:lanceChampion.victory.1"], + defeat: ["dialogue:lanceChampion.defeat.1"], }, [TrainerType.STEVEN]: { encounter: ["dialogue:steven.encounter.1"], @@ -1624,29 +1616,29 @@ export const trainerTypeDialogue: TrainerTypeDialogue = { defeat: ["dialogue:grusha.defeat.1"], }, [TrainerType.MARNIE_ELITE]: { - encounter: ["dialogue:marnie_elite.encounter.1", "dialogue:marnie_elite.encounter.2"], - victory: ["dialogue:marnie_elite.victory.1", "dialogue:marnie_elite.victory.2"], - defeat: ["dialogue:marnie_elite.defeat.1", "dialogue:marnie_elite.defeat.2"], + encounter: ["dialogue:marnieElite.encounter.1", "dialogue:marnieElite.encounter.2"], + victory: ["dialogue:marnieElite.victory.1", "dialogue:marnieElite.victory.2"], + defeat: ["dialogue:marnieElite.defeat.1", "dialogue:marnieElite.defeat.2"], }, [TrainerType.NESSA_ELITE]: { - encounter: ["dialogue:nessa_elite.encounter.1", "dialogue:nessa_elite.encounter.2"], - victory: ["dialogue:nessa_elite.victory.1", "dialogue:nessa_elite.victory.2"], - defeat: ["dialogue:nessa_elite.defeat.1", "dialogue:nessa_elite.defeat.2"], + encounter: ["dialogue:nessaElite.encounter.1", "dialogue:nessaElite.encounter.2"], + victory: ["dialogue:nessaElite.victory.1", "dialogue:nessaElite.victory.2"], + defeat: ["dialogue:nessaElite.defeat.1", "dialogue:nessaElite.defeat.2"], }, [TrainerType.BEA_ELITE]: { - encounter: ["dialogue:bea_elite.encounter.1", "dialogue:bea_elite.encounter.2"], - victory: ["dialogue:bea_elite.victory.1", "dialogue:bea_elite.victory.2"], - defeat: ["dialogue:bea_elite.defeat.1", "dialogue:bea_elite.defeat.2"], + encounter: ["dialogue:beaElite.encounter.1", "dialogue:beaElite.encounter.2"], + victory: ["dialogue:beaElite.victory.1", "dialogue:beaElite.victory.2"], + defeat: ["dialogue:beaElite.defeat.1", "dialogue:beaElite.defeat.2"], }, [TrainerType.ALLISTER_ELITE]: { - encounter: ["dialogue:allister_elite.encounter.1", "dialogue:allister_elite.encounter.2"], - victory: ["dialogue:allister_elite.victory.1", "dialogue:allister_elite.victory.2"], - defeat: ["dialogue:allister_elite.defeat.1", "dialogue:allister_elite.defeat.2"], + encounter: ["dialogue:allisterElite.encounter.1", "dialogue:allisterElite.encounter.2"], + victory: ["dialogue:allisterElite.victory.1", "dialogue:allisterElite.victory.2"], + defeat: ["dialogue:allisterElite.defeat.1", "dialogue:allisterElite.defeat.2"], }, [TrainerType.RAIHAN_ELITE]: { - encounter: ["dialogue:raihan_elite.encounter.1", "dialogue:raihan_elite.encounter.2"], - victory: ["dialogue:raihan_elite.victory.1", "dialogue:raihan_elite.victory.2"], - defeat: ["dialogue:raihan_elite.defeat.1", "dialogue:raihan_elite.defeat.2"], + encounter: ["dialogue:raihanElite.encounter.1", "dialogue:raihanElite.encounter.2"], + victory: ["dialogue:raihanElite.victory.1", "dialogue:raihanElite.victory.2"], + defeat: ["dialogue:raihanElite.defeat.1", "dialogue:raihanElite.defeat.2"], }, [TrainerType.ALDER]: { encounter: ["dialogue:alder.encounter.1"], @@ -1664,62 +1656,62 @@ export const trainerTypeDialogue: TrainerTypeDialogue = { victory: ["dialogue:rival.victory.1"], }, { - encounter: ["dialogue:rival_female.encounter.1"], - victory: ["dialogue:rival_female.victory.1"], + encounter: ["dialogue:rivalFemale.encounter.1"], + victory: ["dialogue:rivalFemale.victory.1"], }, ], [TrainerType.RIVAL_2]: [ { - encounter: ["dialogue:rival_2.encounter.1"], - victory: ["dialogue:rival_2.victory.1"], + encounter: ["dialogue:rival2.encounter.1"], + victory: ["dialogue:rival2.victory.1"], }, { - encounter: ["dialogue:rival_2_female.encounter.1"], - victory: ["dialogue:rival_2_female.victory.1"], - defeat: ["dialogue:rival_2_female.defeat.1"], + encounter: ["dialogue:rival2Female.encounter.1"], + victory: ["dialogue:rival2Female.victory.1"], + defeat: ["dialogue:rival2Female.defeat.1"], }, ], [TrainerType.RIVAL_3]: [ { - encounter: ["dialogue:rival_3.encounter.1"], - victory: ["dialogue:rival_3.victory.1"], + encounter: ["dialogue:rival3.encounter.1"], + victory: ["dialogue:rival3.victory.1"], }, { - encounter: ["dialogue:rival_3_female.encounter.1"], - victory: ["dialogue:rival_3_female.victory.1"], - defeat: ["dialogue:rival_3_female.defeat.1"], + encounter: ["dialogue:rival3Female.encounter.1"], + victory: ["dialogue:rival3Female.victory.1"], + defeat: ["dialogue:rival3Female.defeat.1"], }, ], [TrainerType.RIVAL_4]: [ { - encounter: ["dialogue:rival_4.encounter.1"], - victory: ["dialogue:rival_4.victory.1"], + encounter: ["dialogue:rival4.encounter.1"], + victory: ["dialogue:rival4.victory.1"], }, { - encounter: ["dialogue:rival_4_female.encounter.1"], - victory: ["dialogue:rival_4_female.victory.1"], - defeat: ["dialogue:rival_4_female.defeat.1"], + encounter: ["dialogue:rival4Female.encounter.1"], + victory: ["dialogue:rival4Female.victory.1"], + defeat: ["dialogue:rival4Female.defeat.1"], }, ], [TrainerType.RIVAL_5]: [ { - encounter: ["dialogue:rival_5.encounter.1"], - victory: ["dialogue:rival_5.victory.1"], + encounter: ["dialogue:rival5.encounter.1"], + victory: ["dialogue:rival5.victory.1"], }, { - encounter: ["dialogue:rival_5_female.encounter.1"], - victory: ["dialogue:rival_5_female.victory.1"], - defeat: ["dialogue:rival_5_female.defeat.1"], + encounter: ["dialogue:rival5Female.encounter.1"], + victory: ["dialogue:rival5Female.victory.1"], + defeat: ["dialogue:rival5Female.defeat.1"], }, ], [TrainerType.RIVAL_6]: [ { - encounter: ["dialogue:rival_6.encounter.1"], - victory: ["dialogue:rival_6.victory.1"], + encounter: ["dialogue:rival6.encounter.1"], + victory: ["dialogue:rival6.victory.1"], }, { - encounter: ["dialogue:rival_6_female.encounter.1"], - victory: ["dialogue:rival_6_female.victory.1"], + encounter: ["dialogue:rival6Female.encounter.1"], + victory: ["dialogue:rival6Female.victory.1"], }, ], }; diff --git a/src/data/double-battle-dialogue.ts b/src/data/double-battle-dialogue.ts index f15b74e4729..d4deee62037 100644 --- a/src/data/double-battle-dialogue.ts +++ b/src/data/double-battle-dialogue.ts @@ -2,43 +2,43 @@ // that caused this to be moved out in the first place export const doubleBattleDialogue = { blue_red_double: { - encounter: ["doubleBattleDialogue:blue_red_double.encounter.1"], - victory: ["doubleBattleDialogue:blue_red_double.victory.1"], + encounter: ["doubleBattleDialogue:blueRedDouble.encounter.1"], + victory: ["doubleBattleDialogue:blueRedDouble.victory.1"], }, red_blue_double: { - encounter: ["doubleBattleDialogue:red_blue_double.encounter.1"], - victory: ["doubleBattleDialogue:red_blue_double.victory.1"], + encounter: ["doubleBattleDialogue:redBlueDouble.encounter.1"], + victory: ["doubleBattleDialogue:redBlueDouble.victory.1"], }, tate_liza_double: { - encounter: ["doubleBattleDialogue:tate_liza_double.encounter.1"], - victory: ["doubleBattleDialogue:tate_liza_double.victory.1"], + encounter: ["doubleBattleDialogue:tateLizaDouble.encounter.1"], + victory: ["doubleBattleDialogue:tateLizaDouble.victory.1"], }, liza_tate_double: { - encounter: ["doubleBattleDialogue:liza_tate_double.encounter.1"], - victory: ["doubleBattleDialogue:liza_tate_double.victory.1"], + encounter: ["doubleBattleDialogue:lizaTateDouble.encounter.1"], + victory: ["doubleBattleDialogue:lizaTateDouble.victory.1"], }, wallace_steven_double: { - encounter: ["doubleBattleDialogue:wallace_steven_double.encounter.1"], - victory: ["doubleBattleDialogue:wallace_steven_double.victory.1"], + encounter: ["doubleBattleDialogue:wallaceStevenDouble.encounter.1"], + victory: ["doubleBattleDialogue:wallaceStevenDouble.victory.1"], }, steven_wallace_double: { - encounter: ["doubleBattleDialogue:steven_wallace_double.encounter.1"], - victory: ["doubleBattleDialogue:steven_wallace_double.victory.1"], + encounter: ["doubleBattleDialogue:stevenWallaceDouble.encounter.1"], + victory: ["doubleBattleDialogue:stevenWallaceDouble.victory.1"], }, alder_iris_double: { - encounter: ["doubleBattleDialogue:alder_iris_double.encounter.1"], - victory: ["doubleBattleDialogue:alder_iris_double.victory.1"], + encounter: ["doubleBattleDialogue:alderIrisDouble.encounter.1"], + victory: ["doubleBattleDialogue:alderIrisDouble.victory.1"], }, iris_alder_double: { - encounter: ["doubleBattleDialogue:iris_alder_double.encounter.1"], - victory: ["doubleBattleDialogue:iris_alder_double.victory.1"], + encounter: ["doubleBattleDialogue:irisAlderDouble.encounter.1"], + victory: ["doubleBattleDialogue:irisAlderDouble.victory.1"], }, marnie_piers_double: { - encounter: ["doubleBattleDialogue:marnie_piers_double.encounter.1"], - victory: ["doubleBattleDialogue:marnie_piers_double.victory.1"], + encounter: ["doubleBattleDialogue:marniePiersDouble.encounter.1"], + victory: ["doubleBattleDialogue:marniePiersDouble.victory.1"], }, piers_marnie_double: { - encounter: ["doubleBattleDialogue:piers_marnie_double.encounter.1"], - victory: ["doubleBattleDialogue:piers_marnie_double.victory.1"], + encounter: ["doubleBattleDialogue:piersMarnieDouble.encounter.1"], + victory: ["doubleBattleDialogue:piersMarnieDouble.victory.1"], }, }; diff --git a/src/data/egg-hatch-data.ts b/src/data/egg-hatch-data.ts index 6aead19eb7f..e78dc4d7984 100644 --- a/src/data/egg-hatch-data.ts +++ b/src/data/egg-hatch-data.ts @@ -47,6 +47,7 @@ export class EggHatchData { caughtCount: currDexEntry.caughtCount, hatchedCount: currDexEntry.hatchedCount, ivs: [...currDexEntry.ivs], + ribbons: currDexEntry.ribbons, }; this.starterDataEntryBeforeUpdate = { moveset: currStarterDataEntry.moveset, diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 85185b5e0fd..f67c66bf420 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -6,7 +6,7 @@ import { loggedInUser } from "#app/account"; import type { GameMode } from "#app/game-mode"; import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; -import type { ArenaTrapTag } from "#data/arena-tag"; +import type { EntryHazardTag } from "#data/arena-tag"; import { WeakenMoveTypeTag } from "#data/arena-tag"; import { MoveChargeAnim } from "#data/battle-anims"; import { @@ -87,11 +87,11 @@ import { PokemonHealPhase } from "#phases/pokemon-heal-phase"; import { SwitchSummonPhase } from "#phases/switch-summon-phase"; import type { AttackMoveResult } from "#types/attack-move-result"; import type { Localizable } from "#types/locales"; -import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString } from "#types/move-types"; +import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString, MoveMessageFunc } from "#types/move-types"; import type { TurnMove } from "#types/turn-move"; -import { BooleanHolder, type Constructor, isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common"; +import { BooleanHolder, coerceArray, type Constructor, isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common"; import { getEnumValues } from "#utils/enums"; -import { toTitleCase } from "#utils/strings"; +import { toCamelCase, toTitleCase } from "#utils/strings"; import i18next from "i18next"; import { applyChallenges } from "#utils/challenge-utils"; @@ -163,10 +163,16 @@ export abstract class Move implements Localizable { } localize(): void { - const i18nKey = MoveId[this.id].split("_").filter(f => f).map((f, i) => i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()).join("") as unknown as string; + const i18nKey = toCamelCase(MoveId[this.id]) - this.name = this.id ? `${i18next.t(`move:${i18nKey}.name`)}${this.nameAppend}` : ""; - this.effect = this.id ? `${i18next.t(`move:${i18nKey}.effect`)}${this.nameAppend}` : ""; + if (this.id === MoveId.NONE) { + this.name = ""; + this.effect = "" + return; + } + + this.name = `${i18next.t(`move:${i18nKey}.name`)}${this.nameAppend}`; + this.effect = `${i18next.t(`move:${i18nKey}.effect`)}${this.nameAppend}`; } /** @@ -1174,8 +1180,9 @@ export abstract class MoveAttr { } /** - * @virtual - * @returns the {@linkcode MoveCondition} or {@linkcode MoveConditionFunc} for this {@linkcode Move} + * Return this `MoveAttr`'s associated {@linkcode MoveCondition} or {@linkcode MoveConditionFunc}. + * The specified condition will be added to all {@linkcode Move}s with this attribute, + * and moves **will fail upon use** if _at least 1_ of their attached conditions returns `false`. */ getCondition(): MoveCondition | MoveConditionFunc | null { return null; @@ -1288,15 +1295,21 @@ export class MoveEffectAttr extends MoveAttr { } /** - * Determines whether the {@linkcode Move}'s effects are valid to {@linkcode apply} - * @virtual - * @param user {@linkcode Pokemon} using the move - * @param target {@linkcode Pokemon} target of the move - * @param move {@linkcode Move} with this attribute - * @param args Set of unique arguments needed by this attribute - * @returns true if basic application of the ability attribute should be possible + * Determine whether this {@linkcode MoveAttr}'s effects are able to {@linkcode apply | be applied} to the target. + * + * Will **NOT** cause the move to fail upon returning `false` (unlike {@linkcode getCondition}; + * merely that the effect for this attribute will be nullified. + * @param user - The {@linkcode Pokemon} using the move + * @param target - The {@linkcode Pokemon} being targeted by the move, or {@linkcode user} if the move is + * {@linkcode selfTarget | self-targeting} + * @param move - The {@linkcode Move} being used + * @param _args - Set of unique arguments needed by this attribute + * @returns `true` if basic application of this `MoveAttr`s effects should be possible */ - canApply(user: Pokemon, target: Pokemon, move: Move, args?: any[]) { + // TODO: Decouple this check from the `apply` step + // TODO: Make non-damaging moves fail by default if none of their attributes can apply + canApply(user: Pokemon, target: Pokemon, move: Move, _args?: any[]) { + // TODO: These checks seem redundant return !! (this.selfTarget ? user.hp && !user.getTag(BattlerTagType.FRENZY) : target.hp) && (this.selfTarget || !target.getTag(BattlerTagType.PROTECTED) || move.doesFlagEffectApply({ flag: MoveFlags.IGNORE_PROTECT, user, target })); @@ -1347,20 +1360,20 @@ export class MoveHeaderAttr extends MoveAttr { /** * Header attribute to queue a message at the beginning of a turn. - * @see {@link MoveHeaderAttr} */ export class MessageHeaderAttr extends MoveHeaderAttr { - private message: string | ((user: Pokemon, move: Move) => string); + /** The message to display, or a function producing one. */ + private message: string | MoveMessageFunc; - constructor(message: string | ((user: Pokemon, move: Move) => string)) { + constructor(message: string | MoveMessageFunc) { super(); this.message = message; } - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + apply(user: Pokemon, target: Pokemon, move: Move): boolean { const message = typeof this.message === "string" ? this.message - : this.message(user, move); + : this.message(user, target, move); if (message) { globalScene.phaseManager.queueMessage(message); @@ -1408,21 +1421,21 @@ export class BeakBlastHeaderAttr extends AddBattlerTagHeaderAttr { */ export class PreMoveMessageAttr extends MoveAttr { /** The message to display or a function returning one */ - private message: string | ((user: Pokemon, target: Pokemon, move: Move) => string | undefined); + private message: string | MoveMessageFunc; /** * 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. + * @param message - The message to display before move use, either` a literal string or a function producing one. * @remarks - * If {@linkcode message} evaluates to an empty string (`''`), no message will be displayed + * 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)) { + constructor(message: string | MoveMessageFunc) { super(); this.message = message; } - apply(user: Pokemon, target: Pokemon, move: Move, _args: any[]): boolean { + apply(user: Pokemon, target: Pokemon, move: Move): boolean { const message = typeof this.message === "function" ? this.message(user, target, move) : this.message; @@ -1443,18 +1456,17 @@ export class PreMoveMessageAttr extends MoveAttr { * @extends MoveAttr */ export class PreUseInterruptAttr extends MoveAttr { - protected message?: string | ((user: Pokemon, target: Pokemon, move: Move) => string); - protected overridesFailedMessage: boolean; + protected message: string | MoveMessageFunc; protected conditionFunc: MoveConditionFunc; /** * Create a new MoveInterruptedMessageAttr. * @param message The message to display when the move is interrupted, or a function that formats the message based on the user, target, and move. */ - constructor(message?: string | ((user: Pokemon, target: Pokemon, move: Move) => string), conditionFunc?: MoveConditionFunc) { + constructor(message: string | MoveMessageFunc, conditionFunc: MoveConditionFunc) { super(); this.message = message; - this.conditionFunc = conditionFunc ?? (() => true); + this.conditionFunc = conditionFunc; } /** @@ -1475,11 +1487,9 @@ export class PreUseInterruptAttr extends MoveAttr { */ override getFailedText(user: Pokemon, target: Pokemon, move: Move): string | undefined { if (this.message && this.conditionFunc(user, target, move)) { - const message = - typeof this.message === "string" - ? (this.message as string) + return typeof this.message === "string" + ? this.message : this.message(user, target, move); - return message; } } } @@ -1684,17 +1694,30 @@ export class SurviveDamageAttr extends ModifiedDamageAttr { } } -export class SplashAttr extends MoveEffectAttr { - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:splash")); - return true; - } -} +/** + * Move attribute to display arbitrary text during a move's execution. + */ +export class MessageAttr extends MoveEffectAttr { + /** The message to display, either as a string or a function returning one. */ + private message: string | MoveMessageFunc; -export class CelebrateAttr extends MoveEffectAttr { - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:celebrate", { playerName: loggedInUser?.username })); - return true; + constructor(message: string | MoveMessageFunc, options?: MoveEffectAttrOptions) { + // TODO: Do we need to respect `selfTarget` if we're just displaying text? + super(false, options) + this.message = message; + } + + override apply(user: Pokemon, target: Pokemon, move: Move): 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; + } + return false; } } @@ -1935,19 +1958,17 @@ export class AddSubstituteAttr extends MoveEffectAttr { * @see {@linkcode apply} */ export class HealAttr extends MoveEffectAttr { - /** The percentage of {@linkcode Stat.HP} to heal */ - private healRatio: number; - /** Should an animation be shown? */ - private showAnim: boolean; - - constructor(healRatio?: number, showAnim?: boolean, selfTarget?: boolean) { - super(selfTarget === undefined || selfTarget); - - this.healRatio = healRatio || 1; - this.showAnim = !!showAnim; + constructor( + /** The percentage of {@linkcode Stat.HP} to heal. */ + private healRatio: number, + /** Whether to display a healing animation when healing the target; default `false` */ + private showAnim = false, + selfTarget = true + ) { + super(selfTarget); } - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean { this.addHealPhase(this.selfTarget ? user : target, this.healRatio); return true; } @@ -1956,15 +1977,69 @@ export class HealAttr extends MoveEffectAttr { * Creates a new {@linkcode PokemonHealPhase}. * This heals the target and shows the appropriate message. */ - addHealPhase(target: Pokemon, healRatio: number) { + protected addHealPhase(target: Pokemon, healRatio: number) { globalScene.phaseManager.unshiftNew("PokemonHealPhase", target.getBattlerIndex(), toDmgValue(target.getMaxHp() * healRatio), i18next.t("moveTriggers:healHp", { pokemonName: getPokemonNameWithAffix(target) }), true, !this.showAnim); } - getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { + override getTargetBenefitScore(user: Pokemon, target: Pokemon, _move: Move): number { const score = ((1 - (this.selfTarget ? user : target).getHpRatio()) * 20) - this.healRatio * 10; return Math.round(score / (1 - this.healRatio / 2)); } + + // TODO: Change to fail move + override canApply(user: Pokemon, target: Pokemon, _move: Move, _args?: any[]): boolean { + if (!super.canApply(user, target, _move, _args)) { + return false; + } + + const healedPokemon = this.selfTarget ? user : target; + if (healedPokemon.isFullHp()) { + // Ensure the fail message isn't displayed when checking the move conditions outside of the move execution + // TOOD: Fix this in PR#6276 + if (globalScene.phaseManager.getCurrentPhase()?.is("MovePhase")) { + globalScene.phaseManager.queueMessage(i18next.t("battle:hpIsFull", { + pokemonName: getPokemonNameWithAffix(healedPokemon), + })) + } + return false; + } + return true; + } +} + +/** + * Attribute to put the user to sleep for a fixed duration, fully heal them and cure their status. + * Used for {@linkcode MoveId.REST}. + */ +export class RestAttr extends HealAttr { + private duration: number; + + constructor(duration: number) { + super(1, true); + this.duration = duration; + } + + override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + const wasSet = user.trySetStatus(StatusEffect.SLEEP, user, this.duration, null, true, true, + i18next.t("moveTriggers:restBecameHealthy", { + pokemonName: getPokemonNameWithAffix(user), + })); + return wasSet && super.apply(user, target, move, args); + } + + override addHealPhase(user: Pokemon): void { + globalScene.phaseManager.unshiftNew("PokemonHealPhase", user.getBattlerIndex(), user.getMaxHp(), null) + } + + // TODO: change after HealAttr is changed to fail move + override getCondition(): MoveConditionFunc { + return (user, target, move) => + super.canApply(user, target, move, []) + // Intentionally suppress messages here as we display generic fail msg + // TODO: This might have order-of-operation jank + && user.canSetStatus(StatusEffect.SLEEP, true, true, user) + } } /** @@ -2236,20 +2311,9 @@ export class BoostHealAttr extends HealAttr { * @see {@linkcode apply} */ export class HealOnAllyAttr extends HealAttr { - /** - * @param user {@linkcode Pokemon} using the move - * @param target {@linkcode Pokemon} target of the move - * @param move {@linkcode Move} with this attribute - * @param args N/A - * @returns true if the function succeeds - */ - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - if (user.getAlly() === target) { - super.apply(user, target, move, args); - return true; - } - - return false; + override canApply(user: Pokemon, target: Pokemon, _move: Move, _args?: any[]): boolean { + // Don't trigger if not targeting an ally + return target === user.getAlly() && super.canApply(user, target, _move, _args); } } @@ -2260,6 +2324,7 @@ export class HealOnAllyAttr extends HealAttr { * @see {@linkcode apply} * @see {@linkcode getUserBenefitScore} */ +// TODO: Make Strength Sap its own attribute that extends off of this one export class HitHealAttr extends MoveEffectAttr { private healRatio: number; private healStat: EffectiveStat | null; @@ -2510,49 +2575,50 @@ export class WaterShurikenMultiHitTypeAttr extends ChangeMultiHitTypeAttr { export class StatusEffectAttr extends MoveEffectAttr { public effect: StatusEffect; - public turnsRemaining?: number; - public overrideStatus: boolean = false; - constructor(effect: StatusEffect, selfTarget?: boolean, turnsRemaining?: number, overrideStatus: boolean = false) { + constructor(effect: StatusEffect, selfTarget = false) { super(selfTarget); this.effect = effect; - this.turnsRemaining = turnsRemaining; - this.overrideStatus = overrideStatus; } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true); const statusCheck = moveChance < 0 || moveChance === 100 || user.randBattleSeedInt(100) < moveChance; + if (!statusCheck) { + return false; + } + + // non-status moves don't play sound effects for failures const quiet = move.category !== MoveCategory.STATUS; - if (statusCheck) { - const pokemon = this.selfTarget ? user : target; - if (user !== target && move.category === MoveCategory.STATUS && !target.canSetStatus(this.effect, quiet, false, user, true)) { - return false; - } - if (((!pokemon.status || this.overrideStatus) || (pokemon.status.effect === this.effect && moveChance < 0)) - && pokemon.trySetStatus(this.effect, true, user, this.turnsRemaining, null, this.overrideStatus, quiet)) { - applyAbAttrs("ConfusionOnStatusEffectAbAttr", {pokemon: user, opponent: target, move, effect: this.effect}); - return true; - } + + if ( + target.trySetStatus(this.effect, user, undefined, null, false, quiet) + ) { + applyAbAttrs("ConfusionOnStatusEffectAbAttr", {pokemon: user, opponent: target, move, effect: this.effect}); + return true; } return false; } getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { const moveChance = this.getMoveChance(user, target, move, this.selfTarget, false); - const score = (moveChance < 0) ? -10 : Math.floor(moveChance * -0.1); + const score = moveChance < 0 ? -10 : Math.floor(moveChance * -0.1); const pokemon = this.selfTarget ? user : target; - return !pokemon.status && pokemon.canSetStatus(this.effect, true, false, user) ? score : 0; + return pokemon.canSetStatus(this.effect, true, false, user) ? score : 0; } } +/** + * Attribute to randomly apply one of several statuses to the target. + * Used for {@linkcode Moves.TRI_ATTACK} and {@linkcode Moves.DIRE_CLAW}. + */ export class MultiStatusEffectAttr extends StatusEffectAttr { public effects: StatusEffect[]; - constructor(effects: StatusEffect[], selfTarget?: boolean, turnsRemaining?: number, overrideStatus?: boolean) { - super(effects[0], selfTarget, turnsRemaining, overrideStatus); + constructor(effects: StatusEffect[], selfTarget?: boolean) { + super(effects[0], selfTarget); this.effects = effects; } @@ -2585,26 +2651,41 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr { * @returns - Whether the effect was successfully applied to the target. */ apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean { - const statusToApply: StatusEffect | undefined = user.status?.effect ?? (user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : undefined); + const statusToApply = user.status?.effect ?? + (user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : StatusEffect.NONE); - if (target.status || !statusToApply) { + // Bang is justified as condition func returns early if no status is found + if (!target.trySetStatus(statusToApply, user)) { return false; - } else { - const canSetStatus = target.canSetStatus(statusToApply, true, false, user); - const trySetStatus = canSetStatus ? target.trySetStatus(statusToApply, true, user) : false; + } - if (trySetStatus && user.status) { - // PsychoShiftTag is added to the user if move succeeds so that the user is healed of its status effect after its move - user.addTag(BattlerTagType.PSYCHO_SHIFT); + if (user.status) { + // Add tag to user to heal its status effect after the move ends (unless we have comatose); + // occurs after move use to ensure correct Synchronize timing + user.addTag(BattlerTagType.PSYCHO_SHIFT) + } + + return true; + } + + getCondition(): MoveConditionFunc { + return (user, target) => { + if (target.status?.effect) { + return false; } - return trySetStatus; + const statusToApply = user.status?.effect ?? (user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : StatusEffect.NONE); + return !!statusToApply && target.canSetStatus(statusToApply, false, false, user); } } getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { - const statusToApply = user.status?.effect ?? (user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : undefined); - return !target.status && statusToApply && target.canSetStatus(statusToApply, true, false, user) ? -10 : 0; + const statusToApply = + user.status?.effect ?? + (user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : StatusEffect.NONE); + + // TODO: Give this a positive user benefit score + return !target.status?.effect && statusToApply && target.canSetStatus(statusToApply, true, false, user) ? -10 : 0; } } @@ -2664,7 +2745,7 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr { * Used for Incinerate and Knock Off. * Not Implemented Cases: (Same applies for Thief) * "If the user faints due to the target's Ability (Rough Skin or Iron Barbs) or held Rocky Helmet, it cannot remove the target's held item." - * "If the Pokémon is knocked out by the attack, Sticky Hold does not protect the held item."" + * "If the Pokémon is knocked out by the attack, Sticky Hold does not protect the held item." */ export class RemoveHeldItemAttr extends MoveEffectAttr { @@ -2874,7 +2955,7 @@ export class HealStatusEffectAttr extends MoveEffectAttr { */ constructor(selfTarget: boolean, effects: StatusEffect | StatusEffect[]) { super(selfTarget, { lastHitOnly: true }); - this.effects = [ effects ].flat(1); + this.effects = coerceArray(effects) } /** @@ -4401,6 +4482,10 @@ export class SpitUpPowerAttr extends VariablePowerAttr { * Does NOT remove stockpiled stacks. */ export class SwallowHealAttr extends HealAttr { + constructor() { + super(1) + } + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const stockpilingTag = user.getTag(StockpilingTag); @@ -5949,38 +6034,6 @@ export class ProtectAttr extends AddBattlerTagAttr { } } -export class IgnoreAccuracyAttr extends AddBattlerTagAttr { - constructor() { - super(BattlerTagType.IGNORE_ACCURACY, true, false, 2); - } - - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - if (!super.apply(user, target, move, args)) { - return false; - } - - globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:tookAimAtTarget", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target) })); - - return true; - } -} - -export class FaintCountdownAttr extends AddBattlerTagAttr { - constructor() { - super(BattlerTagType.PERISH_SONG, false, true, 4); - } - - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - if (!super.apply(user, target, move, args)) { - return false; - } - - globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:faintCountdown", { pokemonName: getPokemonNameWithAffix(target), turnCount: this.turnCountMin - 1 })); - - return true; - } -} - /** * Attribute to remove all Substitutes from the field. * @extends MoveEffectAttr @@ -6117,7 +6170,7 @@ export class AddArenaTrapTagAttr extends AddArenaTagAttr { getCondition(): MoveConditionFunc { return (user, target, move) => { const side = (this.selfSideTarget !== user.isPlayer()) ? ArenaTagSide.ENEMY : ArenaTagSide.PLAYER; - const tag = globalScene.arena.getTagOnSide(this.tagType, side) as ArenaTrapTag; + const tag = globalScene.arena.getTagOnSide(this.tagType, side) as EntryHazardTag; if (!tag) { return true; } @@ -6141,7 +6194,7 @@ export class AddArenaTrapTagHitAttr extends AddArenaTagAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true); const side = (this.selfSideTarget ? user : target).isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; - const tag = globalScene.arena.getTagOnSide(this.tagType, side) as ArenaTrapTag; + const tag = globalScene.arena.getTagOnSide(this.tagType, side) as EntryHazardTag; if ((moveChance < 0 || moveChance === 100 || user.randBattleSeedInt(100) < moveChance) && user.getLastXMoves(1)[0]?.result === MoveResult.SUCCESS) { globalScene.arena.addTag(this.tagType, 0, move.id, user.id, side); if (!tag) { @@ -6621,8 +6674,10 @@ export class ChillyReceptionAttr extends ForceSwitchOutAttr { return (user, target, move) => globalScene.arena.weather?.weatherType !== WeatherType.SNOW || super.getSwitchOutCondition()(user, target, move); } } + export class RemoveTypeAttr extends MoveEffectAttr { + // TODO: Remove the message callback private removedType: PokemonType; private messageCallback: ((user: Pokemon) => void) | undefined; @@ -6707,7 +6762,7 @@ export class CopyBiomeTypeAttr extends MoveEffectAttr { user.summonData.types = [ typeChange ]; user.updateInfo(); - globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:transformedIntoType", { pokemonName: getPokemonNameWithAffix(user), typeName: i18next.t(`pokemonInfo:Type.${PokemonType[typeChange]}`) })); + globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:transformedIntoType", { pokemonName: getPokemonNameWithAffix(user), typeName: i18next.t(`pokemonInfo:type.${toCamelCase(PokemonType[typeChange])}`) })); return true; } @@ -6835,7 +6890,7 @@ export class AddTypeAttr extends MoveEffectAttr { target.summonData.addedType = this.type; target.updateInfo(); - globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:addType", { typeName: i18next.t(`pokemonInfo:Type.${PokemonType[this.type]}`), pokemonName: getPokemonNameWithAffix(target) })); + globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:addType", { typeName: i18next.t(`pokemonInfo:type.${toCamelCase(PokemonType[this.type])}`), pokemonName: getPokemonNameWithAffix(target) })); return true; } @@ -6857,7 +6912,7 @@ export class FirstMoveTypeAttr extends MoveEffectAttr { const firstMoveType = target.getMoveset()[0].getMove().type; user.summonData.types = [ firstMoveType ]; - globalScene.phaseManager.queueMessage(i18next.t("battle:transformedIntoType", { pokemonName: getPokemonNameWithAffix(user), type: i18next.t(`pokemonInfo:Type.${PokemonType[firstMoveType]}`) })); + globalScene.phaseManager.queueMessage(i18next.t("battle:transformedIntoType", { pokemonName: getPokemonNameWithAffix(user), type: i18next.t(`pokemonInfo:type.${toCamelCase(PokemonType[firstMoveType])}`) })); return true; } @@ -7882,7 +7937,7 @@ export class StatusIfBoostedAttr extends MoveEffectAttr { */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { if (target.turnData.statStagesIncreased) { - target.trySetStatus(this.effect, true, user); + target.trySetStatus(this.effect, user); } return true; } @@ -8029,11 +8084,11 @@ const failIfDampCondition: MoveConditionFunc = (user, target, move) => { return !cancelled.value; }; -const userSleptOrComatoseCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => user.status?.effect === StatusEffect.SLEEP || user.hasAbility(AbilityId.COMATOSE); +const userSleptOrComatoseCondition: MoveConditionFunc = (user) => user.status?.effect === StatusEffect.SLEEP || user.hasAbility(AbilityId.COMATOSE); -const targetSleptOrComatoseCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => target.status?.effect === StatusEffect.SLEEP || target.hasAbility(AbilityId.COMATOSE); +const targetSleptOrComatoseCondition: MoveConditionFunc = (_user: Pokemon, target: Pokemon, _move: Move) => target.status?.effect === StatusEffect.SLEEP || target.hasAbility(AbilityId.COMATOSE); -const failIfLastCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => globalScene.phaseManager.phaseQueue.find(phase => phase.is("MovePhase")) !== undefined; +const failIfLastCondition: MoveConditionFunc = () => globalScene.phaseManager.findPhase(phase => phase.is("MovePhase")) !== undefined; const failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => { const party: Pokemon[] = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty(); @@ -8258,8 +8313,6 @@ const MoveAttrs = Object.freeze({ RandomLevelDamageAttr, ModifiedDamageAttr, SurviveDamageAttr, - SplashAttr, - CelebrateAttr, RecoilAttr, SacrificialAttr, SacrificialAttrOnHit, @@ -8402,8 +8455,7 @@ const MoveAttrs = Object.freeze({ RechargeAttr, TrapAttr, ProtectAttr, - IgnoreAccuracyAttr, - FaintCountdownAttr, + MessageAttr, RemoveAllSubstitutesAttr, HitsTagAttr, HitsTagForDoubleDamageAttr, @@ -8488,7 +8540,7 @@ export function initMoves() { .punchingMove(), new AttackMove(MoveId.SCRATCH, PokemonType.NORMAL, MoveCategory.PHYSICAL, 40, 100, 35, -1, 0, 1), new AttackMove(MoveId.VISE_GRIP, PokemonType.NORMAL, MoveCategory.PHYSICAL, 55, 100, 30, -1, 0, 1), - new AttackMove(MoveId.GUILLOTINE, PokemonType.NORMAL, MoveCategory.PHYSICAL, 200, 30, 5, -1, 0, 1) + new AttackMove(MoveId.GUILLOTINE, PokemonType.NORMAL, MoveCategory.PHYSICAL, 250, 30, 5, -1, 0, 1) .attr(OneHitKOAttr) .attr(OneHitKOAccuracyAttr), new ChargingAttackMove(MoveId.RAZOR_WIND, PokemonType.NORMAL, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 1) @@ -8541,7 +8593,7 @@ export function initMoves() { new AttackMove(MoveId.HORN_ATTACK, PokemonType.NORMAL, MoveCategory.PHYSICAL, 65, 100, 25, -1, 0, 1), new AttackMove(MoveId.FURY_ATTACK, PokemonType.NORMAL, MoveCategory.PHYSICAL, 15, 85, 20, -1, 0, 1) .attr(MultiHitAttr), - new AttackMove(MoveId.HORN_DRILL, PokemonType.NORMAL, MoveCategory.PHYSICAL, 200, 30, 5, -1, 0, 1) + new AttackMove(MoveId.HORN_DRILL, PokemonType.NORMAL, MoveCategory.PHYSICAL, 250, 30, 5, -1, 0, 1) .attr(OneHitKOAttr) .attr(OneHitKOAccuracyAttr), new AttackMove(MoveId.TACKLE, PokemonType.NORMAL, MoveCategory.PHYSICAL, 40, 100, 35, -1, 0, 1), @@ -8722,7 +8774,7 @@ export function initMoves() { .attr(MovePowerMultiplierAttr, (user, target, move) => globalScene.arena.getTerrainType() === TerrainType.GRASSY && target.isGrounded() ? 0.5 : 1) .makesContact(false) .target(MoveTarget.ALL_NEAR_OTHERS), - new AttackMove(MoveId.FISSURE, PokemonType.GROUND, MoveCategory.PHYSICAL, 200, 30, 5, -1, 0, 1) + new AttackMove(MoveId.FISSURE, PokemonType.GROUND, MoveCategory.PHYSICAL, 250, 30, 5, -1, 0, 1) .attr(OneHitKOAttr) .attr(OneHitKOAccuracyAttr) .attr(HitsTagAttr, BattlerTagType.UNDERGROUND) @@ -8897,7 +8949,7 @@ export function initMoves() { new AttackMove(MoveId.PSYWAVE, PokemonType.PSYCHIC, MoveCategory.SPECIAL, -1, 100, 15, -1, 0, 1) .attr(RandomLevelDamageAttr), new SelfStatusMove(MoveId.SPLASH, PokemonType.NORMAL, -1, 40, -1, 0, 1) - .attr(SplashAttr) + .attr(MessageAttr, i18next.t("moveTriggers:splash")) .condition(failOnGravityCondition), new SelfStatusMove(MoveId.ACID_ARMOR, PokemonType.POISON, -1, 20, -1, 0, 1) .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true), @@ -8914,9 +8966,7 @@ export function initMoves() { .attr(MultiHitAttr, MultiHitType._2) .makesContact(false), new SelfStatusMove(MoveId.REST, PokemonType.PSYCHIC, -1, 5, -1, 0, 1) - .attr(StatusEffectAttr, StatusEffect.SLEEP, true, 3, true) - .attr(HealAttr, 1, true) - .condition((user, target, move) => !user.isFullHp() && user.canSetStatus(StatusEffect.SLEEP, true, true, user)) + .attr(RestAttr, 3) .triageMove(), new AttackMove(MoveId.ROCK_SLIDE, PokemonType.ROCK, MoveCategory.PHYSICAL, 75, 90, 10, 30, 0, 1) .attr(FlinchAttr) @@ -8959,7 +9009,10 @@ export function initMoves() { .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1) .reflectable(), new StatusMove(MoveId.MIND_READER, PokemonType.NORMAL, -1, 5, -1, 0, 2) - .attr(IgnoreAccuracyAttr), + .attr(AddBattlerTagAttr, BattlerTagType.IGNORE_ACCURACY, true, false, 2) + .attr(MessageAttr, (user, target) => + i18next.t("moveTriggers:tookAimAtTarget", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target) }) + ), new StatusMove(MoveId.NIGHTMARE, PokemonType.GHOST, 100, 15, -1, 0, 2) .attr(AddBattlerTagAttr, BattlerTagType.NIGHTMARE) .condition(targetSleptOrComatoseCondition), @@ -9047,7 +9100,9 @@ export function initMoves() { return lastTurnMove.length === 0 || lastTurnMove[0].move !== move.id || lastTurnMove[0].result !== MoveResult.SUCCESS; }), new StatusMove(MoveId.PERISH_SONG, PokemonType.NORMAL, -1, 5, -1, 0, 2) - .attr(FaintCountdownAttr) + .attr(AddBattlerTagAttr, BattlerTagType.PERISH_SONG, false, true, 4) + .attr(MessageAttr, (_user, target) => + i18next.t("moveTriggers:faintCountdown", { pokemonName: getPokemonNameWithAffix(target), turnCount: 3 })) .ignoresProtect() .soundBased() .condition(failOnBossCondition) @@ -9063,7 +9118,10 @@ export function initMoves() { .attr(MultiHitAttr) .makesContact(false), new StatusMove(MoveId.LOCK_ON, PokemonType.NORMAL, -1, 5, -1, 0, 2) - .attr(IgnoreAccuracyAttr), + .attr(AddBattlerTagAttr, BattlerTagType.IGNORE_ACCURACY, true, false, 2) + .attr(MessageAttr, (user, target) => + i18next.t("moveTriggers:tookAimAtTarget", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target) }) + ), new AttackMove(MoveId.OUTRAGE, PokemonType.DRAGON, MoveCategory.PHYSICAL, 120, 100, 10, -1, 0, 2) .attr(FrenzyAttr) .attr(MissEffectAttr, frenzyMissFunc) @@ -9254,14 +9312,16 @@ export function initMoves() { .condition(user => (user.getTag(StockpilingTag)?.stockpiledCount ?? 0) < 3) .attr(AddBattlerTagAttr, BattlerTagType.STOCKPILING, true), new AttackMove(MoveId.SPIT_UP, PokemonType.NORMAL, MoveCategory.SPECIAL, -1, 100, 10, -1, 0, 3) - .condition(hasStockpileStacksCondition) .attr(SpitUpPowerAttr, 100) + .condition(hasStockpileStacksCondition) .attr(RemoveBattlerTagAttr, [ BattlerTagType.STOCKPILING ], true), new SelfStatusMove(MoveId.SWALLOW, PokemonType.NORMAL, -1, 10, -1, 0, 3) - .condition(hasStockpileStacksCondition) .attr(SwallowHealAttr) + .condition(hasStockpileStacksCondition) .attr(RemoveBattlerTagAttr, [ BattlerTagType.STOCKPILING ], true) - .triageMove(), + .triageMove() + // TODO: Verify if using Swallow at full HP still consumes stacks or not + .edgeCase(), new AttackMove(MoveId.HEAT_WAVE, PokemonType.FIRE, MoveCategory.SPECIAL, 95, 90, 10, 10, 0, 3) .attr(HealStatusEffectAttr, true, StatusEffect.FREEZE) .attr(StatusEffectAttr, StatusEffect.BURN) @@ -9290,8 +9350,8 @@ export function initMoves() { && (user.status.effect === StatusEffect.BURN || user.status.effect === StatusEffect.POISON || user.status.effect === StatusEffect.TOXIC || user.status.effect === StatusEffect.PARALYSIS) ? 2 : 1) .attr(BypassBurnDamageReductionAttr), new AttackMove(MoveId.FOCUS_PUNCH, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 150, 100, 20, -1, -3, 3) - .attr(MessageHeaderAttr, (user, move) => i18next.t("moveTriggers:isTighteningFocus", { pokemonName: getPokemonNameWithAffix(user) })) - .attr(PreUseInterruptAttr, (user, target, move) => i18next.t("moveTriggers:lostFocus", { pokemonName: getPokemonNameWithAffix(user) }), user => !!user.turnData.attacksReceived.find(r => r.damage)) + .attr(MessageHeaderAttr, (user) => i18next.t("moveTriggers:isTighteningFocus", { pokemonName: getPokemonNameWithAffix(user) })) + .attr(PreUseInterruptAttr, (user) => i18next.t("moveTriggers:lostFocus", { pokemonName: getPokemonNameWithAffix(user) }), user => user.turnData.attacksReceived.some(r => r.damage > 0)) .punchingMove(), new AttackMove(MoveId.SMELLING_SALTS, PokemonType.NORMAL, MoveCategory.PHYSICAL, 70, 100, 10, -1, 0, 3) .attr(MovePowerMultiplierAttr, (user, target, move) => target.status?.effect === StatusEffect.PARALYSIS ? 2 : 1) @@ -9494,7 +9554,7 @@ export function initMoves() { new AttackMove(MoveId.SAND_TOMB, PokemonType.GROUND, MoveCategory.PHYSICAL, 35, 85, 15, -1, 0, 3) .attr(TrapAttr, BattlerTagType.SAND_TOMB) .makesContact(false), - new AttackMove(MoveId.SHEER_COLD, PokemonType.ICE, MoveCategory.SPECIAL, 200, 30, 5, -1, 0, 3) + new AttackMove(MoveId.SHEER_COLD, PokemonType.ICE, MoveCategory.SPECIAL, 250, 30, 5, -1, 0, 3) .attr(IceNoEffectTypeAttr) .attr(OneHitKOAttr) .attr(SheerColdAccuracyAttr), @@ -9645,14 +9705,8 @@ export function initMoves() { .unimplemented(), new StatusMove(MoveId.PSYCHO_SHIFT, PokemonType.PSYCHIC, 100, 10, -1, 0, 4) .attr(PsychoShiftEffectAttr) - .condition((user, target, move) => { - let statusToApply = user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : undefined; - if (user.status?.effect && isNonVolatileStatusEffect(user.status.effect)) { - statusToApply = user.status.effect; - } - return !!statusToApply && target.canSetStatus(statusToApply, false, false, user); - } - ), + // TODO: Verify status applied if a statused pokemon obtains Comatose (via Transform) and uses Psycho Shift + .edgeCase(), new AttackMove(MoveId.TRUMP_CARD, PokemonType.NORMAL, MoveCategory.SPECIAL, -1, -1, 5, -1, 0, 4) .makesContact() .attr(LessPPMorePowerAttr), @@ -10390,7 +10444,8 @@ export function initMoves() { new AttackMove(MoveId.DAZZLING_GLEAM, PokemonType.FAIRY, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 6) .target(MoveTarget.ALL_NEAR_ENEMIES), new SelfStatusMove(MoveId.CELEBRATE, PokemonType.NORMAL, -1, 40, -1, 0, 6) - .attr(CelebrateAttr), + // NB: This needs a lambda function as the user will not be logged in by the time the moves are initialized + .attr(MessageAttr, () => i18next.t("moveTriggers:celebrate", { playerName: loggedInUser?.username })), new StatusMove(MoveId.HOLD_HANDS, PokemonType.NORMAL, -1, 40, -1, 0, 6) .ignoresSubstitute() .target(MoveTarget.NEAR_ALLY), @@ -10565,7 +10620,12 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.SPD ], -1) .reflectable(), new SelfStatusMove(MoveId.LASER_FOCUS, PokemonType.NORMAL, -1, 30, -1, 0, 7) - .attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_CRIT, true, false), + .attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_CRIT, true, false) + .attr(MessageAttr, (user) => + i18next.t("battlerTags:laserFocusOnAdd", { + pokemonNameWithAffix: getPokemonNameWithAffix(user), + }), + ), new StatusMove(MoveId.GEAR_UP, PokemonType.STEEL, -1, 20, -1, 0, 7) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, { condition: (user, target, move) => !![ AbilityId.PLUS, AbilityId.MINUS ].find(a => target.hasAbility(a, false)) }) .ignoresSubstitute() diff --git a/src/data/moves/pokemon-move.ts b/src/data/moves/pokemon-move.ts index 61945fc768e..81e8acf899d 100644 --- a/src/data/moves/pokemon-move.ts +++ b/src/data/moves/pokemon-move.ts @@ -11,7 +11,7 @@ import { BooleanHolder, toDmgValue } from "#utils/common"; * These are the moves assigned to a {@linkcode Pokemon} object. * It links to {@linkcode Move} class via the move ID. * Compared to {@linkcode Move}, this class also tracks things like - * PP Ups recieved, PP used, etc. + * PP Ups received, PP used, etc. * @see {@linkcode isUsable} - checks if move is restricted, out of PP, or not implemented. * @see {@linkcode getMove} - returns {@linkcode Move} object by looking it up via ID. * @see {@linkcode usePp} - removes a point of PP from the move. diff --git a/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts b/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts index ac3d4def654..66364bfc485 100644 --- a/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts +++ b/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts @@ -82,7 +82,7 @@ export const ATrainersTestEncounter: MysteryEncounter = MysteryEncounterBuilder. encounter.dialogue.intro = [ { speaker: `trainerNames:${trainerNameKey}`, - text: `${namespace}:${trainerNameKey}.intro_dialogue`, + text: `${namespace}:${trainerNameKey}.introDialogue`, }, ]; encounter.options[0].dialogue!.selected = [ diff --git a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts index d6058eb9eaf..bf9c49e8c5b 100644 --- a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts +++ b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts @@ -237,7 +237,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde modifierConfigs: bossModifierConfigs, tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON], mysteryEncounterBattleEffects: (pokemon: Pokemon) => { - queueEncounterMessage(`${namespace}:option.1.boss_enraged`); + queueEncounterMessage(`${namespace}:option.1.bossEnraged`); globalScene.phaseManager.unshiftNew( "StatStageChangePhase", pokemon.getBattlerIndex(), @@ -300,7 +300,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde globalScene.addModifier(seedModifier, false, false, false, true); } }); - queueEncounterMessage(`${namespace}:option.1.food_stash`); + queueEncounterMessage(`${namespace}:option.1.foodStash`); }; setEncounterRewards({ fillRemaining: true }, undefined, givePartyPokemonReviverSeeds); diff --git a/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts b/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts index 619acabe200..7d4fb0ef98f 100644 --- a/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts +++ b/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts @@ -71,7 +71,7 @@ export const AnOfferYouCantRefuseEncounter: MysteryEncounter = MysteryEncounterB text: `${namespace}:intro`, }, { - text: `${namespace}:intro_dialogue`, + text: `${namespace}:introDialogue`, speaker: `${namespace}:speaker`, }, ]) @@ -152,7 +152,7 @@ export const AnOfferYouCantRefuseEncounter: MysteryEncounter = MysteryEncounterB .withDialogue({ buttonLabel: `${namespace}:option.2.label`, buttonTooltip: `${namespace}:option.2.tooltip`, - disabledButtonTooltip: `${namespace}:option.2.tooltip_disabled`, + disabledButtonTooltip: `${namespace}:option.2.tooltipDisabled`, selected: [ { speaker: `${namespace}:speaker`, diff --git a/src/data/mystery-encounters/encounters/berries-abound-encounter.ts b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts index a827c3fcc0a..196ca873f4e 100644 --- a/src/data/mystery-encounters/encounters/berries-abound-encounter.ts +++ b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts @@ -254,7 +254,7 @@ export const BerriesAboundEncounter: MysteryEncounter = MysteryEncounterBuilder. undefined, doBerryRewards, ); - await showEncounterText(`${namespace}:option.2.selected_bad`); + await showEncounterText(`${namespace}:option.2.selectedBad`); await initBattleWithEnemyConfig(config); return; } diff --git a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts index c5553e9bb95..6750051c3c4 100644 --- a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts +++ b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts @@ -198,7 +198,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde }, { speaker: `${namespace}:speaker`, - text: `${namespace}:intro_dialogue`, + text: `${namespace}:introDialogue`, }, ]) .withOnInit(() => { @@ -312,7 +312,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde .withDialogue({ buttonLabel: `${namespace}:option.2.label`, buttonTooltip: `${namespace}:option.2.tooltip`, - disabledButtonTooltip: `${namespace}:option.2.disabled_tooltip`, + disabledButtonTooltip: `${namespace}:option.2.disabledTooltip`, }) .withPreOptionPhase(async () => { // Player shows off their bug types @@ -333,7 +333,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde encounter.selectedOption!.dialogue!.selected = [ { speaker: `${namespace}:speaker`, - text: `${namespace}:option.2.selected_0_to_1`, + text: `${namespace}:option.2.selected0To1`, }, ]; } else if (numBugTypes < 4) { @@ -344,7 +344,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde encounter.selectedOption!.dialogue!.selected = [ { speaker: `${namespace}:speaker`, - text: `${namespace}:option.2.selected_2_to_3`, + text: `${namespace}:option.2.selected2To3`, }, ]; } else if (numBugTypes < 6) { @@ -355,7 +355,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde encounter.selectedOption!.dialogue!.selected = [ { speaker: `${namespace}:speaker`, - text: `${namespace}:option.2.selected_4_to_5`, + text: `${namespace}:option.2.selected4To5`, }, ]; } else { @@ -398,7 +398,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde encounter.selectedOption!.dialogue!.selected = [ { speaker: `${namespace}:speaker`, - text: `${namespace}:option.2.selected_6`, + text: `${namespace}:option.2.selected6`, }, ]; } @@ -421,17 +421,17 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde .withDialogue({ buttonLabel: `${namespace}:option.3.label`, buttonTooltip: `${namespace}:option.3.tooltip`, - disabledButtonTooltip: `${namespace}:option.3.disabled_tooltip`, + disabledButtonTooltip: `${namespace}:option.3.disabledTooltip`, selected: [ { text: `${namespace}:option.3.selected`, }, { speaker: `${namespace}:speaker`, - text: `${namespace}:option.3.selected_dialogue`, + text: `${namespace}:option.3.selectedDialogue`, }, ], - secondOptionPrompt: `${namespace}:option.3.select_prompt`, + secondOptionPrompt: `${namespace}:option.3.selectPrompt`, }) .withPreOptionPhase(async (): Promise => { const encounter = globalScene.currentBattle.mysteryEncounter!; @@ -476,7 +476,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde ); }); if (!hasValidItem) { - return getEncounterText(`${namespace}:option.3.invalid_selection`) ?? null; + return getEncounterText(`${namespace}:option.3.invalidSelection`) ?? null; } return null; @@ -514,7 +514,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde function getTrainerConfigForWave(waveIndex: number) { // Bug type superfan trainer config const config = trainerConfigs[TrainerType.BUG_TYPE_SUPERFAN].clone(); - config.name = i18next.t("trainerNames:bug_type_superfan"); + config.name = i18next.t("trainerNames:bugTypeSuperfan"); let pool3Copy = POOL_3_POKEMON.slice(0); pool3Copy = randSeedShuffle(pool3Copy); @@ -713,7 +713,7 @@ function doBugTypeMoveTutor(): Promise { // biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO explain return new Promise(async resolve => { const moveOptions = globalScene.currentBattle.mysteryEncounter!.misc.moveTutorOptions; - await showEncounterDialogue(`${namespace}:battle_won`, `${namespace}:speaker`); + await showEncounterDialogue(`${namespace}:battleWon`, `${namespace}:speaker`); const moveInfoOverlay = new MoveInfoOverlay({ delayVisibility: false, @@ -748,7 +748,7 @@ function doBugTypeMoveTutor(): Promise { const result = await selectOptionThenPokemon( optionSelectItems, - `${namespace}:teach_move_prompt`, + `${namespace}:teachMovePrompt`, undefined, onHoverOverCancel, ); diff --git a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts index 092cc4931af..09e59c7e391 100644 --- a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts +++ b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts @@ -119,7 +119,7 @@ export const ClowningAroundEncounter: MysteryEncounter = MysteryEncounterBuilder text: `${namespace}:intro`, }, { - text: `${namespace}:intro_dialogue`, + text: `${namespace}:introDialogue`, speaker: `${namespace}:speaker`, }, ]) @@ -233,7 +233,7 @@ export const ClowningAroundEncounter: MysteryEncounter = MysteryEncounterBuilder // After the battle, offer the player the opportunity to permanently swap ability const abilityWasSwapped = await handleSwapAbility(); if (abilityWasSwapped) { - await showEncounterText(`${namespace}:option.1.ability_gained`); + await showEncounterText(`${namespace}:option.1.abilityGained`); } // Play animations once ability swap is complete @@ -267,10 +267,10 @@ export const ClowningAroundEncounter: MysteryEncounter = MysteryEncounterBuilder speaker: `${namespace}:speaker`, }, { - text: `${namespace}:option.2.selected_2`, + text: `${namespace}:option.2.selected2`, }, { - text: `${namespace}:option.2.selected_3`, + text: `${namespace}:option.2.selected3`, speaker: `${namespace}:speaker`, }, ], @@ -359,10 +359,10 @@ export const ClowningAroundEncounter: MysteryEncounter = MysteryEncounterBuilder speaker: `${namespace}:speaker`, }, { - text: `${namespace}:option.3.selected_2`, + text: `${namespace}:option.3.selected2`, }, { - text: `${namespace}:option.3.selected_3`, + text: `${namespace}:option.3.selected3`, speaker: `${namespace}:speaker`, }, ], @@ -432,8 +432,8 @@ export const ClowningAroundEncounter: MysteryEncounter = MysteryEncounterBuilder async function handleSwapAbility() { // biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO: Consider refactoring to avoid async promise executor return new Promise(async resolve => { - await showEncounterDialogue(`${namespace}:option.1.apply_ability_dialogue`, `${namespace}:speaker`); - await showEncounterText(`${namespace}:option.1.apply_ability_message`); + await showEncounterDialogue(`${namespace}:option.1.applyAbilityDialogue`, `${namespace}:speaker`); + await showEncounterText(`${namespace}:option.1.applyAbilityMessage`); globalScene.ui.setMode(UiMode.MESSAGE).then(() => { displayYesNoOptions(resolve); @@ -442,7 +442,7 @@ async function handleSwapAbility() { } function displayYesNoOptions(resolve) { - showEncounterText(`${namespace}:option.1.ability_prompt`, null, 500, false); + showEncounterText(`${namespace}:option.1.abilityPrompt`, null, 500, false); const fullOptions = [ { label: i18next.t("menu:yes"), diff --git a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts index 8dae0eaee3a..e2c330a1106 100644 --- a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts +++ b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts @@ -174,7 +174,7 @@ export const DancingLessonsEncounter: MysteryEncounter = MysteryEncounterBuilder // Gets +1 to all stats except SPD on battle start tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON], mysteryEncounterBattleEffects: (pokemon: Pokemon) => { - queueEncounterMessage(`${namespace}:option.1.boss_enraged`); + queueEncounterMessage(`${namespace}:option.1.bossEnraged`); globalScene.phaseManager.unshiftNew( "StatStageChangePhase", pokemon.getBattlerIndex(), @@ -273,8 +273,8 @@ export const DancingLessonsEncounter: MysteryEncounter = MysteryEncounterBuilder .withDialogue({ buttonLabel: `${namespace}:option.3.label`, buttonTooltip: `${namespace}:option.3.tooltip`, - disabledButtonTooltip: `${namespace}:option.3.disabled_tooltip`, - secondOptionPrompt: `${namespace}:option.3.select_prompt`, + disabledButtonTooltip: `${namespace}:option.3.disabledTooltip`, + secondOptionPrompt: `${namespace}:option.3.selectPrompt`, selected: [ { text: `${namespace}:option.3.selected`, @@ -316,7 +316,7 @@ export const DancingLessonsEncounter: MysteryEncounter = MysteryEncounterBuilder } const meetsReqs = encounter.options[2].pokemonMeetsPrimaryRequirements(pokemon); if (!meetsReqs) { - return getEncounterText(`${namespace}:invalid_selection`) ?? null; + return getEncounterText(`${namespace}:invalidSelection`) ?? null; } return null; diff --git a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts index d90e207cc9a..65d22bfc6de 100644 --- a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts +++ b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts @@ -99,6 +99,7 @@ export const DarkDealEncounter: MysteryEncounter = MysteryEncounterBuilder.withE MysteryEncounterType.DARK_DEAL, ) .withEncounterTier(MysteryEncounterTier.ROGUE) + .withDisallowedChallenges(Challenges.HARDCORE) .withIntroSpriteConfigs([ { spriteKey: "dark_deal_scientist", @@ -118,7 +119,7 @@ export const DarkDealEncounter: MysteryEncounter = MysteryEncounterBuilder.withE }, { speaker: `${namespace}:speaker`, - text: `${namespace}:intro_dialogue`, + text: `${namespace}:introDialogue`, }, ]) .withSceneWaveRangeRequirement(30, CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES[1]) @@ -136,10 +137,10 @@ export const DarkDealEncounter: MysteryEncounter = MysteryEncounterBuilder.withE selected: [ { speaker: `${namespace}:speaker`, - text: `${namespace}:option.1.selected_dialogue`, + text: `${namespace}:option.1.selectedDialogue`, }, { - text: `${namespace}:option.1.selected_message`, + text: `${namespace}:option.1.selectedMessage`, }, ], }) diff --git a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts index 85102a01ce1..8cd4c8bee66 100644 --- a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts +++ b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts @@ -193,7 +193,7 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with .withDialogue({ buttonLabel: `${namespace}:option.2.label`, buttonTooltip: `${namespace}:option.2.tooltip`, - secondOptionPrompt: `${namespace}:option.2.select_prompt`, + secondOptionPrompt: `${namespace}:option.2.selectPrompt`, selected: [ { text: `${namespace}:option.2.selected`, @@ -229,7 +229,7 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with // If pokemon has valid item, it can be selected const meetsReqs = encounter.options[1].pokemonMeetsPrimaryRequirements(pokemon); if (!meetsReqs) { - return getEncounterText(`${namespace}:invalid_selection`) ?? null; + return getEncounterText(`${namespace}:invalidSelection`) ?? null; } return null; @@ -303,7 +303,7 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with .withDialogue({ buttonLabel: `${namespace}:option.3.label`, buttonTooltip: `${namespace}:option.3.tooltip`, - secondOptionPrompt: `${namespace}:option.3.select_prompt`, + secondOptionPrompt: `${namespace}:option.3.selectPrompt`, selected: [ { text: `${namespace}:option.3.selected`, @@ -341,7 +341,7 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with // If pokemon has valid item, it can be selected const meetsReqs = encounter.options[2].pokemonMeetsPrimaryRequirements(pokemon); if (!meetsReqs) { - return getEncounterText(`${namespace}:invalid_selection`) ?? null; + return getEncounterText(`${namespace}:invalidSelection`) ?? null; } return null; diff --git a/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts b/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts index a45c5301a3e..754e8c883e8 100644 --- a/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts +++ b/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts @@ -43,7 +43,7 @@ export const DepartmentStoreSaleEncounter: MysteryEncounter = MysteryEncounterBu text: `${namespace}:intro`, }, { - text: `${namespace}:intro_dialogue`, + text: `${namespace}:introDialogue`, speaker: `${namespace}:speaker`, }, ]) diff --git a/src/data/mystery-encounters/encounters/field-trip-encounter.ts b/src/data/mystery-encounters/encounters/field-trip-encounter.ts index 0413c3d0e1d..67a7cad3466 100644 --- a/src/data/mystery-encounters/encounters/field-trip-encounter.ts +++ b/src/data/mystery-encounters/encounters/field-trip-encounter.ts @@ -56,7 +56,7 @@ export const FieldTripEncounter: MysteryEncounter = MysteryEncounterBuilder.with text: `${namespace}:intro`, }, { - text: `${namespace}:intro_dialogue`, + text: `${namespace}:introDialogue`, speaker: `${namespace}:speaker`, }, ]) @@ -70,7 +70,7 @@ export const FieldTripEncounter: MysteryEncounter = MysteryEncounterBuilder.with .withDialogue({ buttonLabel: `${namespace}:option.1.label`, buttonTooltip: `${namespace}:option.1.tooltip`, - secondOptionPrompt: `${namespace}:second_option_prompt`, + secondOptionPrompt: `${namespace}:secondOptionPrompt`, }) .withPreOptionPhase(async (): Promise => { const encounter = globalScene.currentBattle.mysteryEncounter!; @@ -118,7 +118,7 @@ export const FieldTripEncounter: MysteryEncounter = MysteryEncounterBuilder.with .withDialogue({ buttonLabel: `${namespace}:option.2.label`, buttonTooltip: `${namespace}:option.2.tooltip`, - secondOptionPrompt: `${namespace}:second_option_prompt`, + secondOptionPrompt: `${namespace}:secondOptionPrompt`, }) .withPreOptionPhase(async (): Promise => { const encounter = globalScene.currentBattle.mysteryEncounter!; @@ -166,7 +166,7 @@ export const FieldTripEncounter: MysteryEncounter = MysteryEncounterBuilder.with .withDialogue({ buttonLabel: `${namespace}:option.3.label`, buttonTooltip: `${namespace}:option.3.tooltip`, - secondOptionPrompt: `${namespace}:second_option_prompt`, + secondOptionPrompt: `${namespace}:secondOptionPrompt`, }) .withPreOptionPhase(async (): Promise => { const encounter = globalScene.currentBattle.mysteryEncounter!; @@ -226,7 +226,7 @@ function pokemonAndMoveChosen(pokemon: PlayerPokemon, move: PokemonMove, correct speaker: `${namespace}:speaker`, }, { - text: `${namespace}:incorrect_exp`, + text: `${namespace}:incorrectExp`, }, ]; setEncounterExp( @@ -243,7 +243,7 @@ function pokemonAndMoveChosen(pokemon: PlayerPokemon, move: PokemonMove, correct speaker: `${namespace}:speaker`, }, { - text: `${namespace}:correct_exp`, + text: `${namespace}:correctExp`, }, ]; setEncounterExp([pokemon.id], 100); diff --git a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts index 7d2583a00cb..1cc31eaa21f 100644 --- a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts +++ b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts @@ -243,11 +243,12 @@ export const FieryFalloutEncounter: MysteryEncounter = MysteryEncounterBuilder.w if (burnable?.length > 0) { const roll = randSeedInt(burnable.length); const chosenPokemon = burnable[roll]; - if (chosenPokemon.trySetStatus(StatusEffect.BURN)) { + if (chosenPokemon.canSetStatus(StatusEffect.BURN, true)) { // Burn applied + chosenPokemon.doSetStatus(StatusEffect.BURN); encounter.setDialogueToken("burnedPokemon", chosenPokemon.getNameToRender()); encounter.setDialogueToken("abilityName", allAbilities[AbilityId.HEATPROOF].name); - queueEncounterMessage(`${namespace}:option.2.target_burned`); + queueEncounterMessage(`${namespace}:option.2.targetBurned`); // Also permanently change the burned Pokemon's ability to Heatproof applyAbilityOverrideToPokemon(chosenPokemon, AbilityId.HEATPROOF); @@ -269,7 +270,7 @@ export const FieryFalloutEncounter: MysteryEncounter = MysteryEncounterBuilder.w .withDialogue({ buttonLabel: `${namespace}:option.3.label`, buttonTooltip: `${namespace}:option.3.tooltip`, - disabledButtonTooltip: `${namespace}:option.3.disabled_tooltip`, + disabledButtonTooltip: `${namespace}:option.3.disabledTooltip`, selected: [ { text: `${namespace}:option.3.selected`, @@ -313,6 +314,6 @@ function giveLeadPokemonAttackTypeBoostItem() { const encounter = globalScene.currentBattle.mysteryEncounter!; encounter.setDialogueToken("itemName", boosterModifierType.name); encounter.setDialogueToken("leadPokemon", leadPokemon.getNameToRender()); - queueEncounterMessage(`${namespace}:found_item`); + queueEncounterMessage(`${namespace}:foundItem`); } } diff --git a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts index 6ee2ebcdf67..f7dc89b44fd 100644 --- a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts +++ b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts @@ -69,7 +69,7 @@ export const FightOrFlightEncounter: MysteryEncounter = MysteryEncounterBuilder. isBoss: true, tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON], mysteryEncounterBattleEffects: (pokemon: Pokemon) => { - queueEncounterMessage(`${namespace}:option.1.stat_boost`); + queueEncounterMessage(`${namespace}:option.1.statBoost`); // Randomly boost 1 stat 2 stages // Cannot boost Spd, Acc, or Evasion globalScene.phaseManager.unshiftNew( @@ -165,7 +165,7 @@ export const FightOrFlightEncounter: MysteryEncounter = MysteryEncounterBuilder. .withDialogue({ buttonLabel: `${namespace}:option.2.label`, buttonTooltip: `${namespace}:option.2.tooltip`, - disabledButtonTooltip: `${namespace}:option.2.disabled_tooltip`, + disabledButtonTooltip: `${namespace}:option.2.disabledTooltip`, selected: [ { text: `${namespace}:option.2.selected`, diff --git a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts index bf376c04843..d883fdbb567 100644 --- a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts +++ b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts @@ -78,7 +78,7 @@ export const FunAndGamesEncounter: MysteryEncounter = MysteryEncounterBuilder.wi .withIntroDialogue([ { speaker: `${namespace}:speaker`, - text: `${namespace}:intro_dialogue`, + text: `${namespace}:introDialogue`, }, ]) .setLocalizationKey(`${namespace}`) @@ -118,7 +118,7 @@ export const FunAndGamesEncounter: MysteryEncounter = MysteryEncounterBuilder.wi // Only Pokemon that are not KOed/legal can be selected const selectableFilter = (pokemon: Pokemon) => { - return isPokemonValidForEncounterOptionSelection(pokemon, `${namespace}:invalid_selection`); + return isPokemonValidForEncounterOptionSelection(pokemon, `${namespace}:invalidSelection`); }; return selectPokemonForOption(onPokemonSelected, undefined, selectableFilter); @@ -132,7 +132,7 @@ export const FunAndGamesEncounter: MysteryEncounter = MysteryEncounterBuilder.wi const moneyCost = (encounter.options[0].requirements[0] as MoneyRequirement).requiredMoney; updatePlayerMoney(-moneyCost, true, false); await showEncounterText( - i18next.t("mysteryEncounterMessages:paid_money", { + i18next.t("mysteryEncounterMessages:paidMoney", { amount: moneyCost, }), ); @@ -284,25 +284,25 @@ function handleNextTurn() { guaranteedModifierTypeFuncs: [modifierTypes.MULTI_LENS], fillRemaining: false, }); - resultMessageKey = `${namespace}:best_result`; + resultMessageKey = `${namespace}:bestResult`; } else if (healthRatio < 0.15) { // 2nd prize setEncounterRewards({ guaranteedModifierTypeFuncs: [modifierTypes.SCOPE_LENS], fillRemaining: false, }); - resultMessageKey = `${namespace}:great_result`; + resultMessageKey = `${namespace}:greatResult`; } else if (healthRatio < 0.33) { // 3rd prize setEncounterRewards({ guaranteedModifierTypeFuncs: [modifierTypes.WIDE_LENS], fillRemaining: false, }); - resultMessageKey = `${namespace}:good_result`; + resultMessageKey = `${namespace}:goodResult`; } else { // No prize isHealPhase = true; - resultMessageKey = `${namespace}:bad_result`; + resultMessageKey = `${namespace}:badResult`; } // End the battle @@ -312,7 +312,7 @@ function handleNextTurn() { globalScene.currentBattle.mysteryEncounter!.doContinueEncounter = undefined; leaveEncounterWithoutBattle(isHealPhase); // Must end the TurnInit phase prematurely so battle phases aren't added to queue - queueEncounterMessage(`${namespace}:end_game`); + queueEncounterMessage(`${namespace}:endGame`); queueEncounterMessage(resultMessageKey); // Skip remainder of TurnInitPhase @@ -320,9 +320,9 @@ function handleNextTurn() { } if (encounter.misc.turnsRemaining < 3) { // Display charging messages on turns that aren't the initial turn - queueEncounterMessage(`${namespace}:charging_continue`); + queueEncounterMessage(`${namespace}:chargingContinue`); } - queueEncounterMessage(`${namespace}:turn_remaining_${encounter.misc.turnsRemaining}`); + queueEncounterMessage(`${namespace}:turnRemaining${encounter.misc.turnsRemaining}`); encounter.misc.turnsRemaining--; // Don't skip remainder of TurnInitPhase diff --git a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts index ed49fccf190..11e5c036409 100644 --- a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts +++ b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts @@ -158,7 +158,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil .withDialogue({ buttonLabel: `${namespace}:option.1.label`, buttonTooltip: `${namespace}:option.1.tooltip`, - secondOptionPrompt: `${namespace}:option.1.trade_options_prompt`, + secondOptionPrompt: `${namespace}:option.1.tradeOptionsPrompt`, }) .withPreOptionPhase(async (): Promise => { const encounter = globalScene.currentBattle.mysteryEncounter!; @@ -248,7 +248,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil // Show the trade animation await showTradeBackground(); await doPokemonTradeSequence(tradedPokemon, newPlayerPokemon); - await showEncounterText(`${namespace}:trade_received`, null, 0, true, 4000); + await showEncounterText(`${namespace}:tradeReceived`, null, 0, true, 4000); globalScene.playBgm(encounter.misc.bgmKey); await addPokemonDataToDexAndValidateAchievements(newPlayerPokemon); await hideTradeBackground(); @@ -369,7 +369,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil // Show the trade animation await showTradeBackground(); await doPokemonTradeSequence(tradedPokemon, newPlayerPokemon); - await showEncounterText(`${namespace}:trade_received`, null, 0, true, 4000); + await showEncounterText(`${namespace}:tradeReceived`, null, 0, true, 4000); globalScene.playBgm(encounter.misc.bgmKey); await addPokemonDataToDexAndValidateAchievements(newPlayerPokemon); await hideTradeBackground(); @@ -384,7 +384,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil .withDialogue({ buttonLabel: `${namespace}:option.3.label`, buttonTooltip: `${namespace}:option.3.tooltip`, - secondOptionPrompt: `${namespace}:option.3.trade_options_prompt`, + secondOptionPrompt: `${namespace}:option.3.tradeOptionsPrompt`, }) .withPreOptionPhase(async (): Promise => { const encounter = globalScene.currentBattle.mysteryEncounter!; @@ -416,7 +416,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil return it.isTransferable; }).length > 0; if (!meetsReqs) { - return getEncounterText(`${namespace}:option.3.invalid_selection`) ?? null; + return getEncounterText(`${namespace}:option.3.invalidSelection`) ?? null; } return null; @@ -468,7 +468,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil // Generate a trainer name const traderName = generateRandomTraderName(); encounter.setDialogueToken("tradeTrainerName", traderName.trim()); - await showEncounterText(`${namespace}:item_trade_selected`); + await showEncounterText(`${namespace}:itemTradeSelected`); leaveEncounterWithoutBattle(); }) .build(), @@ -740,10 +740,10 @@ function doPokemonTradeSequence(tradedPokemon: PlayerPokemon, receivedPokemon: P duration: 500, onComplete: async () => { globalScene.fadeOutBgm(1000, false); - await showEncounterText(`${namespace}:pokemon_trade_selected`); + await showEncounterText(`${namespace}:pokemonTradeSelected`); tradedPokemon.cry(); globalScene.playBgm("evolution"); - await showEncounterText(`${namespace}:pokemon_trade_goodbye`); + await showEncounterText(`${namespace}:pokemonTradeGoodbye`); tradedPokeball.setAlpha(0); tradedPokeball.setVisible(true); diff --git a/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts b/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts index 10f45c21b68..f1c79208911 100644 --- a/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts +++ b/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts @@ -63,9 +63,9 @@ export const LostAtSeaEncounter: MysteryEncounter = MysteryEncounterBuilder.with .withPokemonCanLearnMoveRequirement(OPTION_1_REQUIRED_MOVE) .withDialogue({ buttonLabel: `${namespace}:option.1.label`, - disabledButtonLabel: `${namespace}:option.1.label_disabled`, + disabledButtonLabel: `${namespace}:option.1.labelDisabled`, buttonTooltip: `${namespace}:option.1.tooltip`, - disabledButtonTooltip: `${namespace}:option.1.tooltip_disabled`, + disabledButtonTooltip: `${namespace}:option.1.tooltipDisabled`, selected: [ { text: `${namespace}:option.1.selected`, @@ -81,9 +81,9 @@ export const LostAtSeaEncounter: MysteryEncounter = MysteryEncounterBuilder.with .withPokemonCanLearnMoveRequirement(OPTION_2_REQUIRED_MOVE) .withDialogue({ buttonLabel: `${namespace}:option.2.label`, - disabledButtonLabel: `${namespace}:option.2.label_disabled`, + disabledButtonLabel: `${namespace}:option.2.labelDisabled`, buttonTooltip: `${namespace}:option.2.tooltip`, - disabledButtonTooltip: `${namespace}:option.2.tooltip_disabled`, + disabledButtonTooltip: `${namespace}:option.2.tooltipDisabled`, selected: [ { text: `${namespace}:option.2.selected`, diff --git a/src/data/mystery-encounters/encounters/part-timer-encounter.ts b/src/data/mystery-encounters/encounters/part-timer-encounter.ts index a865de3b19d..bf1b077647a 100644 --- a/src/data/mystery-encounters/encounters/part-timer-encounter.ts +++ b/src/data/mystery-encounters/encounters/part-timer-encounter.ts @@ -58,7 +58,7 @@ export const PartTimerEncounter: MysteryEncounter = MysteryEncounterBuilder.with }, { speaker: `${namespace}:speaker`, - text: `${namespace}:intro_dialogue`, + text: `${namespace}:introDialogue`, }, ]) .withOnInit(() => { @@ -128,7 +128,7 @@ export const PartTimerEncounter: MysteryEncounter = MysteryEncounterBuilder.with // Only Pokemon non-KOd pokemon can be selected const selectableFilter = (pokemon: Pokemon) => { - return isPokemonValidForEncounterOptionSelection(pokemon, `${namespace}:invalid_selection`); + return isPokemonValidForEncounterOptionSelection(pokemon, `${namespace}:invalidSelection`); }; return selectPokemonForOption(onPokemonSelected, undefined, selectableFilter); @@ -142,18 +142,18 @@ export const PartTimerEncounter: MysteryEncounter = MysteryEncounterBuilder.with // Give money and do dialogue if (moneyMultiplier > 2.5) { - await showEncounterDialogue(`${namespace}:job_complete_good`, `${namespace}:speaker`); + await showEncounterDialogue(`${namespace}:jobCompleteGood`, `${namespace}:speaker`); } else { - await showEncounterDialogue(`${namespace}:job_complete_bad`, `${namespace}:speaker`); + await showEncounterDialogue(`${namespace}:jobCompleteBad`, `${namespace}:speaker`); } const moneyChange = globalScene.getWaveMoneyAmount(moneyMultiplier); updatePlayerMoney(moneyChange, true, false); await showEncounterText( - i18next.t("mysteryEncounterMessages:receive_money", { + i18next.t("mysteryEncounterMessages:receiveMoney", { amount: moneyChange, }), ); - await showEncounterText(`${namespace}:pokemon_tired`); + await showEncounterText(`${namespace}:pokemonTired`); setEncounterRewards({ fillRemaining: true }); leaveEncounterWithoutBattle(); @@ -210,7 +210,7 @@ export const PartTimerEncounter: MysteryEncounter = MysteryEncounterBuilder.with // Only Pokemon non-KOd pokemon can be selected const selectableFilter = (pokemon: Pokemon) => { - return isPokemonValidForEncounterOptionSelection(pokemon, `${namespace}:invalid_selection`); + return isPokemonValidForEncounterOptionSelection(pokemon, `${namespace}:invalidSelection`); }; return selectPokemonForOption(onPokemonSelected, undefined, selectableFilter); @@ -224,18 +224,18 @@ export const PartTimerEncounter: MysteryEncounter = MysteryEncounterBuilder.with // Give money and do dialogue if (moneyMultiplier > 2.5) { - await showEncounterDialogue(`${namespace}:job_complete_good`, `${namespace}:speaker`); + await showEncounterDialogue(`${namespace}:jobCompleteGood`, `${namespace}:speaker`); } else { - await showEncounterDialogue(`${namespace}:job_complete_bad`, `${namespace}:speaker`); + await showEncounterDialogue(`${namespace}:jobCompleteBad`, `${namespace}:speaker`); } const moneyChange = globalScene.getWaveMoneyAmount(moneyMultiplier); updatePlayerMoney(moneyChange, true, false); await showEncounterText( - i18next.t("mysteryEncounterMessages:receive_money", { + i18next.t("mysteryEncounterMessages:receiveMoney", { amount: moneyChange, }), ); - await showEncounterText(`${namespace}:pokemon_tired`); + await showEncounterText(`${namespace}:pokemonTired`); setEncounterRewards({ fillRemaining: true }); leaveEncounterWithoutBattle(); @@ -248,7 +248,7 @@ export const PartTimerEncounter: MysteryEncounter = MysteryEncounterBuilder.with .withDialogue({ buttonLabel: `${namespace}:option.3.label`, buttonTooltip: `${namespace}:option.3.tooltip`, - disabledButtonTooltip: `${namespace}:option.3.disabled_tooltip`, + disabledButtonTooltip: `${namespace}:option.3.disabledTooltip`, selected: [ { text: `${namespace}:option.3.selected`, @@ -282,15 +282,15 @@ export const PartTimerEncounter: MysteryEncounter = MysteryEncounterBuilder.with await transitionMysteryEncounterIntroVisuals(false, false); // Give money and do dialogue - await showEncounterDialogue(`${namespace}:job_complete_good`, `${namespace}:speaker`); + await showEncounterDialogue(`${namespace}:jobCompleteGood`, `${namespace}:speaker`); const moneyChange = globalScene.getWaveMoneyAmount(2.5); updatePlayerMoney(moneyChange, true, false); await showEncounterText( - i18next.t("mysteryEncounterMessages:receive_money", { + i18next.t("mysteryEncounterMessages:receiveMoney", { amount: moneyChange, }), ); - await showEncounterText(`${namespace}:pokemon_tired`); + await showEncounterText(`${namespace}:pokemonTired`); setEncounterRewards({ fillRemaining: true }); leaveEncounterWithoutBattle(); diff --git a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts index c3400b41327..764951877f0 100644 --- a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts +++ b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts @@ -205,7 +205,7 @@ const safariZoneGameOptions: MysteryEncounterOption[] = [ // 80% chance to increase flee stage +1 const fleeChangeResult = tryChangeFleeStage(1, 8); if (!fleeChangeResult) { - await showEncounterText(getEncounterText(`${namespace}:safari.busy_eating`) ?? "", null, 1000, false); + await showEncounterText(getEncounterText(`${namespace}:safari.busyEating`) ?? "", null, 1000, false); } else { await showEncounterText(getEncounterText(`${namespace}:safari.eating`) ?? "", null, 1000, false); } @@ -233,7 +233,7 @@ const safariZoneGameOptions: MysteryEncounterOption[] = [ // 80% chance to decrease catch stage -1 const catchChangeResult = tryChangeCatchStage(-1, 8); if (!catchChangeResult) { - await showEncounterText(getEncounterText(`${namespace}:safari.beside_itself_angry`) ?? "", null, 1000, false); + await showEncounterText(getEncounterText(`${namespace}:safari.besideItselfAngry`) ?? "", null, 1000, false); } else { await showEncounterText(getEncounterText(`${namespace}:safari.angry`) ?? "", null, 1000, false); } @@ -274,7 +274,7 @@ async function summonSafariPokemon() { const encounter = globalScene.currentBattle.mysteryEncounter!; // Message pokemon remaining encounter.setDialogueToken("remainingCount", encounter.misc.safariPokemonRemaining); - globalScene.phaseManager.queueMessage(getEncounterText(`${namespace}:safari.remaining_count`) ?? "", null, true); + globalScene.phaseManager.queueMessage(getEncounterText(`${namespace}:safari.remainingCount`) ?? "", null, true); // Generate pokemon using safariPokemonRemaining so they are always the same pokemon no matter how many turns are taken // Safari pokemon roll twice on shiny and HA chances, but are otherwise normal diff --git a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts index 47317c12b50..bf232c616d5 100644 --- a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts +++ b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts @@ -70,7 +70,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = MysteryEncounterBui text: `${namespace}:intro`, }, { - text: `${namespace}:intro_dialogue`, + text: `${namespace}:introDialogue`, speaker: `${namespace}:speaker`, }, ]) @@ -119,7 +119,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = MysteryEncounterBui ); } if (!encounter.pokemonMeetsPrimaryRequirements(pokemon)) { - return getEncounterText(`${namespace}:invalid_selection`) ?? null; + return getEncounterText(`${namespace}:invalidSelection`) ?? null; } return null; @@ -155,7 +155,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = MysteryEncounterBui chosenPokemon.setCustomNature(newNature); encounter.setDialogueToken("newNature", getNatureName(newNature)); - queueEncounterMessage(`${namespace}:cheap_side_effects`); + queueEncounterMessage(`${namespace}:cheapSideEffects`); setEncounterExp([chosenPokemon.id], 100); await chosenPokemon.updateInfo(); }) @@ -193,7 +193,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = MysteryEncounterBui // Only Pokemon that can gain benefits are unfainted const selectableFilter = (pokemon: Pokemon) => { - return isPokemonValidForEncounterOptionSelection(pokemon, `${namespace}:invalid_selection`); + return isPokemonValidForEncounterOptionSelection(pokemon, `${namespace}:invalidSelection`); }; return selectPokemonForOption(onPokemonSelected, undefined, selectableFilter); @@ -215,7 +215,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = MysteryEncounterBui const encounter = globalScene.currentBattle.mysteryEncounter!; const chosenPokemon = encounter.misc.chosenPokemon; - queueEncounterMessage(`${namespace}:no_bad_effects`); + queueEncounterMessage(`${namespace}:noBadEffects`); setEncounterExp([chosenPokemon.id], 100); await chosenPokemon.updateInfo(); diff --git a/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts index cddfef1ef76..8c367882445 100644 --- a/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts +++ b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts @@ -157,7 +157,7 @@ export const SlumberingSnorlaxEncounter: MysteryEncounter = MysteryEncounterBuil // Fall asleep waiting for Snorlax // Full heal party globalScene.phaseManager.unshiftNew("PartyHealPhase", true); - queueEncounterMessage(`${namespace}:option.2.rest_result`); + queueEncounterMessage(`${namespace}:option.2.restResult`); leaveEncounterWithoutBattle(); }, ) @@ -167,7 +167,7 @@ export const SlumberingSnorlaxEncounter: MysteryEncounter = MysteryEncounterBuil .withDialogue({ buttonLabel: `${namespace}:option.3.label`, buttonTooltip: `${namespace}:option.3.tooltip`, - disabledButtonTooltip: `${namespace}:option.3.disabled_tooltip`, + disabledButtonTooltip: `${namespace}:option.3.disabledTooltip`, selected: [ { text: `${namespace}:option.3.selected`, diff --git a/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts b/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts index d77326837cd..58ab3f2ec2d 100644 --- a/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts +++ b/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts @@ -122,7 +122,7 @@ export const TeleportingHijinksEncounter: MysteryEncounter = MysteryEncounterBui .withDialogue({ buttonLabel: `${namespace}:option.2.label`, buttonTooltip: `${namespace}:option.2.tooltip`, - disabledButtonTooltip: `${namespace}:option.2.disabled_tooltip`, + disabledButtonTooltip: `${namespace}:option.2.disabledTooltip`, selected: [ { text: `${namespace}:option.2.selected`, @@ -227,7 +227,7 @@ async function doBiomeTransitionDialogueAndBattleInit() { isBoss: true, tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON], mysteryEncounterBattleEffects: (pokemon: Pokemon) => { - queueEncounterMessage(`${namespace}:boss_enraged`); + queueEncounterMessage(`${namespace}:bossEnraged`); globalScene.phaseManager.unshiftNew( "StatStageChangePhase", pokemon.getBattlerIndex(), diff --git a/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts b/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts index 7c528e2305b..5d97dd1fd67 100644 --- a/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts @@ -33,7 +33,7 @@ import i18next from "i18next"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/theExpertPokemonBreeder"; -const trainerNameKey = "trainerNames:expert_pokemon_breeder"; +const trainerNameKey = "trainerNames:expertPokemonBreeder"; const FIRST_STAGE_EVOLUTION_WAVE = 45; const SECOND_STAGE_EVOLUTION_WAVE = 60; @@ -139,7 +139,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter = MysteryEncount }, { speaker: trainerNameKey, - text: `${namespace}:intro_dialogue`, + text: `${namespace}:introDialogue`, }, ]) .withOnInit(() => { @@ -189,13 +189,13 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter = MysteryEncount // Dialogue and egg calcs for Pokemon 1 const [pokemon1CommonEggs, pokemon1RareEggs] = calculateEggRewardsForPokemon(pokemon1); - let pokemon1Tooltip = getEncounterText(`${namespace}:option.1.tooltip_base`)!; + let pokemon1Tooltip = getEncounterText(`${namespace}:option.1.tooltipBase`)!; if (pokemon1RareEggs > 0) { const eggsText = i18next.t(`${namespace}:numEggs`, { count: pokemon1RareEggs, rarity: i18next.t("egg:greatTier"), }); - pokemon1Tooltip += i18next.t(`${namespace}:eggs_tooltip`, { + pokemon1Tooltip += i18next.t(`${namespace}:eggsTooltip`, { eggs: eggsText, }); encounter.setDialogueToken("pokemon1RareEggs", eggsText); @@ -205,7 +205,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter = MysteryEncount count: pokemon1CommonEggs, rarity: i18next.t("egg:defaultTier"), }); - pokemon1Tooltip += i18next.t(`${namespace}:eggs_tooltip`, { + pokemon1Tooltip += i18next.t(`${namespace}:eggsTooltip`, { eggs: eggsText, }); encounter.setDialogueToken("pokemon1CommonEggs", eggsText); @@ -214,13 +214,13 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter = MysteryEncount // Dialogue and egg calcs for Pokemon 2 const [pokemon2CommonEggs, pokemon2RareEggs] = calculateEggRewardsForPokemon(pokemon2); - let pokemon2Tooltip = getEncounterText(`${namespace}:option.2.tooltip_base`)!; + let pokemon2Tooltip = getEncounterText(`${namespace}:option.2.tooltipBase`)!; if (pokemon2RareEggs > 0) { const eggsText = i18next.t(`${namespace}:numEggs`, { count: pokemon2RareEggs, rarity: i18next.t("egg:greatTier"), }); - pokemon2Tooltip += i18next.t(`${namespace}:eggs_tooltip`, { + pokemon2Tooltip += i18next.t(`${namespace}:eggsTooltip`, { eggs: eggsText, }); encounter.setDialogueToken("pokemon2RareEggs", eggsText); @@ -230,7 +230,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter = MysteryEncount count: pokemon2CommonEggs, rarity: i18next.t("egg:defaultTier"), }); - pokemon2Tooltip += i18next.t(`${namespace}:eggs_tooltip`, { + pokemon2Tooltip += i18next.t(`${namespace}:eggsTooltip`, { eggs: eggsText, }); encounter.setDialogueToken("pokemon2CommonEggs", eggsText); @@ -239,13 +239,13 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter = MysteryEncount // Dialogue and egg calcs for Pokemon 3 const [pokemon3CommonEggs, pokemon3RareEggs] = calculateEggRewardsForPokemon(pokemon3); - let pokemon3Tooltip = getEncounterText(`${namespace}:option.3.tooltip_base`)!; + let pokemon3Tooltip = getEncounterText(`${namespace}:option.3.tooltipBase`)!; if (pokemon3RareEggs > 0) { const eggsText = i18next.t(`${namespace}:numEggs`, { count: pokemon3RareEggs, rarity: i18next.t("egg:greatTier"), }); - pokemon3Tooltip += i18next.t(`${namespace}:eggs_tooltip`, { + pokemon3Tooltip += i18next.t(`${namespace}:eggsTooltip`, { eggs: eggsText, }); encounter.setDialogueToken("pokemon3RareEggs", eggsText); @@ -255,7 +255,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter = MysteryEncount count: pokemon3CommonEggs, rarity: i18next.t("egg:defaultTier"), }); - pokemon3Tooltip += i18next.t(`${namespace}:eggs_tooltip`, { + pokemon3Tooltip += i18next.t(`${namespace}:eggsTooltip`, { eggs: eggsText, }); encounter.setDialogueToken("pokemon3CommonEggs", eggsText); @@ -321,14 +321,14 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter = MysteryEncount ]; if (encounter.dialogueTokens.hasOwnProperty("pokemon1CommonEggs")) { encounter.dialogue.outro.push({ - text: i18next.t(`${namespace}:gained_eggs`, { + text: i18next.t(`${namespace}:gainedEggs`, { numEggs: encounter.dialogueTokens["pokemon1CommonEggs"], }), }); } if (encounter.dialogueTokens.hasOwnProperty("pokemon1RareEggs")) { encounter.dialogue.outro.push({ - text: i18next.t(`${namespace}:gained_eggs`, { + text: i18next.t(`${namespace}:gainedEggs`, { numEggs: encounter.dialogueTokens["pokemon1RareEggs"], }), }); @@ -380,14 +380,14 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter = MysteryEncount ]; if (encounter.dialogueTokens.hasOwnProperty("pokemon2CommonEggs")) { encounter.dialogue.outro.push({ - text: i18next.t(`${namespace}:gained_eggs`, { + text: i18next.t(`${namespace}:gainedEggs`, { numEggs: encounter.dialogueTokens["pokemon2CommonEggs"], }), }); } if (encounter.dialogueTokens.hasOwnProperty("pokemon2RareEggs")) { encounter.dialogue.outro.push({ - text: i18next.t(`${namespace}:gained_eggs`, { + text: i18next.t(`${namespace}:gainedEggs`, { numEggs: encounter.dialogueTokens["pokemon2RareEggs"], }), }); @@ -439,14 +439,14 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter = MysteryEncount ]; if (encounter.dialogueTokens.hasOwnProperty("pokemon3CommonEggs")) { encounter.dialogue.outro.push({ - text: i18next.t(`${namespace}:gained_eggs`, { + text: i18next.t(`${namespace}:gainedEggs`, { numEggs: encounter.dialogueTokens["pokemon3CommonEggs"], }), }); } if (encounter.dialogueTokens.hasOwnProperty("pokemon3RareEggs")) { encounter.dialogue.outro.push({ - text: i18next.t(`${namespace}:gained_eggs`, { + text: i18next.t(`${namespace}:gainedEggs`, { numEggs: encounter.dialogueTokens["pokemon3RareEggs"], }), }); @@ -482,7 +482,7 @@ function getPartyConfig(): EnemyPartyConfig { trainerType: TrainerType.EXPERT_POKEMON_BREEDER, pokemonConfigs: [ { - nickname: i18next.t(`${namespace}:cleffa_1_nickname`, { + nickname: i18next.t(`${namespace}:cleffa1Nickname`, { speciesName: getPokemonSpecies(cleffaSpecies).getName(), }), species: getPokemonSpecies(cleffaSpecies), @@ -501,7 +501,7 @@ function getPartyConfig(): EnemyPartyConfig { // All 3 members always Cleffa line, but different configs baseConfig.pokemonConfigs!.push( { - nickname: i18next.t(`${namespace}:cleffa_2_nickname`, { + nickname: i18next.t(`${namespace}:cleffa2Nickname`, { speciesName: getPokemonSpecies(cleffaSpecies).getName(), }), species: getPokemonSpecies(cleffaSpecies), @@ -514,7 +514,7 @@ function getPartyConfig(): EnemyPartyConfig { ivs: [31, 31, 31, 31, 31, 31], }, { - nickname: i18next.t(`${namespace}:cleffa_3_nickname`, { + nickname: i18next.t(`${namespace}:cleffa3Nickname`, { speciesName: getPokemonSpecies(cleffaSpecies).getName(), }), species: getPokemonSpecies(cleffaSpecies), @@ -647,7 +647,7 @@ function onGameOver() { encounter.dialogue.outro = [ { speaker: trainerNameKey, - text: `${namespace}:outro_failed`, + text: `${namespace}:outroFailed`, }, ]; diff --git a/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts b/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts index 91662993a51..99f6e671052 100644 --- a/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts @@ -66,7 +66,7 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter = MysteryEncounterBui text: `${namespace}:intro`, }, { - text: `${namespace}:intro_dialogue`, + text: `${namespace}:introDialogue`, speaker: `${namespace}:speaker`, }, ]) @@ -178,8 +178,8 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter = MysteryEncounterBui // Always max price for shiny (flip HA back to normal), and add special messaging priceMultiplier = MAX_POKEMON_PRICE_MULTIPLIER; pokemon.abilityIndex = 0; - encounter.dialogue.encounterOptionsDialogue!.description = `${namespace}:description_shiny`; - encounter.options[0].dialogue!.buttonTooltip = `${namespace}:option.1.tooltip_shiny`; + encounter.dialogue.encounterOptionsDialogue!.description = `${namespace}:descriptionShiny`; + encounter.options[0].dialogue!.buttonTooltip = `${namespace}:option.1.tooltipShiny`; } const price = globalScene.getWaveMoneyAmount(priceMultiplier); encounter.setDialogueToken("purchasePokemon", pokemon.getNameToRender()); @@ -202,7 +202,7 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter = MysteryEncounterBui buttonTooltip: `${namespace}:option.1.tooltip`, selected: [ { - text: `${namespace}:option.1.selected_message`, + text: `${namespace}:option.1.selectedMessage`, }, ], }) @@ -215,7 +215,7 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter = MysteryEncounterBui updatePlayerMoney(-price, true, false); // Show dialogue - await showEncounterDialogue(`${namespace}:option.1.selected_dialogue`, `${namespace}:speaker`); + await showEncounterDialogue(`${namespace}:option.1.selectedDialogue`, `${namespace}:speaker`); await transitionMysteryEncounterIntroVisuals(); // "Catch" purchased pokemon diff --git a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts index 568dc5de8b1..89bfc659944 100644 --- a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts @@ -115,7 +115,7 @@ export const TheStrongStuffEncounter: MysteryEncounter = MysteryEncounterBuilder ], tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON], mysteryEncounterBattleEffects: (pokemon: Pokemon) => { - queueEncounterMessage(`${namespace}:option.2.stat_boost`); + queueEncounterMessage(`${namespace}:option.2.statBoost`); globalScene.phaseManager.unshiftNew( "StatStageChangePhase", pokemon.getBattlerIndex(), @@ -181,7 +181,7 @@ export const TheStrongStuffEncounter: MysteryEncounter = MysteryEncounterBuilder encounter.setDialogueToken("reductionValue", HIGH_BST_REDUCTION_VALUE.toString()); encounter.setDialogueToken("increaseValue", BST_INCREASE_VALUE.toString()); - await showEncounterText(`${namespace}:option.1.selected_2`, null, undefined, true); + await showEncounterText(`${namespace}:option.1.selected2`, null, undefined, true); encounter.dialogue.outro = [ { diff --git a/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts index e17bf0575d7..71fe961f053 100644 --- a/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts @@ -87,7 +87,7 @@ export const TheWinstrateChallengeEncounter: MysteryEncounter = MysteryEncounter }, { speaker: `${namespace}:speaker`, - text: `${namespace}:intro_dialogue`, + text: `${namespace}:introDialogue`, }, ]) .withAutoHideIntroVisuals(false) @@ -163,7 +163,7 @@ async function spawnNextTrainerOrEndEncounter() { globalScene.playSound("item_fanfare"); await showEncounterText(i18next.t("battle:rewardGain", { modifierName: newModifier?.type.name })); - await showEncounterDialogue(`${namespace}:victory_2`, `${namespace}:speaker`); + await showEncounterDialogue(`${namespace}:victory2`, `${namespace}:speaker`); globalScene.ui.clearText(); // Clears "Winstrate" title from screen as rewards get animated in const machoBrace = generateModifierTypeOption(modifierTypes.MYSTERY_ENCOUNTER_MACHO_BRACE)!; machoBrace.type.tier = ModifierTier.MASTER; diff --git a/src/data/mystery-encounters/encounters/training-session-encounter.ts b/src/data/mystery-encounters/encounters/training-session-encounter.ts index e56c42a3ee5..b8173aa5fed 100644 --- a/src/data/mystery-encounters/encounters/training-session-encounter.ts +++ b/src/data/mystery-encounters/encounters/training-session-encounter.ts @@ -90,7 +90,7 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde // Only Pokemon that are not KOed/legal can be trained const selectableFilter = (pokemon: Pokemon) => { - return isPokemonValidForEncounterOptionSelection(pokemon, `${namespace}:invalid_selection`); + return isPokemonValidForEncounterOptionSelection(pokemon, `${namespace}:invalidSelection`); }; return selectPokemonForOption(onPokemonSelected, undefined, selectableFilter); @@ -174,7 +174,7 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde .withDialogue({ buttonLabel: `${namespace}:option.2.label`, buttonTooltip: `${namespace}:option.2.tooltip`, - secondOptionPrompt: `${namespace}:option.2.select_prompt`, + secondOptionPrompt: `${namespace}:option.2.selectPrompt`, selected: [ { text: `${namespace}:option.selected`, @@ -205,7 +205,7 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde // Only Pokemon that are not KOed/legal can be trained const selectableFilter = (pokemon: Pokemon) => { - return isPokemonValidForEncounterOptionSelection(pokemon, `${namespace}:invalid_selection`); + return isPokemonValidForEncounterOptionSelection(pokemon, `${namespace}:invalidSelection`); }; return selectPokemonForOption(onPokemonSelected, undefined, selectableFilter); @@ -248,7 +248,7 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde .withDialogue({ buttonLabel: `${namespace}:option.3.label`, buttonTooltip: `${namespace}:option.3.tooltip`, - secondOptionPrompt: `${namespace}:option.3.select_prompt`, + secondOptionPrompt: `${namespace}:option.3.selectPrompt`, selected: [ { text: `${namespace}:option.selected`, @@ -295,7 +295,7 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde // Only Pokemon that are not KOed/legal can be trained const selectableFilter = (pokemon: Pokemon) => { - return isPokemonValidForEncounterOptionSelection(pokemon, `${namespace}:invalid_selection`); + return isPokemonValidForEncounterOptionSelection(pokemon, `${namespace}:invalidSelection`); }; return selectPokemonForOption(onPokemonSelected, undefined, selectableFilter); diff --git a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts index 74a36a280d3..84567844a04 100644 --- a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts +++ b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts @@ -194,7 +194,7 @@ export const TrashToTreasureEncounter: MysteryEncounter = MysteryEncounterBuilde .withOptionPhase(async () => { // Investigate garbage, battle Gmax Garbodor globalScene.setFieldScale(0.75); - await showEncounterText(`${namespace}:option.2.selected_2`); + await showEncounterText(`${namespace}:option.2.selected2`); await transitionMysteryEncounterIntroVisuals(); const encounter = globalScene.currentBattle.mysteryEncounter!; diff --git a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts index c9e2ffedeec..6aeff852de7 100644 --- a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts +++ b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts @@ -97,7 +97,7 @@ export const UncommonBreedEncounter: MysteryEncounter = MysteryEncounterBuilder. isBoss: false, tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON], mysteryEncounterBattleEffects: (pokemon: Pokemon) => { - queueEncounterMessage(`${namespace}:option.1.stat_boost`); + queueEncounterMessage(`${namespace}:option.1.statBoost`); globalScene.phaseManager.unshiftNew( "StatStageChangePhase", pokemon.getBattlerIndex(), @@ -191,7 +191,7 @@ export const UncommonBreedEncounter: MysteryEncounter = MysteryEncounterBuilder. .withDialogue({ buttonLabel: `${namespace}:option.2.label`, buttonTooltip: `${namespace}:option.2.tooltip`, - disabledButtonTooltip: `${namespace}:option.2.disabled_tooltip`, + disabledButtonTooltip: `${namespace}:option.2.disabledTooltip`, selected: [ { text: `${namespace}:option.2.selected`, @@ -236,7 +236,7 @@ export const UncommonBreedEncounter: MysteryEncounter = MysteryEncounterBuilder. .withDialogue({ buttonLabel: `${namespace}:option.3.label`, buttonTooltip: `${namespace}:option.3.tooltip`, - disabledButtonTooltip: `${namespace}:option.3.disabled_tooltip`, + disabledButtonTooltip: `${namespace}:option.3.disabledTooltip`, selected: [ { text: `${namespace}:option.3.selected`, diff --git a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts index 57b066e2ba2..790bdf0dbef 100644 --- a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts +++ b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts @@ -143,7 +143,7 @@ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.wit }, { speaker: `${namespace}:speaker`, - text: `${namespace}:intro_dialogue`, + text: `${namespace}:introDialogue`, }, ]) .setLocalizationKey(`${namespace}`) @@ -216,7 +216,7 @@ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.wit await cutsceneDialoguePromise; doHideDreamBackground(); - await showEncounterText(`${namespace}:option.1.dream_complete`); + await showEncounterText(`${namespace}:option.1.dreamComplete`); await doNewTeamPostProcess(transformations); setEncounterRewards({ @@ -329,7 +329,7 @@ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.wit onBeforeRewards, ); - await showEncounterText(`${namespace}:option.2.selected_2`, null, undefined, true); + await showEncounterText(`${namespace}:option.2.selected2`, null, undefined, true); await initBattleWithEnemyConfig(enemyPartyConfig); }, ) diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index b599f923534..5c976cbc8cd 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -467,7 +467,7 @@ export function updatePlayerMoney(changeValue: number, playSound = true, showMes if (showMessage) { if (changeValue < 0) { globalScene.phaseManager.queueMessage( - i18next.t("mysteryEncounterMessages:paid_money", { + i18next.t("mysteryEncounterMessages:paidMoney", { amount: -changeValue, }), null, @@ -475,7 +475,7 @@ export function updatePlayerMoney(changeValue: number, playSound = true, showMes ); } else { globalScene.phaseManager.queueMessage( - i18next.t("mysteryEncounterMessages:receive_money", { + i18next.t("mysteryEncounterMessages:receiveMoney", { amount: changeValue, }), null, @@ -587,7 +587,7 @@ export function selectPokemonForOption( return true; }, onHover: () => { - showEncounterText(i18next.t("mysteryEncounterMessages:cancel_option"), 0, 0, false); + showEncounterText(i18next.t("mysteryEncounterMessages:cancelOption"), 0, 0, false); }, }); @@ -720,7 +720,7 @@ export function selectOptionThenPokemon( if (onHoverOverCancelOption) { onHoverOverCancelOption(); } - showEncounterText(i18next.t("mysteryEncounterMessages:cancel_option"), 0, 0, false); + showEncounterText(i18next.t("mysteryEncounterMessages:cancelOption"), 0, 0, false); }, }); diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index 7617fb5a89e..93ef59d6ee7 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -309,7 +309,7 @@ export function getRandomSpeciesByStarterCost( */ export function koPlayerPokemon(pokemon: PlayerPokemon) { pokemon.hp = 0; - pokemon.trySetStatus(StatusEffect.FAINT); + pokemon.doSetStatus(StatusEffect.FAINT); pokemon.updateInfo(); queueEncounterMessage( i18next.t("battle:fainted", { @@ -673,6 +673,8 @@ export async function catchPokemon( globalScene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs); return new Promise(resolve => { + const addStatus = new BooleanHolder(true); + applyChallenges(ChallengeType.POKEMON_ADD_TO_PARTY, pokemon, addStatus); const doPokemonCatchMenu = () => { const end = () => { // Ensure the pokemon is in the enemy party in all situations @@ -708,9 +710,7 @@ export async function catchPokemon( }); }; Promise.all([pokemon.hideInfo(), globalScene.gameData.setPokemonCaught(pokemon)]).then(() => { - const addStatus = new BooleanHolder(true); - applyChallenges(ChallengeType.POKEMON_ADD_TO_PARTY, pokemon, addStatus); - if (!addStatus.value) { + if (!(isObtain || addStatus.value)) { removePokemon(); end(); return; @@ -807,10 +807,16 @@ export async function catchPokemon( }; if (showCatchObtainMessage) { + let catchMessage: string; + if (isObtain) { + catchMessage = "battle:pokemonObtained"; + } else if (addStatus.value) { + catchMessage = "battle:pokemonCaught"; + } else { + catchMessage = "battle:pokemonCaughtButChallenge"; + } globalScene.ui.showText( - i18next.t(isObtain ? "battle:pokemonObtained" : "battle:pokemonCaught", { - pokemonName: pokemon.getNameToRender(), - }), + i18next.t(catchMessage, { pokemonName: pokemon.getNameToRender() }), null, doPokemonCatchMenu, 0, diff --git a/src/data/nature.ts b/src/data/nature.ts index b085faebb80..65e401cd288 100644 --- a/src/data/nature.ts +++ b/src/data/nature.ts @@ -3,7 +3,7 @@ import { EFFECTIVE_STATS, getShortenedStatKey, Stat } from "#enums/stat"; import { TextStyle } from "#enums/text-style"; import { UiTheme } from "#enums/ui-theme"; import { getBBCodeFrag } from "#ui/text"; -import { toTitleCase } from "#utils/strings"; +import { toCamelCase } from "#utils/strings"; import i18next from "i18next"; export function getNatureName( @@ -13,7 +13,7 @@ export function getNatureName( ignoreBBCode = false, uiTheme: UiTheme = UiTheme.DEFAULT, ): string { - let ret = toTitleCase(Nature[nature]); + let ret = toCamelCase(Nature[nature]); //Translating nature if (i18next.exists(`nature:${ret}`)) { ret = i18next.t(`nature:${ret}` as any); diff --git a/src/data/pokemon-forms/form-change-triggers.ts b/src/data/pokemon-forms/form-change-triggers.ts index 75734bf085b..7c042b27058 100644 --- a/src/data/pokemon-forms/form-change-triggers.ts +++ b/src/data/pokemon-forms/form-change-triggers.ts @@ -12,6 +12,7 @@ import { WeatherType } from "#enums/weather-type"; import type { Pokemon } from "#field/pokemon"; import type { PokemonFormChangeItemModifier } from "#modifiers/modifier"; import { type Constructor, coerceArray } from "#utils/common"; +import { toCamelCase } from "#utils/strings"; import i18next from "i18next"; export abstract class SpeciesFormChangeTrigger { @@ -29,7 +30,7 @@ export abstract class SpeciesFormChangeTrigger { export class SpeciesFormChangeManualTrigger extends SpeciesFormChangeTrigger {} export class SpeciesFormChangeAbilityTrigger extends SpeciesFormChangeTrigger { - public description: string = i18next.t("pokemonEvolutions:Forms.ability"); + public description: string = i18next.t("pokemonEvolutions:forms.ability"); } export class SpeciesFormChangeCompoundTrigger { @@ -68,10 +69,10 @@ export class SpeciesFormChangeItemTrigger extends SpeciesFormChangeTrigger { this.item = item; this.active = active; this.description = this.active - ? i18next.t("pokemonEvolutions:Forms.item", { + ? i18next.t("pokemonEvolutions:forms.item", { item: i18next.t(`modifierType:FormChangeItem.${FormChangeItem[this.item]}`), }) - : i18next.t("pokemonEvolutions:Forms.deactivateItem", { + : i18next.t("pokemonEvolutions:forms.deactivateItem", { item: i18next.t(`modifierType:FormChangeItem.${FormChangeItem[this.item]}`), }); } @@ -96,7 +97,7 @@ export class SpeciesFormChangeTimeOfDayTrigger extends SpeciesFormChangeTrigger constructor(...timesOfDay: TimeOfDay[]) { super(); this.timesOfDay = timesOfDay; - this.description = i18next.t("pokemonEvolutions:Forms.timeOfDay"); + this.description = i18next.t("pokemonEvolutions:orms.timeOfDay"); } canChange(_pokemon: Pokemon): boolean { @@ -110,8 +111,8 @@ export class SpeciesFormChangeActiveTrigger extends SpeciesFormChangeTrigger { super(); this.active = active; this.description = this.active - ? i18next.t("pokemonEvolutions:Forms.enter") - : i18next.t("pokemonEvolutions:Forms.leave"); + ? i18next.t("pokemonEvolutions:forms.enter") + : i18next.t("pokemonEvolutions:forms.leave"); } canChange(pokemon: Pokemon): boolean { @@ -127,7 +128,7 @@ export class SpeciesFormChangeStatusEffectTrigger extends SpeciesFormChangeTrigg super(); this.statusEffects = coerceArray(statusEffects); this.invert = invert; - // this.description = i18next.t("pokemonEvolutions:Forms.statusEffect"); + // this.description = i18next.t("pokemonEvolutions:forms.statusEffect"); } canChange(pokemon: Pokemon): boolean { @@ -143,16 +144,12 @@ export class SpeciesFormChangeMoveLearnedTrigger extends SpeciesFormChangeTrigge super(); this.move = move; this.known = known; - const moveKey = MoveId[this.move] - .split("_") - .filter(f => f) - .map((f, i) => (i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase())) - .join("") as unknown as string; + const moveKey = toCamelCase(MoveId[this.move]); this.description = known - ? i18next.t("pokemonEvolutions:Forms.moveLearned", { + ? i18next.t("pokemonEvolutions:forms.moveLearned", { move: i18next.t(`move:${moveKey}.name`), }) - : i18next.t("pokemonEvolutions:Forms.moveForgotten", { + : i18next.t("pokemonEvolutions:forms.moveForgotten", { move: i18next.t(`move:${moveKey}.name`), }); } @@ -174,7 +171,7 @@ export abstract class SpeciesFormChangeMoveTrigger extends SpeciesFormChangeTrig } export class SpeciesFormChangePreMoveTrigger extends SpeciesFormChangeMoveTrigger { - description = i18next.t("pokemonEvolutions:Forms.preMove"); + description = i18next.t("pokemonEvolutions:forms.preMove"); canChange(pokemon: Pokemon): boolean { const command = globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()]; return !!command?.move && this.movePredicate(command.move.move) === this.used; @@ -182,7 +179,7 @@ export class SpeciesFormChangePreMoveTrigger extends SpeciesFormChangeMoveTrigge } export class SpeciesFormChangePostMoveTrigger extends SpeciesFormChangeMoveTrigger { - description = i18next.t("pokemonEvolutions:Forms.postMove"); + description = i18next.t("pokemonEvolutions:forms.postMove"); canChange(pokemon: Pokemon): boolean { return ( pokemon.summonData && !!pokemon.getLastXMoves(1).filter(m => this.movePredicate(m.move)).length === this.used @@ -247,7 +244,7 @@ export class SpeciesFormChangeWeatherTrigger extends SpeciesFormChangeTrigger { super(); this.ability = ability; this.weathers = weathers; - this.description = i18next.t("pokemonEvolutions:Forms.weather"); + this.description = i18next.t("pokemonEvolutions:forms.weather"); } /** @@ -285,7 +282,7 @@ export class SpeciesFormChangeRevertWeatherFormTrigger extends SpeciesFormChange super(); this.ability = ability; this.weathers = weathers; - this.description = i18next.t("pokemonEvolutions:Forms.weatherRevert"); + this.description = i18next.t("pokemonEvolutions:forms.weatherRevert"); } /** diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index 7bfe02d9086..064ad57cfb3 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -778,7 +778,7 @@ export class PokemonSpecies extends PokemonSpeciesForm implements Localizable { } if (key) { - return i18next.t(`battlePokemonForm:${key}`, { + return i18next.t(`battlePokemonForm:${toCamelCase(key)}`, { pokemonName: this.name, }); } @@ -810,7 +810,9 @@ export class PokemonSpecies extends PokemonSpeciesForm implements Localizable { return this.name; // Other special cases could be put here too } // Everything beyond this point essentially follows the pattern of FORMNAME_SPECIES - return i18next.t(`pokemonForm:appendForm.${SpeciesId[this.speciesId].split("_")[0]}`, { pokemonName: this.name }); + return i18next.t(`pokemonForm:appendForm.${toCamelCase(SpeciesId[this.speciesId].split("_")[0])}`, { + pokemonName: this.name, + }); } /** @@ -827,7 +829,7 @@ export class PokemonSpecies extends PokemonSpeciesForm implements Localizable { const region = this.getRegion(); if (this.speciesId === SpeciesId.ARCEUS) { - ret = i18next.t(`pokemonInfo:Type.${formText.toUpperCase()}`); + ret = i18next.t(`pokemonInfo:type.${toCamelCase(formText)}`); } else if ( [ SpeciesFormKey.MEGA, @@ -841,8 +843,8 @@ export class PokemonSpecies extends PokemonSpeciesForm implements Localizable { ].includes(formKey as SpeciesFormKey) ) { return append - ? i18next.t(`battlePokemonForm:${formKey}`, { pokemonName: this.name }) - : i18next.t(`pokemonForm:battleForm.${formKey}`); + ? i18next.t(`battlePokemonForm:${toCamelCase(formKey)}`, { pokemonName: this.name }) + : i18next.t(`pokemonForm:battleForm.${toCamelCase(formKey)}`); } else if ( region === Region.NORMAL || (this.speciesId === SpeciesId.GALAR_DARMANITAN && formIndex > 0) || @@ -868,10 +870,10 @@ export class PokemonSpecies extends PokemonSpeciesForm implements Localizable { return i18next.t("pokemonForm:ursalunaBloodmoon"); } else { // Only regional forms should be left at this point - return i18next.t(`pokemonForm:regionalForm.${Region[region]}`); + return i18next.t(`pokemonForm:regionalForm.${toCamelCase(Region[region])}`); } return append - ? i18next.t("pokemonForm:appendForm.GENERIC", { + ? i18next.t("pokemonForm:appendForm.generic", { pokemonName: this.name, formName: ret, }) @@ -879,8 +881,8 @@ export class PokemonSpecies extends PokemonSpeciesForm implements Localizable { } localize(): void { - this.name = i18next.t(`pokemon:${SpeciesId[this.speciesId].toLowerCase()}`); - this.category = i18next.t(`pokemonCategory:${SpeciesId[this.speciesId].toLowerCase()}_category`); + this.name = i18next.t(`pokemon:${toCamelCase(SpeciesId[this.speciesId])}`); + this.category = i18next.t(`pokemonCategory:${toCamelCase(SpeciesId[this.speciesId])}Category`); } getWildSpeciesForLevel(level: number, allowEvolving: boolean, isBoss: boolean, gameMode: GameMode): SpeciesId { diff --git a/src/data/splash-messages.ts b/src/data/splash-messages.ts index 3223bbb019e..55ba185bfb2 100644 --- a/src/data/splash-messages.ts +++ b/src/data/splash-messages.ts @@ -100,7 +100,7 @@ const commonSplashMessages = [ "liveWoChienReaction", "itsAFeatureNotABug", "theEggsAreNotForEating", - "7.8outOf10TooManyWaterBiomes", + "tooManyWaterBiomes", "butNothingHappened", "thePowerOfScienceIsAmazing", "freeToPlay", @@ -152,7 +152,7 @@ const commonSplashMessages = [ "insertTextHere", "endingEndlessNotFound", "iLikeMyEggsVouchered", - "YOU", + "you", "noAddedSugar", "notSponsored", "notRated", diff --git a/src/data/trainers/trainer-config.ts b/src/data/trainers/trainer-config.ts index d29b40e0972..b39795d2650 100644 --- a/src/data/trainers/trainer-config.ts +++ b/src/data/trainers/trainer-config.ts @@ -43,7 +43,7 @@ import type { } from "#types/trainer-funcs"; import { coerceArray, isNullOrUndefined, randSeedInt, randSeedIntRange, randSeedItem } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; -import { toSnakeCase, toTitleCase } from "#utils/strings"; +import { toCamelCase, toTitleCase } from "#utils/strings"; import i18next from "i18next"; /** Minimum BST for Pokemon generated onto the Elite Four's teams */ @@ -193,8 +193,7 @@ export class TrainerConfig { initI18n(); } - // Make the title lowercase and replace spaces with underscores - title = title.toLowerCase().replace(/\s/g, "_"); + title = toCamelCase(title); // Get the title from the i18n file this.title = i18next.t(`titles:${title}`); @@ -288,7 +287,7 @@ export class TrainerConfig { initI18n(); } // Set the localized name for the female rival. - this.nameFemale = i18next.t("trainerNames:rival_female"); + this.nameFemale = i18next.t("trainerNames:rivalFemale"); } else { // Otherwise, assign the provided female name. this.nameFemale = nameFemale!; // TODO: is this bang correct? @@ -365,8 +364,7 @@ export class TrainerConfig { initI18n(); } - // Make the title lowercase and replace spaces with underscores - titleDouble = titleDouble.toLowerCase().replace(/\s/g, "_"); + titleDouble = toCamelCase(titleDouble); // Get the title from the i18n file this.titleDouble = i18next.t(`titles:${titleDouble}`); @@ -552,7 +550,7 @@ export class TrainerConfig { this.setPartyMemberFunc(-(s + 1), getRandomPartyMemberFunc(coerceArray(speciesPool))); }); - const nameForCall = this.name.toLowerCase().replace(/\s/g, "_"); + const nameForCall = toCamelCase(this.name); this.name = i18next.t(`trainerNames:${nameForCall}`); this.setHasVoucher(false); this.setTitle(title); @@ -577,7 +575,7 @@ export class TrainerConfig { this.setPartyTemplates(trainerPartyTemplates.ELITE_FOUR); - const nameForCall = this.name.toLowerCase().replace(/\s/g, "_"); + const nameForCall = toCamelCase(this.name); this.name = i18next.t(`trainerNames:${nameForCall}`); this.setMoneyMultiplier(2); this.setBoss(); @@ -618,7 +616,7 @@ export class TrainerConfig { this.setSpeciesFilter(p => p.isOfType(specialtyType)); this.setSpecialtyType(specialtyType); } - const nameForCall = this.name.toLowerCase().replace(/\s/g, "_"); + const nameForCall = toCamelCase(this.name); this.name = i18next.t(`trainerNames:${nameForCall}`); this.setTitle(title); this.setMoneyMultiplier(2.5); @@ -665,14 +663,14 @@ export class TrainerConfig { this.setSpeciesFilter(p => p.isOfType(specialtyType)); this.setSpecialtyType(specialtyType); - // Localize the trainer's name by converting it to lowercase and replacing spaces with underscores. - const nameForCall = this.name.toLowerCase().replace(/\s/g, "_"); + // Localize the trainer's name by converting it to camel case. + const nameForCall = toCamelCase(this.name); this.name = i18next.t(`trainerNames:${nameForCall}`); - // Set the title to "gym_leader". (this is the key in the i18n file) - this.setTitle("gym_leader"); + // Set the title to "gymLeader". (this is the key in the i18n file) + this.setTitle("gymLeader"); if (!isMale) { - this.setTitle("gym_leader_female"); + this.setTitle("gymLeaderFemale"); } // Configure various properties for the Gym Leader. @@ -726,14 +724,14 @@ export class TrainerConfig { this.setSpeciesFilter(p => p.baseTotal >= ELITE_FOUR_MINIMUM_BST); } - // Localize the trainer's name by converting it to lowercase and replacing spaces with underscores. - const nameForCall = toSnakeCase(this.name); + // Localize the trainer's name by converting it to camel case. + const nameForCall = toCamelCase(this.name); this.name = i18next.t(`trainerNames:${nameForCall}`); // Set the title to "elite_four". (this is the key in the i18n file) - this.setTitle("elite_four"); + this.setTitle("eliteFour"); if (!isMale) { - this.setTitle("elite_four_female"); + this.setTitle("eliteFourFemale"); } // Configure various properties for the Elite Four member. @@ -763,14 +761,14 @@ export class TrainerConfig { // Set the party templates for the Champion. this.setPartyTemplates(trainerPartyTemplates.CHAMPION); - // Localize the trainer's name by converting it to lowercase and replacing spaces with underscores. - const nameForCall = this.name.toLowerCase().replace(/\s/g, "_"); + // Localize the trainer's name by converting it to camel case. + const nameForCall = toCamelCase(this.name); this.name = i18next.t(`trainerNames:${nameForCall}`); // Set the title to "champion". (this is the key in the i18n file) this.setTitle("champion"); if (!isMale) { - this.setTitle("champion_female"); + this.setTitle("championFemale"); } // Configure various properties for the Champion. @@ -794,7 +792,7 @@ export class TrainerConfig { if (!getIsInitialized()) { initI18n(); } - this.name = i18next.t(`trainerNames:${name.toLowerCase().replace(/\s/g, "_")}`); + this.name = i18next.t(`trainerNames:${toCamelCase(name)}`); return this; } @@ -830,9 +828,9 @@ export class TrainerConfig { initI18n(); } // Check if the female version exists in the i18n file - if (i18next.exists(`trainerClasses:${this.name.toLowerCase()}`)) { + if (i18next.exists(`trainerClasses:${toCamelCase(this.name)}Female`)) { // If it does, return - return ret + "_female"; + return ret + "Female"; } } } @@ -1865,27 +1863,43 @@ export const trainerConfigs: TrainerConfigs = { .setPartyMemberFunc( 0, getRandomPartyMemberFunc([ + SpeciesId.METAPOD, + SpeciesId.LEDYBA, + SpeciesId.CLEFFA, + SpeciesId.WOOPER, + SpeciesId.TEDDIURSA, + SpeciesId.REMORAID, + SpeciesId.HOUNDOUR, + SpeciesId.SILCOON, SpeciesId.PLUSLE, SpeciesId.VOLBEAT, - SpeciesId.PACHIRISU, - SpeciesId.SILCOON, - SpeciesId.METAPOD, - SpeciesId.IGGLYBUFF, + SpeciesId.SPINDA, + SpeciesId.BONSLY, SpeciesId.PETILIL, - SpeciesId.EEVEE, + SpeciesId.SPRITZEE, + SpeciesId.MILCERY, + SpeciesId.PICHU, ]), ) .setPartyMemberFunc( 1, getRandomPartyMemberFunc( [ + SpeciesId.KAKUNA, + SpeciesId.SPINARAK, + SpeciesId.IGGLYBUFF, + SpeciesId.PALDEA_WOOPER, + SpeciesId.PHANPY, + SpeciesId.MANTYKE, + SpeciesId.ELECTRIKE, + SpeciesId.CASCOON, SpeciesId.MINUN, SpeciesId.ILLUMISE, - SpeciesId.EMOLGA, - SpeciesId.CASCOON, - SpeciesId.KAKUNA, - SpeciesId.CLEFFA, + SpeciesId.SPINDA, + SpeciesId.MIME_JR, SpeciesId.COTTONEE, + SpeciesId.SWIRLIX, + SpeciesId.FIDOUGH, SpeciesId.EEVEE, ], TrainerSlot.TRAINER_PARTNER, diff --git a/src/enums/stat.ts b/src/enums/stat.ts index a2b81b7e64b..72ccc065850 100644 --- a/src/enums/stat.ts +++ b/src/enums/stat.ts @@ -64,7 +64,7 @@ export function getStatStageChangeDescriptionKey(stages: number, isIncrease: boo * @returns the translation key corresponding to the given {@linkcode Stat} */ export function getStatKey(stat: Stat) { - return `pokemonInfo:Stat.${Stat[stat]}`; + return `pokemonInfo:stat.${Stat[stat].toLowerCase()}`; } /** @@ -73,5 +73,5 @@ export function getStatKey(stat: Stat) { * @returns the translation key corresponding to the given {@linkcode Stat} */ export function getShortenedStatKey(stat: PermanentStat) { - return `pokemonInfo:Stat.${Stat[stat]}shortened`; + return `pokemonInfo:stat.${Stat[stat].toLowerCase()}Shortened`; } diff --git a/src/enums/status-effect.ts b/src/enums/status-effect.ts index b79951f530a..3064dbe907f 100644 --- a/src/enums/status-effect.ts +++ b/src/enums/status-effect.ts @@ -1,3 +1,5 @@ +/** Enum representing all non-volatile status effects. */ +// TODO: Remove StatusEffect.FAINT export enum StatusEffect { NONE, POISON, diff --git a/src/enums/ui-mode.ts b/src/enums/ui-mode.ts index bc93e747be2..75c07a5f63c 100644 --- a/src/enums/ui-mode.ts +++ b/src/enums/ui-mode.ts @@ -38,6 +38,7 @@ export enum UiMode { UNAVAILABLE, CHALLENGE_SELECT, RENAME_POKEMON, + RENAME_RUN, RUN_HISTORY, RUN_INFO, TEST_DIALOGUE, diff --git a/src/field/arena.ts b/src/field/arena.ts index 06ba6fdd334..5ae092b562a 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -8,7 +8,7 @@ import Overrides from "#app/overrides"; import type { BiomeTierTrainerPools, PokemonPools } from "#balance/biomes"; import { BiomePoolTier, biomePokemonPools, biomeTrainerPools } from "#balance/biomes"; import type { ArenaTag } from "#data/arena-tag"; -import { ArenaTrapTag, getArenaTag } from "#data/arena-tag"; +import { EntryHazardTag, getArenaTag } from "#data/arena-tag"; import { SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "#data/form-change-triggers"; import type { PokemonSpecies } from "#data/pokemon-species"; import { PositionalTagManager } from "#data/positional-tags/positional-tag-manager"; @@ -54,7 +54,7 @@ export class Arena { public bgm: string; public ignoreAbilities: boolean; public ignoringEffectSource: BattlerIndex | null; - public playerTerasUsed: number; + public playerTerasUsed = 0; /** * Saves the number of times a party pokemon faints during a arena encounter. * {@linkcode globalScene.currentBattle.enemyFaints} is the corresponding faint counter for the enemy (this resets every wave). @@ -68,12 +68,11 @@ export class Arena { public readonly eventTarget: EventTarget = new EventTarget(); - constructor(biome: BiomeId, bgm: string, playerFaints = 0) { + constructor(biome: BiomeId, playerFaints = 0) { this.biomeType = biome; - this.bgm = bgm; + this.bgm = BiomeId[biome].toLowerCase(); this.trainerPool = biomeTrainerPools[biome]; this.updatePoolsForTimeOfDay(); - this.playerTerasUsed = 0; this.playerFaints = playerFaints; } @@ -710,8 +709,8 @@ export class Arena { if (existingTag) { existingTag.onOverlap(this, globalScene.getPokemonById(sourceId)); - if (existingTag instanceof ArenaTrapTag) { - const { tagType, side, turnCount, layers, maxLayers } = existingTag as ArenaTrapTag; + if (existingTag instanceof EntryHazardTag) { + const { tagType, side, turnCount, layers, maxLayers } = existingTag as EntryHazardTag; this.eventTarget.dispatchEvent(new TagAddedEvent(tagType, side, turnCount, layers, maxLayers)); } @@ -724,7 +723,7 @@ export class Arena { newTag.onAdd(this, quiet); this.tags.push(newTag); - const { layers = 0, maxLayers = 0 } = newTag instanceof ArenaTrapTag ? newTag : {}; + const { layers = 0, maxLayers = 0 } = newTag instanceof EntryHazardTag ? newTag : {}; this.eventTarget.dispatchEvent( new TagAddedEvent(newTag.tagType, newTag.side, newTag.turnCount, layers, maxLayers), @@ -895,7 +894,7 @@ export class Arena { case BiomeId.CAVE: return 14.24; case BiomeId.DESERT: - return 1.143; + return 9.02; case BiomeId.ICE_CAVE: return 0.0; case BiomeId.MEADOW: @@ -913,7 +912,7 @@ export class Arena { case BiomeId.RUINS: return 0.0; case BiomeId.WASTELAND: - return 6.336; + return 6.024; case BiomeId.ABYSS: return 20.113; case BiomeId.SPACE: @@ -923,7 +922,7 @@ export class Arena { case BiomeId.JUNGLE: return 0.0; case BiomeId.FAIRY_CAVE: - return 4.542; + return 0.0; case BiomeId.TEMPLE: return 2.547; case BiomeId.ISLAND: diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index c5128b30b8d..8d5d57f74e3 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -1,7 +1,7 @@ import type { Ability, PreAttackModifyDamageAbAttrParams } from "#abilities/ability"; import { applyAbAttrs, applyOnGainAbAttrs, applyOnLoseAbAttrs } from "#abilities/apply-ab-attrs"; import type { AnySound, BattleScene } from "#app/battle-scene"; -import { PLAYER_PARTY_MAX_SIZE } from "#app/constants"; +import { PLAYER_PARTY_MAX_SIZE, RARE_CANDY_FRIENDSHIP_CAP } from "#app/constants"; import { timedEventManager } from "#app/global-event-manager"; import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; @@ -139,6 +139,8 @@ import { populateVariantColors, variantColorCache, variantData } from "#sprites/ import { achvs } from "#system/achv"; import type { StarterDataEntry, StarterMoveset } from "#system/game-data"; import type { PokemonData } from "#system/pokemon-data"; +import { RibbonData } from "#system/ribbons/ribbon-data"; +import { awardRibbonsToSpeciesLine } from "#system/ribbons/ribbon-methods"; import type { AbAttrMap, AbAttrString, TypeMultiplierAbAttrParams } from "#types/ability-types"; import type { DamageCalculationResult, DamageResult } from "#types/damage-result"; import type { IllusionData } from "#types/illusion-data"; @@ -235,6 +237,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { public ivs: number[]; public nature: Nature; public moveset: PokemonMove[]; + /** + * This Pokemon's current {@link https://m.bulbapedia.bulbagarden.net/wiki/Status_condition#Non-volatile_status | non-volatile status condition}, + * or `null` if none exist. + * @todo Make private + */ public status: Status | null; public friendship: number; public metLevel: number; @@ -1825,7 +1832,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { // Overrides moveset based on arrays specified in overrides.ts let overrideArray: MoveId | Array = this.isPlayer() ? Overrides.MOVESET_OVERRIDE - : Overrides.OPP_MOVESET_OVERRIDE; + : Overrides.ENEMY_MOVESET_OVERRIDE; overrideArray = coerceArray(overrideArray); if (overrideArray.length > 0) { if (!this.isPlayer()) { @@ -2030,8 +2037,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { if (Overrides.ABILITY_OVERRIDE && this.isPlayer()) { return allAbilities[Overrides.ABILITY_OVERRIDE]; } - if (Overrides.OPP_ABILITY_OVERRIDE && this.isEnemy()) { - return allAbilities[Overrides.OPP_ABILITY_OVERRIDE]; + if (Overrides.ENEMY_ABILITY_OVERRIDE && this.isEnemy()) { + return allAbilities[Overrides.ENEMY_ABILITY_OVERRIDE]; } if (this.isFusion()) { if (!isNullOrUndefined(this.fusionCustomPokemonData?.ability) && this.fusionCustomPokemonData.ability !== -1) { @@ -2060,8 +2067,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { if (Overrides.PASSIVE_ABILITY_OVERRIDE && this.isPlayer()) { return allAbilities[Overrides.PASSIVE_ABILITY_OVERRIDE]; } - if (Overrides.OPP_PASSIVE_ABILITY_OVERRIDE && this.isEnemy()) { - return allAbilities[Overrides.OPP_PASSIVE_ABILITY_OVERRIDE]; + if (Overrides.ENEMY_PASSIVE_ABILITY_OVERRIDE && this.isEnemy()) { + return allAbilities[Overrides.ENEMY_PASSIVE_ABILITY_OVERRIDE]; } if (!isNullOrUndefined(this.customPokemonData.passive) && this.customPokemonData.passive !== -1) { return allAbilities[this.customPokemonData.passive]; @@ -2128,14 +2135,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { // returns override if valid for current case if ( (Overrides.HAS_PASSIVE_ABILITY_OVERRIDE === false && this.isPlayer()) || - (Overrides.OPP_HAS_PASSIVE_ABILITY_OVERRIDE === false && this.isEnemy()) + (Overrides.ENEMY_HAS_PASSIVE_ABILITY_OVERRIDE === false && this.isEnemy()) ) { return false; } if ( ((Overrides.PASSIVE_ABILITY_OVERRIDE !== AbilityId.NONE || Overrides.HAS_PASSIVE_ABILITY_OVERRIDE) && this.isPlayer()) || - ((Overrides.OPP_PASSIVE_ABILITY_OVERRIDE !== AbilityId.NONE || Overrides.OPP_HAS_PASSIVE_ABILITY_OVERRIDE) && + ((Overrides.ENEMY_PASSIVE_ABILITY_OVERRIDE !== AbilityId.NONE || Overrides.ENEMY_HAS_PASSIVE_ABILITY_OVERRIDE) && this.isEnemy()) ) { return true; @@ -3001,8 +3008,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { if (forStarter && this.isPlayer() && Overrides.STARTER_FUSION_SPECIES_OVERRIDE) { fusionOverride = getPokemonSpecies(Overrides.STARTER_FUSION_SPECIES_OVERRIDE); - } else if (this.isEnemy() && Overrides.OPP_FUSION_SPECIES_OVERRIDE) { - fusionOverride = getPokemonSpecies(Overrides.OPP_FUSION_SPECIES_OVERRIDE); + } else if (this.isEnemy() && Overrides.ENEMY_FUSION_SPECIES_OVERRIDE) { + fusionOverride = getPokemonSpecies(Overrides.ENEMY_FUSION_SPECIES_OVERRIDE); } this.fusionSpecies = @@ -4749,7 +4756,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @param reason - The reason for the status application failure - * can be "overlap" (already has same status), "other" (generic fail message) * or a {@linkcode TerrainType} for terrain-based blockages. - * Defaults to "other". + * Default `"other"` */ queueStatusImmuneMessage( quiet: boolean, @@ -4778,15 +4785,20 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Checks if a status effect can be applied to the Pokemon. + * Check if a status effect can be applied to this {@linkcode Pokemon}. * - * @param effect The {@linkcode StatusEffect} whose applicability is being checked - * @param quiet Whether in-battle messages should trigger or not - * @param overrideStatus Whether the Pokemon's current status can be overriden - * @param sourcePokemon The Pokemon that is setting the status effect - * @param ignoreField Whether any field effects (weather, terrain, etc.) should be considered + * @param effect - The {@linkcode StatusEffect} whose applicability is being checked + * @param quiet - Whether to suppress in-battle messages for status checks; default `false` + * @param overrideStatus - Whether to allow overriding the Pokemon's current status with a different one; default `false` + * @param sourcePokemon - The {@linkcode Pokemon} applying the status effect to the target, + * or `null` if the status is applied from a non-Pokemon source (hazards, etc.); default `null` + * @param ignoreField - Whether to ignore field effects (weather, terrain, etc.) preventing status application; + * default `false` + * @returns Whether {@linkcode effect} can be applied to this Pokemon. */ - canSetStatus( + // TODO: Review and verify the message order precedence in mainline if multiple status-blocking effects are present at once + // TODO: Make argument order consistent with `trySetStatus` + public canSetStatus( effect: StatusEffect, quiet = false, overrideStatus = false, @@ -4794,6 +4806,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { ignoreField = false, ): boolean { if (effect !== StatusEffect.FAINT) { + // Status-overriding moves (i.e. Rest) fail if their respective status already exists; + // all other moves fail if the target already has _any_ status if (overrideStatus ? this.status?.effect === effect : this.status) { this.queueStatusImmuneMessage(quiet, overrideStatus ? "overlap" : "other"); // having different status displays generic fail message return false; @@ -4806,73 +4820,62 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { const types = this.getTypes(true, true); + /* Whether the target is immune to the specific status being applied. */ + let isImmune = false; + /** The reason for a potential blockage; default "other" for type-based. */ + let reason: "other" | Exclude = "other"; + switch (effect) { case StatusEffect.POISON: - case StatusEffect.TOXIC: { - // Check if the Pokemon is immune to Poison/Toxic or if the source pokemon is canceling the immunity - const poisonImmunity = types.map(defType => { - // Check if the Pokemon is not immune to Poison/Toxic + case StatusEffect.TOXIC: + // Check for type based immunities and/or Corrosion from the applier. + isImmune = types.some(defType => { + // only 1 immunity needed to block if (defType !== PokemonType.POISON && defType !== PokemonType.STEEL) { return false; } - // Check if the source Pokemon has an ability that cancels the Poison/Toxic immunity + // No source (such as from Toxic Spikes) = blocked by default + if (!sourcePokemon) { + return true; + } + 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", { - pokemon: sourcePokemon, - cancelled: cancelImmunity, - statusEffect: effect, - defenderType: defType, - }); - if (cancelImmunity.value) { - return false; - } - } - - return true; + applyAbAttrs("IgnoreTypeStatusEffectImmunityAbAttr", { + pokemon: sourcePokemon, + cancelled: cancelImmunity, + statusEffect: effect, + defenderType: defType, + }); + return !cancelImmunity.value; }); - - if (this.isOfType(PokemonType.POISON) || this.isOfType(PokemonType.STEEL)) { - if (poisonImmunity.includes(true)) { - this.queueStatusImmuneMessage(quiet); - return false; - } - } break; - } case StatusEffect.PARALYSIS: - if (this.isOfType(PokemonType.ELECTRIC)) { - this.queueStatusImmuneMessage(quiet); - return false; - } + isImmune = this.isOfType(PokemonType.ELECTRIC); break; case StatusEffect.SLEEP: - if (this.isGrounded() && globalScene.arena.terrain?.terrainType === TerrainType.ELECTRIC) { - this.queueStatusImmuneMessage(quiet, TerrainType.ELECTRIC); - return false; - } + isImmune = this.isGrounded() && globalScene.arena.getTerrainType() === TerrainType.ELECTRIC; + reason = TerrainType.ELECTRIC; break; - case StatusEffect.FREEZE: - if ( + case StatusEffect.FREEZE: { + const weatherType = globalScene.arena.getWeatherType(); + isImmune = this.isOfType(PokemonType.ICE) || - (!ignoreField && - globalScene?.arena?.weather?.weatherType && - [WeatherType.SUNNY, WeatherType.HARSH_SUN].includes(globalScene.arena.weather.weatherType)) - ) { - this.queueStatusImmuneMessage(quiet); - return false; - } + (!ignoreField && (weatherType === WeatherType.SUNNY || weatherType === WeatherType.HARSH_SUN)); break; + } case StatusEffect.BURN: - if (this.isOfType(PokemonType.FIRE)) { - this.queueStatusImmuneMessage(quiet); - return false; - } + isImmune = this.isOfType(PokemonType.FIRE); break; } + if (isImmune) { + this.queueStatusImmuneMessage(quiet, reason); + return false; + } + + // Check for cancellations from self/ally abilities const cancelled = new BooleanHolder(false); applyAbAttrs("StatusEffectImmunityAbAttr", { pokemon: this, effect, cancelled, simulated: quiet }); if (cancelled.value) { @@ -4889,14 +4892,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { source: sourcePokemon, }); if (cancelled.value) { - break; + return false; } } - if (cancelled.value) { - return false; - } - + // Perform safeguard checks if (sourcePokemon && sourcePokemon !== this && this.isSafeguarded(sourcePokemon)) { if (!quiet) { globalScene.phaseManager.queueMessage( @@ -4909,18 +4909,36 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return true; } - trySetStatus( - effect?: StatusEffect, - asPhase = false, + /** + * Attempt to set this Pokemon's status to the specified condition. + * Enqueues a new `ObtainStatusEffectPhase` to trigger animations, etc. + * @param effect - The {@linkcode StatusEffect} to set + * @param sourcePokemon - The {@linkcode Pokemon} applying the status effect to the target, + * or `null` if the status is applied from a non-Pokemon source (hazards, etc.); default `null` + * @param sleepTurnsRemaining - The number of turns to set {@linkcode StatusEffect.SLEEP} for; + * defaults to a random number between 2 and 4 and is unused for non-Sleep statuses + * @param sourceText - The text to show for the source of the status effect, if any; default `null` + * @param overrideStatus - Whether to allow overriding the Pokemon's current status with a different one; default `false` + * @param quiet - Whether to suppress in-battle messages for status checks; default `true` + * @param overrideMessage - String containing text to be displayed upon status setting; defaults to normal key for status + * and is used exclusively for Rest + * @returns Whether the status effect phase was successfully created. + * @see {@linkcode doSetStatus} - alternate function that sets status immediately (albeit without condition checks). + */ + public trySetStatus( + effect: StatusEffect, sourcePokemon: Pokemon | null = null, - turnsRemaining = 0, + sleepTurnsRemaining?: number, sourceText: string | null = null, overrideStatus?: boolean, quiet = true, + overrideMessage?: string, ): boolean { + // TODO: This needs to propagate failure status for status moves if (!effect) { return false; } + if (!this.canSetStatus(effect, quiet, overrideStatus, sourcePokemon)) { return false; } @@ -4940,48 +4958,79 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } } - if (asPhase) { - if (overrideStatus) { - this.resetStatus(false); - } - globalScene.phaseManager.unshiftNew( - "ObtainStatusEffectPhase", - this.getBattlerIndex(), - effect, - turnsRemaining, - sourceText, - sourcePokemon, - ); - return true; + if (overrideStatus) { + this.resetStatus(false); } - let sleepTurnsRemaining: NumberHolder; + globalScene.phaseManager.unshiftNew( + "ObtainStatusEffectPhase", + this.getBattlerIndex(), + effect, + sourcePokemon, + sleepTurnsRemaining, + sourceText, + overrideMessage, + ); + return true; + } + + /** + * Set this Pokemon's {@linkcode status | non-volatile status condition} to the specified effect. + * @param effect - The {@linkcode StatusEffect} to set + * @remarks + * ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller. + */ + doSetStatus(effect: Exclude): void; + /** + * Set this Pokemon's {@linkcode status | non-volatile status condition} to the specified effect. + * @param effect - {@linkcode StatusEffect.SLEEP} + * @param sleepTurnsRemaining - The number of turns to inflict sleep for; defaults to a random number between 2 and 4 + * @remarks + * ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller. + */ + doSetStatus(effect: StatusEffect.SLEEP, sleepTurnsRemaining?: number): void; + /** + * Set this Pokemon's {@linkcode status | non-volatile status condition} to the specified effect. + * @param effect - The {@linkcode StatusEffect} to set + * @param sleepTurnsRemaining - The number of turns to inflict sleep for; defaults to a random number between 2 and 4 + * and is unused for all non-sleep Statuses + * @remarks + * ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller. + */ + doSetStatus(effect: StatusEffect, sleepTurnsRemaining?: number): void; + /** + * Set this Pokemon's {@linkcode status | non-volatile status condition} to the specified effect. + * @param effect - The {@linkcode StatusEffect} to set + * @param sleepTurnsRemaining - The number of turns to inflict sleep for; defaults to a random number between 2 and 4 + * and is unused for all non-sleep Statuses + * @remarks + * ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller. + * @todo Make this and all related fields private and change tests to use a field-based helper or similar + */ + doSetStatus( + effect: StatusEffect, + sleepTurnsRemaining = effect !== StatusEffect.SLEEP ? 0 : this.randBattleSeedIntRange(2, 4), + ): void { if (effect === StatusEffect.SLEEP) { - sleepTurnsRemaining = new NumberHolder(this.randBattleSeedIntRange(2, 4)); - this.setFrameRate(4); - // If the user is invulnerable, lets remove their invulnerability when they fall asleep - const invulnerableTags = [ + // If the user is semi-invulnerable when put asleep (such as due to Yawm), + // remove their invulnerability and cancel the upcoming move from the queue + const invulnTagTypes = [ + BattlerTagType.FLYING, BattlerTagType.UNDERGROUND, BattlerTagType.UNDERWATER, BattlerTagType.HIDDEN, - BattlerTagType.FLYING, ]; - const tag = invulnerableTags.find(t => this.getTag(t)); - - if (tag) { - this.removeTag(tag); - this.getMoveQueue().pop(); + if (this.findTag(t => invulnTagTypes.includes(t.tagType))) { + this.findAndRemoveTags(t => invulnTagTypes.includes(t.tagType)); + this.getMoveQueue().shift(); } } - sleepTurnsRemaining = sleepTurnsRemaining!; // tell TS compiler it's defined - this.status = new Status(effect, 0, sleepTurnsRemaining?.value); - - return true; + this.status = new Status(effect, 0, sleepTurnsRemaining); } /** @@ -5711,7 +5760,11 @@ export class PlayerPokemon extends Pokemon { } if (!dataSource) { - if (globalScene.gameMode.isDaily) { + if ( + globalScene.gameMode.isDaily || + // Keldeo is excluded due to crashes involving its signature move and the associated form change + (Overrides.STARTER_SPECIES_OVERRIDE && Overrides.STARTER_SPECIES_OVERRIDE !== SpeciesId.KELDEO) + ) { this.generateAndPopulateMoveset(); } else { this.moveset = []; @@ -5827,45 +5880,59 @@ export class PlayerPokemon extends Pokemon { ); }); } - - addFriendship(friendship: number): void { - if (friendship > 0) { - const starterSpeciesId = this.species.getRootSpeciesId(); - const fusionStarterSpeciesId = this.isFusion() && this.fusionSpecies ? this.fusionSpecies.getRootSpeciesId() : 0; - const starterData = [ - globalScene.gameData.starterData[starterSpeciesId], - fusionStarterSpeciesId ? globalScene.gameData.starterData[fusionStarterSpeciesId] : null, - ].filter(d => !!d); - const amount = new NumberHolder(friendship); - globalScene.applyModifier(PokemonFriendshipBoosterModifier, true, this, amount); - const candyFriendshipMultiplier = globalScene.gameMode.isClassic - ? timedEventManager.getClassicFriendshipMultiplier() - : 1; - const fusionReduction = fusionStarterSpeciesId - ? timedEventManager.areFusionsBoosted() - ? 1.5 // Divide candy gain for fusions by 1.5 during events - : 2 // 2 for fusions outside events - : 1; // 1 for non-fused mons - const starterAmount = new NumberHolder(Math.floor((amount.value * candyFriendshipMultiplier) / fusionReduction)); - - // Add friendship to this PlayerPokemon - this.friendship = Math.min(this.friendship + amount.value, 255); - if (this.friendship === 255) { - globalScene.validateAchv(achvs.MAX_FRIENDSHIP); - } - // Add to candy progress for this mon's starter species and its fused species (if it has one) - starterData.forEach((sd: StarterDataEntry, i: number) => { - const speciesId = !i ? starterSpeciesId : (fusionStarterSpeciesId as SpeciesId); - sd.friendship = (sd.friendship || 0) + starterAmount.value; - if (sd.friendship >= getStarterValueFriendshipCap(speciesStarterCosts[speciesId])) { - globalScene.gameData.addStarterCandy(getPokemonSpecies(speciesId), 1); - sd.friendship = 0; - } - }); - } else { - // Lose friendship upon fainting + /** + * Add friendship to this Pokemon + * + * @remarks + * This adds friendship to the pokemon's friendship stat (used for evolution, return, etc.) and candy progress. + * For fusions, candy progress for each species in the fusion is halved. + * + * @param friendship - The amount of friendship to add. Negative values will reduce friendship, though not below 0. + * @param capped - If true, don't allow the friendship gain to exceed 200. Used to cap friendship gains from rare candies. + */ + addFriendship(friendship: number, capped = false): void { + // Short-circuit friendship loss, which doesn't impact candy friendship + if (friendship <= 0) { this.friendship = Math.max(this.friendship + friendship, 0); + return; } + + const starterSpeciesId = this.species.getRootSpeciesId(); + const fusionStarterSpeciesId = this.isFusion() && this.fusionSpecies ? this.fusionSpecies.getRootSpeciesId() : 0; + const starterGameData = globalScene.gameData.starterData; + const starterData: [StarterDataEntry, SpeciesId][] = [[starterGameData[starterSpeciesId], starterSpeciesId]]; + if (fusionStarterSpeciesId) { + starterData.push([starterGameData[fusionStarterSpeciesId], fusionStarterSpeciesId]); + } + const amount = new NumberHolder(friendship); + globalScene.applyModifier(PokemonFriendshipBoosterModifier, true, this, amount); + friendship = amount.value; + + const newFriendship = this.friendship + friendship; + // If capped is true, only adjust friendship if the new friendship is less than or equal to 200. + if (!capped || newFriendship <= RARE_CANDY_FRIENDSHIP_CAP) { + this.friendship = Math.min(newFriendship, 255); + if (newFriendship >= 255) { + globalScene.validateAchv(achvs.MAX_FRIENDSHIP); + awardRibbonsToSpeciesLine(this.species.speciesId, RibbonData.FRIENDSHIP); + } + } + + let candyFriendshipMultiplier = globalScene.gameMode.isClassic + ? timedEventManager.getClassicFriendshipMultiplier() + : 1; + if (fusionStarterSpeciesId) { + candyFriendshipMultiplier /= timedEventManager.areFusionsBoosted() ? 1.5 : 2; + } + const candyFriendshipAmount = Math.floor(friendship * candyFriendshipMultiplier); + // Add to candy progress for this mon's starter species and its fused species (if it has one) + starterData.forEach(([sd, id]: [StarterDataEntry, SpeciesId]) => { + sd.friendship = (sd.friendship || 0) + candyFriendshipAmount; + if (sd.friendship >= getStarterValueFriendshipCap(speciesStarterCosts[id])) { + globalScene.gameData.addStarterCandy(getPokemonSpecies(id), 1); + sd.friendship = 0; + } + }); } getPossibleEvolution(evolution: SpeciesFormEvolution | null): Promise { @@ -6246,22 +6313,22 @@ export class EnemyPokemon extends Pokemon { this.setBoss(boss, dataSource?.bossSegments); } - if (Overrides.OPP_STATUS_OVERRIDE) { - this.status = new Status(Overrides.OPP_STATUS_OVERRIDE, 0, 4); + if (Overrides.ENEMY_STATUS_OVERRIDE) { + this.status = new Status(Overrides.ENEMY_STATUS_OVERRIDE, 0, 4); } - if (Overrides.OPP_GENDER_OVERRIDE !== null) { - this.gender = Overrides.OPP_GENDER_OVERRIDE; + if (Overrides.ENEMY_GENDER_OVERRIDE !== null) { + this.gender = Overrides.ENEMY_GENDER_OVERRIDE; } const speciesId = this.species.speciesId; if ( - speciesId in Overrides.OPP_FORM_OVERRIDES && - !isNullOrUndefined(Overrides.OPP_FORM_OVERRIDES[speciesId]) && - this.species.forms[Overrides.OPP_FORM_OVERRIDES[speciesId]] + speciesId in Overrides.ENEMY_FORM_OVERRIDES && + !isNullOrUndefined(Overrides.ENEMY_FORM_OVERRIDES[speciesId]) && + this.species.forms[Overrides.ENEMY_FORM_OVERRIDES[speciesId]] ) { - this.formIndex = Overrides.OPP_FORM_OVERRIDES[speciesId]; + this.formIndex = Overrides.ENEMY_FORM_OVERRIDES[speciesId]; } else if (globalScene.gameMode.isDaily && globalScene.gameMode.isWaveFinal(globalScene.currentBattle.waveIndex)) { const eventBoss = getDailyEventSeedBoss(globalScene.seed); if (!isNullOrUndefined(eventBoss)) { @@ -6271,21 +6338,21 @@ export class EnemyPokemon extends Pokemon { if (!dataSource) { this.generateAndPopulateMoveset(); - if (shinyLock || Overrides.OPP_SHINY_OVERRIDE === false) { + if (shinyLock || Overrides.ENEMY_SHINY_OVERRIDE === false) { this.shiny = false; } else { this.trySetShiny(); } - if (!this.shiny && Overrides.OPP_SHINY_OVERRIDE) { + if (!this.shiny && Overrides.ENEMY_SHINY_OVERRIDE) { this.shiny = true; this.initShinySparkle(); } if (this.shiny) { this.variant = this.generateShinyVariant(); - if (Overrides.OPP_VARIANT_OVERRIDE !== null) { - this.variant = Overrides.OPP_VARIANT_OVERRIDE; + if (Overrides.ENEMY_VARIANT_OVERRIDE !== null) { + this.variant = Overrides.ENEMY_VARIANT_OVERRIDE; } } diff --git a/src/field/trainer.ts b/src/field/trainer.ts index 584c9310932..e94322cb9c6 100644 --- a/src/field/trainer.ts +++ b/src/field/trainer.ts @@ -1,7 +1,7 @@ import { globalScene } from "#app/global-scene"; import { pokemonPrevolutions } from "#balance/pokemon-evolutions"; import { signatureSpecies } from "#balance/signature-species"; -import { ArenaTrapTag } from "#data/arena-tag"; +import { EntryHazardTag } from "#data/arena-tag"; import type { PokemonSpecies } from "#data/pokemon-species"; import { ArenaTagSide } from "#enums/arena-tag-side"; import { PartyMemberStrength } from "#enums/party-member-strength"; @@ -23,7 +23,7 @@ import { } from "#trainers/trainer-party-template"; import { randSeedInt, randSeedItem, randSeedWeightedItem } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; -import { toSnakeCase } from "#utils/strings"; +import { toCamelCase } from "#utils/strings"; import i18next from "i18next"; export class Trainer extends Phaser.GameObjects.Container { @@ -170,7 +170,7 @@ export class Trainer extends Phaser.GameObjects.Container { const evilTeamTitles = ["grunt"]; if (this.name === "" && evilTeamTitles.some(t => name.toLocaleLowerCase().includes(t))) { // This is a evil team grunt so we localize it by only using the "name" as the title - title = i18next.t(`trainerClasses:${toSnakeCase(name)}`); + title = i18next.t(`trainerClasses:${toCamelCase(name)}`); console.log("Localized grunt name: " + title); // Since grunts are not named we can just return the title return title; @@ -187,7 +187,7 @@ export class Trainer extends Phaser.GameObjects.Container { } // Get the localized trainer class name from the i18n file and set it as the title. // This is used for trainer class names, not titles like "Elite Four, Champion, etc." - title = i18next.t(`trainerClasses:${toSnakeCase(name)}`); + title = i18next.t(`trainerClasses:${toCamelCase(name)}`); } // If no specific trainer slot is set. @@ -208,7 +208,7 @@ export class Trainer extends Phaser.GameObjects.Container { if (this.config.titleDouble && this.variant === TrainerVariant.DOUBLE && !this.config.doubleOnly) { title = this.config.titleDouble; - name = i18next.t(`trainerNames:${toSnakeCase(this.config.nameDouble)}`); + name = i18next.t(`trainerNames:${toCamelCase(this.config.nameDouble)}`); } console.log(title ? `${title} ${name}` : name); @@ -584,8 +584,8 @@ export class Trainer extends Phaser.GameObjects.Container { score /= playerField.length; if (forSwitch && !p.isOnField()) { globalScene.arena - .findTagsOnSide(t => t instanceof ArenaTrapTag, ArenaTagSide.ENEMY) - .map(t => (score *= (t as ArenaTrapTag).getMatchupScoreMultiplier(p))); + .findTagsOnSide(t => t instanceof EntryHazardTag, ArenaTagSide.ENEMY) + .map(t => (score *= (t as EntryHazardTag).getMatchupScoreMultiplier(p))); } } diff --git a/src/loading-scene.ts b/src/loading-scene.ts index d2b4a76ef10..bf4d87a99f3 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -90,6 +90,7 @@ export class LoadingScene extends SceneBase { this.loadAtlas("shiny_icons", "ui"); this.loadImage("ha_capsule", "ui", "ha_capsule.png"); this.loadImage("champion_ribbon", "ui", "champion_ribbon.png"); + this.loadImage("champion_ribbon_emerald", "ui", "champion_ribbon_emerald.png"); this.loadImage("icon_spliced", "ui"); this.loadImage("icon_lock", "ui", "icon_lock.png"); this.loadImage("icon_stop", "ui", "icon_stop.png"); @@ -122,6 +123,7 @@ export class LoadingScene extends SceneBase { this.loadImage("party_bg_double", "ui"); this.loadImage("party_bg_double_manage", "ui"); this.loadAtlas("party_slot_main", "ui"); + this.loadAtlas("party_slot_main_short", "ui"); this.loadAtlas("party_slot", "ui"); this.loadImage("party_slot_overlay_lv", "ui"); this.loadImage("party_slot_hp_bar", "ui"); @@ -447,7 +449,9 @@ export class LoadingScene extends SceneBase { ); if (!mobile) { - loadingGraphics.map(g => g.setVisible(false)); + loadingGraphics.forEach(g => { + g.setVisible(false); + }); } const intro = this.add.video(0, 0); diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index aca49313ff5..8b77900cb62 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -130,6 +130,7 @@ import { } from "#utils/common"; import { getEnumKeys, getEnumValues } from "#utils/enums"; import { getModifierPoolForType, getModifierType } from "#utils/modifier-utils"; +import { toCamelCase } from "#utils/strings"; import i18next from "i18next"; const outputModifierData = false; @@ -451,13 +452,13 @@ export class TerastallizeModifierType extends PokemonModifierType { get name(): string { return i18next.t("modifierType:ModifierType.TerastallizeModifierType.name", { - teraType: i18next.t(`pokemonInfo:Type.${PokemonType[this.teraType]}`), + teraType: i18next.t(`pokemonInfo:type.${toCamelCase(PokemonType[this.teraType])}`), }); } getDescription(): string { return i18next.t("modifierType:ModifierType.TerastallizeModifierType.description", { - teraType: i18next.t(`pokemonInfo:Type.${PokemonType[this.teraType]}`), + teraType: i18next.t(`pokemonInfo:type.${toCamelCase(PokemonType[this.teraType])}`), }); } @@ -861,7 +862,7 @@ export class AttackTypeBoosterModifierType getDescription(): string { // TODO: Need getTypeName? return i18next.t("modifierType:ModifierType.AttackTypeBoosterModifierType.description", { - moveType: i18next.t(`pokemonInfo:Type.${PokemonType[this.moveType]}`), + moveType: i18next.t(`pokemonInfo:type.${toCamelCase(PokemonType[this.moveType])}`), }); } diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 6907b6907ca..7d0478628b4 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -121,8 +121,8 @@ export class ModifierBar extends Phaser.GameObjects.Container { } updateModifierOverflowVisibility(ignoreLimit: boolean) { - const modifierIcons = this.getAll().reverse(); - for (const modifier of modifierIcons.map(m => m as Phaser.GameObjects.Container).slice(iconOverflowIndex)) { + const modifierIcons = this.getAll().reverse() as Phaser.GameObjects.Container[]; + for (const modifier of modifierIcons.slice(iconOverflowIndex)) { modifier.setVisible(ignoreLimit); } } @@ -1733,12 +1733,12 @@ export class TurnStatusEffectModifier extends PokemonHeldItemModifier { } /** - * Tries to inflicts the holder with the associated {@linkcode StatusEffect}. - * @param pokemon {@linkcode Pokemon} that holds the held item + * Attempt to inflict the holder with the associated {@linkcode StatusEffect}. + * @param pokemon - The {@linkcode Pokemon} holding the item * @returns `true` if the status effect was applied successfully */ override apply(pokemon: Pokemon): boolean { - return pokemon.trySetStatus(this.effect, true, undefined, undefined, this.type.name); + return pokemon.trySetStatus(this.effect, pokemon, undefined, this.type.name); } getMaxHeldItemCount(_pokemon: Pokemon): number { @@ -2304,7 +2304,7 @@ export class PokemonLevelIncrementModifier extends ConsumablePokemonModifier { playerPokemon.levelExp = 0; } - playerPokemon.addFriendship(FRIENDSHIP_GAIN_FROM_RARE_CANDY); + playerPokemon.addFriendship(FRIENDSHIP_GAIN_FROM_RARE_CANDY, true); globalScene.phaseManager.unshiftNew( "LevelUpPhase", @@ -3605,7 +3605,7 @@ export class EnemyAttackStatusEffectChanceModifier extends EnemyPersistentModifi */ override apply(enemyPokemon: Pokemon): boolean { if (randSeedFloat() <= this.chance * this.getStackCount()) { - return enemyPokemon.trySetStatus(this.effect, true); + return enemyPokemon.trySetStatus(this.effect); } return false; @@ -3755,7 +3755,7 @@ export class EnemyFusionChanceModifier extends EnemyPersistentModifier { export function overrideModifiers(isPlayer = true): void { const modifiersOverride: ModifierOverride[] = isPlayer ? Overrides.STARTING_MODIFIER_OVERRIDE - : Overrides.OPP_MODIFIER_OVERRIDE; + : Overrides.ENEMY_MODIFIER_OVERRIDE; if (!modifiersOverride || modifiersOverride.length === 0 || !globalScene) { return; } @@ -3797,7 +3797,7 @@ export function overrideModifiers(isPlayer = true): void { export function overrideHeldItems(pokemon: Pokemon, isPlayer = true): void { const heldItemsOverride: ModifierOverride[] = isPlayer ? Overrides.STARTING_HELD_ITEMS_OVERRIDE - : Overrides.OPP_HELD_ITEMS_OVERRIDE; + : Overrides.ENEMY_HELD_ITEMS_OVERRIDE; if (!heldItemsOverride || heldItemsOverride.length === 0 || !globalScene) { return; } diff --git a/src/overrides.ts b/src/overrides.ts index de0d1d3f30a..b8212ea8fd6 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -78,15 +78,19 @@ class DefaultOverrides { readonly ARENA_TINT_OVERRIDE: TimeOfDay | null = null; /** Multiplies XP gained by this value including 0. Set to null to ignore the override. */ readonly XP_MULTIPLIER_OVERRIDE: number | null = null; - /** Sets the level cap to this number during experience gain calculations. Set to `0` to disable override & use normal wave-based level caps, - or any negative number to set it to 9 quadrillion (effectively disabling it). */ + /** + * Sets the level cap to this number during experience gain calculations. + * + * Set to `0` to disable override & use normal wave-based level caps, + * or any negative number to disable level caps entirely. + */ readonly LEVEL_CAP_OVERRIDE: number = 0; /** * If defined, overrides random critical hit rolls to always or never succeed. * Ignored if the move is guaranteed to always/never crit. */ readonly CRITICAL_HIT_OVERRIDE: boolean | null = null; - /** default 1000 */ + /** @defaultValue `1000` */ readonly STARTING_MONEY_OVERRIDE: number = 0; /** Sets all shop item prices to 0 */ readonly WAIVE_SHOP_FEES_OVERRIDE: boolean = false; @@ -130,8 +134,8 @@ class DefaultOverrides { // PLAYER OVERRIDES // ---------------- /** - * Set the form index of any starter in the party whose `speciesId` is inside this override - * @see {@link allSpecies} in `src/data/pokemon-species.ts` for form indexes + * Set the form index of any starter in the party whose {@linkcode SpeciesId} is inside this override + * @see `src/data/pokemon-species.ts` for form indexes * @example * ``` * const STARTER_FORM_OVERRIDES = { @@ -141,23 +145,14 @@ class DefaultOverrides { */ readonly STARTER_FORM_OVERRIDES: Partial> = {}; - /** default 5 or 20 for Daily */ + /** @defaultValue `20` for Daily and `5` for all other modes */ readonly STARTING_LEVEL_OVERRIDE: number = 0; - /** - * SPECIES OVERRIDE - * will only apply to the first starter in your party or each enemy pokemon - * default is 0 to not override - * @example SPECIES_OVERRIDE = SpeciesId.Bulbasaur; - */ - readonly STARTER_SPECIES_OVERRIDE: SpeciesId | number = 0; - /** - * This will force your starter to be a random fusion - */ + /** Will override the species of your pokemon when starting a new run */ + readonly STARTER_SPECIES_OVERRIDE: SpeciesId | 0 = 0; + /** This will force your starter to be a random fusion */ readonly STARTER_FUSION_OVERRIDE: boolean = false; - /** - * This will override the species of the fusion - */ - readonly STARTER_FUSION_SPECIES_OVERRIDE: SpeciesId | number = 0; + /** This will override the species of the fusion */ + readonly STARTER_FUSION_SPECIES_OVERRIDE: SpeciesId | 0 = 0; readonly ABILITY_OVERRIDE: AbilityId = AbilityId.NONE; readonly PASSIVE_ABILITY_OVERRIDE: AbilityId = AbilityId.NONE; readonly HAS_PASSIVE_ABILITY_OVERRIDE: boolean | null = null; @@ -179,25 +174,21 @@ class DefaultOverrides { // -------------------------- // OPPONENT / ENEMY OVERRIDES // -------------------------- - // TODO: rename `OPP_` to `ENEMY_` - readonly OPP_SPECIES_OVERRIDE: SpeciesId | number = 0; - /** - * This will make all opponents fused Pokemon - */ - readonly OPP_FUSION_OVERRIDE: boolean = false; - /** - * This will override the species of the fusion only when the opponent is already a fusion - */ - readonly OPP_FUSION_SPECIES_OVERRIDE: SpeciesId | number = 0; - readonly OPP_LEVEL_OVERRIDE: number = 0; - readonly OPP_ABILITY_OVERRIDE: AbilityId = AbilityId.NONE; - readonly OPP_PASSIVE_ABILITY_OVERRIDE: AbilityId = AbilityId.NONE; - readonly OPP_HAS_PASSIVE_ABILITY_OVERRIDE: boolean | null = null; - readonly OPP_STATUS_OVERRIDE: StatusEffect = StatusEffect.NONE; - readonly OPP_GENDER_OVERRIDE: Gender | null = null; - readonly OPP_MOVESET_OVERRIDE: MoveId | Array = []; - readonly OPP_SHINY_OVERRIDE: boolean | null = null; - readonly OPP_VARIANT_OVERRIDE: Variant | null = null; + readonly ENEMY_SPECIES_OVERRIDE: SpeciesId | number = 0; + /** This will make all enemies fused Pokemon */ + readonly ENEMY_FUSION_OVERRIDE: boolean = false; + /** This will override the species of the fusion only when the enemy is already a fusion */ + readonly ENEMY_FUSION_SPECIES_OVERRIDE: SpeciesId | number = 0; + readonly ENEMY_LEVEL_OVERRIDE: number = 0; + readonly ENEMY_ABILITY_OVERRIDE: AbilityId = AbilityId.NONE; + readonly ENEMY_PASSIVE_ABILITY_OVERRIDE: AbilityId = AbilityId.NONE; + readonly ENEMY_HAS_PASSIVE_ABILITY_OVERRIDE: boolean | null = null; + readonly ENEMY_STATUS_OVERRIDE: StatusEffect = StatusEffect.NONE; + readonly ENEMY_GENDER_OVERRIDE: Gender | null = null; + readonly ENEMY_MOVESET_OVERRIDE: MoveId | Array = []; + readonly ENEMY_SHINY_OVERRIDE: boolean | null = null; + readonly ENEMY_VARIANT_OVERRIDE: Variant | null = null; + /** * Overrides the IVs of enemy pokemon. Values must never be outside the range `0` to `31`! * - If set to a number between `0` and `31`, set all IVs of all enemy pokemon to that number. @@ -207,15 +198,15 @@ class DefaultOverrides { readonly ENEMY_IVS_OVERRIDE: number | number[] | null = null; /** Override the nature of all enemy pokemon to the specified nature. Disabled if `null`. */ readonly ENEMY_NATURE_OVERRIDE: Nature | null = null; - readonly OPP_FORM_OVERRIDES: Partial> = {}; + readonly ENEMY_FORM_OVERRIDES: Partial> = {}; /** * Override to give the enemy Pokemon a given amount of health segments * - * 0 (default): the health segments will be handled normally based on wave, level and species - * 1: the Pokemon will have a single health segment and therefore will not be a boss - * 2+: the Pokemon will be a boss with the given number of health segments + * - `0` (default): the health segments will be handled normally based on wave, level and species + * - `1`: the Pokemon will have a single health segment and therefore will not be a boss + * - `2+`: the Pokemon will be a boss with the given number of health segments */ - readonly OPP_HEALTH_SEGMENTS_OVERRIDE: number = 0; + readonly ENEMY_HEALTH_SEGMENTS_OVERRIDE: number = 0; // ------------- // EGG OVERRIDES @@ -277,12 +268,12 @@ class DefaultOverrides { * * Note that any previous modifiers are cleared. */ - readonly OPP_MODIFIER_OVERRIDE: ModifierOverride[] = []; + readonly ENEMY_MODIFIER_OVERRIDE: ModifierOverride[] = []; /** Override array of {@linkcode ModifierOverride}s used to provide held items to first party member when starting a new game. */ readonly STARTING_HELD_ITEMS_OVERRIDE: ModifierOverride[] = []; /** Override array of {@linkcode ModifierOverride}s used to provide held items to enemies on spawn. */ - readonly OPP_HELD_ITEMS_OVERRIDE: ModifierOverride[] = []; + readonly ENEMY_HELD_ITEMS_OVERRIDE: ModifierOverride[] = []; /** * Override array of {@linkcode ModifierOverride}s used to replace the generated item rolls after a wave. @@ -294,9 +285,7 @@ class DefaultOverrides { */ readonly ITEM_REWARD_OVERRIDE: ModifierOverride[] = []; - /** - * If `true`, disable all non-scripted opponent trainer encounters. - */ + /** If `true`, disable all non-scripted opponent trainer encounters. */ readonly DISABLE_STANDARD_TRAINERS_OVERRIDE: boolean = false; /** diff --git a/src/phase.ts b/src/phase.ts index eccbf3127e6..6fea6e3b0f0 100644 --- a/src/phase.ts +++ b/src/phase.ts @@ -2,8 +2,10 @@ import { globalScene } from "#app/global-scene"; import type { PhaseMap, PhaseString } from "#types/phase-types"; export abstract class Phase { + /** Start the current phase. */ start() {} + /** End the current phase and start a new one. */ end() { globalScene.phaseManager.shiftPhase(); } diff --git a/src/phases/attempt-capture-phase.ts b/src/phases/attempt-capture-phase.ts index aea39cff294..699caa2af21 100644 --- a/src/phases/attempt-capture-phase.ts +++ b/src/phases/attempt-capture-phase.ts @@ -253,8 +253,11 @@ export class AttemptCapturePhase extends PokemonPhase { globalScene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs); + const addStatus = new BooleanHolder(true); + applyChallenges(ChallengeType.POKEMON_ADD_TO_PARTY, pokemon, addStatus); + globalScene.ui.showText( - i18next.t("battle:pokemonCaught", { + i18next.t(addStatus.value ? "battle:pokemonCaught" : "battle:pokemonCaughtButChallenge", { pokemonName: getPokemonNameWithAffix(pokemon), }), null, @@ -268,7 +271,7 @@ export class AttemptCapturePhase extends PokemonPhase { const removePokemon = () => { globalScene.addFaintedEnemyScore(pokemon); pokemon.hp = 0; - pokemon.trySetStatus(StatusEffect.FAINT); + pokemon.doSetStatus(StatusEffect.FAINT); globalScene.clearEnemyHeldItemModifiers(); pokemon.leaveField(true, true, true); }; @@ -290,8 +293,6 @@ export class AttemptCapturePhase extends PokemonPhase { }); }; Promise.all([pokemon.hideInfo(), globalScene.gameData.setPokemonCaught(pokemon)]).then(() => { - const addStatus = new BooleanHolder(true); - applyChallenges(ChallengeType.POKEMON_ADD_TO_PARTY, pokemon, addStatus); if (!addStatus.value) { removePokemon(); end(); diff --git a/src/phases/attempt-run-phase.ts b/src/phases/attempt-run-phase.ts index a59667bdd4e..e8212a27243 100644 --- a/src/phases/attempt-run-phase.ts +++ b/src/phases/attempt-run-phase.ts @@ -45,7 +45,7 @@ export class AttemptRunPhase extends FieldPhase { enemyField.forEach(enemyPokemon => { enemyPokemon.hideInfo().then(() => enemyPokemon.destroy()); enemyPokemon.hp = 0; - enemyPokemon.trySetStatus(StatusEffect.FAINT); + enemyPokemon.doSetStatus(StatusEffect.FAINT); }); globalScene.phaseManager.pushNew("BattleEndPhase", false); diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index b778d2dc475..4b9f18c2011 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -469,20 +469,21 @@ export class CommandPhase extends FieldPhase { } if (trappedAbMessages.length > 0) { if (isSwitch) { - globalScene.ui.setMode(UiMode.MESSAGE); + globalScene.ui.setMode(UiMode.MESSAGE).then(() => { + globalScene.ui.showText( + trappedAbMessages[0], + null, + () => { + globalScene.ui.showText("", 0); + if (isSwitch) { + globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); + } + }, + null, + true, + ); + }); } - globalScene.ui.showText( - trappedAbMessages[0], - null, - () => { - globalScene.ui.showText("", 0); - if (isSwitch) { - globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex); - } - }, - null, - true, - ); } else { const trapTag = playerPokemon.getTag(TrappedTag); const fairyLockTag = globalScene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, ArenaTagSide.PLAYER); diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index 79da7134e9a..b870f7f6e7a 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -229,7 +229,7 @@ export class EncounterPhase extends BattlePhase { }), ); } else { - const overridedBossSegments = Overrides.OPP_HEALTH_SEGMENTS_OVERRIDE > 1; + const overridedBossSegments = Overrides.ENEMY_HEALTH_SEGMENTS_OVERRIDE > 1; // for double battles, reduce the health segments for boss Pokemon unless there is an override if (!overridedBossSegments && battle.enemyParty.filter(p => p.isBoss()).length > 1) { for (const enemyPokemon of battle.enemyParty) { diff --git a/src/phases/faint-phase.ts b/src/phases/faint-phase.ts index d1bd0ed0804..2d953043866 100644 --- a/src/phases/faint-phase.ts +++ b/src/phases/faint-phase.ts @@ -205,7 +205,7 @@ export class FaintPhase extends PokemonPhase { pokemon.lapseTags(BattlerTagLapseType.FAINT); pokemon.y -= 150; - pokemon.trySetStatus(StatusEffect.FAINT); + pokemon.doSetStatus(StatusEffect.FAINT); if (pokemon.isPlayer()) { globalScene.currentBattle.removeFaintedParticipant(pokemon as PlayerPokemon); } else { diff --git a/src/phases/game-over-phase.ts b/src/phases/game-over-phase.ts index d4562b5a237..dcde244ecd3 100644 --- a/src/phases/game-over-phase.ts +++ b/src/phases/game-over-phase.ts @@ -19,8 +19,11 @@ import { ChallengeData } from "#system/challenge-data"; import type { SessionSaveData } from "#system/game-data"; import { ModifierData as PersistentModifierData } from "#system/modifier-data"; import { PokemonData } from "#system/pokemon-data"; +import { RibbonData, type RibbonFlag } from "#system/ribbons/ribbon-data"; +import { awardRibbonsToSpeciesLine } from "#system/ribbons/ribbon-methods"; import { TrainerData } from "#system/trainer-data"; import { trainerConfigs } from "#trainers/trainer-config"; +import { checkSpeciesValidForChallenge, isNuzlockeChallenge } from "#utils/challenge-utils"; import { isLocal, isLocalServerConnected } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import i18next from "i18next"; @@ -62,8 +65,8 @@ export class GameOverPhase extends BattlePhase { const genderIndex = globalScene.gameData.gender ?? PlayerGender.UNSET; const genderStr = PlayerGender[genderIndex].toLowerCase(); globalScene.ui.showDialogue( - i18next.t("miscDialogue:ending_endless", { context: genderStr }), - i18next.t("miscDialogue:ending_name"), + i18next.t("miscDialogue:endingEndless", { context: genderStr }), + i18next.t("miscDialogue:endingName"), 0, () => this.handleGameOver(), ); @@ -111,6 +114,46 @@ export class GameOverPhase extends BattlePhase { } } + /** + * Submethod of {@linkcode handleGameOver} that awards ribbons to Pokémon in the player's party based on the current + * game mode and challenges. + */ + private awardRibbons(): void { + let ribbonFlags = 0n; + for (const challenge of globalScene.gameMode.challenges) { + const ribbon = challenge.ribbonAwarded; + if (challenge.value && ribbon) { + ribbonFlags |= ribbon; + } + } + // Block other ribbons if flip stats or inverse is active + const flip_or_inverse = ribbonFlags & (RibbonData.FLIP_STATS | RibbonData.INVERSE); + if (flip_or_inverse) { + ribbonFlags = flip_or_inverse; + } else { + if (globalScene.gameMode.isClassic) { + ribbonFlags |= RibbonData.CLASSIC; + } + if (isNuzlockeChallenge()) { + ribbonFlags |= RibbonData.NUZLOCKE; + } + } + // Award ribbons to all Pokémon in the player's party that are considered valid + // for the current game mode and challenges. + for (const pokemon of globalScene.getPlayerParty()) { + const species = pokemon.species; + if ( + checkSpeciesValidForChallenge( + species, + globalScene.gameData.getSpeciesDexAttrProps(species, pokemon.getDexAttr()), + false, + ) + ) { + awardRibbonsToSpeciesLine(species.speciesId, ribbonFlags as RibbonFlag); + } + } + } + handleGameOver(): void { const doGameOver = (newClear: boolean) => { globalScene.disableMenu = true; @@ -122,12 +165,12 @@ export class GameOverPhase extends BattlePhase { globalScene.validateAchv(achvs.UNEVOLVED_CLASSIC_VICTORY); globalScene.gameData.gameStats.sessionsWon++; for (const pokemon of globalScene.getPlayerParty()) { - this.awardRibbon(pokemon); - + this.awardFirstClassicCompletion(pokemon); if (pokemon.species.getRootSpeciesId() !== pokemon.species.getRootSpeciesId(true)) { - this.awardRibbon(pokemon, true); + this.awardFirstClassicCompletion(pokemon, true); } } + this.awardRibbons(); } else if (globalScene.gameMode.isDaily && newClear) { globalScene.gameData.gameStats.dailyRunSessionsWon++; } @@ -263,7 +306,7 @@ export class GameOverPhase extends BattlePhase { } } - awardRibbon(pokemon: Pokemon, forStarter = false): void { + awardFirstClassicCompletion(pokemon: Pokemon, forStarter = false): void { const speciesId = getPokemonSpecies(pokemon.species.speciesId); const speciesRibbonCount = globalScene.gameData.incrementRibbonCount(speciesId, forStarter); // first time classic win, award voucher diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index f88f9d0cad1..9a8e509e302 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -269,8 +269,8 @@ export class MovePhase extends BattlePhase { globalScene.phaseManager.queueMessage( getStatusEffectHealText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)), ); - this.pokemon.resetStatus(); - this.pokemon.updateInfo(); + // cannot use `asPhase=true` as it will cause status to be reset _after_ move condition checks fire + this.pokemon.resetStatus(false, false, false, false); } } diff --git a/src/phases/obtain-status-effect-phase.ts b/src/phases/obtain-status-effect-phase.ts index fbadbed205b..4846130cf4d 100644 --- a/src/phases/obtain-status-effect-phase.ts +++ b/src/phases/obtain-status-effect-phase.ts @@ -3,71 +3,64 @@ import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; import { CommonBattleAnim } from "#data/battle-anims"; import { SpeciesFormChangeStatusEffectTrigger } from "#data/form-change-triggers"; -import { getStatusEffectObtainText, getStatusEffectOverlapText } from "#data/status-effect"; +import { getStatusEffectObtainText } from "#data/status-effect"; import type { BattlerIndex } from "#enums/battler-index"; import { CommonAnim } from "#enums/move-anims-common"; import { StatusEffect } from "#enums/status-effect"; import type { Pokemon } from "#field/pokemon"; import { PokemonPhase } from "#phases/pokemon-phase"; -import { isNullOrUndefined } from "#utils/common"; export class ObtainStatusEffectPhase extends PokemonPhase { public readonly phaseName = "ObtainStatusEffectPhase"; - private statusEffect?: StatusEffect; - private turnsRemaining?: number; - private sourceText?: string | null; - private sourcePokemon?: Pokemon | null; + /** + * @param battlerIndex - The {@linkcode BattlerIndex} of the Pokemon obtaining the status effect. + * @param statusEffect - The {@linkcode StatusEffect} being applied. + * @param sourcePokemon - The {@linkcode Pokemon} applying the status effect to the target, + * or `null` if the status is applied from a non-Pokemon source (hazards, etc.); default `null`. + * @param sleepTurnsRemaining - The number of turns to set {@linkcode StatusEffect.SLEEP} for; + * defaults to a random number between 2 and 4 and is unused for non-Sleep statuses. + * @param sourceText - The text to show for the source of the status effect, if any; default `null`. + * @param statusMessage - A string containing text to be displayed upon status setting; + * defaults to normal key for status if empty or omitted. + */ constructor( battlerIndex: BattlerIndex, - statusEffect?: StatusEffect, - turnsRemaining?: number, - sourceText?: string | null, - sourcePokemon?: Pokemon | null, + private statusEffect: StatusEffect, + private sourcePokemon: Pokemon | null = null, + private sleepTurnsRemaining?: number, + sourceText: string | null = null, // TODO: This should take `undefined` instead of `null` + private statusMessage = "", ) { super(battlerIndex); - this.statusEffect = statusEffect; - this.turnsRemaining = turnsRemaining; - this.sourceText = sourceText; - this.sourcePokemon = sourcePokemon; + this.statusMessage ||= getStatusEffectObtainText( + statusEffect, + getPokemonNameWithAffix(this.getPokemon()), + sourceText ?? undefined, + ); } start() { const pokemon = this.getPokemon(); - if (pokemon && !pokemon.status) { - if (pokemon.trySetStatus(this.statusEffect, false, this.sourcePokemon)) { - if (this.turnsRemaining) { - pokemon.status!.sleepTurnsRemaining = this.turnsRemaining; - } - pokemon.updateInfo(true); - new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect! - 1), pokemon).play(false, () => { - globalScene.phaseManager.queueMessage( - getStatusEffectObtainText( - this.statusEffect, - getPokemonNameWithAffix(pokemon), - this.sourceText ?? undefined, - ), - ); - if (!isNullOrUndefined(this.statusEffect) && this.statusEffect !== StatusEffect.FAINT) { - 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); - applyAbAttrs("PostSetStatusAbAttr", { - pokemon, - effect: this.statusEffect, - sourcePokemon: this.sourcePokemon ?? undefined, - }); - } - this.end(); + + pokemon.doSetStatus(this.statusEffect, this.sleepTurnsRemaining); + pokemon.updateInfo(true); + + new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect - 1), pokemon).play(false, () => { + globalScene.phaseManager.queueMessage(this.statusMessage); + if (this.statusEffect && this.statusEffect !== StatusEffect.FAINT) { + globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeStatusEffectTrigger, true); + // If the status was applied from a move, ensure abilities are not ignored for follow-up triggers. + // TODO: Ensure this isn't breaking any other phases unshifted afterwards + globalScene.arena.setIgnoreAbilities(false); + applyAbAttrs("PostSetStatusAbAttr", { + pokemon, + effect: this.statusEffect, + sourcePokemon: this.sourcePokemon ?? undefined, }); - return; } - } else if (pokemon.status?.effect === this.statusEffect) { - globalScene.phaseManager.queueMessage( - getStatusEffectOverlapText(this.statusEffect ?? StatusEffect.NONE, getPokemonNameWithAffix(pokemon)), - ); - } - this.end(); + this.end(); + }); } } diff --git a/src/phases/pokemon-heal-phase.ts b/src/phases/pokemon-heal-phase.ts index fa6a3222466..02bb3a0b968 100644 --- a/src/phases/pokemon-heal-phase.ts +++ b/src/phases/pokemon-heal-phase.ts @@ -13,6 +13,7 @@ import { HealAchv } from "#system/achv"; import { NumberHolder } from "#utils/common"; import i18next from "i18next"; +// TODO: Refactor this - it has far too many arguments export class PokemonHealPhase extends CommonAnimPhase { public readonly phaseName = "PokemonHealPhase"; private hpHealed: number; @@ -28,7 +29,7 @@ export class PokemonHealPhase extends CommonAnimPhase { battlerIndex: BattlerIndex, hpHealed: number, message: string | null, - showFullHpMessage: boolean, + showFullHpMessage = true, skipAnim = false, revive = false, healStatus = false, @@ -72,6 +73,7 @@ export class PokemonHealPhase extends CommonAnimPhase { this.message = null; return super.end(); } + if (healOrDamage) { const hpRestoreMultiplier = new NumberHolder(1); if (!this.revive) { diff --git a/src/phases/post-summon-phase.ts b/src/phases/post-summon-phase.ts index e0811d0ab93..913a29cded8 100644 --- a/src/phases/post-summon-phase.ts +++ b/src/phases/post-summon-phase.ts @@ -1,6 +1,6 @@ import { applyAbAttrs } from "#abilities/apply-ab-attrs"; import { globalScene } from "#app/global-scene"; -import { ArenaTrapTag } from "#data/arena-tag"; +import { EntryHazardTag } from "#data/arena-tag"; import { MysteryEncounterPostSummonTag } from "#data/battler-tags"; import { BattlerTagType } from "#enums/battler-tag-type"; import { StatusEffect } from "#enums/status-effect"; @@ -16,7 +16,7 @@ export class PostSummonPhase extends PokemonPhase { if (pokemon.status?.effect === StatusEffect.TOXIC) { pokemon.status.toxicTurnCount = 0; } - globalScene.arena.applyTags(ArenaTrapTag, false, pokemon); + globalScene.arena.applyTags(EntryHazardTag, false, pokemon); // If this is mystery encounter and has post summon phase tag, apply post summon effects if ( diff --git a/src/phases/select-biome-phase.ts b/src/phases/select-biome-phase.ts index 4089f0c2852..d02d69fc934 100644 --- a/src/phases/select-biome-phase.ts +++ b/src/phases/select-biome-phase.ts @@ -16,8 +16,10 @@ export class SelectBiomePhase extends BattlePhase { globalScene.resetSeed(); + const gameMode = globalScene.gameMode; const currentBiome = globalScene.arena.biomeType; - const nextWaveIndex = globalScene.currentBattle.waveIndex + 1; + const currentWaveIndex = globalScene.currentBattle.waveIndex; + const nextWaveIndex = currentWaveIndex + 1; const setNextBiome = (nextBiome: BiomeId) => { if (nextWaveIndex % 10 === 1) { @@ -26,6 +28,15 @@ export class SelectBiomePhase extends BattlePhase { applyChallenges(ChallengeType.PARTY_HEAL, healStatus); if (healStatus.value) { globalScene.phaseManager.unshiftNew("PartyHealPhase", false); + } else { + globalScene.phaseManager.unshiftNew( + "SelectModifierPhase", + undefined, + undefined, + gameMode.isFixedBattle(currentWaveIndex) + ? gameMode.getFixedBattle(currentWaveIndex).customModifierRewardSettings + : undefined, + ); } } globalScene.phaseManager.unshiftNew("SwitchBiomePhase", nextBiome); @@ -33,12 +44,12 @@ export class SelectBiomePhase extends BattlePhase { }; if ( - (globalScene.gameMode.isClassic && globalScene.gameMode.isWaveFinal(nextWaveIndex + 9)) || - (globalScene.gameMode.isDaily && globalScene.gameMode.isWaveFinal(nextWaveIndex)) || - (globalScene.gameMode.hasShortBiomes && !(nextWaveIndex % 50)) + (gameMode.isClassic && gameMode.isWaveFinal(nextWaveIndex + 9)) || + (gameMode.isDaily && gameMode.isWaveFinal(nextWaveIndex)) || + (gameMode.hasShortBiomes && !(nextWaveIndex % 50)) ) { setNextBiome(BiomeId.END); - } else if (globalScene.gameMode.hasRandomBiomes) { + } else if (gameMode.hasRandomBiomes) { setNextBiome(this.generateNextBiome(nextWaveIndex)); } else if (Array.isArray(biomeLinks[currentBiome])) { const biomes: BiomeId[] = (biomeLinks[currentBiome] as (BiomeId | [BiomeId, number])[]) @@ -73,9 +84,6 @@ export class SelectBiomePhase extends BattlePhase { } generateNextBiome(waveIndex: number): BiomeId { - if (!(waveIndex % 50)) { - return BiomeId.END; - } - return globalScene.generateRandomBiome(waveIndex); + return waveIndex % 50 === 0 ? BiomeId.END : globalScene.generateRandomBiome(waveIndex); } } diff --git a/src/phases/tera-phase.ts b/src/phases/tera-phase.ts index 84b05d88abe..5e42fab82ba 100644 --- a/src/phases/tera-phase.ts +++ b/src/phases/tera-phase.ts @@ -7,6 +7,7 @@ import { PokemonType } from "#enums/pokemon-type"; import type { Pokemon } from "#field/pokemon"; import { BattlePhase } from "#phases/battle-phase"; import { achvs } from "#system/achv"; +import { toCamelCase } from "#utils/strings"; import i18next from "i18next"; export class TeraPhase extends BattlePhase { @@ -25,7 +26,7 @@ export class TeraPhase extends BattlePhase { globalScene.phaseManager.queueMessage( i18next.t("battle:pokemonTerastallized", { pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon), - type: i18next.t(`pokemonInfo:Type.${PokemonType[this.pokemon.getTeraType()]}`), + type: i18next.t(`pokemonInfo:type.${toCamelCase(PokemonType[this.pokemon.getTeraType()])}`), }), ); new CommonBattleAnim(CommonAnim.TERASTALLIZE, this.pokemon).play(false, () => { diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts index 9c53a333ed0..c7794ca7f07 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -1,4 +1,5 @@ import { applyAbAttrs } from "#abilities/apply-ab-attrs"; +import type { TurnCommand } from "#app/battle"; import { globalScene } from "#app/global-scene"; import { TrickRoomTag } from "#data/arena-tag"; import { allMoves } from "#data/data-lists"; @@ -14,19 +15,20 @@ import { BooleanHolder, randSeedShuffle } from "#utils/common"; export class TurnStartPhase extends FieldPhase { public readonly phaseName = "TurnStartPhase"; + /** - * This orders the active Pokemon on the field by speed into an BattlerIndex array and returns that array. + * Helper method to retrieve the current speed order of the combattants. * It also checks for Trick Room and reverses the array if it is present. - * @returns {@linkcode BattlerIndex[]} the battle indices of all pokemon on the field ordered by speed + * @returns The {@linkcode BattlerIndex}es of all on-field Pokemon, sorted in speed order. */ getSpeedOrder(): BattlerIndex[] { - const playerField = globalScene.getPlayerField().filter(p => p.isActive()) as Pokemon[]; - const enemyField = globalScene.getEnemyField().filter(p => p.isActive()) as Pokemon[]; + const playerField = globalScene.getPlayerField().filter(p => p.isActive()); + const enemyField = globalScene.getEnemyField().filter(p => p.isActive()); - // We shuffle the list before sorting so speed ties produce random results - let orderedTargets: Pokemon[] = playerField.concat(enemyField); - // We seed it with the current turn to prevent an inconsistency where it - // was varying based on how long since you last reloaded + // Shuffle the list before sorting so speed ties produce random results + // This is seeded with the current turn to prevent turn order varying + // based on how long since you last reloaded. + let orderedTargets = (playerField as Pokemon[]).concat(enemyField); globalScene.executeWithSeedOffset( () => { orderedTargets = randSeedShuffle(orderedTargets); @@ -35,25 +37,25 @@ export class TurnStartPhase extends FieldPhase { globalScene.waveSeed, ); - // Next, a check for Trick Room is applied to determine sort order. + // Check for Trick Room and reverse sort order if active. + // Notably, Pokerogue does NOT have the "outspeed trick room" glitch at >1809 spd. const speedReversed = new BooleanHolder(false); globalScene.arena.applyTags(TrickRoomTag, false, speedReversed); - // Adjust the sort function based on whether Trick Room is active. orderedTargets.sort((a: Pokemon, b: Pokemon) => { - const aSpeed = a?.getEffectiveStat(Stat.SPD) ?? 0; - const bSpeed = b?.getEffectiveStat(Stat.SPD) ?? 0; + const aSpeed = a.getEffectiveStat(Stat.SPD); + const bSpeed = b.getEffectiveStat(Stat.SPD); return speedReversed.value ? aSpeed - bSpeed : bSpeed - aSpeed; }); - return orderedTargets.map(t => t.getFieldIndex() + (!t.isPlayer() ? BattlerIndex.ENEMY : BattlerIndex.PLAYER)); + return orderedTargets.map(t => t.getFieldIndex() + (t.isEnemy() ? BattlerIndex.ENEMY : BattlerIndex.PLAYER)); } /** - * This takes the result of getSpeedOrder and applies priority / bypass speed attributes to it. - * This also considers the priority levels of various commands and changes the result of getSpeedOrder based on such. - * @returns {@linkcode BattlerIndex[]} the final sequence of commands for this turn + * This takes the result of {@linkcode getSpeedOrder} and applies priority / bypass speed attributes to it. + * This also considers the priority levels of various commands and changes the result of `getSpeedOrder` based on such. + * @returns The `BattlerIndex`es of all on-field Pokemon sorted in action order. */ getCommandOrder(): BattlerIndex[] { let moveOrder = this.getSpeedOrder(); @@ -114,7 +116,8 @@ export class TurnStartPhase extends FieldPhase { } } - // If there is no difference between the move's calculated priorities, the game checks for differences in battlerBypassSpeed and returns the result. + // If there is no difference between the move's calculated priorities, + // check for differences in battlerBypassSpeed and returns the result. if (battlerBypassSpeed[a].value !== battlerBypassSpeed[b].value) { return battlerBypassSpeed[a].value ? -1 : 1; } @@ -135,8 +138,6 @@ export class TurnStartPhase extends FieldPhase { const field = globalScene.getField(); const moveOrder = this.getCommandOrder(); - let orderIndex = 0; - for (const o of this.getSpeedOrder()) { const pokemon = field[o]; const preTurnCommand = globalScene.currentBattle.preTurnCommands[o]; @@ -153,71 +154,24 @@ export class TurnStartPhase extends FieldPhase { const phaseManager = globalScene.phaseManager; - for (const o of moveOrder) { + moveOrder.forEach((o, index) => { const pokemon = field[o]; const turnCommand = globalScene.currentBattle.turnCommands[o]; - if (turnCommand?.skip) { - continue; + if (!turnCommand || turnCommand.skip) { + return; } - switch (turnCommand?.command) { - case Command.FIGHT: { - const queuedMove = turnCommand.move; - pokemon.turnData.order = orderIndex++; - if (!queuedMove) { - continue; - } - const move = - pokemon.getMoveset().find(m => m.moveId === queuedMove.move && m.ppUsed < m.getMovePp()) ?? - new PokemonMove(queuedMove.move); - if (move.getMove().hasAttr("MoveHeaderAttr")) { - phaseManager.unshiftNew("MoveHeaderPhase", pokemon, move); - } - - if (pokemon.isPlayer() && turnCommand.cursor === -1) { - phaseManager.pushNew( - "MovePhase", - pokemon, - turnCommand.targets || turnCommand.move!.targets, - move, - turnCommand.move!.useMode, - ); //TODO: is the bang correct here? - } else { - phaseManager.pushNew( - "MovePhase", - pokemon, - turnCommand.targets || turnCommand.move!.targets, - move, - queuedMove.useMode, - ); // TODO: is the bang correct here? - } - break; - } - case Command.BALL: - phaseManager.unshiftNew("AttemptCapturePhase", turnCommand.targets![0] % 2, turnCommand.cursor!); //TODO: is the bang correct here? - break; - case Command.POKEMON: - { - const switchType = turnCommand.args?.[0] ? SwitchType.BATON_PASS : SwitchType.SWITCH; - phaseManager.unshiftNew( - "SwitchSummonPhase", - switchType, - pokemon.getFieldIndex(), - turnCommand.cursor!, - true, - pokemon.isPlayer(), - ); - } - break; - case Command.RUN: - { - // Running (like ball throwing) is a team action taking up both Pokemon's turns. - phaseManager.unshiftNew("AttemptRunPhase"); - } - break; + // TODO: Remove `turnData.order` - + // it is used exclusively for Fusion Flare/Bolt + // and uses a really jank (and incorrect) implementation + if (turnCommand.command === Command.FIGHT) { + pokemon.turnData.order = index; } - } + this.handleTurnCommand(turnCommand, pokemon); + }); + + // Queue various effects for the end of the turn. phaseManager.pushNew("CheckInterludePhase"); // TODO: Re-order these phases to be consistent with mainline turn order: @@ -239,4 +193,52 @@ export class TurnStartPhase extends FieldPhase { */ this.end(); } + + private handleTurnCommand(turnCommand: TurnCommand, pokemon: Pokemon) { + switch (turnCommand?.command) { + case Command.FIGHT: + this.handleFightCommand(turnCommand, pokemon); + break; + case Command.BALL: + globalScene.phaseManager.unshiftNew("AttemptCapturePhase", turnCommand.targets![0] % 2, turnCommand.cursor!); //TODO: is the bang correct here? + break; + case Command.POKEMON: + globalScene.phaseManager.unshiftNew( + "SwitchSummonPhase", + turnCommand.args?.[0] ? SwitchType.BATON_PASS : SwitchType.SWITCH, + pokemon.getFieldIndex(), + turnCommand.cursor!, // TODO: Is this bang correct? + true, + pokemon.isPlayer(), + ); + break; + case Command.RUN: + globalScene.phaseManager.unshiftNew("AttemptRunPhase"); + break; + } + } + + private handleFightCommand(turnCommand: TurnCommand, pokemon: Pokemon) { + const queuedMove = turnCommand.move; + if (!queuedMove) { + return; + } + + // TODO: This seems somewhat dubious + const move = + pokemon.getMoveset().find(m => m.moveId === queuedMove.move && m.ppUsed < m.getMovePp()) ?? + new PokemonMove(queuedMove.move); + + if (move.getMove().hasAttr("MoveHeaderAttr")) { + globalScene.phaseManager.unshiftNew("MoveHeaderPhase", pokemon, move); + } + + globalScene.phaseManager.pushNew( + "MovePhase", + pokemon, + turnCommand.targets ?? queuedMove.targets, + move, + queuedMove.useMode, + ); + } } diff --git a/src/phases/victory-phase.ts b/src/phases/victory-phase.ts index c0f4a32d7e1..ac567cc99c5 100644 --- a/src/phases/victory-phase.ts +++ b/src/phases/victory-phase.ts @@ -3,13 +3,9 @@ import { globalScene } from "#app/global-scene"; import { modifierTypes } from "#data/data-lists"; import { BattleType } from "#enums/battle-type"; import type { BattlerIndex } from "#enums/battler-index"; -import { ChallengeType } from "#enums/challenge-type"; import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves"; -import type { CustomModifierSettings } from "#modifiers/modifier-type"; import { handleMysteryEncounterVictory } from "#mystery-encounters/encounter-phase-utils"; import { PokemonPhase } from "#phases/pokemon-phase"; -import { applyChallenges } from "#utils/challenge-utils"; -import { BooleanHolder } from "#utils/common"; export class VictoryPhase extends PokemonPhase { public readonly phaseName = "VictoryPhase"; @@ -49,15 +45,19 @@ export class VictoryPhase extends PokemonPhase { if (globalScene.currentBattle.battleType === BattleType.TRAINER) { globalScene.phaseManager.pushNew("TrainerVictoryPhase"); } - if (globalScene.gameMode.isEndless || !globalScene.gameMode.isWaveFinal(globalScene.currentBattle.waveIndex)) { + + const gameMode = globalScene.gameMode; + const currentWaveIndex = globalScene.currentBattle.waveIndex; + + if (gameMode.isEndless || !gameMode.isWaveFinal(currentWaveIndex)) { globalScene.phaseManager.pushNew("EggLapsePhase"); - if (globalScene.gameMode.isClassic) { - switch (globalScene.currentBattle.waveIndex) { + if (gameMode.isClassic) { + switch (currentWaveIndex) { case ClassicFixedBossWaves.RIVAL_1: case ClassicFixedBossWaves.RIVAL_2: // Get event modifiers for this wave timedEventManager - .getFixedBattleEventRewards(globalScene.currentBattle.waveIndex) + .getFixedBattleEventRewards(currentWaveIndex) .map(r => globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes[r])); break; case ClassicFixedBossWaves.EVIL_BOSS_2: @@ -66,59 +66,53 @@ export class VictoryPhase extends PokemonPhase { break; } } - const healStatus = new BooleanHolder(globalScene.currentBattle.waveIndex % 10 === 0); - applyChallenges(ChallengeType.PARTY_HEAL, healStatus); - if (!healStatus.value) { + if (currentWaveIndex % 10) { globalScene.phaseManager.pushNew( "SelectModifierPhase", undefined, undefined, - this.getFixedBattleCustomModifiers(), + gameMode.isFixedBattle(currentWaveIndex) + ? gameMode.getFixedBattle(currentWaveIndex).customModifierRewardSettings + : undefined, ); - } else if (globalScene.gameMode.isDaily) { + } else if (gameMode.isDaily) { globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes.EXP_CHARM); - if ( - globalScene.currentBattle.waveIndex > 10 && - !globalScene.gameMode.isWaveFinal(globalScene.currentBattle.waveIndex) - ) { + if (currentWaveIndex > 10 && !gameMode.isWaveFinal(currentWaveIndex)) { globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes.GOLDEN_POKEBALL); } } else { - const superExpWave = !globalScene.gameMode.isEndless ? (globalScene.offsetGym ? 0 : 20) : 10; - if (globalScene.gameMode.isEndless && globalScene.currentBattle.waveIndex === 10) { + const superExpWave = !gameMode.isEndless ? (globalScene.offsetGym ? 0 : 20) : 10; + if (gameMode.isEndless && currentWaveIndex === 10) { globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes.EXP_SHARE); } - if ( - globalScene.currentBattle.waveIndex <= 750 && - (globalScene.currentBattle.waveIndex <= 500 || globalScene.currentBattle.waveIndex % 30 === superExpWave) - ) { + if (currentWaveIndex <= 750 && (currentWaveIndex <= 500 || currentWaveIndex % 30 === superExpWave)) { globalScene.phaseManager.pushNew( "ModifierRewardPhase", - globalScene.currentBattle.waveIndex % 30 !== superExpWave || globalScene.currentBattle.waveIndex > 250 + currentWaveIndex % 30 !== superExpWave || currentWaveIndex > 250 ? modifierTypes.EXP_CHARM : modifierTypes.SUPER_EXP_CHARM, ); } - if (globalScene.currentBattle.waveIndex <= 150 && !(globalScene.currentBattle.waveIndex % 50)) { + if (currentWaveIndex <= 150 && !(currentWaveIndex % 50)) { globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes.GOLDEN_POKEBALL); } - if (globalScene.gameMode.isEndless && !(globalScene.currentBattle.waveIndex % 50)) { + if (gameMode.isEndless && !(currentWaveIndex % 50)) { globalScene.phaseManager.pushNew( "ModifierRewardPhase", - !(globalScene.currentBattle.waveIndex % 250) ? modifierTypes.VOUCHER_PREMIUM : modifierTypes.VOUCHER_PLUS, + !(currentWaveIndex % 250) ? modifierTypes.VOUCHER_PREMIUM : modifierTypes.VOUCHER_PLUS, ); globalScene.phaseManager.pushNew("AddEnemyBuffModifierPhase"); } } - if (globalScene.gameMode.hasRandomBiomes || globalScene.isNewBiome()) { + if (gameMode.hasRandomBiomes || globalScene.isNewBiome()) { globalScene.phaseManager.pushNew("SelectBiomePhase"); } globalScene.phaseManager.pushNew("NewBattlePhase"); } else { globalScene.currentBattle.battleType = BattleType.CLEAR; - globalScene.score += globalScene.gameMode.getClearScoreBonus(); + globalScene.score += gameMode.getClearScoreBonus(); globalScene.updateScoreText(); globalScene.phaseManager.pushNew("GameOverPhase", true); } @@ -126,18 +120,4 @@ export class VictoryPhase extends PokemonPhase { this.end(); } - - /** - * If this wave is a fixed battle with special custom modifier rewards, - * will pass those settings to the upcoming {@linkcode SelectModifierPhase}`. - */ - getFixedBattleCustomModifiers(): CustomModifierSettings | undefined { - const gameMode = globalScene.gameMode; - const waveIndex = globalScene.currentBattle.waveIndex; - if (gameMode.isFixedBattle(waveIndex)) { - return gameMode.getFixedBattle(waveIndex).customModifierRewardSettings; - } - - return undefined; - } } diff --git a/src/plugins/api/pokerogue-session-savedata-api.ts b/src/plugins/api/pokerogue-session-savedata-api.ts index 4ffb0a5d8da..39fa292f9f1 100644 --- a/src/plugins/api/pokerogue-session-savedata-api.ts +++ b/src/plugins/api/pokerogue-session-savedata-api.ts @@ -56,15 +56,15 @@ export class PokerogueSessionSavedataApi extends ApiBase { /** * Update a session savedata. - * @param params The {@linkcode UpdateSessionSavedataRequest} to send - * @param rawSavedata The raw savedata (as `string`) + * @param params - The request to send + * @param rawSavedata - The raw, unencrypted savedata * @returns An error message if something went wrong */ - public async update(params: UpdateSessionSavedataRequest, rawSavedata: string) { + public async update(params: UpdateSessionSavedataRequest, rawSavedata: string): Promise { try { const urlSearchParams = this.toUrlSearchParams(params); - const response = await this.doPost(`/savedata/session/update?${urlSearchParams}`, rawSavedata); + const response = await this.doPost(`/savedata/session/update?${urlSearchParams}`, rawSavedata); return await response.text(); } catch (err) { console.warn("Could not update session savedata!", err); diff --git a/src/system/achv.ts b/src/system/achv.ts index 383d07252e6..f51e5ea459c 100644 --- a/src/system/achv.ts +++ b/src/system/achv.ts @@ -5,7 +5,6 @@ import { FlipStatChallenge, FreshStartChallenge, InverseBattleChallenge, - LimitedCatchChallenge, SingleGenerationChallenge, SingleTypeChallenge, } from "#data/challenge"; @@ -14,7 +13,9 @@ import { PlayerGender } from "#enums/player-gender"; import { getShortenedStatKey, Stat } from "#enums/stat"; import { TurnHeldItemTransferModifier } from "#modifiers/modifier"; import type { ConditionFn } from "#types/common"; +import { isNuzlockeChallenge } from "#utils/challenge-utils"; import { NumberHolder } from "#utils/common"; +import { toCamelCase } from "#utils/strings"; import i18next from "i18next"; import type { Modifier } from "typescript"; @@ -214,242 +215,244 @@ export function getAchievementDescription(localizationKey: string): string { const genderStr = PlayerGender[genderIndex].toLowerCase(); switch (localizationKey) { - case "10K_MONEY": - return i18next.t("achv:MoneyAchv.description", { + case "10KMoney": + return i18next.t("achv:moneyAchv.description", { context: genderStr, moneyAmount: achvs._10K_MONEY.moneyAmount.toLocaleString("en-US"), }); - case "100K_MONEY": - return i18next.t("achv:MoneyAchv.description", { + case "100KMoney": + return i18next.t("achv:moneyAchv.description", { context: genderStr, moneyAmount: achvs._100K_MONEY.moneyAmount.toLocaleString("en-US"), }); - case "1M_MONEY": - return i18next.t("achv:MoneyAchv.description", { + case "1MMoney": + return i18next.t("achv:moneyAchv.description", { context: genderStr, moneyAmount: achvs._1M_MONEY.moneyAmount.toLocaleString("en-US"), }); - case "10M_MONEY": - return i18next.t("achv:MoneyAchv.description", { + case "10MMoney": + return i18next.t("achv:moneyAchv.description", { context: genderStr, moneyAmount: achvs._10M_MONEY.moneyAmount.toLocaleString("en-US"), }); - case "250_DMG": - return i18next.t("achv:DamageAchv.description", { + case "250Dmg": + return i18next.t("achv:damageAchv.description", { context: genderStr, damageAmount: achvs._250_DMG.damageAmount.toLocaleString("en-US"), }); - case "1000_DMG": - return i18next.t("achv:DamageAchv.description", { + case "1000Dmg": + return i18next.t("achv:damageAchv.description", { context: genderStr, damageAmount: achvs._1000_DMG.damageAmount.toLocaleString("en-US"), }); - case "2500_DMG": - return i18next.t("achv:DamageAchv.description", { + case "2500Dmg": + return i18next.t("achv:damageAchv.description", { context: genderStr, damageAmount: achvs._2500_DMG.damageAmount.toLocaleString("en-US"), }); - case "10000_DMG": - return i18next.t("achv:DamageAchv.description", { + case "10000Dmg": + return i18next.t("achv:damageAchv.description", { context: genderStr, damageAmount: achvs._10000_DMG.damageAmount.toLocaleString("en-US"), }); - case "250_HEAL": - return i18next.t("achv:HealAchv.description", { + case "250Heal": + return i18next.t("achv:healAchv.description", { context: genderStr, healAmount: achvs._250_HEAL.healAmount.toLocaleString("en-US"), HP: i18next.t(getShortenedStatKey(Stat.HP)), }); - case "1000_HEAL": - return i18next.t("achv:HealAchv.description", { + case "1000Heal": + return i18next.t("achv:healAchv.description", { context: genderStr, healAmount: achvs._1000_HEAL.healAmount.toLocaleString("en-US"), HP: i18next.t(getShortenedStatKey(Stat.HP)), }); - case "2500_HEAL": - return i18next.t("achv:HealAchv.description", { + case "2500Heal": + return i18next.t("achv:healAchv.description", { context: genderStr, healAmount: achvs._2500_HEAL.healAmount.toLocaleString("en-US"), HP: i18next.t(getShortenedStatKey(Stat.HP)), }); - case "10000_HEAL": - return i18next.t("achv:HealAchv.description", { + case "10000Heal": + return i18next.t("achv:healAchv.description", { context: genderStr, healAmount: achvs._10000_HEAL.healAmount.toLocaleString("en-US"), HP: i18next.t(getShortenedStatKey(Stat.HP)), }); - case "LV_100": - return i18next.t("achv:LevelAchv.description", { + case "lv100": + return i18next.t("achv:levelAchv.description", { context: genderStr, level: achvs.LV_100.level, }); - case "LV_250": - return i18next.t("achv:LevelAchv.description", { + case "lv250": + return i18next.t("achv:levelAchv.description", { context: genderStr, level: achvs.LV_250.level, }); - case "LV_1000": - return i18next.t("achv:LevelAchv.description", { + case "lv1000": + return i18next.t("achv:levelAchv.description", { context: genderStr, level: achvs.LV_1000.level, }); - case "10_RIBBONS": - return i18next.t("achv:RibbonAchv.description", { + case "10Ribbons": + return i18next.t("achv:ribbonAchv.description", { context: genderStr, ribbonAmount: achvs._10_RIBBONS.ribbonAmount.toLocaleString("en-US"), }); - case "25_RIBBONS": - return i18next.t("achv:RibbonAchv.description", { + case "25Ribbons": + return i18next.t("achv:ribbonAchv.description", { context: genderStr, ribbonAmount: achvs._25_RIBBONS.ribbonAmount.toLocaleString("en-US"), }); - case "50_RIBBONS": - return i18next.t("achv:RibbonAchv.description", { + case "50Ribbons": + return i18next.t("achv:ribbonAchv.description", { context: genderStr, ribbonAmount: achvs._50_RIBBONS.ribbonAmount.toLocaleString("en-US"), }); - case "75_RIBBONS": - return i18next.t("achv:RibbonAchv.description", { + case "75Ribbons": + return i18next.t("achv:ribbonAchv.description", { context: genderStr, ribbonAmount: achvs._75_RIBBONS.ribbonAmount.toLocaleString("en-US"), }); - case "100_RIBBONS": - return i18next.t("achv:RibbonAchv.description", { + case "100Ribbons": + return i18next.t("achv:ribbonAchv.description", { context: genderStr, ribbonAmount: achvs._100_RIBBONS.ribbonAmount.toLocaleString("en-US"), }); - case "TRANSFER_MAX_STAT_STAGE": - return i18next.t("achv:TRANSFER_MAX_STAT_STAGE.description", { + case "transferMaxStatStage": + return i18next.t("achv:transferMaxStatStage.description", { context: genderStr, }); - case "MAX_FRIENDSHIP": - return i18next.t("achv:MAX_FRIENDSHIP.description", { + case "maxFriendship": + return i18next.t("achv:maxFriendship.description", { context: genderStr, }); - case "MEGA_EVOLVE": - return i18next.t("achv:MEGA_EVOLVE.description", { context: genderStr }); - case "GIGANTAMAX": - return i18next.t("achv:GIGANTAMAX.description", { context: genderStr }); - case "TERASTALLIZE": - return i18next.t("achv:TERASTALLIZE.description", { context: genderStr }); - case "STELLAR_TERASTALLIZE": - return i18next.t("achv:STELLAR_TERASTALLIZE.description", { + case "megaEvolve": + return i18next.t("achv:megaEvolve.description", { context: genderStr }); + case "gigantamax": + return i18next.t("achv:gigantamax.description", { context: genderStr }); + case "terastallize": + return i18next.t("achv:terastallize.description", { context: genderStr }); + case "stellarTerastallize": + return i18next.t("achv:stellarTerastallize.description", { context: genderStr, }); - case "SPLICE": - return i18next.t("achv:SPLICE.description", { context: genderStr }); - case "MINI_BLACK_HOLE": - return i18next.t("achv:MINI_BLACK_HOLE.description", { + case "splice": + return i18next.t("achv:splice.description", { context: genderStr }); + case "miniBlackHole": + return i18next.t("achv:miniBlackHole.description", { context: genderStr, }); - case "CATCH_MYTHICAL": - return i18next.t("achv:CATCH_MYTHICAL.description", { + case "catchMythical": + return i18next.t("achv:catchMythical.description", { context: genderStr, }); - case "CATCH_SUB_LEGENDARY": - return i18next.t("achv:CATCH_SUB_LEGENDARY.description", { + case "catchSubLegendary": + return i18next.t("achv:catchSubLegendary.description", { context: genderStr, }); - case "CATCH_LEGENDARY": - return i18next.t("achv:CATCH_LEGENDARY.description", { + case "catchLegendary": + return i18next.t("achv:catchLegendary.description", { context: genderStr, }); - case "SEE_SHINY": - return i18next.t("achv:SEE_SHINY.description", { context: genderStr }); - case "SHINY_PARTY": - return i18next.t("achv:SHINY_PARTY.description", { context: genderStr }); - case "HATCH_MYTHICAL": - return i18next.t("achv:HATCH_MYTHICAL.description", { + case "seeShiny": + return i18next.t("achv:seeShiny.description", { context: genderStr }); + case "shinyParty": + return i18next.t("achv:shinyParty.description", { context: genderStr }); + case "hatchMythical": + return i18next.t("achv:hatchMythical.description", { context: genderStr, }); - case "HATCH_SUB_LEGENDARY": - return i18next.t("achv:HATCH_SUB_LEGENDARY.description", { + case "hatchSubLegendary": + return i18next.t("achv:hatchSubLegendary.description", { context: genderStr, }); - case "HATCH_LEGENDARY": - return i18next.t("achv:HATCH_LEGENDARY.description", { + case "hatchLegendary": + return i18next.t("achv:hatchLegendary.description", { context: genderStr, }); - case "HATCH_SHINY": - return i18next.t("achv:HATCH_SHINY.description", { context: genderStr }); - case "HIDDEN_ABILITY": - return i18next.t("achv:HIDDEN_ABILITY.description", { + case "hatchShiny": + return i18next.t("achv:hatchShiny.description", { context: genderStr }); + case "hiddenAbility": + return i18next.t("achv:hiddenAbility.description", { context: genderStr, }); - case "PERFECT_IVS": - return i18next.t("achv:PERFECT_IVS.description", { context: genderStr }); - case "CLASSIC_VICTORY": - return i18next.t("achv:CLASSIC_VICTORY.description", { + case "perfectIvs": + return i18next.t("achv:perfectIvs.description", { context: genderStr }); + case "classicVictory": + return i18next.t("achv:classicVictory.description", { context: genderStr, }); - case "UNEVOLVED_CLASSIC_VICTORY": - return i18next.t("achv:UNEVOLVED_CLASSIC_VICTORY.description", { + case "unevolvedClassicVictory": + return i18next.t("achv:unevolvedClassicVictory.description", { context: genderStr, }); - case "MONO_GEN_ONE": - return i18next.t("achv:MONO_GEN_ONE.description", { context: genderStr }); - case "MONO_GEN_TWO": - return i18next.t("achv:MONO_GEN_TWO.description", { context: genderStr }); - case "MONO_GEN_THREE": - return i18next.t("achv:MONO_GEN_THREE.description", { + case "monoGenOne": + return i18next.t("achv:monoGenOne.description", { context: genderStr }); + case "monoGenTwo": + return i18next.t("achv:monoGenTwo.description", { context: genderStr }); + case "monoGenThree": + return i18next.t("achv:monoGenThree.description", { context: genderStr, }); - case "MONO_GEN_FOUR": - return i18next.t("achv:MONO_GEN_FOUR.description", { + case "monoGenFour": + return i18next.t("achv:monoGenFour.description", { context: genderStr, }); - case "MONO_GEN_FIVE": - return i18next.t("achv:MONO_GEN_FIVE.description", { + case "monoGenFive": + return i18next.t("achv:monoGenFive.description", { context: genderStr, }); - case "MONO_GEN_SIX": - return i18next.t("achv:MONO_GEN_SIX.description", { context: genderStr }); - case "MONO_GEN_SEVEN": - return i18next.t("achv:MONO_GEN_SEVEN.description", { + case "monoGenSix": + return i18next.t("achv:monoGenSix.description", { context: genderStr }); + case "monoGenSeven": + return i18next.t("achv:monoGenSeven.description", { context: genderStr, }); - case "MONO_GEN_EIGHT": - return i18next.t("achv:MONO_GEN_EIGHT.description", { + case "monoGenEight": + return i18next.t("achv:monoGenEight.description", { context: genderStr, }); - case "MONO_GEN_NINE": - return i18next.t("achv:MONO_GEN_NINE.description", { + case "monoGenNine": + return i18next.t("achv:monoGenNine.description", { context: genderStr, }); - case "MONO_NORMAL": - case "MONO_FIGHTING": - case "MONO_FLYING": - case "MONO_POISON": - case "MONO_GROUND": - case "MONO_ROCK": - case "MONO_BUG": - case "MONO_GHOST": - case "MONO_STEEL": - case "MONO_FIRE": - case "MONO_WATER": - case "MONO_GRASS": - case "MONO_ELECTRIC": - case "MONO_PSYCHIC": - case "MONO_ICE": - case "MONO_DRAGON": - case "MONO_DARK": - case "MONO_FAIRY": - return i18next.t("achv:MonoType.description", { + case "monoNormal": + case "monoFighting": + case "monoFlying": + case "monoPoison": + case "monoGround": + case "monoRock": + case "monoBug": + case "monoGhost": + case "monoSteel": + case "monoFire": + case "monoWater": + case "monoGrass": + case "monoElectric": + case "monoPsychic": + case "monoIce": + case "monoDragon": + case "monoDark": + case "monoFairy": + return i18next.t("achv:monoType.description", { context: genderStr, - type: i18next.t(`pokemonInfo:Type.${localizationKey.slice(5)}`), + type: i18next.t(`pokemonInfo:type.${toCamelCase(localizationKey.slice(4))}`), }); - case "FRESH_START": - return i18next.t("achv:FRESH_START.description", { context: genderStr }); - case "INVERSE_BATTLE": - return i18next.t("achv:INVERSE_BATTLE.description", { + case "freshStart": + return i18next.t("achv:freshStart.description", { context: genderStr }); + case "inverseBattle": + return i18next.t("achv:inverseBattle.description", { context: genderStr, }); - case "FLIP_STATS": - return i18next.t("achv:FLIP_STATS.description", { context: genderStr }); - case "FLIP_INVERSE": - return i18next.t("achv:FLIP_INVERSE.description", { context: genderStr }); - case "BREEDERS_IN_SPACE": - return i18next.t("achv:BREEDERS_IN_SPACE.description", { + case "flipStats": + return i18next.t("achv:flipStats.description", { context: genderStr }); + case "flipInverse": + return i18next.t("achv:flipInverse.description", { context: genderStr }); + case "nuzlocke": + return i18next.t("achv:nuzlocke.description", { context: genderStr }); + case "breedersInSpace": + return i18next.t("achv:breedersInSpace.description", { context: genderStr, }); default: @@ -458,84 +461,84 @@ export function getAchievementDescription(localizationKey: string): string { } export const achvs = { - _10K_MONEY: new MoneyAchv("10K_MONEY", "", 10000, "nugget", 10), - _100K_MONEY: new MoneyAchv("100K_MONEY", "", 100000, "big_nugget", 25).setSecret(true), - _1M_MONEY: new MoneyAchv("1M_MONEY", "", 1000000, "relic_gold", 50).setSecret(true), - _10M_MONEY: new MoneyAchv("10M_MONEY", "", 10000000, "coin_case", 100).setSecret(true), - _250_DMG: new DamageAchv("250_DMG", "", 250, "lucky_punch", 10), - _1000_DMG: new DamageAchv("1000_DMG", "", 1000, "lucky_punch_great", 25).setSecret(true), - _2500_DMG: new DamageAchv("2500_DMG", "", 2500, "lucky_punch_ultra", 50).setSecret(true), - _10000_DMG: new DamageAchv("10000_DMG", "", 10000, "lucky_punch_master", 100).setSecret(true), - _250_HEAL: new HealAchv("250_HEAL", "", 250, "potion", 10), - _1000_HEAL: new HealAchv("1000_HEAL", "", 1000, "super_potion", 25).setSecret(true), - _2500_HEAL: new HealAchv("2500_HEAL", "", 2500, "hyper_potion", 50).setSecret(true), - _10000_HEAL: new HealAchv("10000_HEAL", "", 10000, "max_potion", 100).setSecret(true), - LV_100: new LevelAchv("LV_100", "", 100, "rare_candy", 25).setSecret(), - LV_250: new LevelAchv("LV_250", "", 250, "rarer_candy", 50).setSecret(true), - LV_1000: new LevelAchv("LV_1000", "", 1000, "candy_jar", 100).setSecret(true), - _10_RIBBONS: new RibbonAchv("10_RIBBONS", "", 10, "bronze_ribbon", 10), - _25_RIBBONS: new RibbonAchv("25_RIBBONS", "", 25, "great_ribbon", 25).setSecret(true), - _50_RIBBONS: new RibbonAchv("50_RIBBONS", "", 50, "ultra_ribbon", 50).setSecret(true), - _75_RIBBONS: new RibbonAchv("75_RIBBONS", "", 75, "rogue_ribbon", 75).setSecret(true), - _100_RIBBONS: new RibbonAchv("100_RIBBONS", "", 100, "master_ribbon", 100).setSecret(true), - TRANSFER_MAX_STAT_STAGE: new Achv("TRANSFER_MAX_STAT_STAGE", "", "TRANSFER_MAX_STAT_STAGE.description", "baton", 20), - MAX_FRIENDSHIP: new Achv("MAX_FRIENDSHIP", "", "MAX_FRIENDSHIP.description", "soothe_bell", 25), - MEGA_EVOLVE: new Achv("MEGA_EVOLVE", "", "MEGA_EVOLVE.description", "mega_bracelet", 50), - GIGANTAMAX: new Achv("GIGANTAMAX", "", "GIGANTAMAX.description", "dynamax_band", 50), - TERASTALLIZE: new Achv("TERASTALLIZE", "", "TERASTALLIZE.description", "tera_orb", 25), + _10K_MONEY: new MoneyAchv("10KMoney", "", 10000, "nugget", 10), + _100K_MONEY: new MoneyAchv("100KMoney", "", 100000, "big_nugget", 25).setSecret(true), + _1M_MONEY: new MoneyAchv("1MMoney", "", 1000000, "relic_gold", 50).setSecret(true), + _10M_MONEY: new MoneyAchv("10MMoney", "", 10000000, "coin_case", 100).setSecret(true), + _250_DMG: new DamageAchv("250Dmg", "", 250, "lucky_punch", 10), + _1000_DMG: new DamageAchv("1000Dmg", "", 1000, "lucky_punch_great", 25).setSecret(true), + _2500_DMG: new DamageAchv("2500Dmg", "", 2500, "lucky_punch_ultra", 50).setSecret(true), + _10000_DMG: new DamageAchv("10000Dmg", "", 10000, "lucky_punch_master", 100).setSecret(true), + _250_HEAL: new HealAchv("250Heal", "", 250, "potion", 10), + _1000_HEAL: new HealAchv("1000Heal", "", 1000, "super_potion", 25).setSecret(true), + _2500_HEAL: new HealAchv("2500Heal", "", 2500, "hyper_potion", 50).setSecret(true), + _10000_HEAL: new HealAchv("10000Heal", "", 10000, "max_potion", 100).setSecret(true), + LV_100: new LevelAchv("lv100", "", 100, "rare_candy", 25).setSecret(), + LV_250: new LevelAchv("lv250", "", 250, "rarer_candy", 50).setSecret(true), + LV_1000: new LevelAchv("lv1000", "", 1000, "candy_jar", 100).setSecret(true), + _10_RIBBONS: new RibbonAchv("10Ribbons", "", 10, "bronze_ribbon", 10), + _25_RIBBONS: new RibbonAchv("25Ribbons", "", 25, "great_ribbon", 25).setSecret(true), + _50_RIBBONS: new RibbonAchv("50Ribbons", "", 50, "ultra_ribbon", 50).setSecret(true), + _75_RIBBONS: new RibbonAchv("75Ribbons", "", 75, "rogue_ribbon", 75).setSecret(true), + _100_RIBBONS: new RibbonAchv("100Ribbons", "", 100, "master_ribbon", 100).setSecret(true), + TRANSFER_MAX_STAT_STAGE: new Achv("transferMaxStatStage", "", "transferMaxStatStage.description", "baton", 20), + MAX_FRIENDSHIP: new Achv("maxFriendship", "", "maxFriendship.description", "soothe_bell", 25), + MEGA_EVOLVE: new Achv("megaEvolve", "", "megaEvolve.description", "mega_bracelet", 50), + GIGANTAMAX: new Achv("gigantamax", "", "gigantamax.description", "dynamax_band", 50), + TERASTALLIZE: new Achv("terastallize", "", "terastallize.description", "tera_orb", 25), STELLAR_TERASTALLIZE: new Achv( - "STELLAR_TERASTALLIZE", + "stellarTerastallize", "", - "STELLAR_TERASTALLIZE.description", + "stellarTerastallize.description", "stellar_tera_shard", 25, ).setSecret(true), - SPLICE: new Achv("SPLICE", "", "SPLICE.description", "dna_splicers", 10), + SPLICE: new Achv("splice", "", "splice.description", "dna_splicers", 10), MINI_BLACK_HOLE: new ModifierAchv( - "MINI_BLACK_HOLE", + "miniBlackHole", "", - "MINI_BLACK_HOLE.description", + "miniBlackHole.description", "mini_black_hole", 25, modifier => modifier instanceof TurnHeldItemTransferModifier, ).setSecret(), - CATCH_MYTHICAL: new Achv("CATCH_MYTHICAL", "", "CATCH_MYTHICAL.description", "strange_ball", 50).setSecret(), - CATCH_SUB_LEGENDARY: new Achv("CATCH_SUB_LEGENDARY", "", "CATCH_SUB_LEGENDARY.description", "rb", 75).setSecret(), - CATCH_LEGENDARY: new Achv("CATCH_LEGENDARY", "", "CATCH_LEGENDARY.description", "mb", 100).setSecret(), - SEE_SHINY: new Achv("SEE_SHINY", "", "SEE_SHINY.description", "pb_gold", 75), - SHINY_PARTY: new Achv("SHINY_PARTY", "", "SHINY_PARTY.description", "shiny_charm", 100).setSecret(true), - HATCH_MYTHICAL: new Achv("HATCH_MYTHICAL", "", "HATCH_MYTHICAL.description", "mystery_egg", 75).setSecret(), + CATCH_MYTHICAL: new Achv("catchMythical", "", "catchMythical.description", "strange_ball", 50).setSecret(), + CATCH_SUB_LEGENDARY: new Achv("catchSubLegendary", "", "catchSubLegendary.description", "rb", 75).setSecret(), + CATCH_LEGENDARY: new Achv("catchLegendary", "", "catchLegendary.description", "mb", 100).setSecret(), + SEE_SHINY: new Achv("seeShiny", "", "seeShiny.description", "pb_gold", 75), + SHINY_PARTY: new Achv("shinyParty", "", "shinyParty.description", "shiny_charm", 100).setSecret(true), + HATCH_MYTHICAL: new Achv("hatchMythical", "", "hatchMythical.description", "mystery_egg", 75).setSecret(), HATCH_SUB_LEGENDARY: new Achv( - "HATCH_SUB_LEGENDARY", + "hatchSubLegendary", "", - "HATCH_SUB_LEGENDARY.description", + "hatchSubLegendary.description", "oval_stone", 100, ).setSecret(), - HATCH_LEGENDARY: new Achv("HATCH_LEGENDARY", "", "HATCH_LEGENDARY.description", "lucky_egg", 125).setSecret(), - HATCH_SHINY: new Achv("HATCH_SHINY", "", "HATCH_SHINY.description", "golden_egg", 100).setSecret(), - HIDDEN_ABILITY: new Achv("HIDDEN_ABILITY", "", "HIDDEN_ABILITY.description", "ability_charm", 75), - PERFECT_IVS: new Achv("PERFECT_IVS", "", "PERFECT_IVS.description", "blunder_policy", 100), + HATCH_LEGENDARY: new Achv("hatchLegendary", "", "hatchLegendary.description", "lucky_egg", 125).setSecret(), + HATCH_SHINY: new Achv("hatchShiny", "", "hatchShiny.description", "golden_egg", 100).setSecret(), + HIDDEN_ABILITY: new Achv("hiddenAbility", "", "hiddenAbility.description", "ability_charm", 75), + PERFECT_IVS: new Achv("perfectIvs", "", "perfectIvs.description", "blunder_policy", 100), CLASSIC_VICTORY: new Achv( - "CLASSIC_VICTORY", + "classicVictory", "", - "CLASSIC_VICTORY.description", + "classicVictory.description", "relic_crown", 150, _ => globalScene.gameData.gameStats.sessionsWon === 0, ), UNEVOLVED_CLASSIC_VICTORY: new Achv( - "UNEVOLVED_CLASSIC_VICTORY", + "unevolvedClassicVictory", "", - "UNEVOLVED_CLASSIC_VICTORY.description", + "unevolvedClassicVictory.description", "eviolite", 175, _ => globalScene.getPlayerParty().some(p => p.getSpeciesForm(true).speciesId in pokemonEvolutions), ), MONO_GEN_ONE_VICTORY: new ChallengeAchv( - "MONO_GEN_ONE", + "monoGenOne", "", - "MONO_GEN_ONE.description", + "monoGenOne.description", "ribbon_gen1", 100, c => @@ -546,9 +549,9 @@ export const achvs = { ), ), MONO_GEN_TWO_VICTORY: new ChallengeAchv( - "MONO_GEN_TWO", + "monoGenTwo", "", - "MONO_GEN_TWO.description", + "monoGenTwo.description", "ribbon_gen2", 100, c => @@ -559,9 +562,9 @@ export const achvs = { ), ), MONO_GEN_THREE_VICTORY: new ChallengeAchv( - "MONO_GEN_THREE", + "monoGenThree", "", - "MONO_GEN_THREE.description", + "monoGenThree.description", "ribbon_gen3", 100, c => @@ -572,9 +575,9 @@ export const achvs = { ), ), MONO_GEN_FOUR_VICTORY: new ChallengeAchv( - "MONO_GEN_FOUR", + "monoGenFour", "", - "MONO_GEN_FOUR.description", + "monoGenFour.description", "ribbon_gen4", 100, c => @@ -585,9 +588,9 @@ export const achvs = { ), ), MONO_GEN_FIVE_VICTORY: new ChallengeAchv( - "MONO_GEN_FIVE", + "monoGenFive", "", - "MONO_GEN_FIVE.description", + "monoGenFive.description", "ribbon_gen5", 100, c => @@ -598,9 +601,9 @@ export const achvs = { ), ), MONO_GEN_SIX_VICTORY: new ChallengeAchv( - "MONO_GEN_SIX", + "monoGenSix", "", - "MONO_GEN_SIX.description", + "monoGenSix.description", "ribbon_gen6", 100, c => @@ -611,9 +614,9 @@ export const achvs = { ), ), MONO_GEN_SEVEN_VICTORY: new ChallengeAchv( - "MONO_GEN_SEVEN", + "monoGenSeven", "", - "MONO_GEN_SEVEN.description", + "monoGenSeven.description", "ribbon_gen7", 100, c => @@ -624,9 +627,9 @@ export const achvs = { ), ), MONO_GEN_EIGHT_VICTORY: new ChallengeAchv( - "MONO_GEN_EIGHT", + "monoGenEight", "", - "MONO_GEN_EIGHT.description", + "monoGenEight.description", "ribbon_gen8", 100, c => @@ -637,9 +640,9 @@ export const achvs = { ), ), MONO_GEN_NINE_VICTORY: new ChallengeAchv( - "MONO_GEN_NINE", + "monoGenNine", "", - "MONO_GEN_NINE.description", + "monoGenNine.description", "ribbon_gen9", 100, c => @@ -650,9 +653,9 @@ export const achvs = { ), ), MONO_NORMAL: new ChallengeAchv( - "MONO_NORMAL", + "monoNormal", "", - "MONO_NORMAL.description", + "monoNormal.description", "silk_scarf", 100, c => @@ -663,9 +666,9 @@ export const achvs = { ), ), MONO_FIGHTING: new ChallengeAchv( - "MONO_FIGHTING", + "monoFighting", "", - "MONO_FIGHTING.description", + "monoFighting.description", "black_belt", 100, c => @@ -676,9 +679,9 @@ export const achvs = { ), ), MONO_FLYING: new ChallengeAchv( - "MONO_FLYING", + "monoFlying", "", - "MONO_FLYING.description", + "monoFlying.description", "sharp_beak", 100, c => @@ -689,9 +692,9 @@ export const achvs = { ), ), MONO_POISON: new ChallengeAchv( - "MONO_POISON", + "monoPoison", "", - "MONO_POISON.description", + "monoPoison.description", "poison_barb", 100, c => @@ -702,9 +705,9 @@ export const achvs = { ), ), MONO_GROUND: new ChallengeAchv( - "MONO_GROUND", + "monoGround", "", - "MONO_GROUND.description", + "monoGround.description", "soft_sand", 100, c => @@ -715,9 +718,9 @@ export const achvs = { ), ), MONO_ROCK: new ChallengeAchv( - "MONO_ROCK", + "monoRock", "", - "MONO_ROCK.description", + "monoRock.description", "hard_stone", 100, c => @@ -728,9 +731,9 @@ export const achvs = { ), ), MONO_BUG: new ChallengeAchv( - "MONO_BUG", + "monoBug", "", - "MONO_BUG.description", + "monoBug.description", "silver_powder", 100, c => @@ -741,9 +744,9 @@ export const achvs = { ), ), MONO_GHOST: new ChallengeAchv( - "MONO_GHOST", + "monoGhost", "", - "MONO_GHOST.description", + "monoGhost.description", "spell_tag", 100, c => @@ -754,9 +757,9 @@ export const achvs = { ), ), MONO_STEEL: new ChallengeAchv( - "MONO_STEEL", + "monoSteel", "", - "MONO_STEEL.description", + "monoSteel.description", "metal_coat", 100, c => @@ -767,9 +770,9 @@ export const achvs = { ), ), MONO_FIRE: new ChallengeAchv( - "MONO_FIRE", + "monoFire", "", - "MONO_FIRE.description", + "monoFire.description", "charcoal", 100, c => @@ -780,9 +783,9 @@ export const achvs = { ), ), MONO_WATER: new ChallengeAchv( - "MONO_WATER", + "monoWater", "", - "MONO_WATER.description", + "monoWater.description", "mystic_water", 100, c => @@ -793,9 +796,9 @@ export const achvs = { ), ), MONO_GRASS: new ChallengeAchv( - "MONO_GRASS", + "monoGrass", "", - "MONO_GRASS.description", + "monoGrass.description", "miracle_seed", 100, c => @@ -806,9 +809,9 @@ export const achvs = { ), ), MONO_ELECTRIC: new ChallengeAchv( - "MONO_ELECTRIC", + "monoElectric", "", - "MONO_ELECTRIC.description", + "monoElectric.description", "magnet", 100, c => @@ -819,9 +822,9 @@ export const achvs = { ), ), MONO_PSYCHIC: new ChallengeAchv( - "MONO_PSYCHIC", + "monoPsychic", "", - "MONO_PSYCHIC.description", + "monoPsychic.description", "twisted_spoon", 100, c => @@ -832,9 +835,9 @@ export const achvs = { ), ), MONO_ICE: new ChallengeAchv( - "MONO_ICE", + "monoIce", "", - "MONO_ICE.description", + "monoIce.description", "never_melt_ice", 100, c => @@ -845,9 +848,9 @@ export const achvs = { ), ), MONO_DRAGON: new ChallengeAchv( - "MONO_DRAGON", + "monoDragon", "", - "MONO_DRAGON.description", + "monoDragon.description", "dragon_fang", 100, c => @@ -858,9 +861,9 @@ export const achvs = { ), ), MONO_DARK: new ChallengeAchv( - "MONO_DARK", + "monoDark", "", - "MONO_DARK.description", + "monoDark.description", "black_glasses", 100, c => @@ -871,9 +874,9 @@ export const achvs = { ), ), MONO_FAIRY: new ChallengeAchv( - "MONO_FAIRY", + "monoFairy", "", - "MONO_FAIRY.description", + "monoFairy.description", "fairy_feather", 100, c => @@ -884,9 +887,9 @@ export const achvs = { ), ), FRESH_START: new ChallengeAchv( - "FRESH_START", + "freshStart", "", - "FRESH_START.description", + "freshStart.description", "reviver_seed", 100, c => @@ -897,25 +900,25 @@ export const achvs = { ), ), INVERSE_BATTLE: new ChallengeAchv( - "INVERSE_BATTLE", + "inverseBattle", "", - "INVERSE_BATTLE.description", + "inverseBattle.description", "inverse", 100, c => c instanceof InverseBattleChallenge && c.value > 0, ), FLIP_STATS: new ChallengeAchv( - "FLIP_STATS", + "flipStats", "", - "FLIP_STATS.description", + "flipStats.description", "dubious_disc", 100, c => c instanceof FlipStatChallenge && c.value > 0, ), FLIP_INVERSE: new ChallengeAchv( - "FLIP_INVERSE", + "flipInverse", "", - "FLIP_INVERSE.description", + "flipInverse.description", "cracked_pot", 100, c => @@ -924,19 +927,8 @@ export const achvs = { globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0), ).setSecret(), // TODO: Decide on icon - NUZLOCKE: new ChallengeAchv( - "NUZLOCKE", - "", - "NUZLOCKE.description", - "leaf_stone", - 100, - c => - c instanceof LimitedCatchChallenge && - c.value > 0 && - globalScene.gameMode.challenges.some(c => c.id === Challenges.HARDCORE && c.value > 0) && - globalScene.gameMode.challenges.some(c => c.id === Challenges.FRESH_START && c.value > 0), - ), - BREEDERS_IN_SPACE: new Achv("BREEDERS_IN_SPACE", "", "BREEDERS_IN_SPACE.description", "moon_stone", 50).setSecret(), + NUZLOCKE: new ChallengeAchv("nuzlocke", "", "nuzlocke.description", "leaf_stone", 100, isNuzlockeChallenge), + BREEDERS_IN_SPACE: new Achv("breedersInSpace", "", "breedersInSpace.description", "moon_stone", 50).setSecret(), }; export function initAchievements() { diff --git a/src/system/game-data.ts b/src/system/game-data.ts index ae559072e35..14224751262 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -10,7 +10,7 @@ import { Tutorial } from "#app/tutorial"; import { speciesEggMoves } from "#balance/egg-moves"; import { pokemonPrevolutions } from "#balance/pokemon-evolutions"; import { speciesStarterCosts } from "#balance/starters"; -import { ArenaTrapTag } from "#data/arena-tag"; +import { EntryHazardTag } from "#data/arena-tag"; import { allMoves, allSpecies } from "#data/data-lists"; import type { Egg } from "#data/egg"; import { pokemonFormChanges } from "#data/pokemon-forms"; @@ -48,6 +48,7 @@ import { EggData } from "#system/egg-data"; import { GameStats } from "#system/game-stats"; import { ModifierData as PersistentModifierData } from "#system/modifier-data"; import { PokemonData } from "#system/pokemon-data"; +import { RibbonData } from "#system/ribbons/ribbon-data"; import { resetSettings, SettingKeys, setSetting } from "#system/settings"; import { SettingGamepad, setSettingGamepad, settingGamepadDefaults } from "#system/settings-gamepad"; import type { SettingKeyboard } from "#system/settings-keyboard"; @@ -127,6 +128,8 @@ export interface SessionSaveData { battleType: BattleType; trainer: TrainerData; gameVersion: string; + /** The player-chosen name of the run */ + name: string; timestamp: number; challenges: ChallengeData[]; mysteryEncounterType: MysteryEncounterType | -1; // Only defined when current wave is ME, @@ -206,10 +209,12 @@ export interface StarterData { [key: number]: StarterDataEntry; } -export interface TutorialFlags { - [key: string]: boolean; -} +// TODO: Rework into a bitmask +export type TutorialFlags = { + [key in Tutorial]: boolean; +}; +// TODO: Rework into a bitmask export interface SeenDialogues { [key: string]: boolean; } @@ -399,121 +404,121 @@ export class GameData { } public initSystem(systemDataStr: string, cachedSystemDataStr?: string): Promise { - return new Promise(resolve => { - try { - let systemData = this.parseSystemData(systemDataStr); + const { promise, resolve } = Promise.withResolvers(); + try { + let systemData = this.parseSystemData(systemDataStr); - if (cachedSystemDataStr) { - const cachedSystemData = this.parseSystemData(cachedSystemDataStr); - if (cachedSystemData.timestamp > systemData.timestamp) { - console.debug("Use cached system"); - systemData = cachedSystemData; - systemDataStr = cachedSystemDataStr; - } else { - this.clearLocalData(); - } - } - - console.debug(systemData); - - localStorage.setItem(`data_${loggedInUser?.username}`, encrypt(systemDataStr, bypassLogin)); - - const lsItemKey = `runHistoryData_${loggedInUser?.username}`; - const lsItem = localStorage.getItem(lsItemKey); - if (!lsItem) { - localStorage.setItem(lsItemKey, ""); - } - - applySystemVersionMigration(systemData); - - this.trainerId = systemData.trainerId; - this.secretId = systemData.secretId; - - this.gender = systemData.gender; - - this.saveSetting(SettingKeys.Player_Gender, systemData.gender === PlayerGender.FEMALE ? 1 : 0); - - if (!systemData.starterData) { - this.initStarterData(); - - if (systemData["starterMoveData"]) { - const starterMoveData = systemData["starterMoveData"]; - for (const s of Object.keys(starterMoveData)) { - this.starterData[s].moveset = starterMoveData[s]; - } - } - - if (systemData["starterEggMoveData"]) { - const starterEggMoveData = systemData["starterEggMoveData"]; - for (const s of Object.keys(starterEggMoveData)) { - this.starterData[s].eggMoves = starterEggMoveData[s]; - } - } - - this.migrateStarterAbilities(systemData, this.starterData); - - const starterIds = Object.keys(this.starterData).map(s => Number.parseInt(s) as SpeciesId); - for (const s of starterIds) { - this.starterData[s].candyCount += systemData.dexData[s].caughtCount; - this.starterData[s].candyCount += systemData.dexData[s].hatchedCount * 2; - if (systemData.dexData[s].caughtAttr & DexAttr.SHINY) { - this.starterData[s].candyCount += 4; - } - } + if (cachedSystemDataStr) { + const cachedSystemData = this.parseSystemData(cachedSystemDataStr); + if (cachedSystemData.timestamp > systemData.timestamp) { + console.debug("Use cached system"); + systemData = cachedSystemData; + systemDataStr = cachedSystemDataStr; } else { - this.starterData = systemData.starterData; + this.clearLocalData(); } - - if (systemData.gameStats) { - this.gameStats = systemData.gameStats; - } - - if (systemData.unlocks) { - for (const key of Object.keys(systemData.unlocks)) { - if (this.unlocks.hasOwnProperty(key)) { - this.unlocks[key] = systemData.unlocks[key]; - } - } - } - - if (systemData.achvUnlocks) { - for (const a of Object.keys(systemData.achvUnlocks)) { - if (achvs.hasOwnProperty(a)) { - this.achvUnlocks[a] = systemData.achvUnlocks[a]; - } - } - } - - if (systemData.voucherUnlocks) { - for (const v of Object.keys(systemData.voucherUnlocks)) { - if (vouchers.hasOwnProperty(v)) { - this.voucherUnlocks[v] = systemData.voucherUnlocks[v]; - } - } - } - - if (systemData.voucherCounts) { - getEnumKeys(VoucherType).forEach(key => { - const index = VoucherType[key]; - this.voucherCounts[index] = systemData.voucherCounts[index] || 0; - }); - } - - this.eggs = systemData.eggs ? systemData.eggs.map(e => e.toEgg()) : []; - - this.eggPity = systemData.eggPity ? systemData.eggPity.slice(0) : [0, 0, 0, 0]; - this.unlockPity = systemData.unlockPity ? systemData.unlockPity.slice(0) : [0, 0, 0, 0]; - - this.dexData = Object.assign(this.dexData, systemData.dexData); - this.consolidateDexData(this.dexData); - this.defaultDexData = null; - - resolve(true); - } catch (err) { - console.error(err); - resolve(false); } - }); + + console.debug(systemData); + + localStorage.setItem(`data_${loggedInUser?.username}`, encrypt(systemDataStr, bypassLogin)); + + const lsItemKey = `runHistoryData_${loggedInUser?.username}`; + const lsItem = localStorage.getItem(lsItemKey); + if (!lsItem) { + localStorage.setItem(lsItemKey, ""); + } + + applySystemVersionMigration(systemData); + + this.trainerId = systemData.trainerId; + this.secretId = systemData.secretId; + + this.gender = systemData.gender; + + this.saveSetting(SettingKeys.Player_Gender, systemData.gender === PlayerGender.FEMALE ? 1 : 0); + + if (!systemData.starterData) { + this.initStarterData(); + + if (systemData["starterMoveData"]) { + const starterMoveData = systemData["starterMoveData"]; + for (const s of Object.keys(starterMoveData)) { + this.starterData[s].moveset = starterMoveData[s]; + } + } + + if (systemData["starterEggMoveData"]) { + const starterEggMoveData = systemData["starterEggMoveData"]; + for (const s of Object.keys(starterEggMoveData)) { + this.starterData[s].eggMoves = starterEggMoveData[s]; + } + } + + this.migrateStarterAbilities(systemData, this.starterData); + + const starterIds = Object.keys(this.starterData).map(s => Number.parseInt(s) as SpeciesId); + for (const s of starterIds) { + this.starterData[s].candyCount += systemData.dexData[s].caughtCount; + this.starterData[s].candyCount += systemData.dexData[s].hatchedCount * 2; + if (systemData.dexData[s].caughtAttr & DexAttr.SHINY) { + this.starterData[s].candyCount += 4; + } + } + } else { + this.starterData = systemData.starterData; + } + + if (systemData.gameStats) { + this.gameStats = systemData.gameStats; + } + + if (systemData.unlocks) { + for (const key of Object.keys(systemData.unlocks)) { + if (this.unlocks.hasOwnProperty(key)) { + this.unlocks[key] = systemData.unlocks[key]; + } + } + } + + if (systemData.achvUnlocks) { + for (const a of Object.keys(systemData.achvUnlocks)) { + if (achvs.hasOwnProperty(a)) { + this.achvUnlocks[a] = systemData.achvUnlocks[a]; + } + } + } + + if (systemData.voucherUnlocks) { + for (const v of Object.keys(systemData.voucherUnlocks)) { + if (vouchers.hasOwnProperty(v)) { + this.voucherUnlocks[v] = systemData.voucherUnlocks[v]; + } + } + } + + if (systemData.voucherCounts) { + getEnumKeys(VoucherType).forEach(key => { + const index = VoucherType[key]; + this.voucherCounts[index] = systemData.voucherCounts[index] || 0; + }); + } + + this.eggs = systemData.eggs ? systemData.eggs.map(e => e.toEgg()) : []; + + this.eggPity = systemData.eggPity ? systemData.eggPity.slice(0) : [0, 0, 0, 0]; + this.unlockPity = systemData.unlockPity ? systemData.unlockPity.slice(0) : [0, 0, 0, 0]; + + this.dexData = Object.assign(this.dexData, systemData.dexData); + this.consolidateDexData(this.dexData); + this.defaultDexData = null; + + resolve(true); + } catch (err) { + console.error(err); + resolve(false); + } + return promise; } /** @@ -624,6 +629,9 @@ export class GameData { } return ret; } + if (k === "ribbons") { + return RibbonData.fromJSON(v); + } return k.endsWith("Attr") && !["natureAttr", "abilityAttr", "passiveAttr"].includes(k) ? BigInt(v ?? 0) : v; }) as SystemSaveData; @@ -822,52 +830,51 @@ export class GameData { return true; // TODO: is `true` the correct return value? } - private loadGamepadSettings(): boolean { - Object.values(SettingGamepad) - .map(setting => setting as SettingGamepad) - .forEach(setting => setSettingGamepad(setting, settingGamepadDefaults[setting])); + private loadGamepadSettings(): void { + Object.values(SettingGamepad).forEach(setting => { + setSettingGamepad(setting, settingGamepadDefaults[setting]); + }); if (!localStorage.hasOwnProperty("settingsGamepad")) { - return false; + return; } const settingsGamepad = JSON.parse(localStorage.getItem("settingsGamepad")!); // TODO: is this bang correct? for (const setting of Object.keys(settingsGamepad)) { setSettingGamepad(setting as SettingGamepad, settingsGamepad[setting]); } - - return true; // TODO: is `true` the correct return value? } - public saveTutorialFlag(tutorial: Tutorial, flag: boolean): boolean { - const key = getDataTypeKey(GameDataType.TUTORIALS); - let tutorials: object = {}; - if (localStorage.hasOwnProperty(key)) { - tutorials = JSON.parse(localStorage.getItem(key)!); // TODO: is this bang correct? + /** + * Save the specified tutorial as having the specified completion status. + * @param tutorial - The {@linkcode Tutorial} whose completion status is being saved + * @param status - The completion status to set + */ + public saveTutorialFlag(tutorial: Tutorial, status: boolean): void { + // Grab the prior save data tutorial + const saveDataKey = getDataTypeKey(GameDataType.TUTORIALS); + const tutorials: TutorialFlags = localStorage.hasOwnProperty(saveDataKey) + ? JSON.parse(localStorage.getItem(saveDataKey)!) + : {}; + + // TODO: We shouldn't be storing this like that + for (const key of Object.values(Tutorial)) { + if (key === tutorial) { + tutorials[key] = status; + } else { + tutorials[key] ??= false; + } } - Object.keys(Tutorial) - .map(t => t as Tutorial) - .forEach(t => { - const key = Tutorial[t]; - if (key === tutorial) { - tutorials[key] = flag; - } else { - tutorials[key] ??= false; - } - }); - - localStorage.setItem(key, JSON.stringify(tutorials)); - - return true; + localStorage.setItem(saveDataKey, JSON.stringify(tutorials)); } public getTutorialFlags(): TutorialFlags { const key = getDataTypeKey(GameDataType.TUTORIALS); - const ret: TutorialFlags = {}; - Object.values(Tutorial) - .map(tutorial => tutorial as Tutorial) - .forEach(tutorial => (ret[Tutorial[tutorial]] = false)); + const ret: TutorialFlags = Object.values(Tutorial).reduce((acc, tutorial) => { + acc[Tutorial[tutorial]] = false; + return acc; + }, {} as TutorialFlags); if (!localStorage.hasOwnProperty(key)) { return ret; @@ -979,6 +986,48 @@ export class GameData { }); } + async renameSession(slotId: number, newName: string): Promise { + if (slotId < 0) { + return false; + } + if (newName === "") { + return true; + } + const sessionData: SessionSaveData | null = await this.getSession(slotId); + + if (!sessionData) { + return false; + } + + sessionData.name = newName; + // update timestamp by 1 to ensure the session is saved + sessionData.timestamp += 1; + const updatedDataStr = JSON.stringify(sessionData); + const encrypted = encrypt(updatedDataStr, bypassLogin); + const secretId = this.secretId; + const trainerId = this.trainerId; + + if (bypassLogin) { + localStorage.setItem( + `sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`, + encrypt(updatedDataStr, bypassLogin), + ); + return true; + } + + const response = await pokerogueApi.savedata.session.update( + { slot: slotId, trainerId, secretId, clientSessionId }, + updatedDataStr, + ); + + if (response) { + return false; + } + localStorage.setItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`, encrypted); + const success = await updateUserInfo(); + return !(success !== null && !success); + } + loadSession(slotId: number, sessionData?: SessionSaveData): Promise { // biome-ignore lint/suspicious/noAsyncPromiseExecutor: TODO: fix this return new Promise(async (resolve, reject) => { @@ -1086,8 +1135,8 @@ export class GameData { globalScene.arena.tags = sessionData.arena.tags; if (globalScene.arena.tags) { for (const tag of globalScene.arena.tags) { - if (tag instanceof ArenaTrapTag) { - const { tagType, side, turnCount, layers, maxLayers } = tag as ArenaTrapTag; + if (tag instanceof EntryHazardTag) { + const { tagType, side, turnCount, layers, maxLayers } = tag as EntryHazardTag; globalScene.arena.eventTarget.dispatchEvent( new TagAddedEvent(tagType, side, turnCount, layers, maxLayers), ); @@ -1584,6 +1633,7 @@ export class GameData { caughtCount: 0, hatchedCount: 0, ivs: [0, 0, 0, 0, 0, 0], + ribbons: new RibbonData(0), }; } @@ -1828,6 +1878,12 @@ export class GameData { }); } + /** + * Increase the number of classic ribbons won with this species. + * @param species - The species to increment the ribbon count for + * @param forStarter - If true, will increment the ribbon count for the root species of the given species + * @returns The number of classic wins after incrementing. + */ incrementRibbonCount(species: PokemonSpecies, forStarter = false): number { const speciesIdToIncrement: SpeciesId = species.getRootSpeciesId(forStarter); @@ -2127,6 +2183,9 @@ export class GameData { if (!entry.hasOwnProperty("natureAttr") || (entry.caughtAttr && !entry.natureAttr)) { entry.natureAttr = this.defaultDexData?.[k].natureAttr || 1 << randInt(25, 1); } + if (!entry.hasOwnProperty("ribbons")) { + entry.ribbons = new RibbonData(0); + } } } diff --git a/src/system/ribbons/ribbon-data.ts b/src/system/ribbons/ribbon-data.ts new file mode 100644 index 00000000000..27e68a36a15 --- /dev/null +++ b/src/system/ribbons/ribbon-data.ts @@ -0,0 +1,148 @@ +import type { Brander } from "#types/type-helpers"; + +export type RibbonFlag = (bigint & Brander<"RibbonFlag">) | 0n; + +/** + * Class for ribbon data management. Usually constructed via the {@linkcode fromJSON} method. + * + * @remarks + * Stores information about the ribbons earned by a species using a bitfield. + */ +export class RibbonData { + /** Internal bitfield storing the unlock state for each ribbon */ + private payload: bigint; + + //#region Ribbons + //#region Monotype challenge ribbons + /** Ribbon for winning the normal monotype challenge */ + public static readonly MONO_NORMAL = 0x1n as RibbonFlag; + /** Ribbon for winning the fighting monotype challenge */ + public static readonly MONO_FIGHTING = 0x2n as RibbonFlag; + /** Ribbon for winning the flying monotype challenge */ + public static readonly MONO_FLYING = 0x4n as RibbonFlag; + /** Ribbon for winning the poision monotype challenge */ + public static readonly MONO_POISON = 0x8n as RibbonFlag; + /** Ribbon for winning the ground monotype challenge */ + public static readonly MONO_GROUND = 0x10n as RibbonFlag; + /** Ribbon for winning the rock monotype challenge */ + public static readonly MONO_ROCK = 0x20n as RibbonFlag; + /** Ribbon for winning the bug monotype challenge */ + public static readonly MONO_BUG = 0x40n as RibbonFlag; + /** Ribbon for winning the ghost monotype challenge */ + public static readonly MONO_GHOST = 0x80n as RibbonFlag; + /** Ribbon for winning the steel monotype challenge */ + public static readonly MONO_STEEL = 0x100n as RibbonFlag; + /** Ribbon for winning the fire monotype challenge */ + public static readonly MONO_FIRE = 0x200n as RibbonFlag; + /** Ribbon for winning the water monotype challenge */ + public static readonly MONO_WATER = 0x400n as RibbonFlag; + /** Ribbon for winning the grass monotype challenge */ + public static readonly MONO_GRASS = 0x800n as RibbonFlag; + /** Ribbon for winning the electric monotype challenge */ + public static readonly MONO_ELECTRIC = 0x1000n as RibbonFlag; + /** Ribbon for winning the psychic monotype challenge */ + public static readonly MONO_PSYCHIC = 0x2000n as RibbonFlag; + /** Ribbon for winning the ice monotype challenge */ + public static readonly MONO_ICE = 0x4000n as RibbonFlag; + /** Ribbon for winning the dragon monotype challenge */ + public static readonly MONO_DRAGON = 0x8000n as RibbonFlag; + /** Ribbon for winning the dark monotype challenge */ + public static readonly MONO_DARK = 0x10000n as RibbonFlag; + /** Ribbon for winning the fairy monotype challenge */ + public static readonly MONO_FAIRY = 0x20000n as RibbonFlag; + //#endregion Monotype ribbons + + //#region Monogen ribbons + /** Ribbon for winning the the mono gen 1 challenge */ + public static readonly MONO_GEN_1 = 0x40000n as RibbonFlag; + /** Ribbon for winning the the mono gen 2 challenge */ + public static readonly MONO_GEN_2 = 0x80000n as RibbonFlag; + /** Ribbon for winning the mono gen 3 challenge */ + public static readonly MONO_GEN_3 = 0x100000n as RibbonFlag; + /** Ribbon for winning the mono gen 4 challenge */ + public static readonly MONO_GEN_4 = 0x200000n as RibbonFlag; + /** Ribbon for winning the mono gen 5 challenge */ + public static readonly MONO_GEN_5 = 0x400000n as RibbonFlag; + /** Ribbon for winning the mono gen 6 challenge */ + public static readonly MONO_GEN_6 = 0x800000n as RibbonFlag; + /** Ribbon for winning the mono gen 7 challenge */ + public static readonly MONO_GEN_7 = 0x1000000n as RibbonFlag; + /** Ribbon for winning the mono gen 8 challenge */ + public static readonly MONO_GEN_8 = 0x2000000n as RibbonFlag; + /** Ribbon for winning the mono gen 9 challenge */ + public static readonly MONO_GEN_9 = 0x4000000n as RibbonFlag; + //#endregion Monogen ribbons + + /** Ribbon for winning classic */ + public static readonly CLASSIC = 0x8000000n as RibbonFlag; + /** Ribbon for winning the nuzzlocke challenge */ + public static readonly NUZLOCKE = 0x10000000n as RibbonFlag; + /** Ribbon for reaching max friendship */ + public static readonly FRIENDSHIP = 0x20000000n as RibbonFlag; + /** Ribbon for winning the flip stats challenge */ + public static readonly FLIP_STATS = 0x40000000n as RibbonFlag; + /** Ribbon for winning the inverse challenge */ + public static readonly INVERSE = 0x80000000n as RibbonFlag; + /** Ribbon for winning the fresh start challenge */ + public static readonly FRESH_START = 0x100000000n as RibbonFlag; + /** Ribbon for winning the hardcore challenge */ + public static readonly HARDCORE = 0x200000000n as RibbonFlag; + /** Ribbon for winning the limited catch challenge */ + public static readonly LIMITED_CATCH = 0x400000000n as RibbonFlag; + /** Ribbon for winning the limited support challenge set to no heal */ + public static readonly NO_HEAL = 0x800000000n as RibbonFlag; + /** Ribbon for winning the limited uspport challenge set to no shop */ + public static readonly NO_SHOP = 0x1000000000n as RibbonFlag; + /** Ribbon for winning the limited support challenge set to both*/ + public static readonly NO_SUPPORT = 0x2000000000n as RibbonFlag; + + // NOTE: max possible ribbon flag is 0x20000000000000 (53 total ribbons) + // Once this is exceeded, bitfield needs to be changed to a bigint or even a uint array + // Note that this has no impact on serialization as it is stored in hex. + + //#endregion Ribbons + + /** Create a new instance of RibbonData. Generally, {@linkcode fromJSON} is used instead. */ + constructor(value: number) { + this.payload = BigInt(value); + } + + /** Serialize the bitfield payload as a hex encoded string */ + public toJSON(): string { + return this.payload.toString(16); + } + + /** + * Decode a hexadecimal string representation of the bitfield into a `RibbonData` instance + * + * @param value - Hexadecimal string representation of the bitfield (without the leading 0x) + * @returns A new instance of `RibbonData` initialized with the provided bitfield. + */ + public static fromJSON(value: string): RibbonData { + try { + return new RibbonData(Number.parseInt(value, 16)); + } catch { + return new RibbonData(0); + } + } + + /** + * Award one or more ribbons to the ribbon data by setting the corresponding flags in the bitfield. + * + * @param flags - The flags to set. Can be a single flag or multiple flags. + */ + public award(...flags: [RibbonFlag, ...RibbonFlag[]]): void { + for (const f of flags) { + this.payload |= f; + } + } + + /** + * Check if a specific ribbon has been awarded + * @param flag - The ribbon to check + * @returns Whether the specified flag has been awarded + */ + public has(flag: RibbonFlag): boolean { + return !!(this.payload & flag); + } +} diff --git a/src/system/ribbons/ribbon-methods.ts b/src/system/ribbons/ribbon-methods.ts new file mode 100644 index 00000000000..138c0be7b51 --- /dev/null +++ b/src/system/ribbons/ribbon-methods.ts @@ -0,0 +1,20 @@ +import { globalScene } from "#app/global-scene"; +import { pokemonPrevolutions } from "#balance/pokemon-evolutions"; +import type { SpeciesId } from "#enums/species-id"; +import type { RibbonFlag } from "#system/ribbons/ribbon-data"; +import { isNullOrUndefined } from "#utils/common"; + +/** + * Award one or more ribbons to a species and its pre-evolutions + * + * @param id - The ID of the species to award ribbons to + * @param ribbons - The ribbon(s) to award (use bitwise OR to combine multiple) + */ +export function awardRibbonsToSpeciesLine(id: SpeciesId, ribbons: RibbonFlag): void { + const dexData = globalScene.gameData.dexData; + dexData[id].ribbons.award(ribbons); + // Mark all pre-evolutions of the Pokémon with the same ribbon flags. + for (let prevoId = pokemonPrevolutions[id]; !isNullOrUndefined(prevoId); prevoId = pokemonPrevolutions[prevoId]) { + dexData[prevoId].ribbons.award(ribbons); + } +} diff --git a/src/system/settings/settings.ts b/src/system/settings/settings.ts index 32d9e0ee2be..86305b3f7ed 100644 --- a/src/system/settings/settings.ts +++ b/src/system/settings/settings.ts @@ -197,35 +197,35 @@ export const Setting: Array = [ options: [ { value: "1", - label: i18next.t("settings:gameSpeed1x"), + label: i18next.t("settings:gameSpeed100x"), }, { value: "1.25", - label: i18next.t("settings:gameSpeed1_25x"), + label: i18next.t("settings:gameSpeed125x"), }, { value: "1.5", - label: i18next.t("settings:gameSpeed1_5x"), + label: i18next.t("settings:gameSpeed150x"), }, { value: "2", - label: i18next.t("settings:gameSpeed2x"), + label: i18next.t("settings:gameSpeed200x"), }, { value: "2.5", - label: i18next.t("settings:gameSpeed2_5x"), + label: i18next.t("settings:gameSpeed250x"), }, { value: "3", - label: i18next.t("settings:gameSpeed3x"), + label: i18next.t("settings:gameSpeed300x"), }, { value: "4", - label: i18next.t("settings:gameSpeed4x"), + label: i18next.t("settings:gameSpeed400x"), }, { value: "5", - label: i18next.t("settings:gameSpeed5x"), + label: i18next.t("settings:gameSpeed500x"), }, ], default: 3, @@ -566,7 +566,7 @@ export const Setting: Array = [ }, { value: "Back", - label: i18next.t("settings:timeOfDay_back"), + label: i18next.t("settings:timeOfDayBack"), }, ], default: 0, diff --git a/src/ui/achvs-ui-handler.ts b/src/ui/achvs-ui-handler.ts index 2c04e24e0f2..7763e3be7fe 100644 --- a/src/ui/achvs-ui-handler.ts +++ b/src/ui/achvs-ui-handler.ts @@ -96,7 +96,7 @@ export class AchvsUiHandler extends MessageUiHandler { const genderIndex = globalScene.gameData.gender ?? PlayerGender.MALE; const genderStr = PlayerGender[genderIndex].toLowerCase(); - this.achvsName = i18next.t("achv:Achievements.name", { context: genderStr }); + this.achvsName = i18next.t("achv:achievements.name", { context: genderStr }); this.vouchersName = i18next.t("voucher:vouchers"); this.iconsBg = addWindow(0, this.headerBg.height, WIDTH - 2, HEIGHT - this.headerBg.height - 68).setOrigin(0); @@ -214,7 +214,7 @@ export class AchvsUiHandler extends MessageUiHandler { this.showText(!hidden ? achv.description : ""); this.scoreText.setText(`${achv.score}pt`); this.unlockText.setText( - unlocked ? new Date(achvUnlocks[achv.id]).toLocaleDateString() : i18next.t("achv:Locked.name"), + unlocked ? new Date(achvUnlocks[achv.id]).toLocaleDateString() : i18next.t("achv:locked.name"), ); } diff --git a/src/ui/arena-flyout.ts b/src/ui/arena-flyout.ts index e243bef342e..da062f5c96f 100644 --- a/src/ui/arena-flyout.ts +++ b/src/ui/arena-flyout.ts @@ -1,5 +1,5 @@ import { globalScene } from "#app/global-scene"; -import { ArenaTrapTag } from "#data/arena-tag"; +import { EntryHazardTag } from "#data/arena-tag"; import { TerrainType } from "#data/terrain"; import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagType } from "#enums/arena-tag-type"; @@ -287,7 +287,7 @@ export class ArenaFlyout extends Phaser.GameObjects.Container { switch (arenaEffectChangedEvent.constructor) { case TagAddedEvent: { const tagAddedEvent = arenaEffectChangedEvent as TagAddedEvent; - const isArenaTrapTag = globalScene.arena.getTag(tagAddedEvent.arenaTagType) instanceof ArenaTrapTag; + const isArenaTrapTag = globalScene.arena.getTag(tagAddedEvent.arenaTagType) instanceof EntryHazardTag; let arenaEffectType: ArenaEffectType; if (tagAddedEvent.arenaTagSide === ArenaTagSide.BOTH) { diff --git a/src/ui/base-stats-overlay.ts b/src/ui/base-stats-overlay.ts index 3b432e13096..b3cccf34298 100644 --- a/src/ui/base-stats-overlay.ts +++ b/src/ui/base-stats-overlay.ts @@ -4,6 +4,7 @@ import { TextStyle } from "#enums/text-style"; import { addTextObject } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; import { fixedInt } from "#utils/common"; +import { toCamelCase } from "#utils/strings"; import i18next from "i18next"; interface BaseStatsOverlaySettings { @@ -68,7 +69,9 @@ export class BaseStatsOverlay extends Phaser.GameObjects.Container implements In // show this component with infos for the specific move show(values: number[], total: number): boolean { for (let i = 0; i < 6; i++) { - this.statsLabels[i].setText(i18next.t(`pokemonInfo:Stat.${shortStats[i]}shortened`) + ": " + `${values[i]}`); + this.statsLabels[i].setText( + i18next.t(`pokemonInfo:stat.${toCamelCase(shortStats[i])}Shortened`) + ": " + `${values[i]}`, + ); // This accounts for base stats up to 200, might not be enough. // TODO: change color based on value. this.statsShadows[i].setSize(values[i] / 2, 5); diff --git a/src/ui/battle-info/battle-info.ts b/src/ui/battle-info/battle-info.ts index 0aedfbdf5e7..810d0c7c328 100644 --- a/src/ui/battle-info/battle-info.ts +++ b/src/ui/battle-info/battle-info.ts @@ -9,6 +9,7 @@ import type { Pokemon } from "#field/pokemon"; import { getVariantTint } from "#sprites/variant"; import { addTextObject } from "#ui/text"; import { fixedInt, getLocalizedSpriteKey, getShinyDescriptor } from "#utils/common"; +import { toCamelCase } from "#utils/strings"; import i18next from "i18next"; /** @@ -361,7 +362,7 @@ export abstract class BattleInfo extends Phaser.GameObjects.Container { globalScene.ui.showTooltip( "", i18next.t("fightUiHandler:teraHover", { - type: i18next.t(`pokemonInfo:Type.${PokemonType[this.lastTeraType]}`), + type: i18next.t(`pokemonInfo:type.${toCamelCase(PokemonType[this.lastTeraType])}`), }), ); } diff --git a/src/ui/bgm-bar.ts b/src/ui/bgm-bar.ts index e2c6925ec30..f24f372a804 100644 --- a/src/ui/bgm-bar.ts +++ b/src/ui/bgm-bar.ts @@ -1,7 +1,7 @@ import { globalScene } from "#app/global-scene"; import { TextStyle } from "#enums/text-style"; import { addTextObject } from "#ui/text"; -import { toTitleCase } from "#utils/strings"; +import { toCamelCase, toTitleCase } from "#utils/strings"; import i18next from "i18next"; const hiddenX = -150; @@ -100,7 +100,7 @@ export class BgmBar extends Phaser.GameObjects.Container { } getRealBgmName(bgmName: string): string { - return i18next.t([`bgmName:${bgmName}`, "bgmName:missing_entries"], { + return i18next.t([`bgmName:${toCamelCase(bgmName)}`, "bgmName:missingEntries"], { name: toTitleCase(bgmName), }); } diff --git a/src/ui/confirm-ui-handler.ts b/src/ui/confirm-ui-handler.ts index 49e88556f1b..529d1bd8bbb 100644 --- a/src/ui/confirm-ui-handler.ts +++ b/src/ui/confirm-ui-handler.ts @@ -31,14 +31,14 @@ export class ConfirmUiHandler extends AbstractOptionSelectUiHandler { const config: OptionSelectConfig = { options: [ { - label: i18next.t("partyUiHandler:SUMMARY"), + label: i18next.t("partyUiHandler:summary"), handler: () => { args[0](); return true; }, }, { - label: i18next.t("partyUiHandler:POKEDEX"), + label: i18next.t("partyUiHandler:pokedex"), handler: () => { args[1](); return true; diff --git a/src/ui/game-stats-ui-handler.ts b/src/ui/game-stats-ui-handler.ts index ed66230bed7..4ddb80fcc00 100644 --- a/src/ui/game-stats-ui-handler.ts +++ b/src/ui/game-stats-ui-handler.ts @@ -1,7 +1,9 @@ +import { loggedInUser } from "#app/account"; import { globalScene } from "#app/global-scene"; import { speciesStarterCosts } from "#balance/starters"; import { Button } from "#enums/buttons"; import { DexAttr } from "#enums/dex-attr"; +import { PlayerGender } from "#enums/player-gender"; import { TextStyle } from "#enums/text-style"; import { UiTheme } from "#enums/ui-theme"; import type { GameData } from "#system/game-data"; @@ -227,6 +229,9 @@ export class GameStatsUiHandler extends UiHandler { private arrowUp: Phaser.GameObjects.Sprite; private arrowDown: Phaser.GameObjects.Sprite; + /** Logged in username */ + private headerText: Phaser.GameObjects.Text; + /** Whether the UI is single column mode */ private get singleCol(): boolean { const resolvedLang = i18next.resolvedLanguage ?? "en"; @@ -296,6 +301,23 @@ export class GameStatsUiHandler extends UiHandler { return GameStatsUiHandler.ROWS_PER_PAGE * this.columnCount; } + /** + * Returns the username of logged in user. If the username is hidden, the trainer name based on gender will be displayed. + * @returns The username of logged in user + */ + private getUsername(): string { + const usernameReplacement = + globalScene.gameData.gender === PlayerGender.FEMALE + ? i18next.t("trainerNames:playerF") + : i18next.t("trainerNames:playerM"); + + const displayName = !globalScene.hideUsername + ? (loggedInUser?.username ?? i18next.t("common:guest")) + : usernameReplacement; + + return i18next.t("gameStatsUiHandler:stats", { username: displayName }); + } + // #endregion Columnar-specific properties setup() { @@ -316,11 +338,11 @@ export class GameStatsUiHandler extends UiHandler { const headerBg = addWindow(0, 0, sWidth - 2, 24).setOrigin(0); - const headerText = addTextObject(0, 0, i18next.t("gameStatsUiHandler:stats"), TextStyle.HEADER_LABEL) + this.headerText = addTextObject(0, 0, this.getUsername(), TextStyle.HEADER_LABEL) .setOrigin(0) .setPositionRelative(headerBg, 8, 4); - this.gameStatsContainer.add([headerBg, headerText]); + this.gameStatsContainer.add([headerBg, this.headerText]); const colWidth = this.colWidth; @@ -368,6 +390,10 @@ export class GameStatsUiHandler extends UiHandler { show(args: any[]): boolean { super.show(args); + + // show updated username on every render + this.headerText.setText(this.getUsername()); + this.gameStatsContainer.setActive(true).setVisible(true); this.arrowUp.setActive(true).play("prompt").setVisible(false); diff --git a/src/ui/menu-ui-handler.ts b/src/ui/menu-ui-handler.ts index da6bc9ced78..4f6d6ede488 100644 --- a/src/ui/menu-ui-handler.ts +++ b/src/ui/menu-ui-handler.ts @@ -17,6 +17,7 @@ import { addWindow, WindowVariant } from "#ui/ui-theme"; import { fixedInt, isLocal, sessionIdKey } from "#utils/common"; import { getCookie } from "#utils/cookies"; import { getEnumValues } from "#utils/enums"; +import { toCamelCase } from "#utils/strings"; import { isBeta } from "#utils/utility-vars"; import i18next from "i18next"; @@ -138,7 +139,7 @@ export class MenuUiHandler extends MessageUiHandler { this.optionSelectText = addTextObject( 0, 0, - this.menuOptions.map(o => `${i18next.t(`menuUiHandler:${MenuOptions[o]}`)}`).join("\n"), + this.menuOptions.map(o => `${i18next.t(`menuUiHandler:${toCamelCase(MenuOptions[o])}`)}`).join("\n"), TextStyle.WINDOW, { maxLines: this.menuOptions.length }, ); diff --git a/src/ui/mystery-encounter-ui-handler.ts b/src/ui/mystery-encounter-ui-handler.ts index 881c375fa8a..37f77cf43b9 100644 --- a/src/ui/mystery-encounter-ui-handler.ts +++ b/src/ui/mystery-encounter-ui-handler.ts @@ -473,7 +473,7 @@ export class MysteryEncounterUiHandler extends UiHandler { const viewPartyText = addBBCodeTextObject( globalScene.scaledCanvas.width, -24, - getBBCodeFrag(i18next.t("mysteryEncounterMessages:view_party_button"), TextStyle.PARTY), + getBBCodeFrag(i18next.t("mysteryEncounterMessages:viewPartyButton"), TextStyle.PARTY), TextStyle.PARTY, ); this.optionsContainer.add(viewPartyText); @@ -694,7 +694,7 @@ export class MysteryEncounterUiHandler extends UiHandler { duration: 750, onComplete: () => { this.dexProgressContainer.on("pointerover", () => { - globalScene.ui.showTooltip("", i18next.t("mysteryEncounterMessages:affects_pokedex"), true); + globalScene.ui.showTooltip("", i18next.t("mysteryEncounterMessages:affectsPokedex"), true); }); this.dexProgressContainer.on("pointerout", () => { globalScene.ui.hideTooltip(); diff --git a/src/ui/party-ui-handler.ts b/src/ui/party-ui-handler.ts index ff5e7246a6f..6ca134ebd75 100644 --- a/src/ui/party-ui-handler.ts +++ b/src/ui/party-ui-handler.ts @@ -27,10 +27,15 @@ import { addBBCodeTextObject, addTextObject, getTextColor } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; import { applyChallenges } from "#utils/challenge-utils"; import { BooleanHolder, getLocalizedSpriteKey, randInt } from "#utils/common"; -import { toTitleCase } from "#utils/strings"; +import { toCamelCase, toTitleCase } from "#utils/strings"; import i18next from "i18next"; import type BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext"; +const DISCARD_BUTTON_X = 60; +const DISCARD_BUTTON_X_DOUBLES = 64; +const DISCARD_BUTTON_Y = -73; +const DISCARD_BUTTON_Y_DOUBLES = -58; + const defaultMessage = i18next.t("partyUiHandler:choosePokemon"); /** @@ -301,7 +306,7 @@ export class PartyUiHandler extends MessageUiHandler { const partyMessageText = addTextObject(10, 8, defaultMessage, TextStyle.WINDOW, { maxLines: 2 }); partyMessageText.setName("text-party-msg"); - partyMessageText.setOrigin(0, 0); + partyMessageText.setOrigin(0); partyMessageBoxContainer.add(partyMessageText); this.message = partyMessageText; @@ -317,10 +322,8 @@ export class PartyUiHandler extends MessageUiHandler { this.iconAnimHandler = new PokemonIconAnimHandler(); this.iconAnimHandler.setup(); - const partyDiscardModeButton = new PartyDiscardModeButton(60, -globalScene.game.canvas.height / 15 - 1, this); - + const partyDiscardModeButton = new PartyDiscardModeButton(DISCARD_BUTTON_X, DISCARD_BUTTON_Y, this); partyContainer.add(partyDiscardModeButton); - this.partyDiscardModeButton = partyDiscardModeButton; // prepare move overlay @@ -1233,7 +1236,7 @@ export class PartyUiHandler extends MessageUiHandler { } if (!this.optionsCursorObj) { this.optionsCursorObj = globalScene.add.image(0, 0, "cursor"); - this.optionsCursorObj.setOrigin(0, 0); + this.optionsCursorObj.setOrigin(0); this.optionsContainer.add(this.optionsCursorObj); } this.optionsCursorObj.setPosition( @@ -1570,12 +1573,12 @@ export class PartyUiHandler extends MessageUiHandler { const formChangeItemModifiers = this.getFormChangeItemsModifiers(pokemon); if (formChangeItemModifiers && option >= PartyOption.FORM_CHANGE_ITEM) { const modifier = formChangeItemModifiers[option - PartyOption.FORM_CHANGE_ITEM]; - optionName = `${modifier.active ? i18next.t("partyUiHandler:DEACTIVATE") : i18next.t("partyUiHandler:ACTIVATE")} ${modifier.type.name}`; + optionName = `${modifier.active ? i18next.t("partyUiHandler:deactivate") : i18next.t("partyUiHandler:activate")} ${modifier.type.name}`; } else if (option === PartyOption.UNPAUSE_EVOLUTION) { - optionName = `${pokemon.pauseEvolutions ? i18next.t("partyUiHandler:UNPAUSE_EVOLUTION") : i18next.t("partyUiHandler:PAUSE_EVOLUTION")}`; + optionName = `${pokemon.pauseEvolutions ? i18next.t("partyUiHandler:unpausedEvolution") : i18next.t("partyUiHandler:pauseEvolution")}`; } else { if (this.localizedOptions.includes(option)) { - optionName = i18next.t(`partyUiHandler:${PartyOption[option]}`); + optionName = i18next.t(`partyUiHandler:${toCamelCase(PartyOption[option])}`); } else { optionName = toTitleCase(PartyOption[option]); } @@ -1592,7 +1595,7 @@ export class PartyUiHandler extends MessageUiHandler { .getLevelMoves() .find(plm => plm[1] === move); } else if (option === PartyOption.ALL) { - optionName = i18next.t("partyUiHandler:ALL"); + optionName = i18next.t("partyUiHandler:all"); } else { const itemModifiers = this.getItemModifiers(pokemon); const itemModifier = itemModifiers[option]; @@ -1605,7 +1608,7 @@ export class PartyUiHandler extends MessageUiHandler { optionText.setColor("#40c8f8"); optionText.setShadowColor("#006090"); } - optionText.setOrigin(0, 0); + optionText.setOrigin(0); /** For every item that has stack bigger than 1, display the current quantity selection */ const itemModifiers = this.getItemModifiers(pokemon); @@ -1802,6 +1805,7 @@ class PartySlot extends Phaser.GameObjects.Container { private selected: boolean; private transfer: boolean; private slotIndex: number; + private isBenched: boolean; private pokemon: PlayerPokemon; private slotBg: Phaser.GameObjects.Image; @@ -1812,6 +1816,7 @@ class PartySlot extends Phaser.GameObjects.Container { public slotHpText: Phaser.GameObjects.Text; public slotDescriptionLabel: Phaser.GameObjects.Text; // this is used to show text instead of the HP bar i.e. for showing "Able"/"Not Able" for TMs when you try to learn them + private slotBgKey: string; private pokemonIcon: Phaser.GameObjects.Container; private iconAnimHandler: PokemonIconAnimHandler; @@ -1822,19 +1827,34 @@ class PartySlot extends Phaser.GameObjects.Container { partyUiMode: PartyUiMode, tmMoveId: MoveId, ) { - super( - globalScene, - slotIndex >= globalScene.currentBattle.getBattlerCount() ? 230.5 : 64, - slotIndex >= globalScene.currentBattle.getBattlerCount() - ? -184 + - (globalScene.currentBattle.double ? -40 : 0) + - (28 + (globalScene.currentBattle.double ? 8 : 0)) * slotIndex - : partyUiMode === PartyUiMode.MODIFIER_TRANSFER - ? -124 + (globalScene.currentBattle.double ? -20 : 0) + slotIndex * 55 - : -124 + (globalScene.currentBattle.double ? -8 : 0) + slotIndex * 64, - ); + const isBenched = slotIndex >= globalScene.currentBattle.getBattlerCount(); + const isDoubleBattle = globalScene.currentBattle.double; + const isItemManageMode = partyUiMode === PartyUiMode.MODIFIER_TRANSFER || partyUiMode === PartyUiMode.DISCARD; + + /* + * Here we determine the position of the slot. + * The x coordinate depends on whether the pokemon is on the field or in the bench. + * The y coordinate depends on various factors, such as the number of pokémon on the field, + * and whether the transfer/discard button is also on the screen. + */ + const slotPositionX = isBenched ? 143 : 9; + + let slotPositionY: number; + if (isBenched) { + slotPositionY = -196 + (isDoubleBattle ? -40 : 0); + slotPositionY += (28 + (isDoubleBattle ? 8 : 0)) * slotIndex; + } else { + slotPositionY = -148.5; + if (isDoubleBattle) { + slotPositionY += isItemManageMode ? -20 : -8; + } + slotPositionY += (isItemManageMode ? (isDoubleBattle ? 47 : 55) : 64) * slotIndex; + } + + super(globalScene, slotPositionX, slotPositionY); this.slotIndex = slotIndex; + this.isBenched = isBenched; this.pokemon = pokemon; this.iconAnimHandler = iconAnimHandler; @@ -1848,27 +1868,75 @@ class PartySlot extends Phaser.GameObjects.Container { setup(partyUiMode: PartyUiMode, tmMoveId: MoveId) { const currentLanguage = i18next.resolvedLanguage ?? "en"; const offsetJa = currentLanguage === "ja"; + const isItemManageMode = partyUiMode === PartyUiMode.MODIFIER_TRANSFER || partyUiMode === PartyUiMode.DISCARD; - const battlerCount = globalScene.currentBattle.getBattlerCount(); + this.slotBgKey = this.isBenched + ? "party_slot" + : isItemManageMode && globalScene.currentBattle.double + ? "party_slot_main_short" + : "party_slot_main"; + const fullSlotBgKey = this.pokemon.hp ? this.slotBgKey : `${this.slotBgKey}${"_fnt"}`; + this.slotBg = globalScene.add.sprite(0, 0, this.slotBgKey, fullSlotBgKey); + this.slotBg.setOrigin(0); + this.add(this.slotBg); - const slotKey = `party_slot${this.slotIndex >= battlerCount ? "" : "_main"}`; + const genderSymbol = getGenderSymbol(this.pokemon.getGender(true)); + const isFusion = this.pokemon.isFusion(); - const slotBg = globalScene.add.sprite(0, 0, slotKey, `${slotKey}${this.pokemon.hp ? "" : "_fnt"}`); - this.slotBg = slotBg; + // Here we define positions and offsets + // Base values are for the active pokemon; they are changed for benched pokemon, + // or for active pokemon if in a double battle in item management mode. - this.add(slotBg); + // icon position relative to slot background + let slotPb = { x: 4, y: 4 }; + // name position relative to slot background + let namePosition = { x: 24, y: 10 + (offsetJa ? 2 : 0) }; + // maximum allowed length of name; must accomodate fusion symbol + let maxNameTextWidth = 76 - (isFusion ? 8 : 0); + // "Lv." label position relative to slot background + let levelLabelPosition = { x: 24 + 8, y: 10 + 12 }; + // offset from "Lv." to the level number; should not be changed. + const levelTextToLevelLabelOffset = { x: 9, y: offsetJa ? 1.5 : 0 }; + // offests from "Lv." to gender, spliced and status icons, these depend on the type of slot. + let genderTextToLevelLabelOffset = { x: 68 - (isFusion ? 8 : 0), y: -9 }; + let splicedIconToLevelLabelOffset = { x: 68, y: 3.5 - 12 }; + let statusIconToLevelLabelOffset = { x: 55, y: 0 }; + // offset from the name to the shiny icon (on the left); should not be changed. + const shinyIconToNameOffset = { x: -9, y: 3 }; + // hp bar position relative to slot background + let hpBarPosition = { x: 8, y: 31 }; + // offsets of hp bar overlay (showing the remaining hp) and number; should not be changed. + const hpOverlayToBarOffset = { x: 16, y: 2 }; + const hpTextToBarOffset = { x: -3, y: -2 + (offsetJa ? 2 : 0) }; + // description position relative to slot background + let descriptionLabelPosition = { x: 32, y: 46 }; - const slotPb = globalScene.add.sprite( - this.slotIndex >= battlerCount ? -85.5 : -51, - this.slotIndex >= battlerCount ? 0 : -20.5, - "party_pb", - ); - this.slotPb = slotPb; + // If in item management mode, the active slots are shorter + if (isItemManageMode && globalScene.currentBattle.double && !this.isBenched) { + namePosition.y -= 8; + levelLabelPosition.y -= 8; + hpBarPosition.y -= 8; + descriptionLabelPosition.y -= 8; + } - this.add(slotPb); + // Benched slots have significantly different parameters + if (this.isBenched) { + slotPb = { x: 2, y: 12 }; + namePosition = { x: 21, y: 2 + (offsetJa ? 2 : 0) }; + maxNameTextWidth = 52; + levelLabelPosition = { x: 21 + 8, y: 2 + 12 }; + genderTextToLevelLabelOffset = { x: 36, y: 0 }; + splicedIconToLevelLabelOffset = { x: 36 + (genderSymbol ? 8 : 0), y: 0.5 }; + statusIconToLevelLabelOffset = { x: 43, y: 0 }; + hpBarPosition = { x: 72, y: 6 }; + descriptionLabelPosition = { x: 94, y: 16 }; + } - this.pokemonIcon = globalScene.addPokemonIcon(this.pokemon, slotPb.x, slotPb.y, 0.5, 0.5, true); + this.slotPb = globalScene.add.sprite(0, 0, "party_pb"); + this.slotPb.setPosition(slotPb.x, slotPb.y); + this.add(this.slotPb); + this.pokemonIcon = globalScene.addPokemonIcon(this.pokemon, this.slotPb.x, this.slotPb.y, 0.5, 0.5, true); this.add(this.pokemonIcon); this.iconAnimHandler.addOrUpdate(this.pokemonIcon, PokemonIconAnimMode.PASSIVE); @@ -1882,7 +1950,7 @@ class PartySlot extends Phaser.GameObjects.Container { const nameSizeTest = addTextObject(0, 0, displayName, TextStyle.PARTY); nameTextWidth = nameSizeTest.displayWidth; - while (nameTextWidth > (this.slotIndex >= battlerCount ? 52 : 76 - (this.pokemon.fusionSpecies ? 8 : 0))) { + while (nameTextWidth > maxNameTextWidth) { displayName = `${displayName.slice(0, displayName.endsWith(".") ? -2 : -1).trimEnd()}.`; nameSizeTest.setText(displayName); nameTextWidth = nameSizeTest.displayWidth; @@ -1891,78 +1959,59 @@ class PartySlot extends Phaser.GameObjects.Container { nameSizeTest.destroy(); this.slotName = addTextObject(0, 0, displayName, TextStyle.PARTY); - this.slotName.setPositionRelative( - slotBg, - this.slotIndex >= battlerCount ? 21 : 24, - (this.slotIndex >= battlerCount ? 2 : 10) + (offsetJa ? 2 : 0), - ); - this.slotName.setOrigin(0, 0); + this.slotName.setPositionRelative(this.slotBg, namePosition.x, namePosition.y); + this.slotName.setOrigin(0); - const slotLevelLabel = globalScene.add.image(0, 0, "party_slot_overlay_lv"); - slotLevelLabel.setPositionRelative( - slotBg, - (this.slotIndex >= battlerCount ? 21 : 24) + 8, - (this.slotIndex >= battlerCount ? 2 : 10) + 12, - ); - slotLevelLabel.setOrigin(0, 0); + const slotLevelLabel = globalScene.add + .image(0, 0, "party_slot_overlay_lv") + .setPositionRelative(this.slotBg, levelLabelPosition.x, levelLabelPosition.y) + .setOrigin(0); const slotLevelText = addTextObject( 0, 0, this.pokemon.level.toString(), this.pokemon.level < globalScene.getMaxExpLevel() ? TextStyle.PARTY : TextStyle.PARTY_RED, - ); - slotLevelText.setPositionRelative(slotLevelLabel, 9, offsetJa ? 1.5 : 0); - slotLevelText.setOrigin(0, 0.25); - + ) + .setPositionRelative(slotLevelLabel, levelTextToLevelLabelOffset.x, levelTextToLevelLabelOffset.y) + .setOrigin(0, 0.25); slotInfoContainer.add([this.slotName, slotLevelLabel, slotLevelText]); - const genderSymbol = getGenderSymbol(this.pokemon.getGender(true)); - if (genderSymbol) { - const slotGenderText = addTextObject(0, 0, genderSymbol, TextStyle.PARTY); - slotGenderText.setColor(getGenderColor(this.pokemon.getGender(true))); - slotGenderText.setShadowColor(getGenderColor(this.pokemon.getGender(true), true)); - if (this.slotIndex >= battlerCount) { - slotGenderText.setPositionRelative(slotLevelLabel, 36, 0); - } else { - slotGenderText.setPositionRelative(this.slotName, 76 - (this.pokemon.fusionSpecies ? 8 : 0), 3); - } - slotGenderText.setOrigin(0, 0.25); - + const slotGenderText = addTextObject(0, 0, genderSymbol, TextStyle.PARTY) + .setColor(getGenderColor(this.pokemon.getGender(true))) + .setShadowColor(getGenderColor(this.pokemon.getGender(true), true)) + .setPositionRelative(slotLevelLabel, genderTextToLevelLabelOffset.x, genderTextToLevelLabelOffset.y) + .setOrigin(0, 0.25); slotInfoContainer.add(slotGenderText); } - if (this.pokemon.fusionSpecies) { - const splicedIcon = globalScene.add.image(0, 0, "icon_spliced"); - splicedIcon.setScale(0.5); - splicedIcon.setOrigin(0, 0); - if (this.slotIndex >= battlerCount) { - splicedIcon.setPositionRelative(slotLevelLabel, 36 + (genderSymbol ? 8 : 0), 0.5); - } else { - splicedIcon.setPositionRelative(this.slotName, 76, 3.5); - } - + if (isFusion) { + const splicedIcon = globalScene.add + .image(0, 0, "icon_spliced") + .setScale(0.5) + .setOrigin(0) + .setPositionRelative(slotLevelLabel, splicedIconToLevelLabelOffset.x, splicedIconToLevelLabelOffset.y); slotInfoContainer.add(splicedIcon); } if (this.pokemon.status) { - const statusIndicator = globalScene.add.sprite(0, 0, getLocalizedSpriteKey("statuses")); - statusIndicator.setFrame(StatusEffect[this.pokemon.status?.effect].toLowerCase()); - statusIndicator.setOrigin(0, 0); - statusIndicator.setPositionRelative(slotLevelLabel, this.slotIndex >= battlerCount ? 43 : 55, 0); - + const statusIndicator = globalScene.add + .sprite(0, 0, getLocalizedSpriteKey("statuses")) + .setFrame(StatusEffect[this.pokemon.status?.effect].toLowerCase()) + .setOrigin(0) + .setPositionRelative(slotLevelLabel, statusIconToLevelLabelOffset.x, statusIconToLevelLabelOffset.y); slotInfoContainer.add(statusIndicator); } if (this.pokemon.isShiny()) { const doubleShiny = this.pokemon.isDoubleShiny(false); - const shinyStar = globalScene.add.image(0, 0, `shiny_star_small${doubleShiny ? "_1" : ""}`); - shinyStar.setOrigin(0, 0); - shinyStar.setPositionRelative(this.slotName, -9, 3); - shinyStar.setTint(getVariantTint(this.pokemon.getBaseVariant())); - + const shinyStar = globalScene.add + .image(0, 0, `shiny_star_small${doubleShiny ? "_1" : ""}`) + .setOrigin(0) + .setPositionRelative(this.slotName, shinyIconToNameOffset.x, shinyIconToNameOffset.y) + .setTint(getVariantTint(this.pokemon.getBaseVariant())); slotInfoContainer.add(shinyStar); if (doubleShiny) { @@ -1971,50 +2020,38 @@ class PartySlot extends Phaser.GameObjects.Container { .setOrigin(0) .setPosition(shinyStar.x, shinyStar.y) .setTint(getVariantTint(this.pokemon.fusionVariant)); - slotInfoContainer.add(fusionShinyStar); } } - this.slotHpBar = globalScene.add.image(0, 0, "party_slot_hp_bar"); - this.slotHpBar.setPositionRelative( - slotBg, - this.slotIndex >= battlerCount ? 72 : 8, - this.slotIndex >= battlerCount ? 6 : 31, - ); - this.slotHpBar.setOrigin(0, 0); - this.slotHpBar.setVisible(false); + this.slotHpBar = globalScene.add + .image(0, 0, "party_slot_hp_bar") + .setOrigin(0) + .setVisible(false) + .setPositionRelative(this.slotBg, hpBarPosition.x, hpBarPosition.y); const hpRatio = this.pokemon.getHpRatio(); - this.slotHpOverlay = globalScene.add.sprite( - 0, - 0, - "party_slot_hp_overlay", - hpRatio > 0.5 ? "high" : hpRatio > 0.25 ? "medium" : "low", - ); - this.slotHpOverlay.setPositionRelative(this.slotHpBar, 16, 2); - this.slotHpOverlay.setOrigin(0, 0); - this.slotHpOverlay.setScale(hpRatio, 1); - this.slotHpOverlay.setVisible(false); + this.slotHpOverlay = globalScene.add + .sprite(0, 0, "party_slot_hp_overlay", hpRatio > 0.5 ? "high" : hpRatio > 0.25 ? "medium" : "low") + .setOrigin(0) + .setPositionRelative(this.slotHpBar, hpOverlayToBarOffset.x, hpOverlayToBarOffset.y) + .setScale(hpRatio, 1) + .setVisible(false); - this.slotHpText = addTextObject(0, 0, `${this.pokemon.hp}/${this.pokemon.getMaxHp()}`, TextStyle.PARTY); - this.slotHpText.setPositionRelative( - this.slotHpBar, - this.slotHpBar.width - 3, - this.slotHpBar.height - 2 + (offsetJa ? 2 : 0), - ); - this.slotHpText.setOrigin(1, 0); - this.slotHpText.setVisible(false); + this.slotHpText = addTextObject(0, 0, `${this.pokemon.hp}/${this.pokemon.getMaxHp()}`, TextStyle.PARTY) + .setOrigin(1, 0) + .setPositionRelative( + this.slotHpBar, + this.slotHpBar.width + hpTextToBarOffset.x, + this.slotHpBar.height + hpTextToBarOffset.y, + ) // TODO: annoying because it contains the width + .setVisible(false); - this.slotDescriptionLabel = addTextObject(0, 0, "", TextStyle.MESSAGE); - this.slotDescriptionLabel.setPositionRelative( - slotBg, - this.slotIndex >= battlerCount ? 94 : 32, - this.slotIndex >= battlerCount ? 16 : 46, - ); - this.slotDescriptionLabel.setOrigin(0, 1); - this.slotDescriptionLabel.setVisible(false); + this.slotDescriptionLabel = addTextObject(0, 0, "", TextStyle.MESSAGE) + .setOrigin(0, 1) + .setVisible(false) + .setPositionRelative(this.slotBg, descriptionLabelPosition.x, descriptionLabelPosition.y); slotInfoContainer.add([this.slotHpBar, this.slotHpOverlay, this.slotHpText, this.slotDescriptionLabel]); @@ -2076,10 +2113,9 @@ class PartySlot extends Phaser.GameObjects.Container { } private updateSlotTexture(): void { - const battlerCount = globalScene.currentBattle.getBattlerCount(); this.slotBg.setTexture( - `party_slot${this.slotIndex >= battlerCount ? "" : "_main"}`, - `party_slot${this.slotIndex >= battlerCount ? "" : "_main"}${this.transfer ? "_swap" : this.pokemon.hp ? "" : "_fnt"}${this.selected ? "_sel" : ""}`, + this.slotBgKey, + `${this.slotBgKey}${this.transfer ? "_swap" : this.pokemon.hp ? "" : "_fnt"}${this.selected ? "_sel" : ""}`, ); } } @@ -2106,7 +2142,12 @@ class PartyCancelButton extends Phaser.GameObjects.Container { this.partyCancelPb = partyCancelPb; - const partyCancelText = addTextObject(-10, -7, i18next.t("partyUiHandler:cancel"), TextStyle.PARTY_CANCEL_BUTTON); + const partyCancelText = addTextObject( + -10, + -7, + i18next.t("partyUiHandler:cancelButton"), + TextStyle.PARTY_CANCEL_BUTTON, + ); this.add(partyCancelText); } @@ -2149,7 +2190,7 @@ class PartyDiscardModeButton extends Phaser.GameObjects.Container { setup(party: PartyUiHandler) { this.transferIcon = globalScene.add.sprite(0, 0, "party_transfer"); this.discardIcon = globalScene.add.sprite(0, 0, "party_discard"); - this.textBox = addTextObject(-8, -7, i18next.t("partyUiHandler:TRANSFER"), TextStyle.PARTY); + this.textBox = addTextObject(-8, -7, i18next.t("partyUiHandler:transfer"), TextStyle.PARTY); this.party = party; this.add(this.transferIcon); @@ -2197,25 +2238,21 @@ class PartyDiscardModeButton extends Phaser.GameObjects.Container { this.transferIcon.setVisible(true); this.discardIcon.setVisible(false); this.textBox.setVisible(true); - this.textBox.setText(i18next.t("partyUiHandler:TRANSFER")); - this.setPosition( - globalScene.currentBattle.double ? 64 : 60, - globalScene.currentBattle.double ? -48 : -globalScene.game.canvas.height / 15 - 1, - ); + this.textBox.setText(i18next.t("partyUiHandler:transfer")); this.transferIcon.displayWidth = this.textBox.text.length * 9 + 3; break; case PartyUiMode.DISCARD: this.transferIcon.setVisible(false); this.discardIcon.setVisible(true); this.textBox.setVisible(true); - this.textBox.setText(i18next.t("partyUiHandler:DISCARD")); - this.setPosition( - globalScene.currentBattle.double ? 64 : 60, - globalScene.currentBattle.double ? -48 : -globalScene.game.canvas.height / 15 - 1, - ); + this.textBox.setText(i18next.t("partyUiHandler:discard")); this.discardIcon.displayWidth = this.textBox.text.length * 9 + 3; break; } + this.setPosition( + globalScene.currentBattle.double ? DISCARD_BUTTON_X_DOUBLES : DISCARD_BUTTON_X, + globalScene.currentBattle.double ? DISCARD_BUTTON_Y_DOUBLES : DISCARD_BUTTON_Y, + ); } clear() { diff --git a/src/ui/pokedex-mon-container.ts b/src/ui/pokedex-mon-container.ts index cfb8555e6c9..832d7e4bcd6 100644 --- a/src/ui/pokedex-mon-container.ts +++ b/src/ui/pokedex-mon-container.ts @@ -208,6 +208,26 @@ export class PokedexMonContainer extends Phaser.GameObjects.Container { ); this.checkIconId(defaultProps.female, defaultProps.formIndex, defaultProps.shiny, defaultProps.variant); this.add(this.icon); + + [ + this.hiddenAbilityIcon, + this.favoriteIcon, + this.classicWinIcon, + this.candyUpgradeIcon, + this.candyUpgradeOverlayIcon, + this.eggMove1Icon, + this.tmMove1Icon, + this.eggMove2Icon, + this.tmMove2Icon, + this.passive1Icon, + this.passive2Icon, + this.passive1OverlayIcon, + this.passive2OverlayIcon, + ].forEach(icon => { + if (icon) { + this.bringToTop(icon); + } + }); } checkIconId(female, formIndex, shiny, variant) { diff --git a/src/ui/pokedex-page-ui-handler.ts b/src/ui/pokedex-page-ui-handler.ts index 49658d9cfc9..5b5dca29641 100644 --- a/src/ui/pokedex-page-ui-handler.ts +++ b/src/ui/pokedex-page-ui-handler.ts @@ -57,7 +57,7 @@ import { addWindow } from "#ui/ui-theme"; import { BooleanHolder, getLocalizedSpriteKey, isNullOrUndefined, padInt, rgbHexToRgba } from "#utils/common"; import { getEnumValues } from "#utils/enums"; import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils"; -import { toTitleCase } from "#utils/strings"; +import { toCamelCase, toTitleCase } from "#utils/strings"; import { argbFromRgba } from "@material/material-color-utilities"; import i18next from "i18next"; import type BBCodeText from "phaser3-rex-plugins/plugins/gameobjects/tagtext/bbcodetext/BBCodeText"; @@ -640,7 +640,7 @@ export class PokedexPageUiHandler extends MessageUiHandler { this.optionSelectText = addBBCodeTextObject( 0, 0, - this.menuOptions.map(o => `${i18next.t(`pokedexUiHandler:${MenuOptions[o]}`)}`).join("\n"), + this.menuOptions.map(o => `${i18next.t(`pokedexUiHandler:${toCamelCase(`menu${MenuOptions[o]}`)}`)}`).join("\n"), TextStyle.WINDOW, { maxLines: this.menuOptions.length, lineSpacing: 12 }, ); @@ -757,7 +757,7 @@ export class PokedexPageUiHandler extends MessageUiHandler { return this.menuOptions .map(o => { - const label = `${i18next.t(`pokedexUiHandler:${MenuOptions[o]}`)}`; + const label = i18next.t(`pokedexUiHandler:${toCamelCase(`menu${MenuOptions[o]}`)}`); const isDark = !isSeen || (!isStarterCaught && (o === MenuOptions.TOGGLE_IVS || o === MenuOptions.NATURES)) || @@ -1517,13 +1517,13 @@ export class PokedexPageUiHandler extends MessageUiHandler { this.biomes.map(b => { options.push({ label: - i18next.t(`biome:${BiomeId[b.biome].toUpperCase()}`) + + i18next.t(`biome:${toCamelCase(BiomeId[b.biome])}`) + " - " + - i18next.t(`biome:${BiomePoolTier[b.tier].toUpperCase()}`) + + i18next.t(`biome:${toCamelCase(BiomePoolTier[b.tier])}`) + (b.tod.length === 1 && b.tod[0] === -1 ? "" : " (" + - b.tod.map(tod => i18next.t(`biome:${TimeOfDay[tod].toUpperCase()}`)).join(", ") + + b.tod.map(tod => i18next.t(`biome:${toCamelCase(TimeOfDay[tod])}`)).join(", ") + ")"), handler: () => false, }); @@ -1538,13 +1538,13 @@ export class PokedexPageUiHandler extends MessageUiHandler { this.preBiomes.map(b => { options.push({ label: - i18next.t(`biome:${BiomeId[b.biome].toUpperCase()}`) + + i18next.t(`biome:${toCamelCase(BiomeId[b.biome])}`) + " - " + - i18next.t(`biome:${BiomePoolTier[b.tier].toUpperCase()}`) + + i18next.t(`biome:${toCamelCase(BiomePoolTier[b.tier])}`) + (b.tod.length === 1 && b.tod[0] === -1 ? "" : " (" + - b.tod.map(tod => i18next.t(`biome:${TimeOfDay[tod].toUpperCase()}`)).join(", ") + + b.tod.map(tod => i18next.t(`biome:${toCamelCase(TimeOfDay[tod])}`)).join(", ") + ")"), handler: () => false, }); @@ -2612,7 +2612,7 @@ export class PokedexPageUiHandler extends MessageUiHandler { // Setting growth rate text if (isFormCaught) { let growthReadable = toTitleCase(GrowthRate[species.growthRate]); - const growthAux = growthReadable.replace(" ", "_"); + const growthAux = toCamelCase(growthReadable); if (i18next.exists("growth:" + growthAux)) { growthReadable = i18next.t(("growth:" + growthAux) as any); } diff --git a/src/ui/pokedex-ui-handler.ts b/src/ui/pokedex-ui-handler.ts index aa2a5cda459..034f4f8e615 100644 --- a/src/ui/pokedex-ui-handler.ts +++ b/src/ui/pokedex-ui-handler.ts @@ -47,6 +47,7 @@ import { BooleanHolder, fixedInt, getLocalizedSpriteKey, padInt, randIntRange, r import type { StarterPreferences } from "#utils/data"; import { loadStarterPreferences } from "#utils/data"; import { getPokemonSpeciesForm, getPokerusStarters } from "#utils/pokemon-utils"; +import { toCamelCase } from "#utils/strings"; import { argbFromRgba } from "@material/material-color-utilities"; import i18next from "i18next"; @@ -324,7 +325,7 @@ export class PokedexUiHandler extends MessageUiHandler { .filter(value => typeof value === "number") // Filter numeric values from the enum .map( (biomeValue, index) => - new DropDownOption(index, new DropDownLabel(i18next.t(`biome:${BiomeId[biomeValue].toUpperCase()}`))), + new DropDownOption(index, new DropDownLabel(i18next.t(`biome:${toCamelCase(BiomeId[biomeValue])}`))), ); biomeOptions.push(new DropDownOption(biomeOptions.length, new DropDownLabel(i18next.t("filterBar:uncatchable")))); const biomeDropDown: DropDown = new DropDown(0, 0, biomeOptions, this.updateStarters, DropDownType.HYBRID); @@ -410,6 +411,11 @@ export class PokedexUiHandler extends MessageUiHandler { new DropDownLabel(i18next.t("filterBar:hasHiddenAbility"), undefined, DropDownState.ON), new DropDownLabel(i18next.t("filterBar:noHiddenAbility"), undefined, DropDownState.EXCLUDE), ]; + const seenSpeciesLabels = [ + new DropDownLabel(i18next.t("filterBar:seenSpecies"), undefined, DropDownState.OFF), + new DropDownLabel(i18next.t("filterBar:isSeen"), undefined, DropDownState.ON), + new DropDownLabel(i18next.t("filterBar:isUnseen"), undefined, DropDownState.EXCLUDE), + ]; const eggLabels = [ new DropDownLabel(i18next.t("filterBar:egg"), undefined, DropDownState.OFF), new DropDownLabel(i18next.t("filterBar:eggPurchasable"), undefined, DropDownState.ON), @@ -423,6 +429,7 @@ export class PokedexUiHandler extends MessageUiHandler { new DropDownOption("FAVORITE", favoriteLabels), new DropDownOption("WIN", winLabels), new DropDownOption("HIDDEN_ABILITY", hiddenAbilityLabels), + new DropDownOption("SEEN_SPECIES", seenSpeciesLabels), new DropDownOption("EGG", eggLabels), new DropDownOption("POKERUS", pokerusLabels), ]; @@ -792,13 +799,15 @@ export class PokedexUiHandler extends MessageUiHandler { this.starterSelectMessageBoxContainer.setVisible(!!text?.length); } - isSeen(species: PokemonSpecies, dexEntry: DexEntry): boolean { + isSeen(species: PokemonSpecies, dexEntry: DexEntry, seenFilter?: boolean): boolean { if (dexEntry?.seenAttr) { return true; } - - const starterDexEntry = globalScene.gameData.dexData[this.getStarterSpeciesId(species.speciesId)]; - return !!starterDexEntry?.caughtAttr; + if (!seenFilter) { + const starterDexEntry = globalScene.gameData.dexData[this.getStarterSpeciesId(species.speciesId)]; + return !!starterDexEntry?.caughtAttr; + } + return false; } /** @@ -1617,6 +1626,21 @@ export class PokedexUiHandler extends MessageUiHandler { } }); + // Seen Filter + const dexEntry = globalScene.gameData.dexData[species.speciesId]; + const isItSeen = this.isSeen(species, dexEntry, true); + const fitsSeen = this.filterBar.getVals(DropDownColumn.MISC).some(misc => { + if (misc.val === "SEEN_SPECIES" && misc.state === DropDownState.ON) { + return isItSeen; + } + if (misc.val === "SEEN_SPECIES" && misc.state === DropDownState.EXCLUDE) { + return !isItSeen; + } + if (misc.val === "SEEN_SPECIES" && misc.state === DropDownState.OFF) { + return true; + } + }); + // Egg Purchasable Filter const isEggPurchasable = this.isSameSpeciesEggAvailable(species.speciesId); const fitsEgg = this.filterBar.getVals(DropDownColumn.MISC).some(misc => { @@ -1658,6 +1682,7 @@ export class PokedexUiHandler extends MessageUiHandler { fitsFavorite && fitsWin && fitsHA && + fitsSeen && fitsEgg && fitsPokerus ) { diff --git a/src/ui/rename-run-ui-handler.ts b/src/ui/rename-run-ui-handler.ts new file mode 100644 index 00000000000..a94b7b08fb9 --- /dev/null +++ b/src/ui/rename-run-ui-handler.ts @@ -0,0 +1,54 @@ +import i18next from "i18next"; +import type { InputFieldConfig } from "./form-modal-ui-handler"; +import { FormModalUiHandler } from "./form-modal-ui-handler"; +import type { ModalConfig } from "./modal-ui-handler"; + +export class RenameRunFormUiHandler extends FormModalUiHandler { + getModalTitle(_config?: ModalConfig): string { + return i18next.t("menu:renameRun"); + } + + getWidth(_config?: ModalConfig): number { + return 160; + } + + getMargin(_config?: ModalConfig): [number, number, number, number] { + return [0, 0, 48, 0]; + } + + getButtonLabels(_config?: ModalConfig): string[] { + return [i18next.t("menu:rename"), i18next.t("menu:cancel")]; + } + + getReadableErrorMessage(error: string): string { + const colonIndex = error?.indexOf(":"); + if (colonIndex > 0) { + error = error.slice(0, colonIndex); + } + + return super.getReadableErrorMessage(error); + } + + override getInputFieldConfigs(): InputFieldConfig[] { + return [{ label: i18next.t("menu:runName") }]; + } + + show(args: any[]): boolean { + if (!super.show(args)) { + return false; + } + if (this.inputs?.length) { + this.inputs.forEach(input => { + input.text = ""; + }); + } + const config = args[0] as ModalConfig; + this.submitAction = _ => { + this.sanitizeInputs(); + const sanitizedName = btoa(encodeURIComponent(this.inputs[0].text)); + config.buttonActions[0](sanitizedName); + return true; + }; + return true; + } +} diff --git a/src/ui/run-history-ui-handler.ts b/src/ui/run-history-ui-handler.ts index 457c48654a3..6f4d9024832 100644 --- a/src/ui/run-history-ui-handler.ts +++ b/src/ui/run-history-ui-handler.ts @@ -337,7 +337,7 @@ class RunEntryContainer extends Phaser.GameObjects.Container { // Because of the interesting mechanics behind rival names, the rival name and title have to be retrieved differently const RIVAL_TRAINER_ID_THRESHOLD = 375; if (data.trainer.trainerType >= RIVAL_TRAINER_ID_THRESHOLD) { - const rivalName = tObj.variant === TrainerVariant.FEMALE ? "trainerNames:rival_female" : "trainerNames:rival"; + const rivalName = tObj.variant === TrainerVariant.FEMALE ? "trainerNames:rivalFemale" : "trainerNames:rival"; const gameOutcomeLabel = addTextObject( 8, 5, diff --git a/src/ui/run-info-ui-handler.ts b/src/ui/run-info-ui-handler.ts index 2def302c1d5..5fc0f37c72d 100644 --- a/src/ui/run-info-ui-handler.ts +++ b/src/ui/run-info-ui-handler.ts @@ -26,6 +26,7 @@ import { addBBCodeTextObject, addTextObject, getTextColor } from "#ui/text"; import { UiHandler } from "#ui/ui-handler"; import { addWindow } from "#ui/ui-theme"; import { formatFancyLargeNumber, formatLargeNumber, formatMoney, getPlayTimeString } from "#utils/common"; +import { toCamelCase } from "#utils/strings"; import i18next from "i18next"; import RoundRectangle from "phaser3-rex-plugins/plugins/roundrectangle"; @@ -207,6 +208,11 @@ export class RunInfoUiHandler extends UiHandler { headerText.setOrigin(0, 0); headerText.setPositionRelative(headerBg, 8, 4); this.runContainer.add(headerText); + const runName = addTextObject(0, 0, this.runInfo.name, TextStyle.WINDOW); + runName.setOrigin(0, 0); + const runNameX = headerText.width / 6 + headerText.x + 4; + runName.setPositionRelative(headerBg, runNameX, 4); + this.runContainer.add(runName); } /** @@ -331,7 +337,7 @@ export class RunInfoUiHandler extends UiHandler { if (this.runInfo.trainer.trainerType >= RIVAL_TRAINER_ID_THRESHOLD) { trainerName = trainerObj.variant === TrainerVariant.FEMALE - ? i18next.t("trainerNames:rival_female") + ? i18next.t("trainerNames:rivalFemale") : i18next.t("trainerNames:rival"); } else { trainerName = trainerObj.getName(0, true); @@ -694,7 +700,11 @@ export class RunInfoUiHandler extends UiHandler { const typeTextColor = `[color=${TypeColor[typeRule]}]`; const typeShadowColor = `[shadow=${TypeShadow[typeRule]}]`; const typeText = - typeTextColor + typeShadowColor + i18next.t(`pokemonInfo:Type.${typeRule}`)! + "[/color]" + "[/shadow]"; + typeTextColor + + typeShadowColor + + i18next.t(`pokemonInfo:type.${toCamelCase(typeRule)}`)! + + "[/color]" + + "[/shadow]"; rules.push(typeText); break; } @@ -702,10 +712,7 @@ export class RunInfoUiHandler extends UiHandler { rules.push(i18next.t("challenges:inverseBattle.shortName")); break; default: { - const localizationKey = Challenges[this.runInfo.challenges[i].id] - .split("_") - .map((f, i) => (i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase())) - .join(""); + const localizationKey = toCamelCase(Challenges[this.runInfo.challenges[i].id]); rules.push(i18next.t(`challenges:${localizationKey}.name`)); break; } @@ -791,15 +798,15 @@ export class RunInfoUiHandler extends UiHandler { pStats[i] = isMult < 1 ? pStats[i] + "[color=#40c8f8]↓[/color]" : pStats[i]; pStats[i] = isMult > 1 ? pStats[i] + "[color=#f89890]↑[/color]" : pStats[i]; } - const hp = i18next.t("pokemonInfo:Stat.HPshortened") + ": " + pStats[0]; - const atk = i18next.t("pokemonInfo:Stat.ATKshortened") + ": " + pStats[1]; - const def = i18next.t("pokemonInfo:Stat.DEFshortened") + ": " + pStats[2]; - const spatk = i18next.t("pokemonInfo:Stat.SPATKshortened") + ": " + pStats[3]; - const spdef = i18next.t("pokemonInfo:Stat.SPDEFshortened") + ": " + pStats[4]; + const hp = i18next.t("pokemonInfo:stat.hpShortened") + ": " + pStats[0]; + const atk = i18next.t("pokemonInfo:stat.atkShortened") + ": " + pStats[1]; + const def = i18next.t("pokemonInfo:stat.defShortened") + ": " + pStats[2]; + const spatk = i18next.t("pokemonInfo:stat.spatkShortened") + ": " + pStats[3]; + const spdef = i18next.t("pokemonInfo:stat.spdefShortened") + ": " + pStats[4]; const speedLabel = currentLanguage === "es-ES" || currentLanguage === "pt_BR" - ? i18next.t("runHistory:SPDshortened") - : i18next.t("pokemonInfo:Stat.SPDshortened"); + ? i18next.t("runHistory:spdShortened") + : i18next.t("pokemonInfo:stat.spdShortened"); const speed = speedLabel + ": " + pStats[5]; // Column 1: HP Atk Def const pokeStatText1 = addBBCodeTextObject(-5, 0, hp, TextStyle.SUMMARY, { diff --git a/src/ui/save-slot-select-ui-handler.ts b/src/ui/save-slot-select-ui-handler.ts index 9c2f8488b22..e9f9c5a0038 100644 --- a/src/ui/save-slot-select-ui-handler.ts +++ b/src/ui/save-slot-select-ui-handler.ts @@ -1,12 +1,14 @@ import { GameMode } from "#app/game-mode"; import { globalScene } from "#app/global-scene"; import { Button } from "#enums/buttons"; +import { GameModes } from "#enums/game-modes"; import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; // biome-ignore lint/performance/noNamespaceImport: See `src/system/game-data.ts` import * as Modifier from "#modifiers/modifier"; import type { SessionSaveData } from "#system/game-data"; import type { PokemonData } from "#system/pokemon-data"; +import type { OptionSelectConfig } from "#ui/abstract-option-select-ui-handler"; import { MessageUiHandler } from "#ui/message-ui-handler"; import { RunDisplayMode } from "#ui/run-info-ui-handler"; import { addTextObject } from "#ui/text"; @@ -15,7 +17,7 @@ import { fixedInt, formatLargeNumber, getPlayTimeString, isNullOrUndefined } fro import i18next from "i18next"; const SESSION_SLOTS_COUNT = 5; -const SLOTS_ON_SCREEN = 3; +const SLOTS_ON_SCREEN = 2; export enum SaveSlotUiMode { LOAD, @@ -33,6 +35,7 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler { private uiMode: SaveSlotUiMode; private saveSlotSelectCallback: SaveSlotSelectCallback | null; + protected manageDataConfig: OptionSelectConfig; private scrollCursor = 0; @@ -101,6 +104,7 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler { processInput(button: Button): boolean { const ui = this.getUi(); + const manageDataOptions: any[] = []; let success = false; let error = false; @@ -109,14 +113,115 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler { const originalCallback = this.saveSlotSelectCallback; if (button === Button.ACTION) { const cursor = this.cursor + this.scrollCursor; - if (this.uiMode === SaveSlotUiMode.LOAD && !this.sessionSlots[cursor].hasData) { + const sessionSlot = this.sessionSlots[cursor]; + if (this.uiMode === SaveSlotUiMode.LOAD && !sessionSlot.hasData) { error = true; } else { switch (this.uiMode) { case SaveSlotUiMode.LOAD: - this.saveSlotSelectCallback = null; - originalCallback?.(cursor); + if (!sessionSlot.malformed) { + manageDataOptions.push({ + label: i18next.t("menu:loadGame"), + handler: () => { + globalScene.ui.revertMode(); + originalCallback?.(cursor); + return true; + }, + keepOpen: false, + }); + + manageDataOptions.push({ + label: i18next.t("saveSlotSelectUiHandler:renameRun"), + handler: () => { + globalScene.ui.revertMode(); + ui.setOverlayMode( + UiMode.RENAME_RUN, + { + buttonActions: [ + (sanitizedName: string) => { + const name = decodeURIComponent(atob(sanitizedName)); + globalScene.gameData.renameSession(cursor, name).then(response => { + if (response[0] === false) { + globalScene.reset(true); + } else { + this.clearSessionSlots(); + this.cursorObj = null; + this.populateSessionSlots(); + this.setScrollCursor(0); + this.setCursor(0); + ui.revertMode(); + ui.showText("", 0); + } + }); + }, + () => { + ui.revertMode(); + }, + ], + }, + "", + ); + return true; + }, + }); + } + + this.manageDataConfig = { + xOffset: 0, + yOffset: 48, + options: manageDataOptions, + maxOptions: 4, + }; + + manageDataOptions.push({ + label: i18next.t("saveSlotSelectUiHandler:deleteRun"), + handler: () => { + globalScene.ui.revertMode(); + ui.showText(i18next.t("saveSlotSelectUiHandler:deleteData"), null, () => { + ui.setOverlayMode( + UiMode.CONFIRM, + () => { + globalScene.gameData.tryClearSession(cursor).then(response => { + if (response[0] === false) { + globalScene.reset(true); + } else { + this.clearSessionSlots(); + this.cursorObj = null; + this.populateSessionSlots(); + this.setScrollCursor(0); + this.setCursor(0); + ui.revertMode(); + ui.showText("", 0); + } + }); + }, + () => { + ui.revertMode(); + ui.showText("", 0); + }, + false, + 0, + 19, + import.meta.env.DEV ? 300 : 2000, + ); + }); + return true; + }, + keepOpen: false, + }); + + manageDataOptions.push({ + label: i18next.t("menuUiHandler:cancel"), + handler: () => { + globalScene.ui.revertMode(); + return true; + }, + keepOpen: true, + }); + + ui.setOverlayMode(UiMode.MENU_OPTION_SELECT, this.manageDataConfig); break; + case SaveSlotUiMode.SAVE: { const saveAndCallback = () => { const originalCallback = this.saveSlotSelectCallback; @@ -161,6 +266,7 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler { } } else { this.saveSlotSelectCallback = null; + ui.showText("", 0); originalCallback?.(-1); success = true; } @@ -267,33 +373,34 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler { this.cursorObj = globalScene.add.container(0, 0); const cursorBox = globalScene.add.nineslice( 0, - 0, + 15, "select_cursor_highlight_thick", undefined, - 296, - 44, + 294, + this.sessionSlots[prevSlotIndex ?? 0]?.saveData?.name ? 50 : 60, 6, 6, 6, 6, ); const rightArrow = globalScene.add.image(0, 0, "cursor"); - rightArrow.setPosition(160, 0); + rightArrow.setPosition(160, 15); rightArrow.setName("rightArrow"); this.cursorObj.add([cursorBox, rightArrow]); this.sessionSlotsContainer.add(this.cursorObj); } const cursorPosition = cursor + this.scrollCursor; - const cursorIncrement = cursorPosition * 56; + const cursorIncrement = cursorPosition * 76; if (this.sessionSlots[cursorPosition] && this.cursorObj) { - const hasData = this.sessionSlots[cursorPosition].hasData; + const session = this.sessionSlots[cursorPosition]; + const hasData = session.hasData && !session.malformed; // If the session slot lacks session data, it does not move from its default, central position. // Only session slots with session data will move leftwards and have a visible arrow. if (!hasData) { - this.cursorObj.setPosition(151, 26 + cursorIncrement); + this.cursorObj.setPosition(151, 20 + cursorIncrement); this.sessionSlots[cursorPosition].setPosition(0, cursorIncrement); } else { - this.cursorObj.setPosition(145, 26 + cursorIncrement); + this.cursorObj.setPosition(145, 20 + cursorIncrement); this.sessionSlots[cursorPosition].setPosition(-6, cursorIncrement); } this.setArrowVisibility(hasData); @@ -311,7 +418,8 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler { revertSessionSlot(slotIndex: number): void { const sessionSlot = this.sessionSlots[slotIndex]; if (sessionSlot) { - sessionSlot.setPosition(0, slotIndex * 56); + const valueHeight = 76; + sessionSlot.setPosition(0, slotIndex * valueHeight); } } @@ -340,7 +448,7 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler { this.setCursor(this.cursor, prevSlotIndex); globalScene.tweens.add({ targets: this.sessionSlotsContainer, - y: this.sessionSlotsContainerInitialY - 56 * scrollCursor, + y: this.sessionSlotsContainerInitialY - 76 * scrollCursor, duration: fixedInt(325), ease: "Sine.easeInOut", }); @@ -374,12 +482,14 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler { class SessionSlot extends Phaser.GameObjects.Container { public slotId: number; public hasData: boolean; + /** Indicates the save slot ran into an error while being loaded */ + public malformed: boolean; + private slotWindow: Phaser.GameObjects.NineSlice; private loadingLabel: Phaser.GameObjects.Text; - public saveData: SessionSaveData; constructor(slotId: number) { - super(globalScene, 0, slotId * 56); + super(globalScene, 0, slotId * 76); this.slotId = slotId; @@ -387,32 +497,89 @@ class SessionSlot extends Phaser.GameObjects.Container { } setup() { - const slotWindow = addWindow(0, 0, 304, 52); - this.add(slotWindow); + this.slotWindow = addWindow(0, 0, 304, 70); + this.add(this.slotWindow); - this.loadingLabel = addTextObject(152, 26, i18next.t("saveSlotSelectUiHandler:loading"), TextStyle.WINDOW); + this.loadingLabel = addTextObject(152, 33, i18next.t("saveSlotSelectUiHandler:loading"), TextStyle.WINDOW); this.loadingLabel.setOrigin(0.5, 0.5); this.add(this.loadingLabel); } + /** + * Generates a name for sessions that don't have a name yet. + * @param data - The {@linkcode SessionSaveData} being checked + * @returns The default name for the given data. + */ + decideFallback(data: SessionSaveData): string { + let fallbackName = `${GameMode.getModeName(data.gameMode)}`; + switch (data.gameMode) { + case GameModes.CLASSIC: + fallbackName += ` (${globalScene.gameData.gameStats.classicSessionsPlayed + 1})`; + break; + case GameModes.ENDLESS: + case GameModes.SPLICED_ENDLESS: + fallbackName += ` (${globalScene.gameData.gameStats.endlessSessionsPlayed + 1})`; + break; + case GameModes.DAILY: { + const runDay = new Date(data.timestamp).toLocaleDateString(); + fallbackName += ` (${runDay})`; + break; + } + case GameModes.CHALLENGE: { + const activeChallenges = data.challenges.filter(c => c.value !== 0); + if (activeChallenges.length === 0) { + break; + } + + fallbackName = ""; + for (const challenge of activeChallenges.slice(0, 3)) { + if (fallbackName !== "") { + fallbackName += ", "; + } + fallbackName += challenge.toChallenge().getName(); + } + + if (activeChallenges.length > 3) { + fallbackName += ", ..."; + } else if (fallbackName === "") { + // Something went wrong when retrieving the names of the active challenges, + // so fall back to just naming the run "Challenge" + fallbackName = `${GameMode.getModeName(data.gameMode)}`; + } + break; + } + } + return fallbackName; + } + async setupWithData(data: SessionSaveData) { + const hasName = data?.name; this.remove(this.loadingLabel, true); + if (hasName) { + const nameLabel = addTextObject(8, 5, data.name, TextStyle.WINDOW); + this.add(nameLabel); + } else { + const fallbackName = this.decideFallback(data); + await globalScene.gameData.renameSession(this.slotId, fallbackName); + const nameLabel = addTextObject(8, 5, fallbackName, TextStyle.WINDOW); + this.add(nameLabel); + } const gameModeLabel = addTextObject( 8, - 5, + 19, `${GameMode.getModeName(data.gameMode) || i18next.t("gameMode:unknown")} - ${i18next.t("saveSlotSelectUiHandler:wave")} ${data.waveIndex}`, TextStyle.WINDOW, ); this.add(gameModeLabel); - const timestampLabel = addTextObject(8, 19, new Date(data.timestamp).toLocaleString(), TextStyle.WINDOW); + const timestampLabel = addTextObject(8, 33, new Date(data.timestamp).toLocaleString(), TextStyle.WINDOW); this.add(timestampLabel); - const playTimeLabel = addTextObject(8, 33, getPlayTimeString(data.playTime), TextStyle.WINDOW); + const playTimeLabel = addTextObject(8, 47, getPlayTimeString(data.playTime), TextStyle.WINDOW); this.add(playTimeLabel); - const pokemonIconsContainer = globalScene.add.container(144, 4); + const pokemonIconsContainer = globalScene.add.container(144, 16); data.party.forEach((p: PokemonData, i: number) => { const iconContainer = globalScene.add.container(26 * i, 0); iconContainer.setScale(0.75); @@ -427,13 +594,9 @@ class SessionSlot extends Phaser.GameObjects.Container { TextStyle.PARTY, { fontSize: "54px", color: "#f8f8f8" }, ); - text.setShadow(0, 0, undefined); - text.setStroke("#424242", 14); - text.setOrigin(1, 0); - - iconContainer.add(icon); - iconContainer.add(text); + text.setShadow(0, 0, undefined).setStroke("#424242", 14).setOrigin(1, 0); + iconContainer.add([icon, text]); pokemonIconsContainer.add(iconContainer); pokemon.destroy(); @@ -441,7 +604,7 @@ class SessionSlot extends Phaser.GameObjects.Container { this.add(pokemonIconsContainer); - const modifierIconsContainer = globalScene.add.container(148, 30); + const modifierIconsContainer = globalScene.add.container(148, 38); modifierIconsContainer.setScale(0.5); let visibleModifierIndex = 0; for (const m of data.modifiers) { @@ -464,22 +627,33 @@ class SessionSlot extends Phaser.GameObjects.Container { load(): Promise { return new Promise(resolve => { - globalScene.gameData.getSession(this.slotId).then(async sessionData => { - // Ignore the results if the view was exited - if (!this.active) { - return; - } - if (!sessionData) { - this.hasData = false; - this.loadingLabel.setText(i18next.t("saveSlotSelectUiHandler:empty")); - resolve(false); - return; - } - this.hasData = true; - this.saveData = sessionData; - await this.setupWithData(sessionData); - resolve(true); - }); + globalScene.gameData + .getSession(this.slotId) + .then(async sessionData => { + // Ignore the results if the view was exited + if (!this.active) { + return; + } + this.hasData = !!sessionData; + if (!sessionData) { + this.loadingLabel.setText(i18next.t("saveSlotSelectUiHandler:empty")); + resolve(false); + return; + } + this.saveData = sessionData; + this.setupWithData(sessionData); + resolve(true); + }) + .catch(e => { + if (!this.active) { + return; + } + console.warn(`Failed to load session slot #${this.slotId}:`, e); + this.loadingLabel.setText(i18next.t("menu:failedToLoadSession")); + this.hasData = true; + this.malformed = true; + resolve(true); + }); }); } } diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index fbcc6ae7e32..83a38ed7fe0 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -45,6 +45,7 @@ import type { Variant } from "#sprites/variant"; import { getVariantIcon, getVariantTint } from "#sprites/variant"; import { achvs } from "#system/achv"; import type { DexAttrProps, StarterAttributes, StarterMoveset } from "#system/game-data"; +import { RibbonData } from "#system/ribbons/ribbon-data"; import { SettingKeyboard } from "#system/settings-keyboard"; import type { DexEntry } from "#types/dex-data"; import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; @@ -72,7 +73,7 @@ import { import type { StarterPreferences } from "#utils/data"; import { loadStarterPreferences, saveStarterPreferences } from "#utils/data"; import { getPokemonSpeciesForm, getPokerusStarters } from "#utils/pokemon-utils"; -import { toTitleCase } from "#utils/strings"; +import { toCamelCase, toTitleCase } from "#utils/strings"; import { argbFromRgba } from "@material/material-color-utilities"; import i18next from "i18next"; import type { GameObjects } from "phaser"; @@ -2263,7 +2264,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { }); }; options.push({ - label: i18next.t("menuUiHandler:POKEDEX"), + label: i18next.t("menuUiHandler:pokedex"), handler: () => { ui.setMode(UiMode.STARTER_SELECT).then(() => { const attributes = { @@ -3226,6 +3227,8 @@ export class StarterSelectUiHandler extends MessageUiHandler { onScreenFirstIndex + maxRows * maxColumns - 1, ); + const gameData = globalScene.gameData; + this.starterSelectScrollBar.setScrollCursor(this.scrollCursor); let pokerusCursorIndex = 0; @@ -3265,9 +3268,9 @@ export class StarterSelectUiHandler extends MessageUiHandler { container.label.setVisible(true); const speciesVariants = - speciesId && globalScene.gameData.dexData[speciesId].caughtAttr & DexAttr.SHINY + speciesId && gameData.dexData[speciesId].caughtAttr & DexAttr.SHINY ? [DexAttr.DEFAULT_VARIANT, DexAttr.VARIANT_2, DexAttr.VARIANT_3].filter( - v => !!(globalScene.gameData.dexData[speciesId].caughtAttr & v), + v => !!(gameData.dexData[speciesId].caughtAttr & v), ) : []; for (let v = 0; v < 3; v++) { @@ -3282,12 +3285,15 @@ export class StarterSelectUiHandler extends MessageUiHandler { } } - container.starterPassiveBgs.setVisible(!!globalScene.gameData.starterData[speciesId].passiveAttr); + container.starterPassiveBgs.setVisible(!!gameData.starterData[speciesId].passiveAttr); container.hiddenAbilityIcon.setVisible( - !!globalScene.gameData.dexData[speciesId].caughtAttr && - !!(globalScene.gameData.starterData[speciesId].abilityAttr & 4), + !!gameData.dexData[speciesId].caughtAttr && !!(gameData.starterData[speciesId].abilityAttr & 4), ); - container.classicWinIcon.setVisible(globalScene.gameData.starterData[speciesId].classicWinCount > 0); + container.classicWinIcon + .setVisible(gameData.starterData[speciesId].classicWinCount > 0) + .setTexture( + gameData.dexData[speciesId].ribbons.has(RibbonData.NUZLOCKE) ? "champion_ribbon_emerald" : "champion_ribbon", + ); container.favoriteIcon.setVisible(this.starterPreferences[speciesId]?.favorite ?? false); // 'Candy Icon' mode @@ -3464,7 +3470,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { //Growth translate let growthReadable = toTitleCase(GrowthRate[species.growthRate]); - const growthAux = growthReadable.replace(" ", "_"); + const growthAux = toCamelCase(growthReadable); if (i18next.exists("growth:" + growthAux)) { growthReadable = i18next.t(("growth:" + growthAux) as any); } diff --git a/src/ui/summary-ui-handler.ts b/src/ui/summary-ui-handler.ts index b6ce0a706f8..b6447f03587 100644 --- a/src/ui/summary-ui-handler.ts +++ b/src/ui/summary-ui-handler.ts @@ -37,7 +37,7 @@ import { rgbHexToRgba, } from "#utils/common"; import { getEnumValues } from "#utils/enums"; -import { toTitleCase } from "#utils/strings"; +import { toCamelCase, toTitleCase } from "#utils/strings"; import { argbFromRgba } from "@material/material-color-utilities"; import i18next from "i18next"; @@ -808,8 +808,8 @@ export class SummaryUiHandler extends UiHandler { globalScene.gameData.gender === PlayerGender.FEMALE ? TextStyle.SUMMARY_PINK : TextStyle.SUMMARY_BLUE; const usernameReplacement = globalScene.gameData.gender === PlayerGender.FEMALE - ? i18next.t("trainerNames:player_f") - : i18next.t("trainerNames:player_m"); + ? i18next.t("trainerNames:playerF") + : i18next.t("trainerNames:playerM"); // TODO: should add field for original trainer name to Pokemon object, to support gift/traded Pokemon from MEs const trainerText = addBBCodeTextObject( @@ -962,7 +962,7 @@ export class SummaryUiHandler extends UiHandler { this.passiveContainer?.descriptionText?.setVisible(false); const closeFragment = getBBCodeFrag("", TextStyle.WINDOW_ALT); - const rawNature = toTitleCase(Nature[this.pokemon?.getNature()!]); // TODO: is this bang correct? + const rawNature = toCamelCase(Nature[this.pokemon?.getNature()!]); // TODO: is this bang correct? const nature = `${getBBCodeFrag(toTitleCase(getNatureName(this.pokemon?.getNature()!)), TextStyle.SUMMARY_RED)}${closeFragment}`; // TODO: is this bang correct? const memoString = i18next.t("pokemonSummary:memoString", { diff --git a/src/ui/test-dialogue-ui-handler.ts b/src/ui/test-dialogue-ui-handler.ts index 4f825ed95ea..6f7c79a151b 100644 --- a/src/ui/test-dialogue-ui-handler.ts +++ b/src/ui/test-dialogue-ui-handler.ts @@ -31,7 +31,7 @@ export class TestDialogueUiHandler extends FormModalUiHandler { // we check for null or undefined here as per above - the typeof is still an object but the value is null so we need to exit out of this and pass the null key // Return in the format expected by i18next - return middleKey ? `${topKey}:${middleKey.map(m => m).join(".")}.${t}` : `${topKey}:${t}`; + return middleKey ? `${topKey}:${middleKey.join(".")}.${t}` : `${topKey}:${t}`; } }) .filter(t => t); diff --git a/src/ui/title-ui-handler.ts b/src/ui/title-ui-handler.ts index 36e37500a64..5ae195231e5 100644 --- a/src/ui/title-ui-handler.ts +++ b/src/ui/title-ui-handler.ts @@ -122,8 +122,8 @@ export class TitleUiHandler extends OptionSelectUiHandler { genderSplash(): void { if (this.splashMessage === "splashMessages:aprilFools.helloKyleAmber") { globalScene.gameData.gender === PlayerGender.MALE - ? this.splashMessageText.setText(i18next.t(this.splashMessage, { name: i18next.t("trainerNames:player_m") })) - : this.splashMessageText.setText(i18next.t(this.splashMessage, { name: i18next.t("trainerNames:player_f") })); + ? this.splashMessageText.setText(i18next.t(this.splashMessage, { name: i18next.t("trainerNames:playerM") })) + : this.splashMessageText.setText(i18next.t(this.splashMessage, { name: i18next.t("trainerNames:playerF") })); } } diff --git a/src/ui/ui.ts b/src/ui/ui.ts index d5baea07ed5..e381d205b78 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -60,6 +60,7 @@ import { addWindow } from "#ui/ui-theme"; import { UnavailableModalUiHandler } from "#ui/unavailable-modal-ui-handler"; import { executeIf } from "#utils/common"; import i18next from "i18next"; +import { RenameRunFormUiHandler } from "./rename-run-ui-handler"; const transitionModes = [ UiMode.SAVE_SLOT, @@ -98,6 +99,7 @@ const noTransitionModes = [ UiMode.SESSION_RELOAD, UiMode.UNAVAILABLE, UiMode.RENAME_POKEMON, + UiMode.RENAME_RUN, UiMode.TEST_DIALOGUE, UiMode.AUTO_COMPLETE, UiMode.ADMIN, @@ -168,6 +170,7 @@ export class UI extends Phaser.GameObjects.Container { new UnavailableModalUiHandler(), new GameChallengesUiHandler(), new RenameFormUiHandler(), + new RenameRunFormUiHandler(), new RunHistoryUiHandler(), new RunInfoUiHandler(), new TestDialogueUiHandler(UiMode.TEST_DIALOGUE), diff --git a/src/utils/challenge-utils.ts b/src/utils/challenge-utils.ts index 43297027e04..c4fac3a0323 100644 --- a/src/utils/challenge-utils.ts +++ b/src/utils/challenge-utils.ts @@ -4,6 +4,7 @@ import { pokemonEvolutions } from "#balance/pokemon-evolutions"; import { pokemonFormChanges } from "#data/pokemon-forms"; import type { PokemonSpecies } from "#data/pokemon-species"; import { ChallengeType } from "#enums/challenge-type"; +import { Challenges } from "#enums/challenges"; import type { MoveId } from "#enums/move-id"; import type { MoveSourceType } from "#enums/move-source-type"; import type { SpeciesId } from "#enums/species-id"; @@ -378,7 +379,7 @@ export function checkStarterValidForChallenge(species: PokemonSpecies, props: De * @param soft - If `true`, allow it if it could become valid through a form change. * @returns `true` if the species is considered valid. */ -function checkSpeciesValidForChallenge(species: PokemonSpecies, props: DexAttrProps, soft: boolean) { +export function checkSpeciesValidForChallenge(species: PokemonSpecies, props: DexAttrProps, soft: boolean) { const isValidForChallenge = new BooleanHolder(true); applyChallenges(ChallengeType.STARTER_CHOICE, species, isValidForChallenge, props); if (!soft || !pokemonFormChanges.hasOwnProperty(species.speciesId)) { @@ -407,3 +408,28 @@ function checkSpeciesValidForChallenge(species: PokemonSpecies, props: DexAttrPr }); return result; } + +/** @returns Whether the current game mode meets the criteria to be considered a Nuzlocke challenge */ +export function isNuzlockeChallenge(): boolean { + let isFreshStart = false; + let isLimitedCatch = false; + let isHardcore = false; + for (const challenge of globalScene.gameMode.challenges) { + // value is 0 if challenge is not active + if (!challenge.value) { + continue; + } + switch (challenge.id) { + case Challenges.FRESH_START: + isFreshStart = true; + break; + case Challenges.LIMITED_CATCH: + isLimitedCatch = true; + break; + case Challenges.HARDCORE: + isHardcore = true; + break; + } + } + return isFreshStart && isLimitedCatch && isHardcore; +} diff --git a/src/utils/data.ts b/src/utils/data.ts index 932ea38d504..6580ecf2ee9 100644 --- a/src/utils/data.ts +++ b/src/utils/data.ts @@ -45,17 +45,17 @@ export function deepMergeSpriteData(dest: object, source: object) { } export function encrypt(data: string, bypassLogin: boolean): string { - return (bypassLogin - ? (data: string) => btoa(encodeURIComponent(data)) - : (data: string) => AES.encrypt(data, saveKey))(data) as unknown as string; // TODO: is this correct? + if (bypassLogin) { + return btoa(encodeURIComponent(data)); + } + return AES.encrypt(data, saveKey).toString(); } export function decrypt(data: string, bypassLogin: boolean): string { - return ( - bypassLogin - ? (data: string) => decodeURIComponent(atob(data)) - : (data: string) => AES.decrypt(data, saveKey).toString(enc.Utf8) - )(data); + if (bypassLogin) { + return decodeURIComponent(atob(data)); + } + return AES.decrypt(data, saveKey).toString(enc.Utf8); } // the latest data saved/loaded for the Starter Preferences. Required to reduce read/writes. Initialize as "{}", since this is the default value and no data needs to be stored if present. diff --git a/test/@types/test-helpers.ts b/test/@types/test-helpers.ts new file mode 100644 index 00000000000..b867eb32570 --- /dev/null +++ b/test/@types/test-helpers.ts @@ -0,0 +1,27 @@ +import type { AtLeastOne, NonFunctionPropertiesRecursive as nonFunc } from "#types/type-helpers"; + +/** + * Helper type to admit an object containing the given properties + * _and_ at least 1 other non-function property. + * @example + * ```ts + * type foo = { + * qux: 1 | 2 | 3, + * bar: number, + * baz: string + * quux: () => void; // ignored! + * } + * + * type quxAndSomethingElse = OneOther + * + * const good1: quxAndSomethingElse = {qux: 1, bar: 3} // OK! + * const good2: quxAndSomethingElse = {qux: 2, baz: "4", bar: 12} // OK! + * const bad1: quxAndSomethingElse = {baz: "4", bar: 12} // Errors because `qux` is required + * const bad2: quxAndSomethingElse = {qux: 1} // Errors because at least 1 thing _other_ than `qux` is required + * ``` + * @typeParam O - The object to source keys from + * @typeParam K - One or more of O's keys to render mandatory + */ +export type OneOther = AtLeastOne, K>> & { + [key in K]: O[K]; +}; diff --git a/test/@types/vitest.d.ts b/test/@types/vitest.d.ts index 7b756c45a57..2ed0512538a 100644 --- a/test/@types/vitest.d.ts +++ b/test/@types/vitest.d.ts @@ -1,23 +1,34 @@ +import "vitest"; + import type { TerrainType } from "#app/data/terrain"; +import type Overrides from "#app/overrides"; +import type { ArenaTag } from "#data/arena-tag"; +import type { PositionalTag } from "#data/positional-tags/positional-tag"; import type { AbilityId } from "#enums/ability-id"; +import type { ArenaTagSide } from "#enums/arena-tag-side"; +import type { ArenaTagType } from "#enums/arena-tag-type"; import type { BattlerTagType } from "#enums/battler-tag-type"; import type { MoveId } from "#enums/move-id"; import type { PokemonType } from "#enums/pokemon-type"; +import type { PositionalTagType } from "#enums/positional-tag-type"; import type { BattleStat, EffectiveStat, Stat } from "#enums/stat"; import type { StatusEffect } from "#enums/status-effect"; import type { WeatherType } from "#enums/weather-type"; +import type { Arena } from "#field/arena"; import type { Pokemon } from "#field/pokemon"; -import type { ToHaveEffectiveStatMatcherOptions } from "#test/test-utils/matchers/to-have-effective-stat"; +import type { PokemonMove } from "#moves/pokemon-move"; +import type { toHaveArenaTagOptions } from "#test/test-utils/matchers/to-have-arena-tag"; +import type { toHaveEffectiveStatOptions } from "#test/test-utils/matchers/to-have-effective-stat"; +import type { toHavePositionalTagOptions } from "#test/test-utils/matchers/to-have-positional-tag"; import type { expectedStatusType } from "#test/test-utils/matchers/to-have-status-effect"; import type { toHaveTypesOptions } from "#test/test-utils/matchers/to-have-types"; import type { TurnMove } from "#types/turn-move"; import type { AtLeastOne } from "#types/type-helpers"; +import type { toDmgValue } from "utils/common"; import type { expect } from "vitest"; -import type Overrides from "#app/overrides"; -import type { PokemonMove } from "#moves/pokemon-move"; declare module "vitest" { - interface Assertion { + interface Assertion { /** * Check whether an array contains EXACTLY the given items (in any order). * @@ -27,45 +38,9 @@ declare module "vitest" { * @param expected - The expected contents of the array, in any order * @see {@linkcode expect.arrayContaining} */ - toEqualArrayUnsorted(expected: E[]): void; + toEqualArrayUnsorted(expected: T[]): void; - /** - * Check whether a {@linkcode Pokemon}'s current typing includes the given types. - * - * @param expected - The expected types (in any order) - * @param options - The options passed to the matcher - */ - toHaveTypes(expected: [PokemonType, ...PokemonType[]], options?: toHaveTypesOptions): void; - - /** - * Matcher to check the contents of a {@linkcode Pokemon}'s move history. - * - * @param expectedValue - The expected value; can be a {@linkcode MoveId} or a partially filled {@linkcode TurnMove} - * containing the desired properties to check - * @param index - The index of the move history entry to check, in order from most recent to least recent. - * Default `0` (last used move) - * @see {@linkcode Pokemon.getLastXMoves} - */ - toHaveUsedMove(expected: MoveId | AtLeastOne, index?: number): void; - - /** - * Check whether a {@linkcode Pokemon}'s effective stat is as expected - * (checked after all stat value modifications). - * - * @param stat - The {@linkcode EffectiveStat} to check - * @param expectedValue - The expected value of {@linkcode stat} - * @param options - (Optional) The {@linkcode ToHaveEffectiveStatMatcherOptions} - * @remarks - * If you want to check the stat **before** modifiers are applied, use {@linkcode Pokemon.getStat} instead. - */ - toHaveEffectiveStat(stat: EffectiveStat, expectedValue: number, options?: ToHaveEffectiveStatMatcherOptions): void; - - /** - * Check whether a {@linkcode Pokemon} has taken a specific amount of damage. - * @param expectedDamageTaken - The expected amount of damage taken - * @param roundDown - Whether to round down {@linkcode expectedDamageTaken} with {@linkcode toDmgValue}; default `true` - */ - toHaveTakenDamage(expectedDamageTaken: number, roundDown?: boolean): void; + // #region Arena Matchers /** * Check whether the current {@linkcode WeatherType} is as expected. @@ -80,9 +55,60 @@ declare module "vitest" { toHaveTerrain(expectedTerrainType: TerrainType): void; /** - * Check whether a {@linkcode Pokemon} is at full HP. + * Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}. + * @param expectedTag - A partially-filled {@linkcode ArenaTag} containing the desired properties */ - toHaveFullHp(): void; + toHaveArenaTag(expectedTag: toHaveArenaTagOptions): void; + /** + * Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}. + * @param expectedType - The {@linkcode ArenaTagType} of the desired tag + * @param side - The {@linkcode ArenaTagSide | side(s) of the field} the tag should affect; default {@linkcode ArenaTagSide.BOTH} + */ + toHaveArenaTag(expectedType: ArenaTagType, side?: ArenaTagSide): void; + + /** + * Check whether the current {@linkcode Arena} contains the given {@linkcode PositionalTag}. + * @param expectedTag - A partially-filled `PositionalTag` containing the desired properties + */ + toHavePositionalTag

(expectedTag: toHavePositionalTagOptions

): void; + /** + * Check whether the current {@linkcode Arena} contains the given number of {@linkcode PositionalTag}s. + * @param expectedType - The {@linkcode PositionalTagType} of the desired tag + * @param count - The number of instances of {@linkcode expectedType} that should be active; + * defaults to `1` and must be within the range `[0, 4]` + */ + toHavePositionalTag(expectedType: PositionalTagType, count?: number): void; + + // #endregion Arena Matchers + + // #region Pokemon Matchers + + /** + * Check whether a {@linkcode Pokemon}'s current typing includes the given types. + * @param expectedTypes - The expected {@linkcode PokemonType}s to check against; must have length `>0` + * @param options - The {@linkcode toHaveTypesOptions | options} passed to the matcher + */ + toHaveTypes(expectedTypes: PokemonType[], options?: toHaveTypesOptions): void; + + /** + * Check whether a {@linkcode Pokemon} has used a move matching the given criteria. + * @param expectedMove - The {@linkcode MoveId} the Pokemon is expected to have used, + * or a partially filled {@linkcode TurnMove} containing the desired properties to check + * @param index - The index of the move history entry to check, in order from most recent to least recent; default `0` + * @see {@linkcode Pokemon.getLastXMoves} + */ + toHaveUsedMove(expectedMove: MoveId | AtLeastOne, index?: number): void; + + /** + * Check whether a {@linkcode Pokemon}'s effective stat is as expected + * (checked after all stat value modifications). + * @param stat - The {@linkcode EffectiveStat} to check + * @param expectedValue - The expected value of {@linkcode stat} + * @param options - The {@linkcode toHaveEffectiveStatOptions | options} passed to the matcher + * @remarks + * If you want to check the stat **before** modifiers are applied, use {@linkcode Pokemon.getStat} instead. + */ + toHaveEffectiveStat(stat: EffectiveStat, expectedValue: number, options?: toHaveEffectiveStatOptions): void; /** * Check whether a {@linkcode Pokemon} has a specific {@linkcode StatusEffect | non-volatile status effect}. @@ -106,7 +132,7 @@ declare module "vitest" { /** * Check whether a {@linkcode Pokemon} has applied a specific {@linkcode AbilityId}. - * @param expectedAbilityId - The expected {@linkcode AbilityId} + * @param expectedAbilityId - The `AbilityId` to check for */ toHaveAbilityApplied(expectedAbilityId: AbilityId): void; @@ -116,24 +142,36 @@ declare module "vitest" { */ toHaveHp(expectedHp: number): void; + /** + * Check whether a {@linkcode Pokemon} has taken a specific amount of damage. + * @param expectedDamageTaken - The expected amount of damage taken + * @param roundDown - Whether to round down `expectedDamageTaken` with {@linkcode toDmgValue}; default `true` + */ + toHaveTakenDamage(expectedDamageTaken: number, roundDown?: boolean): void; + /** * Check whether a {@linkcode Pokemon} is currently fainted (as determined by {@linkcode Pokemon.isFainted}). * @remarks - * When checking whether an enemy wild Pokemon is fainted, one must reference it in a variable _before_ the fainting effect occurs - * as otherwise the Pokemon will be GC'ed and rendered `undefined`. + * When checking whether an enemy wild Pokemon is fainted, one must store a reference to it in a variable _before_ the fainting effect occurs. + * Otherwise, the Pokemon will be removed from the field and garbage collected. */ toHaveFainted(): void; + /** + * Check whether a {@linkcode Pokemon} is at full HP. + */ + toHaveFullHp(): void; /** * Check whether a {@linkcode Pokemon} has consumed the given amount of PP for one of its moves. - * @param expectedValue - The {@linkcode MoveId} of the {@linkcode PokemonMove} that should have consumed PP + * @param moveId - The {@linkcode MoveId} corresponding to the {@linkcode PokemonMove} that should have consumed PP * @param ppUsed - The numerical amount of PP that should have been consumed, * or `all` to indicate the move should be _out_ of PP * @remarks - * If the Pokemon's moveset has been set via {@linkcode Overrides.MOVESET_OVERRIDE}/{@linkcode Overrides.OPP_MOVESET_OVERRIDE}, - * does not contain {@linkcode expectedMove} - * or contains the desired move more than once, this will fail the test. + * If the Pokemon's moveset has been set via {@linkcode Overrides.MOVESET_OVERRIDE}/{@linkcode Overrides.ENEMY_MOVESET_OVERRIDE} + * or does not contain exactly one copy of `moveId`, this will fail the test. */ - toHaveUsedPP(expectedMove: MoveId, ppUsed: number | "all"): void; + toHaveUsedPP(moveId: MoveId, ppUsed: number | "all"): void; + + // #endregion Pokemon Matchers } } diff --git a/test/abilities/corrosion.test.ts b/test/abilities/corrosion.test.ts index 490a365394b..965c00290fe 100644 --- a/test/abilities/corrosion.test.ts +++ b/test/abilities/corrosion.test.ts @@ -1,6 +1,7 @@ import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; +import { StatusEffect } from "#enums/status-effect"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -22,25 +23,66 @@ describe("Abilities - Corrosion", () => { beforeEach(() => { game = new GameManager(phaserGame); game.override - .moveset([MoveId.SPLASH]) .battleStyle("single") .criticalHits(false) .enemySpecies(SpeciesId.GRIMER) - .enemyAbility(AbilityId.CORROSION) - .enemyMoveset(MoveId.TOXIC); + .ability(AbilityId.CORROSION) + .enemyAbility(AbilityId.NO_GUARD) + .enemyMoveset(MoveId.SPLASH); }); - it("If a Poison- or Steel-type Pokémon with this Ability poisons a target with Synchronize, Synchronize does not gain the ability to poison Poison- or Steel-type Pokémon.", async () => { - game.override.ability(AbilityId.SYNCHRONIZE); - await game.classicMode.startBattle([SpeciesId.FEEBAS]); + it.each<{ name: string; species: SpeciesId }>([ + { name: "Poison", species: SpeciesId.GRIMER }, + { name: "Steel", species: SpeciesId.KLINK }, + ])("should grant the user the ability to poison $name-type opponents", async ({ species }) => { + game.override.enemySpecies(species); + await game.classicMode.startBattle([SpeciesId.SALANDIT]); - const playerPokemon = game.scene.getPlayerPokemon(); - const enemyPokemon = game.scene.getEnemyPokemon(); - expect(playerPokemon!.status).toBeUndefined(); + const enemy = game.field.getEnemyPokemon(); + expect(enemy.status?.effect).toBeUndefined(); - game.move.select(MoveId.SPLASH); - await game.phaseInterceptor.to("BerryPhase"); - expect(playerPokemon!.status).toBeDefined(); - expect(enemyPokemon!.status).toBeUndefined(); + game.move.use(MoveId.POISON_GAS); + await game.toEndOfTurn(); + + expect(enemy.status?.effect).toBe(StatusEffect.POISON); + }); + + it("should not affect Toxic Spikes", async () => { + await game.classicMode.startBattle([SpeciesId.SALANDIT]); + + game.move.use(MoveId.TOXIC_SPIKES); + await game.doKillOpponents(); + await game.toNextWave(); + + const enemyPokemon = game.field.getEnemyPokemon(); + expect(enemyPokemon.status).toBeUndefined(); + }); + + it("should not affect an opponent's Synchronize ability", async () => { + game.override.enemyAbility(AbilityId.SYNCHRONIZE); + await game.classicMode.startBattle([SpeciesId.ARBOK]); + + const playerPokemon = game.field.getPlayerPokemon(); + const enemyPokemon = game.field.getEnemyPokemon(); + expect(enemyPokemon.status?.effect).toBeUndefined(); + + game.move.use(MoveId.TOXIC); + await game.toEndOfTurn(); + + expect(enemyPokemon.status?.effect).toBe(StatusEffect.TOXIC); + expect(playerPokemon.status?.effect).toBeUndefined(); + }); + + it("should affect the user's held Toxic Orb", async () => { + game.override.startingHeldItems([{ name: "TOXIC_ORB", count: 1 }]); + await game.classicMode.startBattle([SpeciesId.SALAZZLE]); + + const salazzle = game.field.getPlayerPokemon(); + expect(salazzle.status?.effect).toBeUndefined(); + + game.move.use(MoveId.SPLASH); + await game.toNextTurn(); + + expect(salazzle.status?.effect).toBe(StatusEffect.TOXIC); }); }); diff --git a/test/abilities/disguise.test.ts b/test/abilities/disguise.test.ts index 4745d6ab609..f36501cb647 100644 --- a/test/abilities/disguise.test.ts +++ b/test/abilities/disguise.test.ts @@ -134,7 +134,7 @@ describe("Abilities - Disguise", () => { }); await game.classicMode.startBattle([SpeciesId.FURRET, SpeciesId.MIMIKYU]); - const mimikyu = game.scene.getPlayerParty()[1]!; + const mimikyu = game.scene.getPlayerParty()[1]; expect(mimikyu.formIndex).toBe(bustedForm); game.move.select(MoveId.SPLASH); diff --git a/test/abilities/healer.test.ts b/test/abilities/healer.test.ts index 52f47535bf4..43280ff8271 100644 --- a/test/abilities/healer.test.ts +++ b/test/abilities/healer.test.ts @@ -49,6 +49,7 @@ describe("Abilities - Healer", () => { const user = game.field.getPlayerPokemon(); // Only want one magikarp to have the ability vi.spyOn(user, "getAbility").mockReturnValue(allAbilities[AbilityId.HEALER]); + game.move.select(MoveId.SPLASH); // faint the ally game.move.select(MoveId.LUNAR_DANCE, 1); @@ -62,9 +63,10 @@ describe("Abilities - Healer", () => { it("should heal the status of an ally if the ally has a status", async () => { await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]); const [user, ally] = game.scene.getPlayerField(); + // Only want one magikarp to have the ability. vi.spyOn(user, "getAbility").mockReturnValue(allAbilities[AbilityId.HEALER]); - expect(ally.trySetStatus(StatusEffect.BURN)).toBe(true); + ally.doSetStatus(StatusEffect.BURN); game.move.select(MoveId.SPLASH); game.move.select(MoveId.SPLASH, 1); @@ -80,7 +82,7 @@ describe("Abilities - Healer", () => { const [user, ally] = game.scene.getPlayerField(); // Only want one magikarp to have the ability. vi.spyOn(user, "getAbility").mockReturnValue(allAbilities[AbilityId.HEALER]); - expect(ally.trySetStatus(StatusEffect.BURN)).toBe(true); + ally.doSetStatus(StatusEffect.BURN); game.move.select(MoveId.SPLASH); game.move.select(MoveId.SPLASH, 1); await game.phaseInterceptor.to("TurnEndPhase"); diff --git a/test/abilities/infiltrator.test.ts b/test/abilities/infiltrator.test.ts index a093fbbe6c6..24fb1b24540 100644 --- a/test/abilities/infiltrator.test.ts +++ b/test/abilities/infiltrator.test.ts @@ -79,9 +79,9 @@ describe("Abilities - Infiltrator", () => { game.scene.arena.addTag(ArenaTagType.SAFEGUARD, 1, MoveId.NONE, enemy.id, ArenaTagSide.ENEMY, true); - game.move.select(MoveId.SPORE); + game.move.use(MoveId.SPORE); + await game.toEndOfTurn(); - await game.phaseInterceptor.to("BerryPhase", false); expect(enemy.status?.effect).toBe(StatusEffect.SLEEP); expect(player.waveData.abilitiesApplied).toContain(AbilityId.INFILTRATOR); }); diff --git a/test/abilities/insomnia.test.ts b/test/abilities/insomnia.test.ts deleted file mode 100644 index 679220687b9..00000000000 --- a/test/abilities/insomnia.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { AbilityId } from "#enums/ability-id"; -import { MoveId } from "#enums/move-id"; -import { SpeciesId } from "#enums/species-id"; -import { StatusEffect } from "#enums/status-effect"; -import { GameManager } from "#test/test-utils/game-manager"; -import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; - -describe("Abilities - Insomnia", () => { - let phaserGame: Phaser.Game; - let game: GameManager; - - beforeAll(() => { - phaserGame = new Phaser.Game({ - type: Phaser.HEADLESS, - }); - }); - - afterEach(() => { - game.phaseInterceptor.restoreOg(); - }); - - beforeEach(() => { - game = new GameManager(phaserGame); - game.override - .moveset([MoveId.SPLASH]) - .ability(AbilityId.BALL_FETCH) - .battleStyle("single") - .criticalHits(false) - .enemySpecies(SpeciesId.MAGIKARP) - .enemyAbility(AbilityId.BALL_FETCH) - .enemyMoveset(MoveId.SPLASH); - }); - - it("should remove sleep when gained", async () => { - game.override - .ability(AbilityId.INSOMNIA) - .enemyAbility(AbilityId.BALL_FETCH) - .moveset(MoveId.SKILL_SWAP) - .enemyMoveset(MoveId.SPLASH); - await game.classicMode.startBattle([SpeciesId.FEEBAS]); - const enemy = game.scene.getEnemyPokemon(); - enemy?.trySetStatus(StatusEffect.SLEEP); - expect(enemy?.status?.effect).toBe(StatusEffect.SLEEP); - - game.move.select(MoveId.SKILL_SWAP); - await game.phaseInterceptor.to("BerryPhase"); - - expect(enemy?.status).toBeNull(); - }); -}); diff --git a/test/abilities/limber.test.ts b/test/abilities/limber.test.ts deleted file mode 100644 index e65a54b545d..00000000000 --- a/test/abilities/limber.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { AbilityId } from "#enums/ability-id"; -import { MoveId } from "#enums/move-id"; -import { SpeciesId } from "#enums/species-id"; -import { StatusEffect } from "#enums/status-effect"; -import { GameManager } from "#test/test-utils/game-manager"; -import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; - -describe("Abilities - Limber", () => { - let phaserGame: Phaser.Game; - let game: GameManager; - - beforeAll(() => { - phaserGame = new Phaser.Game({ - type: Phaser.HEADLESS, - }); - }); - - afterEach(() => { - game.phaseInterceptor.restoreOg(); - }); - - beforeEach(() => { - game = new GameManager(phaserGame); - game.override - .moveset([MoveId.SPLASH]) - .ability(AbilityId.BALL_FETCH) - .battleStyle("single") - .criticalHits(false) - .enemySpecies(SpeciesId.MAGIKARP) - .enemyAbility(AbilityId.BALL_FETCH) - .enemyMoveset(MoveId.SPLASH); - }); - - it("should remove paralysis when gained", async () => { - game.override - .ability(AbilityId.LIMBER) - .enemyAbility(AbilityId.BALL_FETCH) - .moveset(MoveId.SKILL_SWAP) - .enemyMoveset(MoveId.SPLASH); - await game.classicMode.startBattle([SpeciesId.FEEBAS]); - const enemy = game.scene.getEnemyPokemon(); - enemy?.trySetStatus(StatusEffect.PARALYSIS); - expect(enemy?.status?.effect).toBe(StatusEffect.PARALYSIS); - - game.move.select(MoveId.SKILL_SWAP); - await game.phaseInterceptor.to("BerryPhase"); - - expect(enemy?.status).toBeNull(); - }); -}); diff --git a/test/abilities/magma-armor.test.ts b/test/abilities/magma-armor.test.ts deleted file mode 100644 index 2e7176fdf96..00000000000 --- a/test/abilities/magma-armor.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { AbilityId } from "#enums/ability-id"; -import { MoveId } from "#enums/move-id"; -import { SpeciesId } from "#enums/species-id"; -import { StatusEffect } from "#enums/status-effect"; -import { GameManager } from "#test/test-utils/game-manager"; -import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; - -describe("Abilities - Magma Armor", () => { - let phaserGame: Phaser.Game; - let game: GameManager; - - beforeAll(() => { - phaserGame = new Phaser.Game({ - type: Phaser.HEADLESS, - }); - }); - - afterEach(() => { - game.phaseInterceptor.restoreOg(); - }); - - beforeEach(() => { - game = new GameManager(phaserGame); - game.override - .moveset([MoveId.SPLASH]) - .ability(AbilityId.BALL_FETCH) - .battleStyle("single") - .criticalHits(false) - .enemySpecies(SpeciesId.MAGIKARP) - .enemyAbility(AbilityId.BALL_FETCH) - .enemyMoveset(MoveId.SPLASH); - }); - - it("should remove freeze when gained", async () => { - game.override - .ability(AbilityId.MAGMA_ARMOR) - .enemyAbility(AbilityId.BALL_FETCH) - .moveset(MoveId.SKILL_SWAP) - .enemyMoveset(MoveId.SPLASH); - await game.classicMode.startBattle([SpeciesId.FEEBAS]); - const enemy = game.scene.getEnemyPokemon(); - enemy?.trySetStatus(StatusEffect.FREEZE); - expect(enemy?.status?.effect).toBe(StatusEffect.FREEZE); - - game.move.select(MoveId.SKILL_SWAP); - await game.phaseInterceptor.to("BerryPhase"); - - expect(enemy?.status).toBeNull(); - }); -}); diff --git a/test/abilities/moxie.test.ts b/test/abilities/moxie.test.ts index 042a8ddd058..d762187baba 100644 --- a/test/abilities/moxie.test.ts +++ b/test/abilities/moxie.test.ts @@ -3,9 +3,6 @@ import { BattlerIndex } from "#enums/battler-index"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; -import { EnemyCommandPhase } from "#phases/enemy-command-phase"; -import { TurnEndPhase } from "#phases/turn-end-phase"; -import { VictoryPhase } from "#phases/victory-phase"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -46,7 +43,7 @@ describe("Abilities - Moxie", () => { expect(playerPokemon.getStatStage(Stat.ATK)).toBe(0); game.move.select(moveToUse); - await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(VictoryPhase); + await game.phaseInterceptor.to("VictoryPhase"); expect(playerPokemon.getStatStage(Stat.ATK)).toBe(1); }); @@ -67,7 +64,7 @@ describe("Abilities - Moxie", () => { game.move.select(moveToUse, BattlerIndex.PLAYER_2); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); expect(firstPokemon.getStatStage(Stat.ATK)).toBe(1); }, diff --git a/test/abilities/sheer-force.test.ts b/test/abilities/sheer-force.test.ts index 0e2ce85bfca..10fd454d6c2 100644 --- a/test/abilities/sheer-force.test.ts +++ b/test/abilities/sheer-force.test.ts @@ -121,8 +121,8 @@ describe("Abilities - Sheer Force", () => { await game.classicMode.startBattle([SpeciesId.PIDGEOT]); - const pidgeot = game.scene.getPlayerParty()[0]; - const onix = game.scene.getEnemyParty()[0]; + const pidgeot = game.field.getPlayerPokemon(); + const onix = game.field.getEnemyPokemon(); pidgeot.stats[Stat.DEF] = 10000; onix.stats[Stat.DEF] = 10000; diff --git a/test/abilities/shields-down.test.ts b/test/abilities/shields-down.test.ts index 98a1cfffa8e..77cec525442 100644 --- a/test/abilities/shields-down.test.ts +++ b/test/abilities/shields-down.test.ts @@ -2,14 +2,17 @@ import { Status } from "#data/status-effect"; import { AbilityId } from "#enums/ability-id"; import { BattlerTagType } from "#enums/battler-tag-type"; import { MoveId } from "#enums/move-id"; +import { MoveResult } from "#enums/move-result"; import { SpeciesId } from "#enums/species-id"; import { StatusEffect } from "#enums/status-effect"; -import { QuietFormChangePhase } from "#phases/quiet-form-change-phase"; -import { TurnEndPhase } from "#phases/turn-end-phase"; import { GameManager } from "#test/test-utils/game-manager"; -import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Abilities - Shields Down", () => { + const redMeteorForm = 0; + const redCoreForm = 7; + const orangeCoreForm = 8; -describe("Abilities - SHIELDS DOWN", () => { let phaserGame: Phaser.Game; let game: GameManager; @@ -25,159 +28,163 @@ describe("Abilities - SHIELDS DOWN", () => { beforeEach(() => { game = new GameManager(phaserGame); - const moveToUse = MoveId.SPLASH; game.override .battleStyle("single") .ability(AbilityId.SHIELDS_DOWN) - .moveset([moveToUse]) - .enemyMoveset([MoveId.TACKLE]); + .enemySpecies(SpeciesId.PSYDUCK) + .enemyMoveset(MoveId.SPLASH); }); - test("check if fainted pokemon switched to base form on arena reset", async () => { - const meteorForm = 0, - coreForm = 7; - game.override.startingWave(4).starterForms({ - [SpeciesId.MINIOR]: coreForm, - }); + it.each([0, 1, 2, 3, 4, 5, 6])( + "should change from Meteor Form to Core Form on entry/turn end based on HP - form index %i", + async meteorIndex => { + game.override.starterForms({ + // Start in meteor form + [SpeciesId.MINIOR]: meteorIndex, + }); + await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MINIOR]); + + const minior = game.scene.getPlayerParty()[1]; + expect(minior.formIndex).toBe(meteorIndex); + minior.hp *= 0.49; + + // Switch to minior - should change to Core due to being <50% HP + game.doSwitchPokemon(1); + await game.toNextTurn(); + + expect(minior.formIndex).toBe(meteorIndex + 7); + + // Use roost to regain 50% HP; should transform back into Meteor Form at turn end + game.move.use(MoveId.ROOST); + await game.toNextTurn(); + + expect(minior.formIndex).toBe(meteorIndex); + }, + ); + + it("should revert to base form on arena reset, even when fainted", async () => { + game.override.startingWave(4).starterForms({ + [SpeciesId.MINIOR]: redCoreForm, + }); await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MINIOR]); - const minior = game.scene.getPlayerParty().find(p => p.species.speciesId === SpeciesId.MINIOR)!; - expect(minior).not.toBe(undefined); - expect(minior.formIndex).toBe(coreForm); + const minior = game.scene.getPlayerParty()[1]; + expect(minior.formIndex).toBe(redCoreForm); minior.hp = 0; minior.status = new Status(StatusEffect.FAINT); expect(minior.isFainted()).toBe(true); - game.move.select(MoveId.SPLASH); + game.move.use(MoveId.SPLASH); await game.doKillOpponents(); - await game.phaseInterceptor.to(TurnEndPhase); + await game.toEndOfTurn(); game.doSelectModifier(); - await game.phaseInterceptor.to(QuietFormChangePhase); + await game.phaseInterceptor.to("QuietFormChangePhase"); - expect(minior.formIndex).toBe(meteorForm); + expect(minior.formIndex).toBe(redMeteorForm); }); - test("should ignore non-volatile status moves", async () => { - game.override.enemyMoveset([MoveId.SPORE]); - - await game.classicMode.startBattle([SpeciesId.MINIOR]); - game.move.select(MoveId.SPLASH); - await game.phaseInterceptor.to(TurnEndPhase); - - expect(game.field.getPlayerPokemon().status).toBe(undefined); - }); - - test("should still ignore non-volatile status moves used by a pokemon with mold breaker", async () => { + // TODO: Move to mold breaker test file + it("should ignore Mold Breaker", async () => { game.override.enemyAbility(AbilityId.MOLD_BREAKER).enemyMoveset([MoveId.SPORE]); await game.classicMode.startBattle([SpeciesId.MINIOR]); - game.move.select(MoveId.SPLASH); - await game.move.selectEnemyMove(MoveId.SPORE); - await game.phaseInterceptor.to(TurnEndPhase); + game.move.use(MoveId.SPLASH); + await game.move.forceEnemyMove(MoveId.SPORE); + await game.toEndOfTurn(); - expect(game.field.getPlayerPokemon().status).toBe(undefined); + expect(game.field.getPlayerPokemon()).toHaveStatusEffect(StatusEffect.NONE); }); - test("should ignore non-volatile secondary status effects", async () => { - game.override.enemyMoveset([MoveId.NUZZLE]); - + it("should ignore non-volatile status effects & Yawn in Meteor Form", async () => { await game.classicMode.startBattle([SpeciesId.MINIOR]); - game.move.select(MoveId.SPLASH); - await game.phaseInterceptor.to(TurnEndPhase); - - expect(game.field.getPlayerPokemon().status).toBe(undefined); - }); - - test("should ignore status moves even through mold breaker", async () => { - game.override.enemyMoveset([MoveId.SPORE]).enemyAbility(AbilityId.MOLD_BREAKER); - - await game.classicMode.startBattle([SpeciesId.MINIOR]); - - game.move.select(MoveId.SPLASH); - - await game.phaseInterceptor.to(TurnEndPhase); - - expect(game.field.getPlayerPokemon().status).toBe(undefined); - }); - - // toxic spikes currently does not poison flying types when gravity is in effect - test.todo("should become poisoned by toxic spikes when grounded", async () => { - game.override - .enemyMoveset([MoveId.GRAVITY, MoveId.TOXIC_SPIKES, MoveId.SPLASH]) - .moveset([MoveId.GRAVITY, MoveId.SPLASH]); - - await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MINIOR]); - - // turn 1 - game.move.select(MoveId.GRAVITY); - await game.move.selectEnemyMove(MoveId.TOXIC_SPIKES); + game.move.use(MoveId.SPLASH); + await game.move.forceEnemyMove(MoveId.NUZZLE); + await game.toNextTurn(); + + const minior = game.field.getPlayerPokemon(); + expect(minior).toHaveStatusEffect(StatusEffect.NONE); + + game.move.use(MoveId.SPLASH); + await game.move.forceEnemyMove(MoveId.YAWN); + await game.toEndOfTurn(); + + // TODO: Yawn doesn't fail ATM when failing to be added + // expect(game.field.getEnemyPokemon()).toHaveUsedMove({ move: MoveId.YAWN, result: MoveResult.FAIL }); + expect(minior).not.toHaveBattlerTag(BattlerTagType.DROWSY); + }); + + it("should not ignore non-volatile status effects & Yawn in Core Form", async () => { + await game.classicMode.startBattle([SpeciesId.MINIOR]); + + // Drop minior to below half to prevent reverting to Core Form + const minior = game.field.getPlayerPokemon(); + minior.hp *= 0.49; + minior.formIndex = orangeCoreForm; + + game.move.use(MoveId.SPLASH); + await game.move.forceEnemyMove(MoveId.NUZZLE); + await game.toEndOfTurn(); + + expect(minior).toHaveStatusEffect(StatusEffect.PARALYSIS); + + minior.clearStatus(false, false); + expect(minior).toHaveStatusEffect(StatusEffect.NONE); + + game.move.use(MoveId.SPLASH); + await game.move.forceEnemyMove(MoveId.YAWN); + await game.toEndOfTurn(); + + expect(game.field.getEnemyPokemon()).toHaveUsedMove({ move: MoveId.YAWN, result: MoveResult.SUCCESS }); + expect(minior).toHaveBattlerTag(BattlerTagType.DROWSY); + }); + + // TODO: Gravity does not make a Pokemon be considered as "grounded" for hazards + it.todo("should be poisoned by toxic spikes when Gravity is active before changing forms", async () => { + await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MINIOR]); + + // Change minior to Core form in a state where it would revert to Meteor form on switch + const minior = game.scene.getPlayerParty()[1]; + minior.formIndex = redCoreForm; + + game.move.use(MoveId.GRAVITY); + await game.move.forceEnemyMove(MoveId.TOXIC_SPIKES); await game.toNextTurn(); - // turn 2 game.doSwitchPokemon(1); - await game.move.selectEnemyMove(MoveId.SPLASH); await game.toNextTurn(); - expect(game.field.getPlayerPokemon().species.speciesId).toBe(SpeciesId.MINIOR); - expect(game.field.getPlayerPokemon().species.formIndex).toBe(0); - expect(game.field.getPlayerPokemon().status?.effect).toBe(StatusEffect.POISON); + expect(minior.isOnField()).toBe(true); + expect(minior.formIndex).toBe(redMeteorForm); + expect(minior.isGrounded()).toBe(true); + expect(minior).toHaveStatusEffect(StatusEffect.POISON); }); - test("should ignore yawn", async () => { - game.override.enemyMoveset([MoveId.YAWN]); - - await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MINIOR]); - - game.move.select(MoveId.SPLASH); - await game.move.selectEnemyMove(MoveId.YAWN); - - await game.phaseInterceptor.to(TurnEndPhase); - expect(game.field.getPlayerPokemon().findTag(tag => tag.tagType === BattlerTagType.DROWSY)).toBe(undefined); - }); - - test("should not ignore volatile status effects", async () => { + it("should not ignore volatile status effects", async () => { game.override.enemyMoveset([MoveId.CONFUSE_RAY]); await game.classicMode.startBattle([SpeciesId.MINIOR]); - game.move.select(MoveId.SPLASH); - await game.move.selectEnemyMove(MoveId.CONFUSE_RAY); + game.move.use(MoveId.SPLASH); + await game.move.forceEnemyMove(MoveId.CONFUSE_RAY); - await game.phaseInterceptor.to(TurnEndPhase); + await game.toEndOfTurn(); - expect(game.field.getPlayerPokemon().findTag(tag => tag.tagType === BattlerTagType.CONFUSED)).not.toBe(undefined); + expect(game.field.getPlayerPokemon()).toHaveBattlerTag(BattlerTagType.CONFUSED); }); - // the `NoTransformAbilityAbAttr` attribute is not checked anywhere, so this test cannot pass. - test.todo("ditto should not be immune to status after transforming", async () => { - game.override.enemySpecies(SpeciesId.DITTO).enemyAbility(AbilityId.IMPOSTER).moveset([MoveId.SPLASH, MoveId.SPORE]); - + // TODO: The `NoTransformAbilityAbAttr` attribute is not checked anywhere, so this test cannot pass. + // TODO: Move this to a transform test + it.todo("should not activate when transformed", async () => { + game.override.enemyAbility(AbilityId.IMPOSTER); await game.classicMode.startBattle([SpeciesId.MINIOR]); - game.move.select(MoveId.SPORE); - await game.move.selectEnemyMove(MoveId.SPLASH); + game.move.use(MoveId.SPORE); + await game.toEndOfTurn(); - await game.phaseInterceptor.to(TurnEndPhase); - expect(game.field.getEnemyPokemon().status?.effect).toBe(StatusEffect.SLEEP); - }); - - test("should not prevent minior from receiving the fainted status effect in trainer battles", async () => { - game.override - .enemyMoveset([MoveId.TACKLE]) - .moveset([MoveId.THUNDERBOLT]) - .startingLevel(100) - .startingWave(5) - .enemySpecies(SpeciesId.MINIOR); - await game.classicMode.startBattle([SpeciesId.REGIELEKI]); - const minior = game.field.getEnemyPokemon(); - - game.move.select(MoveId.THUNDERBOLT); - await game.toNextTurn(); - expect(minior.isFainted()).toBe(true); - expect(minior.status?.effect).toBe(StatusEffect.FAINT); + expect(game.field.getEnemyPokemon()).toHaveStatusEffect(StatusEffect.SLEEP); }); }); diff --git a/test/abilities/status-immunity-ab-attrs.test.ts b/test/abilities/status-immunity-ab-attrs.test.ts new file mode 100644 index 00000000000..7df621d5577 --- /dev/null +++ b/test/abilities/status-immunity-ab-attrs.test.ts @@ -0,0 +1,95 @@ +import { allMoves } from "#data/data-lists"; +import { AbilityId } from "#enums/ability-id"; +import { MoveId } from "#enums/move-id"; +import { MoveResult } from "#enums/move-result"; +import { SpeciesId } from "#enums/species-id"; +import { StatusEffect } from "#enums/status-effect"; +import { StatusEffectAttr } from "#moves/move"; +import { GameManager } from "#test/test-utils/game-manager"; +import { toTitleCase } from "#utils/strings"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe.each<{ name: string; ability: AbilityId; status: StatusEffect }>([ + { name: "Vital Spirit", ability: AbilityId.VITAL_SPIRIT, status: StatusEffect.SLEEP }, + { name: "Insomnia", ability: AbilityId.INSOMNIA, status: StatusEffect.SLEEP }, + { name: "Immunity", ability: AbilityId.IMMUNITY, status: StatusEffect.POISON }, + { name: "Magma Armor", ability: AbilityId.MAGMA_ARMOR, status: StatusEffect.FREEZE }, + { name: "Limber", ability: AbilityId.LIMBER, status: StatusEffect.PARALYSIS }, + { name: "Thermal Exchange", ability: AbilityId.THERMAL_EXCHANGE, status: StatusEffect.BURN }, + { name: "Water Veil", ability: AbilityId.WATER_VEIL, status: StatusEffect.BURN }, + { name: "Water Bubble", ability: AbilityId.WATER_BUBBLE, status: StatusEffect.BURN }, +])("Abilities - $name", ({ ability, status }) => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .battleStyle("single") + .criticalHits(false) + .enemyLevel(100) + .enemySpecies(SpeciesId.MAGIKARP) + .enemyAbility(ability) + .enemyMoveset(MoveId.SPLASH); + + // Mock Lumina Crash and Spore to be our status-inflicting moves of choice + vi.spyOn(allMoves[MoveId.LUMINA_CRASH], "attrs", "get").mockReturnValue([new StatusEffectAttr(status, false)]); + vi.spyOn(allMoves[MoveId.SPORE], "attrs", "get").mockReturnValue([new StatusEffectAttr(status, false)]); + }); + + const statusStr = toTitleCase(StatusEffect[status]); + + it(`should prevent application of ${statusStr} without failing damaging moves`, async () => { + await game.classicMode.startBattle([SpeciesId.FEEBAS]); + + const karp = game.field.getEnemyPokemon(); + expect(karp.status?.effect).toBeUndefined(); + expect(karp.canSetStatus(status)).toBe(false); + + game.move.use(MoveId.LUMINA_CRASH); + await game.toEndOfTurn(); + + expect(karp.status?.effect).toBeUndefined(); + expect(game.field.getPlayerPokemon().getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); + }); + + it(`should cure ${statusStr} upon being gained`, async () => { + await game.classicMode.startBattle([SpeciesId.FEEBAS]); + + const feebas = game.field.getPlayerPokemon(); + feebas.doSetStatus(status); + expect(feebas.status?.effect).toBe(status); + + game.move.use(MoveId.SPLASH); + await game.move.forceEnemyMove(MoveId.SKILL_SWAP); + await game.toEndOfTurn(); + + expect(feebas.status?.effect).toBeUndefined(); + }); + + // TODO: This does not propagate failures currently + it.todo( + `should cause status moves inflicting ${statusStr} to count as failed if no other effects can be applied`, + async () => { + await game.classicMode.startBattle([SpeciesId.FEEBAS]); + + game.move.use(MoveId.SPORE); + await game.toEndOfTurn(); + + const karp = game.field.getEnemyPokemon(); + expect(karp.status?.effect).toBeUndefined(); + expect(game.field.getPlayerPokemon().getLastXMoves()[0].result).toBe(MoveResult.FAIL); + }, + ); +}); diff --git a/test/abilities/sturdy.test.ts b/test/abilities/sturdy.test.ts index 28d3098a420..4480653c12f 100644 --- a/test/abilities/sturdy.test.ts +++ b/test/abilities/sturdy.test.ts @@ -1,7 +1,6 @@ import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; -import type { EnemyPokemon } from "#field/pokemon"; import { DamageAnimPhase } from "#phases/damage-anim-phase"; import { MoveEndPhase } from "#phases/move-end-phase"; import { GameManager } from "#test/test-utils/game-manager"; @@ -38,13 +37,13 @@ describe("Abilities - Sturdy", () => { await game.classicMode.startBattle(); game.move.select(MoveId.CLOSE_COMBAT); await game.phaseInterceptor.to(MoveEndPhase); - expect(game.scene.getEnemyParty()[0].hp).toBe(1); + expect(game.field.getEnemyPokemon().hp).toBe(1); }); test("Sturdy doesn't activate when user is not at full HP", async () => { await game.classicMode.startBattle(); - const enemyPokemon: EnemyPokemon = game.scene.getEnemyParty()[0]; + const enemyPokemon = game.field.getEnemyPokemon(); enemyPokemon.hp = enemyPokemon.getMaxHp() - 1; game.move.select(MoveId.CLOSE_COMBAT); @@ -59,19 +58,7 @@ describe("Abilities - Sturdy", () => { game.move.select(MoveId.FISSURE); await game.phaseInterceptor.to(MoveEndPhase); - const enemyPokemon: EnemyPokemon = game.scene.getEnemyParty()[0]; + const enemyPokemon = game.field.getEnemyPokemon(); expect(enemyPokemon.isFullHp()).toBe(true); }); - - test("Sturdy is ignored by pokemon with `AbilityId.MOLD_BREAKER`", async () => { - game.override.ability(AbilityId.MOLD_BREAKER); - - await game.classicMode.startBattle(); - game.move.select(MoveId.CLOSE_COMBAT); - await game.phaseInterceptor.to(DamageAnimPhase); - - const enemyPokemon: EnemyPokemon = game.scene.getEnemyParty()[0]; - expect(enemyPokemon.hp).toBe(0); - expect(enemyPokemon.isFainted()).toBe(true); - }); }); diff --git a/test/abilities/thermal-exchange.test.ts b/test/abilities/thermal-exchange.test.ts deleted file mode 100644 index 193676ccc18..00000000000 --- a/test/abilities/thermal-exchange.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { AbilityId } from "#enums/ability-id"; -import { MoveId } from "#enums/move-id"; -import { SpeciesId } from "#enums/species-id"; -import { StatusEffect } from "#enums/status-effect"; -import { GameManager } from "#test/test-utils/game-manager"; -import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; - -describe("Abilities - Thermal Exchange", () => { - let phaserGame: Phaser.Game; - let game: GameManager; - - beforeAll(() => { - phaserGame = new Phaser.Game({ - type: Phaser.HEADLESS, - }); - }); - - afterEach(() => { - game.phaseInterceptor.restoreOg(); - }); - - beforeEach(() => { - game = new GameManager(phaserGame); - game.override - .moveset([MoveId.SPLASH]) - .ability(AbilityId.BALL_FETCH) - .battleStyle("single") - .criticalHits(false) - .enemySpecies(SpeciesId.MAGIKARP) - .enemyAbility(AbilityId.BALL_FETCH) - .enemyMoveset(MoveId.SPLASH); - }); - - it("should remove burn when gained", async () => { - game.override - .ability(AbilityId.THERMAL_EXCHANGE) - .enemyAbility(AbilityId.BALL_FETCH) - .moveset(MoveId.SKILL_SWAP) - .enemyMoveset(MoveId.SPLASH); - await game.classicMode.startBattle([SpeciesId.FEEBAS]); - const enemy = game.scene.getEnemyPokemon(); - enemy?.trySetStatus(StatusEffect.BURN); - expect(enemy?.status?.effect).toBe(StatusEffect.BURN); - - game.move.select(MoveId.SKILL_SWAP); - await game.phaseInterceptor.to("BerryPhase"); - - expect(enemy?.status).toBeNull(); - }); -}); diff --git a/test/abilities/vital-spirit.test.ts b/test/abilities/vital-spirit.test.ts deleted file mode 100644 index e5d80a66a8e..00000000000 --- a/test/abilities/vital-spirit.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { AbilityId } from "#enums/ability-id"; -import { MoveId } from "#enums/move-id"; -import { SpeciesId } from "#enums/species-id"; -import { StatusEffect } from "#enums/status-effect"; -import { GameManager } from "#test/test-utils/game-manager"; -import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; - -describe("Abilities - Vital Spirit", () => { - let phaserGame: Phaser.Game; - let game: GameManager; - - beforeAll(() => { - phaserGame = new Phaser.Game({ - type: Phaser.HEADLESS, - }); - }); - - afterEach(() => { - game.phaseInterceptor.restoreOg(); - }); - - beforeEach(() => { - game = new GameManager(phaserGame); - game.override - .moveset([MoveId.SPLASH]) - .ability(AbilityId.BALL_FETCH) - .battleStyle("single") - .criticalHits(false) - .enemySpecies(SpeciesId.MAGIKARP) - .enemyAbility(AbilityId.BALL_FETCH) - .enemyMoveset(MoveId.SPLASH); - }); - - it("should remove sleep when gained", async () => { - game.override - .ability(AbilityId.INSOMNIA) - .enemyAbility(AbilityId.BALL_FETCH) - .moveset(MoveId.SKILL_SWAP) - .enemyMoveset(MoveId.SPLASH); - await game.classicMode.startBattle([SpeciesId.FEEBAS]); - const enemy = game.scene.getEnemyPokemon(); - enemy?.trySetStatus(StatusEffect.SLEEP); - expect(enemy?.status?.effect).toBe(StatusEffect.SLEEP); - - game.move.select(MoveId.SKILL_SWAP); - await game.phaseInterceptor.to("BerryPhase"); - - expect(enemy?.status).toBeNull(); - }); -}); diff --git a/test/abilities/water-bubble.test.ts b/test/abilities/water-bubble.test.ts deleted file mode 100644 index 6be1ac51094..00000000000 --- a/test/abilities/water-bubble.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { AbilityId } from "#enums/ability-id"; -import { MoveId } from "#enums/move-id"; -import { SpeciesId } from "#enums/species-id"; -import { StatusEffect } from "#enums/status-effect"; -import { GameManager } from "#test/test-utils/game-manager"; -import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; - -describe("Abilities - Water Bubble", () => { - let phaserGame: Phaser.Game; - let game: GameManager; - - beforeAll(() => { - phaserGame = new Phaser.Game({ - type: Phaser.HEADLESS, - }); - }); - - afterEach(() => { - game.phaseInterceptor.restoreOg(); - }); - - beforeEach(() => { - game = new GameManager(phaserGame); - game.override - .moveset([MoveId.SPLASH]) - .ability(AbilityId.BALL_FETCH) - .battleStyle("single") - .criticalHits(false) - .enemySpecies(SpeciesId.MAGIKARP) - .enemyAbility(AbilityId.BALL_FETCH) - .enemyMoveset(MoveId.SPLASH); - }); - - it("should remove burn when gained", async () => { - game.override - .ability(AbilityId.THERMAL_EXCHANGE) - .enemyAbility(AbilityId.BALL_FETCH) - .moveset(MoveId.SKILL_SWAP) - .enemyMoveset(MoveId.SPLASH); - await game.classicMode.startBattle([SpeciesId.FEEBAS]); - const enemy = game.scene.getEnemyPokemon(); - enemy?.trySetStatus(StatusEffect.BURN); - expect(enemy?.status?.effect).toBe(StatusEffect.BURN); - - game.move.select(MoveId.SKILL_SWAP); - await game.phaseInterceptor.to("BerryPhase"); - - expect(enemy?.status).toBeNull(); - }); -}); diff --git a/test/abilities/water-veil.test.ts b/test/abilities/water-veil.test.ts deleted file mode 100644 index 0c7068ae209..00000000000 --- a/test/abilities/water-veil.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { AbilityId } from "#enums/ability-id"; -import { MoveId } from "#enums/move-id"; -import { SpeciesId } from "#enums/species-id"; -import { StatusEffect } from "#enums/status-effect"; -import { GameManager } from "#test/test-utils/game-manager"; -import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; - -describe("Abilities - Water Veil", () => { - let phaserGame: Phaser.Game; - let game: GameManager; - - beforeAll(() => { - phaserGame = new Phaser.Game({ - type: Phaser.HEADLESS, - }); - }); - - afterEach(() => { - game.phaseInterceptor.restoreOg(); - }); - - beforeEach(() => { - game = new GameManager(phaserGame); - game.override - .moveset([MoveId.SPLASH]) - .ability(AbilityId.BALL_FETCH) - .battleStyle("single") - .criticalHits(false) - .enemySpecies(SpeciesId.MAGIKARP) - .enemyAbility(AbilityId.BALL_FETCH) - .enemyMoveset(MoveId.SPLASH); - }); - - it("should remove burn when gained", async () => { - game.override - .ability(AbilityId.THERMAL_EXCHANGE) - .enemyAbility(AbilityId.BALL_FETCH) - .moveset(MoveId.SKILL_SWAP) - .enemyMoveset(MoveId.SPLASH); - await game.classicMode.startBattle([SpeciesId.FEEBAS]); - const enemy = game.scene.getEnemyPokemon(); - enemy?.trySetStatus(StatusEffect.BURN); - expect(enemy?.status?.effect).toBe(StatusEffect.BURN); - - game.move.select(MoveId.SKILL_SWAP); - await game.phaseInterceptor.to("BerryPhase"); - - expect(enemy?.status).toBeNull(); - }); -}); diff --git a/test/abilities/wimp-out.test.ts b/test/abilities/wimp-out.test.ts index 1e129f34a19..46fd5094255 100644 --- a/test/abilities/wimp-out.test.ts +++ b/test/abilities/wimp-out.test.ts @@ -336,7 +336,7 @@ describe("Abilities - Wimp Out", () => { game.move.select(MoveId.SPLASH); await game.phaseInterceptor.to("TurnEndPhase"); - expect(game.scene.getPlayerParty()[0].getHpRatio()).toEqual(0.51); + expect(game.field.getPlayerPokemon().getHpRatio()).toEqual(0.51); expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase"); expect(game.field.getPlayerPokemon().species.speciesId).toBe(SpeciesId.WIMPOD); }); @@ -344,8 +344,7 @@ describe("Abilities - Wimp Out", () => { it("Wimp Out activating should not cancel a double battle", async () => { game.override.battleStyle("double").enemyAbility(AbilityId.WIMP_OUT).enemyMoveset([MoveId.SPLASH]).enemyLevel(1); await game.classicMode.startBattle([SpeciesId.WIMPOD, SpeciesId.TYRUNT]); - const enemyLeadPokemon = game.scene.getEnemyParty()[0]; - const enemySecPokemon = game.scene.getEnemyParty()[1]; + const [enemyLeadPokemon, enemySecPokemon] = game.scene.getEnemyParty(); game.move.select(MoveId.FALSE_SWIPE, 0, BattlerIndex.ENEMY); game.move.select(MoveId.SPLASH, 1); diff --git a/test/abilities/zero-to-hero.test.ts b/test/abilities/zero-to-hero.test.ts index d9fa3580da2..003684894df 100644 --- a/test/abilities/zero-to-hero.test.ts +++ b/test/abilities/zero-to-hero.test.ts @@ -40,8 +40,7 @@ describe("Abilities - ZERO TO HERO", () => { await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.PALAFIN, SpeciesId.PALAFIN]); - const palafin1 = game.scene.getPlayerParty()[1]; - const palafin2 = game.scene.getPlayerParty()[2]; + const [, palafin1, palafin2] = game.scene.getPlayerParty(); expect(palafin1.formIndex).toBe(heroForm); expect(palafin2.formIndex).toBe(heroForm); palafin2.hp = 0; diff --git a/test/battle/battle-order.test.ts b/test/battle/battle-order.test.ts index 0ee23cd6418..0b24fcbfa7d 100644 --- a/test/battle/battle-order.test.ts +++ b/test/battle/battle-order.test.ts @@ -1,9 +1,7 @@ import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; -import { EnemyCommandPhase } from "#phases/enemy-command-phase"; -import { SelectTargetPhase } from "#phases/select-target-phase"; -import { TurnStartPhase } from "#phases/turn-start-phase"; +import type { TurnStartPhase } from "#phases/turn-start-phase"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -41,7 +39,7 @@ describe("Battle order", () => { vi.spyOn(enemyPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set enemyPokemon's speed to 150 game.move.select(MoveId.TACKLE); - await game.phaseInterceptor.run(EnemyCommandPhase); + await game.phaseInterceptor.to("TurnStartPhase", false); const playerPokemonIndex = playerPokemon.getBattlerIndex(); const enemyPokemonIndex = enemyPokemon.getBattlerIndex(); @@ -60,7 +58,7 @@ describe("Battle order", () => { vi.spyOn(enemyPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 50]); // set enemyPokemon's speed to 50 game.move.select(MoveId.TACKLE); - await game.phaseInterceptor.run(EnemyCommandPhase); + await game.phaseInterceptor.to("TurnStartPhase", false); const playerPokemonIndex = playerPokemon.getBattlerIndex(); const enemyPokemonIndex = enemyPokemon.getBattlerIndex(); @@ -84,7 +82,7 @@ describe("Battle order", () => { game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE, 1); - await game.phaseInterceptor.runFrom(SelectTargetPhase).to(TurnStartPhase, false); + await game.phaseInterceptor.to("TurnStartPhase", false); const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; const order = phase.getCommandOrder(); @@ -108,7 +106,7 @@ describe("Battle order", () => { game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE, 1); - await game.phaseInterceptor.runFrom(SelectTargetPhase).to(TurnStartPhase, false); + await game.phaseInterceptor.to("TurnStartPhase", false); const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; const order = phase.getCommandOrder(); @@ -132,7 +130,7 @@ describe("Battle order", () => { game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE, 1); - await game.phaseInterceptor.runFrom(SelectTargetPhase).to(TurnStartPhase, false); + await game.phaseInterceptor.to("TurnStartPhase", false); const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; const order = phase.getCommandOrder(); diff --git a/test/battle/battle.test.ts b/test/battle/battle.test.ts index 3dd154cf4eb..36e9bdd17b0 100644 --- a/test/battle/battle.test.ts +++ b/test/battle/battle.test.ts @@ -1,28 +1,13 @@ -import { getGameMode } from "#app/game-mode"; import { allSpecies } from "#data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { BiomeId } from "#enums/biome-id"; -import { GameModes } from "#enums/game-modes"; import { MoveId } from "#enums/move-id"; -import { PlayerGender } from "#enums/player-gender"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; import { UiMode } from "#enums/ui-mode"; -import { BattleEndPhase } from "#phases/battle-end-phase"; import { CommandPhase } from "#phases/command-phase"; -import { DamageAnimPhase } from "#phases/damage-anim-phase"; -import { EncounterPhase } from "#phases/encounter-phase"; -import { EnemyCommandPhase } from "#phases/enemy-command-phase"; -import { LoginPhase } from "#phases/login-phase"; import { NextEncounterPhase } from "#phases/next-encounter-phase"; -import { SelectGenderPhase } from "#phases/select-gender-phase"; -import { SelectStarterPhase } from "#phases/select-starter-phase"; -import { SummonPhase } from "#phases/summon-phase"; -import { SwitchPhase } from "#phases/switch-phase"; -import { TitlePhase } from "#phases/title-phase"; -import { TurnInitPhase } from "#phases/turn-init-phase"; import { GameManager } from "#test/test-utils/game-manager"; -import { generateStarter } from "#test/test-utils/game-manager-utils"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -45,55 +30,11 @@ describe("Phase - Battle Phase", () => { game.scene.gameData.gender = undefined!; // just for these tests! }); - it("test phase interceptor with prompt", async () => { - await game.phaseInterceptor.run(LoginPhase); - - game.onNextPrompt("SelectGenderPhase", UiMode.OPTION_SELECT, () => { - game.scene.gameData.gender = PlayerGender.MALE; - game.endPhase(); - }); - - await game.phaseInterceptor.run(SelectGenderPhase); - - await game.phaseInterceptor.run(TitlePhase); - await game.waitMode(UiMode.TITLE); - - expect(game.scene.ui?.getMode()).toBe(UiMode.TITLE); - expect(game.scene.gameData.gender).toBe(PlayerGender.MALE); - }); - - it("test phase interceptor with prompt with preparation for a future prompt", async () => { - await game.phaseInterceptor.run(LoginPhase); - - game.onNextPrompt("SelectGenderPhase", UiMode.OPTION_SELECT, () => { - game.scene.gameData.gender = PlayerGender.MALE; - game.endPhase(); - }); - - game.onNextPrompt("CheckSwitchPhase", UiMode.CONFIRM, () => { - game.setMode(UiMode.MESSAGE); - game.endPhase(); - }); - await game.phaseInterceptor.run(SelectGenderPhase); - - await game.phaseInterceptor.run(TitlePhase); - await game.waitMode(UiMode.TITLE); - - expect(game.scene.ui?.getMode()).toBe(UiMode.TITLE); - expect(game.scene.gameData.gender).toBe(PlayerGender.MALE); - }); - - it("newGame one-liner", async () => { - await game.classicMode.startBattle(); - expect(game.scene.ui?.getMode()).toBe(UiMode.COMMAND); - expect(game.scene.phaseManager.getCurrentPhase()?.phaseName).toBe("CommandPhase"); - }); - it("do attack wave 3 - single battle - regular - OHKO", async () => { game.override.enemySpecies(SpeciesId.RATTATA).startingLevel(2000).battleStyle("single").startingWave(3); await game.classicMode.startBattle([SpeciesId.MEWTWO]); game.move.use(MoveId.TACKLE); - await game.phaseInterceptor.to("SelectModifierPhase"); + await game.toNextWave(); }); it("do attack wave 3 - single battle - regular - NO OHKO with opponent using non damage attack", async () => { @@ -107,7 +48,7 @@ describe("Phase - Battle Phase", () => { .battleStyle("single"); await game.classicMode.startBattle([SpeciesId.MEWTWO]); game.move.select(MoveId.TACKLE); - await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnInitPhase, false); + await game.phaseInterceptor.to("TurnInitPhase", false); }); it("load 100% data file", async () => { @@ -135,68 +76,6 @@ describe("Phase - Battle Phase", () => { } }); - it("wrong phase", async () => { - await game.phaseInterceptor.run(LoginPhase); - await game.phaseInterceptor.run(LoginPhase).catch(e => { - expect(e).toBe("Wrong phase: this is SelectGenderPhase and not LoginPhase"); - }); - }); - - it("wrong phase but skip", async () => { - await game.phaseInterceptor.run(LoginPhase); - await game.phaseInterceptor.run(LoginPhase, () => game.isCurrentPhase(SelectGenderPhase)); - }); - - it("good run", async () => { - await game.phaseInterceptor.run(LoginPhase); - game.onNextPrompt( - "SelectGenderPhase", - UiMode.OPTION_SELECT, - () => { - game.scene.gameData.gender = PlayerGender.MALE; - game.endPhase(); - }, - () => game.isCurrentPhase(TitlePhase), - ); - await game.phaseInterceptor.run(SelectGenderPhase, () => game.isCurrentPhase(TitlePhase)); - await game.phaseInterceptor.run(TitlePhase); - }); - - it("good run from select gender to title", async () => { - await game.phaseInterceptor.run(LoginPhase); - game.onNextPrompt( - "SelectGenderPhase", - UiMode.OPTION_SELECT, - () => { - game.scene.gameData.gender = PlayerGender.MALE; - game.endPhase(); - }, - () => game.isCurrentPhase(TitlePhase), - ); - await game.phaseInterceptor.runFrom(SelectGenderPhase).to(TitlePhase); - }); - - it("good run to SummonPhase phase", async () => { - await game.phaseInterceptor.run(LoginPhase); - game.onNextPrompt( - "SelectGenderPhase", - UiMode.OPTION_SELECT, - () => { - game.scene.gameData.gender = PlayerGender.MALE; - game.endPhase(); - }, - () => game.isCurrentPhase(TitlePhase), - ); - game.onNextPrompt("TitlePhase", UiMode.TITLE, () => { - game.scene.gameMode = getGameMode(GameModes.CLASSIC); - const starters = generateStarter(game.scene); - const selectStarterPhase = new SelectStarterPhase(); - game.scene.phaseManager.pushPhase(new EncounterPhase(false)); - selectStarterPhase.initBattle(starters); - }); - await game.phaseInterceptor.runFrom(SelectGenderPhase).to(SummonPhase); - }); - it.each([ { name: "1v1", double: false, qty: 1 }, { name: "2v1", double: false, qty: 2 }, @@ -232,7 +111,7 @@ describe("Phase - Battle Phase", () => { await game.classicMode.startBattle([SpeciesId.DARMANITAN, SpeciesId.CHARIZARD]); game.move.select(moveToUse); - await game.phaseInterceptor.to(DamageAnimPhase, false); + await game.phaseInterceptor.to("DamageAnimPhase", false); await game.killPokemon(game.scene.currentBattle.enemyParty[0]); expect(game.scene.currentBattle.enemyParty[0].isFainted()).toBe(true); await game.phaseInterceptor.to("VictoryPhase"); @@ -296,7 +175,7 @@ describe("Phase - Battle Phase", () => { game.field.getPlayerPokemon().hp = 1; game.move.select(moveToUse); - await game.phaseInterceptor.to(BattleEndPhase); + await game.phaseInterceptor.to("BattleEndPhase"); game.doRevivePokemon(0); // pretend max revive was picked game.doSelectModifier(); @@ -308,6 +187,6 @@ describe("Phase - Battle Phase", () => { }, () => game.isCurrentPhase(NextEncounterPhase), ); - await game.phaseInterceptor.to(SwitchPhase); + await game.phaseInterceptor.to("SwitchPhase"); }); }); diff --git a/test/boss-pokemon.test.ts b/test/boss-pokemon.test.ts index b0dfbf19794..6ad405d58e6 100644 --- a/test/boss-pokemon.test.ts +++ b/test/boss-pokemon.test.ts @@ -64,11 +64,9 @@ describe("Boss Pokemon / Shields", () => { it("should reduce the number of shields if we are in a double battle", async () => { game.override.battleStyle("double").startingWave(150); // Floor 150 > 2 shields / 3 health segments - await game.classicMode.startBattle([SpeciesId.MEWTWO]); - const boss1: EnemyPokemon = game.scene.getEnemyParty()[0]!; - const boss2: EnemyPokemon = game.scene.getEnemyParty()[1]!; + const [boss1, boss2] = game.scene.getEnemyParty(); expect(boss1.isBoss()).toBe(true); expect(boss1.bossSegments).toBe(2); expect(boss2.isBoss()).toBe(true); @@ -112,7 +110,7 @@ describe("Boss Pokemon / Shields", () => { // In this test we want to break through 3 shields at once const brokenShields = 3; - const boss1: EnemyPokemon = game.scene.getEnemyParty()[0]!; + const boss1 = game.field.getEnemyPokemon(); const boss1SegmentHp = boss1.getMaxHp() / boss1.bossSegments; const requiredDamageBoss1 = boss1SegmentHp * (1 + Math.pow(2, brokenShields)); expect(boss1.isBoss()).toBe(true); @@ -124,7 +122,7 @@ describe("Boss Pokemon / Shields", () => { expect(boss1.bossSegmentIndex).toBe(1); expect(boss1.hp).toBe(boss1.getMaxHp() - toDmgValue(boss1SegmentHp * 3)); - const boss2: EnemyPokemon = game.scene.getEnemyParty()[1]!; + const boss2 = game.scene.getEnemyParty()[1]; const boss2SegmentHp = boss2.getMaxHp() / boss2.bossSegments; const requiredDamageBoss2 = boss2SegmentHp * (1 + Math.pow(2, brokenShields)); @@ -144,7 +142,7 @@ describe("Boss Pokemon / Shields", () => { await game.classicMode.startBattle([SpeciesId.MEWTWO]); - const boss1: EnemyPokemon = game.scene.getEnemyParty()[0]!; + const boss1 = game.field.getEnemyPokemon(); const boss1SegmentHp = boss1.getMaxHp() / boss1.bossSegments; const singleShieldDamage = Math.ceil(boss1SegmentHp); expect(boss1.isBoss()).toBe(true); @@ -167,7 +165,7 @@ describe("Boss Pokemon / Shields", () => { expect(getTotalStatStageBoosts(boss1)).toBe(totalStatStages); } - const boss2: EnemyPokemon = game.scene.getEnemyParty()[1]!; + const boss2 = game.scene.getEnemyParty()[1]; const boss2SegmentHp = boss2.getMaxHp() / boss2.bossSegments; const requiredDamage = boss2SegmentHp * (1 + Math.pow(2, shieldsToBreak - 1)); diff --git a/test/challenges/limited-catch.test.ts b/test/challenges/limited-catch.test.ts index 80be52df2fb..b51732305d0 100644 --- a/test/challenges/limited-catch.test.ts +++ b/test/challenges/limited-catch.test.ts @@ -1,8 +1,10 @@ import { AbilityId } from "#enums/ability-id"; import { Challenges } from "#enums/challenges"; import { MoveId } from "#enums/move-id"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { PokeballType } from "#enums/pokeball"; import { SpeciesId } from "#enums/species-id"; +import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounter-test-utils"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -52,4 +54,18 @@ describe("Challenges - Limited Catch", () => { expect(game.scene.getPlayerParty()).toHaveLength(1); }); + + it("should allow gift Pokémon from Mystery Encounters to be added to party", async () => { + game.override + .mysteryEncounterChance(100) + .mysteryEncounter(MysteryEncounterType.THE_POKEMON_SALESMAN) + .startingWave(12); + game.scene.money = 20000; + + await game.challengeMode.runToSummon([SpeciesId.NUZLEAF]); + + await runMysteryEncounterToEnd(game, 1); + + expect(game.scene.getPlayerParty()).toHaveLength(2); + }); }); diff --git a/test/challenges/limited-support.test.ts b/test/challenges/limited-support.test.ts index 5c0eb2bd420..35413220550 100644 --- a/test/challenges/limited-support.test.ts +++ b/test/challenges/limited-support.test.ts @@ -3,6 +3,7 @@ import { Challenges } from "#enums/challenges"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { UiMode } from "#enums/ui-mode"; +import { ExpBoosterModifier } from "#modifiers/modifier"; import { GameManager } from "#test/test-utils/game-manager"; import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; import Phaser from "phaser"; @@ -75,6 +76,7 @@ describe("Challenges - Limited Support", () => { await game.doKillOpponents(); await game.toNextWave(); + expect(game.scene.getModifiers(ExpBoosterModifier)).toHaveLength(1); expect(playerPokemon).not.toHaveFullHp(); game.move.use(MoveId.SPLASH); diff --git a/test/evolution.test.ts b/test/evolution.test.ts index afe557ff2c0..3fb763e9190 100644 --- a/test/evolution.test.ts +++ b/test/evolution.test.ts @@ -34,8 +34,7 @@ describe("Evolution", () => { it("should keep hidden ability after evolving", async () => { await game.classicMode.runToSummon([SpeciesId.EEVEE, SpeciesId.TRAPINCH]); - const eevee = game.scene.getPlayerParty()[0]; - const trapinch = game.scene.getPlayerParty()[1]; + const [eevee, trapinch] = game.scene.getPlayerParty(); eevee.abilityIndex = 2; trapinch.abilityIndex = 2; @@ -49,8 +48,7 @@ describe("Evolution", () => { it("should keep same ability slot after evolving", async () => { await game.classicMode.runToSummon([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER]); - const bulbasaur = game.scene.getPlayerParty()[0]; - const charmander = game.scene.getPlayerParty()[1]; + const [bulbasaur, charmander] = game.scene.getPlayerParty(); bulbasaur.abilityIndex = 0; charmander.abilityIndex = 1; @@ -80,8 +78,7 @@ describe("Evolution", () => { nincada.gender = 1; await nincada.evolve(pokemonEvolutions[SpeciesId.NINCADA][0], nincada.getSpeciesForm()); - const ninjask = game.scene.getPlayerParty()[0]; - const shedinja = game.scene.getPlayerParty()[1]; + const [ninjask, shedinja] = game.scene.getPlayerParty(); expect(ninjask.abilityIndex).toBe(2); expect(shedinja.abilityIndex).toBe(1); expect(ninjask.gender).toBe(1); diff --git a/test/abilities/immunity.test.ts b/test/field/pokemon-funcs.test.ts similarity index 55% rename from test/abilities/immunity.test.ts rename to test/field/pokemon-funcs.test.ts index dccee93ac10..eea1f80192a 100644 --- a/test/abilities/immunity.test.ts +++ b/test/field/pokemon-funcs.test.ts @@ -6,7 +6,7 @@ import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -describe("Abilities - Immunity", () => { +describe("Spec - Pokemon Functions", () => { let phaserGame: Phaser.Game; let game: GameManager; @@ -23,29 +23,29 @@ describe("Abilities - Immunity", () => { beforeEach(() => { game = new GameManager(phaserGame); game.override - .moveset([MoveId.SPLASH]) - .ability(AbilityId.BALL_FETCH) .battleStyle("single") + .startingLevel(100) .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) .enemyAbility(AbilityId.BALL_FETCH) + .ability(AbilityId.BALL_FETCH) .enemyMoveset(MoveId.SPLASH); }); - it("should remove poison when gained", async () => { - game.override - .ability(AbilityId.IMMUNITY) - .enemyAbility(AbilityId.BALL_FETCH) - .moveset(MoveId.SKILL_SWAP) - .enemyMoveset(MoveId.SPLASH); - await game.classicMode.startBattle([SpeciesId.FEEBAS]); - const enemy = game.scene.getEnemyPokemon(); - enemy?.trySetStatus(StatusEffect.POISON); - expect(enemy?.status?.effect).toBe(StatusEffect.POISON); + describe("doSetStatus", () => { + it("should change the Pokemon's status, ignoring feasibility checks", async () => { + await game.classicMode.startBattle([SpeciesId.ACCELGOR]); - game.move.select(MoveId.SKILL_SWAP); - await game.phaseInterceptor.to("BerryPhase"); + const player = game.field.getPlayerPokemon(); - expect(enemy?.status).toBeNull(); + expect(player.status?.effect).toBeUndefined(); + player.doSetStatus(StatusEffect.BURN); + expect(player.status?.effect).toBe(StatusEffect.BURN); + + expect(player.canSetStatus(StatusEffect.SLEEP)).toBe(false); + player.doSetStatus(StatusEffect.SLEEP, 5); + expect(player.status?.effect).toBe(StatusEffect.SLEEP); + expect(player.status?.sleepTurnsRemaining).toBe(5); + }); }); }); diff --git a/test/field/pokemon.test.ts b/test/field/pokemon.test.ts index 02058ad6cb1..87a0da98f25 100644 --- a/test/field/pokemon.test.ts +++ b/test/field/pokemon.test.ts @@ -25,15 +25,6 @@ describe("Spec - Pokemon", () => { game = new GameManager(phaserGame); }); - it("should not crash when trying to set status of undefined", async () => { - await game.classicMode.runToSummon([SpeciesId.ABRA]); - - const pkm = game.field.getPlayerPokemon(); - expect(pkm).toBeDefined(); - - expect(pkm.trySetStatus(undefined)).toBe(false); - }); - describe("Add To Party", () => { let scene: BattleScene; @@ -85,17 +76,14 @@ describe("Spec - Pokemon", () => { }); describe("Get correct fusion type", () => { - let scene: BattleScene; - beforeEach(async () => { game.override.enemySpecies(SpeciesId.ZUBAT).starterSpecies(SpeciesId.ABRA).enableStarterFusion(); - scene = game.scene; }); it("Fusing two mons with a single type", async () => { game.override.starterFusionSpecies(SpeciesId.CHARMANDER); await game.classicMode.startBattle(); - const pokemon = scene.getPlayerParty()[0]; + const pokemon = game.field.getPlayerPokemon(); let types = pokemon.getTypes(); expect(types[0]).toBe(PokemonType.PSYCHIC); @@ -136,7 +124,7 @@ describe("Spec - Pokemon", () => { it("Fusing two mons with same single type", async () => { game.override.starterFusionSpecies(SpeciesId.DROWZEE); await game.classicMode.startBattle(); - const pokemon = scene.getPlayerParty()[0]; + const pokemon = game.field.getPlayerPokemon(); const types = pokemon.getTypes(); expect(types[0]).toBe(PokemonType.PSYCHIC); @@ -146,7 +134,7 @@ describe("Spec - Pokemon", () => { it("Fusing mons with one and two types", async () => { game.override.starterSpecies(SpeciesId.CHARMANDER).starterFusionSpecies(SpeciesId.HOUNDOUR); await game.classicMode.startBattle(); - const pokemon = scene.getPlayerParty()[0]; + const pokemon = game.field.getPlayerPokemon(); const types = pokemon.getTypes(); expect(types[0]).toBe(PokemonType.FIRE); @@ -156,7 +144,7 @@ describe("Spec - Pokemon", () => { it("Fusing mons with two and one types", async () => { game.override.starterSpecies(SpeciesId.NUMEL).starterFusionSpecies(SpeciesId.CHARMANDER); await game.classicMode.startBattle(); - const pokemon = scene.getPlayerParty()[0]; + const pokemon = game.field.getPlayerPokemon(); const types = pokemon.getTypes(); expect(types[0]).toBe(PokemonType.FIRE); @@ -166,7 +154,7 @@ describe("Spec - Pokemon", () => { it("Fusing two mons with two types", async () => { game.override.starterSpecies(SpeciesId.NATU).starterFusionSpecies(SpeciesId.HOUNDOUR); await game.classicMode.startBattle(); - const pokemon = scene.getPlayerParty()[0]; + const pokemon = game.field.getPlayerPokemon(); let types = pokemon.getTypes(); expect(types[0]).toBe(PokemonType.PSYCHIC); diff --git a/test/items/light-ball.test.ts b/test/items/light-ball.test.ts index a7f41255ff3..280b70f3d5a 100644 --- a/test/items/light-ball.test.ts +++ b/test/items/light-ball.test.ts @@ -33,7 +33,7 @@ describe("Items - Light Ball", () => { const consoleSpy = vi.spyOn(console, "log"); await game.classicMode.startBattle([SpeciesId.PIKACHU]); - const partyMember = game.scene.getPlayerParty()[0]; + const partyMember = game.field.getPlayerPokemon(); // Checking console log to make sure Light Ball is applied when getEffectiveStat (with the appropriate stat) is called partyMember.getEffectiveStat(Stat.DEF); @@ -84,7 +84,7 @@ describe("Items - Light Ball", () => { it("LIGHT_BALL held by PIKACHU", async () => { await game.classicMode.startBattle([SpeciesId.PIKACHU]); - const partyMember = game.scene.getPlayerParty()[0]; + const partyMember = game.field.getPlayerPokemon(); const atkStat = partyMember.getStat(Stat.ATK); const spAtkStat = partyMember.getStat(Stat.SPATK); @@ -113,8 +113,7 @@ describe("Items - Light Ball", () => { it("LIGHT_BALL held by fused PIKACHU (base)", async () => { await game.classicMode.startBattle([SpeciesId.PIKACHU, SpeciesId.MAROWAK]); - const partyMember = game.scene.getPlayerParty()[0]; - const ally = game.scene.getPlayerParty()[1]; + const [partyMember, ally] = game.scene.getPlayerParty(); // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -152,8 +151,7 @@ describe("Items - Light Ball", () => { it("LIGHT_BALL held by fused PIKACHU (part)", async () => { await game.classicMode.startBattle([SpeciesId.MAROWAK, SpeciesId.PIKACHU]); - const partyMember = game.scene.getPlayerParty()[0]; - const ally = game.scene.getPlayerParty()[1]; + const [partyMember, ally] = game.scene.getPlayerParty(); // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -191,7 +189,7 @@ describe("Items - Light Ball", () => { it("LIGHT_BALL not held by PIKACHU", async () => { await game.classicMode.startBattle([SpeciesId.MAROWAK]); - const partyMember = game.scene.getPlayerParty()[0]; + const partyMember = game.field.getPlayerPokemon(); const atkStat = partyMember.getStat(Stat.ATK); const spAtkStat = partyMember.getStat(Stat.SPATK); diff --git a/test/items/metal-powder.test.ts b/test/items/metal-powder.test.ts index 4dac8dd39b1..1a749f0ac3d 100644 --- a/test/items/metal-powder.test.ts +++ b/test/items/metal-powder.test.ts @@ -33,7 +33,7 @@ describe("Items - Metal Powder", () => { const consoleSpy = vi.spyOn(console, "log"); await game.classicMode.startBattle([SpeciesId.DITTO]); - const partyMember = game.scene.getPlayerParty()[0]; + const partyMember = game.field.getPlayerPokemon(); // Checking console log to make sure Metal Powder is applied when getEffectiveStat (with the appropriate stat) is called partyMember.getEffectiveStat(Stat.DEF); @@ -84,7 +84,7 @@ describe("Items - Metal Powder", () => { it("METAL_POWDER held by DITTO", async () => { await game.classicMode.startBattle([SpeciesId.DITTO]); - const partyMember = game.scene.getPlayerParty()[0]; + const partyMember = game.field.getPlayerPokemon(); const defStat = partyMember.getStat(Stat.DEF); @@ -107,8 +107,7 @@ describe("Items - Metal Powder", () => { it("METAL_POWDER held by fused DITTO (base)", async () => { await game.classicMode.startBattle([SpeciesId.DITTO, SpeciesId.MAROWAK]); - const partyMember = game.scene.getPlayerParty()[0]; - const ally = game.scene.getPlayerParty()[1]; + const [partyMember, ally] = game.scene.getPlayerParty(); // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -140,8 +139,7 @@ describe("Items - Metal Powder", () => { it("METAL_POWDER held by fused DITTO (part)", async () => { await game.classicMode.startBattle([SpeciesId.MAROWAK, SpeciesId.DITTO]); - const partyMember = game.scene.getPlayerParty()[0]; - const ally = game.scene.getPlayerParty()[1]; + const [partyMember, ally] = game.scene.getPlayerParty(); // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -173,7 +171,7 @@ describe("Items - Metal Powder", () => { it("METAL_POWDER not held by DITTO", async () => { await game.classicMode.startBattle([SpeciesId.MAROWAK]); - const partyMember = game.scene.getPlayerParty()[0]; + const partyMember = game.field.getPlayerPokemon(); const defStat = partyMember.getStat(Stat.DEF); diff --git a/test/items/quick-powder.test.ts b/test/items/quick-powder.test.ts index 2200e8cf96e..0295361ed13 100644 --- a/test/items/quick-powder.test.ts +++ b/test/items/quick-powder.test.ts @@ -33,7 +33,7 @@ describe("Items - Quick Powder", () => { const consoleSpy = vi.spyOn(console, "log"); await game.classicMode.startBattle([SpeciesId.DITTO]); - const partyMember = game.scene.getPlayerParty()[0]; + const partyMember = game.field.getPlayerPokemon(); // Checking console log to make sure Quick Powder is applied when getEffectiveStat (with the appropriate stat) is called partyMember.getEffectiveStat(Stat.DEF); @@ -84,7 +84,7 @@ describe("Items - Quick Powder", () => { it("QUICK_POWDER held by DITTO", async () => { await game.classicMode.startBattle([SpeciesId.DITTO]); - const partyMember = game.scene.getPlayerParty()[0]; + const partyMember = game.field.getPlayerPokemon(); const spdStat = partyMember.getStat(Stat.SPD); @@ -107,8 +107,7 @@ describe("Items - Quick Powder", () => { it("QUICK_POWDER held by fused DITTO (base)", async () => { await game.classicMode.startBattle([SpeciesId.DITTO, SpeciesId.MAROWAK]); - const partyMember = game.scene.getPlayerParty()[0]; - const ally = game.scene.getPlayerParty()[1]; + const [partyMember, ally] = game.scene.getPlayerParty(); // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -140,8 +139,7 @@ describe("Items - Quick Powder", () => { it("QUICK_POWDER held by fused DITTO (part)", async () => { await game.classicMode.startBattle([SpeciesId.MAROWAK, SpeciesId.DITTO]); - const partyMember = game.scene.getPlayerParty()[0]; - const ally = game.scene.getPlayerParty()[1]; + const [partyMember, ally] = game.scene.getPlayerParty(); // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -173,7 +171,7 @@ describe("Items - Quick Powder", () => { it("QUICK_POWDER not held by DITTO", async () => { await game.classicMode.startBattle([SpeciesId.MAROWAK]); - const partyMember = game.scene.getPlayerParty()[0]; + const partyMember = game.field.getPlayerPokemon(); const spdStat = partyMember.getStat(Stat.SPD); diff --git a/test/items/temp-stat-stage-booster.test.ts b/test/items/temp-stat-stage-booster.test.ts index f95fe553faf..05ea5a03eae 100644 --- a/test/items/temp-stat-stage-booster.test.ts +++ b/test/items/temp-stat-stage-booster.test.ts @@ -6,7 +6,6 @@ import { SpeciesId } from "#enums/species-id"; import { BATTLE_STATS, Stat } from "#enums/stat"; import { UiMode } from "#enums/ui-mode"; import { TempStatStageBoosterModifier } from "#modifiers/modifier"; -import { TurnEndPhase } from "#phases/turn-end-phase"; import { GameManager } from "#test/test-utils/game-manager"; import type { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; import Phaser from "phaser"; @@ -47,7 +46,7 @@ describe("Items - Temporary Stat Stage Boosters", () => { game.move.select(MoveId.TACKLE); - await game.phaseInterceptor.runFrom("EnemyCommandPhase").to(TurnEndPhase); + await game.toEndOfTurn(); expect(partyMember.getStatStageMultiplier).toHaveReturnedWith(1.3); }); @@ -64,11 +63,11 @@ describe("Items - Temporary Stat Stage Boosters", () => { // Raise ACC by +2 stat stages game.move.select(MoveId.HONE_CLAWS); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); game.move.select(MoveId.TACKLE); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); // ACC at +3 stat stages yields a x2 multiplier expect(partyMember.getAccuracyMultiplier).toHaveReturnedWith(2); @@ -84,11 +83,11 @@ describe("Items - Temporary Stat Stage Boosters", () => { // Raise ATK by +1 stat stage game.move.select(MoveId.HONE_CLAWS); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); game.move.select(MoveId.TACKLE); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); // ATK at +1 stat stage yields a x1.5 multiplier, add 0.3 from X_ATTACK expect(partyMember.getStatStageMultiplier).toHaveReturnedWith(1.8); @@ -112,7 +111,7 @@ describe("Items - Temporary Stat Stage Boosters", () => { game.move.select(MoveId.TACKLE); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); expect(partyMember.getAccuracyMultiplier).toHaveReturnedWith(3); expect(partyMember.getStatStageMultiplier).toHaveReturnedWith(4); diff --git a/test/items/thick-club.test.ts b/test/items/thick-club.test.ts index c497cef6338..d14b4f955e7 100644 --- a/test/items/thick-club.test.ts +++ b/test/items/thick-club.test.ts @@ -33,7 +33,7 @@ describe("Items - Thick Club", () => { const consoleSpy = vi.spyOn(console, "log"); await game.classicMode.startBattle([SpeciesId.CUBONE]); - const partyMember = game.scene.getPlayerParty()[0]; + const partyMember = game.field.getPlayerPokemon(); // Checking console log to make sure Thick Club is applied when getEffectiveStat (with the appropriate stat) is called partyMember.getEffectiveStat(Stat.DEF); @@ -84,7 +84,7 @@ describe("Items - Thick Club", () => { it("THICK_CLUB held by CUBONE", async () => { await game.classicMode.startBattle([SpeciesId.CUBONE]); - const partyMember = game.scene.getPlayerParty()[0]; + const partyMember = game.field.getPlayerPokemon(); const atkStat = partyMember.getStat(Stat.ATK); @@ -107,7 +107,7 @@ describe("Items - Thick Club", () => { it("THICK_CLUB held by MAROWAK", async () => { await game.classicMode.startBattle([SpeciesId.MAROWAK]); - const partyMember = game.scene.getPlayerParty()[0]; + const partyMember = game.field.getPlayerPokemon(); const atkStat = partyMember.getStat(Stat.ATK); @@ -130,7 +130,7 @@ describe("Items - Thick Club", () => { it("THICK_CLUB held by ALOLA_MAROWAK", async () => { await game.classicMode.startBattle([SpeciesId.ALOLA_MAROWAK]); - const partyMember = game.scene.getPlayerParty()[0]; + const partyMember = game.field.getPlayerPokemon(); const atkStat = partyMember.getStat(Stat.ATK); @@ -157,8 +157,7 @@ describe("Items - Thick Club", () => { await game.classicMode.startBattle([species[randSpecies], SpeciesId.PIKACHU]); - const partyMember = game.scene.getPlayerParty()[0]; - const ally = game.scene.getPlayerParty()[1]; + const [partyMember, ally] = game.scene.getPlayerParty(); // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -194,8 +193,7 @@ describe("Items - Thick Club", () => { await game.classicMode.startBattle([SpeciesId.PIKACHU, species[randSpecies]]); - const partyMember = game.scene.getPlayerParty()[0]; - const ally = game.scene.getPlayerParty()[1]; + const [partyMember, ally] = game.scene.getPlayerParty(); // Fuse party members (taken from PlayerPokemon.fuse(...) function) partyMember.fusionSpecies = ally.species; @@ -227,7 +225,7 @@ describe("Items - Thick Club", () => { it("THICK_CLUB not held by CUBONE", async () => { await game.classicMode.startBattle([SpeciesId.PIKACHU]); - const partyMember = game.scene.getPlayerParty()[0]; + const partyMember = game.field.getPlayerPokemon(); const atkStat = partyMember.getStat(Stat.ATK); diff --git a/test/matchers.setup.ts b/test/matchers.setup.ts index 03b29302916..f76a9423ab3 100644 --- a/test/matchers.setup.ts +++ b/test/matchers.setup.ts @@ -1,10 +1,12 @@ import { toEqualArrayUnsorted } from "#test/test-utils/matchers/to-equal-array-unsorted"; import { toHaveAbilityApplied } from "#test/test-utils/matchers/to-have-ability-applied"; +import { toHaveArenaTag } from "#test/test-utils/matchers/to-have-arena-tag"; import { toHaveBattlerTag } from "#test/test-utils/matchers/to-have-battler-tag"; import { toHaveEffectiveStat } from "#test/test-utils/matchers/to-have-effective-stat"; import { toHaveFainted } from "#test/test-utils/matchers/to-have-fainted"; import { toHaveFullHp } from "#test/test-utils/matchers/to-have-full-hp"; import { toHaveHp } from "#test/test-utils/matchers/to-have-hp"; +import { toHavePositionalTag } from "#test/test-utils/matchers/to-have-positional-tag"; import { toHaveStatStage } from "#test/test-utils/matchers/to-have-stat-stage"; import { toHaveStatusEffect } from "#test/test-utils/matchers/to-have-status-effect"; import { toHaveTakenDamage } from "#test/test-utils/matchers/to-have-taken-damage"; @@ -22,18 +24,20 @@ import { expect } from "vitest"; expect.extend({ toEqualArrayUnsorted, + toHaveWeather, + toHaveTerrain, + toHaveArenaTag, + toHavePositionalTag, toHaveTypes, toHaveUsedMove, toHaveEffectiveStat, - toHaveTakenDamage, - toHaveWeather, - toHaveTerrain, - toHaveFullHp, toHaveStatusEffect, toHaveStatStage, toHaveBattlerTag, toHaveAbilityApplied, toHaveHp, + toHaveTakenDamage, + toHaveFullHp, toHaveFainted, toHaveUsedPP, }); diff --git a/test/moves/beat-up.test.ts b/test/moves/beat-up.test.ts index cfb3d35bed5..ff08b55cefa 100644 --- a/test/moves/beat-up.test.ts +++ b/test/moves/beat-up.test.ts @@ -73,7 +73,7 @@ describe("Moves - Beat Up", () => { const playerPokemon = game.field.getPlayerPokemon(); - game.scene.getPlayerParty()[1].trySetStatus(StatusEffect.BURN); + game.scene.getPlayerParty()[1].doSetStatus(StatusEffect.BURN); game.move.select(MoveId.BEAT_UP); diff --git a/test/moves/ceaseless-edge.test.ts b/test/moves/ceaseless-edge.test.ts index 64f4cf15511..b06ea84308c 100644 --- a/test/moves/ceaseless-edge.test.ts +++ b/test/moves/ceaseless-edge.test.ts @@ -1,4 +1,4 @@ -import { ArenaTrapTag } from "#data/arena-tag"; +import { EntryHazardTag } from "#data/arena-tag"; import { allMoves } from "#data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { ArenaTagSide } from "#enums/arena-tag-side"; @@ -50,12 +50,12 @@ describe("Moves - Ceaseless Edge", () => { await game.phaseInterceptor.to(MoveEffectPhase, false); // Spikes should not have any layers before move effect is applied - const tagBefore = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag; - expect(tagBefore instanceof ArenaTrapTag).toBeFalsy(); + const tagBefore = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as EntryHazardTag; + expect(tagBefore instanceof EntryHazardTag).toBeFalsy(); await game.phaseInterceptor.to(TurnEndPhase); - const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag; - expect(tagAfter instanceof ArenaTrapTag).toBeTruthy(); + const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as EntryHazardTag; + expect(tagAfter instanceof EntryHazardTag).toBeTruthy(); expect(tagAfter.layers).toBe(1); expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp); }); @@ -72,12 +72,12 @@ describe("Moves - Ceaseless Edge", () => { await game.phaseInterceptor.to(MoveEffectPhase, false); // Spikes should not have any layers before move effect is applied - const tagBefore = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag; - expect(tagBefore instanceof ArenaTrapTag).toBeFalsy(); + const tagBefore = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as EntryHazardTag; + expect(tagBefore instanceof EntryHazardTag).toBeFalsy(); await game.phaseInterceptor.to(TurnEndPhase); - const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag; - expect(tagAfter instanceof ArenaTrapTag).toBeTruthy(); + const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as EntryHazardTag; + expect(tagAfter instanceof EntryHazardTag).toBeTruthy(); expect(tagAfter.layers).toBe(2); expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp); }); @@ -90,12 +90,12 @@ describe("Moves - Ceaseless Edge", () => { game.move.select(MoveId.CEASELESS_EDGE); await game.phaseInterceptor.to(MoveEffectPhase, false); // Spikes should not have any layers before move effect is applied - const tagBefore = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag; - expect(tagBefore instanceof ArenaTrapTag).toBeFalsy(); + const tagBefore = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as EntryHazardTag; + expect(tagBefore instanceof EntryHazardTag).toBeFalsy(); await game.toNextTurn(); - const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag; - expect(tagAfter instanceof ArenaTrapTag).toBeTruthy(); + const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as EntryHazardTag; + expect(tagAfter instanceof EntryHazardTag).toBeTruthy(); expect(tagAfter.layers).toBe(2); const hpBeforeSpikes = game.scene.currentBattle.enemyParty[1].hp; diff --git a/test/moves/destiny-bond.test.ts b/test/moves/destiny-bond.test.ts index 9c397717335..118a45e7682 100644 --- a/test/moves/destiny-bond.test.ts +++ b/test/moves/destiny-bond.test.ts @@ -1,4 +1,4 @@ -import type { ArenaTrapTag } from "#data/arena-tag"; +import type { EntryHazardTag } from "#data/arena-tag"; import { allMoves } from "#data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { ArenaTagSide } from "#enums/arena-tag-side"; @@ -195,7 +195,7 @@ describe("Moves - Destiny Bond", () => { expect(playerPokemon.isFainted()).toBe(true); // Ceaseless Edge spikes effect should still activate - const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag; + const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as EntryHazardTag; expect(tagAfter.tagType).toBe(ArenaTagType.SPIKES); expect(tagAfter.layers).toBe(1); }); @@ -220,7 +220,10 @@ describe("Moves - Destiny Bond", () => { expect(playerPokemon1?.isFainted()).toBe(true); // Pledge secondary effect should still activate - const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.GRASS_WATER_PLEDGE, ArenaTagSide.ENEMY) as ArenaTrapTag; + const tagAfter = game.scene.arena.getTagOnSide( + ArenaTagType.GRASS_WATER_PLEDGE, + ArenaTagSide.ENEMY, + ) as EntryHazardTag; expect(tagAfter.tagType).toBe(ArenaTagType.GRASS_WATER_PLEDGE); }); diff --git a/test/moves/dragon-rage.test.ts b/test/moves/dragon-rage.test.ts index c90e2b78abd..dffdbf9badc 100644 --- a/test/moves/dragon-rage.test.ts +++ b/test/moves/dragon-rage.test.ts @@ -46,7 +46,7 @@ describe("Moves - Dragon Rage", () => { await game.classicMode.startBattle(); - partyPokemon = game.scene.getPlayerParty()[0]; + partyPokemon = game.field.getPlayerPokemon(); enemyPokemon = game.field.getEnemyPokemon(); }); diff --git a/test/moves/dragon-tail.test.ts b/test/moves/dragon-tail.test.ts index 1cea6f908a0..e3a5bf459e8 100644 --- a/test/moves/dragon-tail.test.ts +++ b/test/moves/dragon-tail.test.ts @@ -76,10 +76,9 @@ describe("Moves - Dragon Tail", () => { game.override.battleStyle("double").enemyMoveset(MoveId.SPLASH).enemyAbility(AbilityId.ROUGH_SKIN); await game.classicMode.startBattle([SpeciesId.DRATINI, SpeciesId.DRATINI, SpeciesId.WAILORD, SpeciesId.WAILORD]); - const leadPokemon = game.scene.getPlayerParty()[0]!; + const leadPokemon = game.field.getPlayerPokemon(); - const enemyLeadPokemon = game.scene.getEnemyParty()[0]!; - const enemySecPokemon = game.scene.getEnemyParty()[1]!; + const [enemyLeadPokemon, enemySecPokemon] = game.scene.getEnemyParty(); game.move.select(MoveId.DRAGON_TAIL, 0, BattlerIndex.ENEMY); game.move.select(MoveId.SPLASH, 1); @@ -105,11 +104,9 @@ describe("Moves - Dragon Tail", () => { game.override.battleStyle("double").enemyMoveset(MoveId.SPLASH).enemyAbility(AbilityId.ROUGH_SKIN); await game.classicMode.startBattle([SpeciesId.DRATINI, SpeciesId.DRATINI, SpeciesId.WAILORD, SpeciesId.WAILORD]); - const leadPokemon = game.scene.getPlayerParty()[0]!; - const secPokemon = game.scene.getPlayerParty()[1]!; + const [leadPokemon, secPokemon] = game.scene.getPlayerParty(); - const enemyLeadPokemon = game.scene.getEnemyParty()[0]!; - const enemySecPokemon = game.scene.getEnemyParty()[1]!; + const [enemyLeadPokemon, enemySecPokemon] = game.scene.getEnemyParty(); game.move.select(MoveId.DRAGON_TAIL, 0, BattlerIndex.ENEMY); // target the same pokemon, second move should be redirected after first flees diff --git a/test/moves/entry-hazards.test.ts b/test/moves/entry-hazards.test.ts new file mode 100644 index 00000000000..c4dead1bb67 --- /dev/null +++ b/test/moves/entry-hazards.test.ts @@ -0,0 +1,233 @@ +import { getPokemonNameWithAffix } from "#app/messages"; +import { allMoves } from "#data/data-lists"; +import type { TypeDamageMultiplier } from "#data/type"; +import { AbilityId } from "#enums/ability-id"; +import { ArenaTagSide } from "#enums/arena-tag-side"; +import { ArenaTagType } from "#enums/arena-tag-type"; +import { BattleType } from "#enums/battle-type"; +import { BattlerIndex } from "#enums/battler-index"; +import { MoveId } from "#enums/move-id"; +import { MoveResult } from "#enums/move-result"; +import { PokemonType } from "#enums/pokemon-type"; +import { SpeciesId } from "#enums/species-id"; +import { Stat } from "#enums/stat"; +import { StatusEffect } from "#enums/status-effect"; +import { GameManager } from "#test/test-utils/game-manager"; +import type { EntryHazardTagType } from "#types/arena-tags"; +import i18next from "i18next"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Entry Hazards", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .battleStyle("single") + .enemySpecies(SpeciesId.BLISSEY) + .startingLevel(100) + .enemyLevel(100) + .enemyAbility(AbilityId.BALL_FETCH) + .ability(AbilityId.BALL_FETCH) + .enemyMoveset(MoveId.SPLASH) + .battleType(BattleType.TRAINER); + }); + + describe.each<{ name: string; move: MoveId; tagType: EntryHazardTagType }>([ + { name: "Spikes", move: MoveId.SPIKES, tagType: ArenaTagType.SPIKES }, + { + name: "Toxic Spikes", + move: MoveId.TOXIC_SPIKES, + tagType: ArenaTagType.TOXIC_SPIKES, + }, + { + name: "Stealth Rock", + move: MoveId.STEALTH_ROCK, + tagType: ArenaTagType.STEALTH_ROCK, + }, + { + name: "Sticky Web", + move: MoveId.STICKY_WEB, + tagType: ArenaTagType.STICKY_WEB, + }, + ])("General checks - $name", ({ move, tagType }) => { + it("should add a persistent tag to the opposing side of the field", async () => { + await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.POOCHYENA]); + + expect(game).not.toHaveArenaTag(tagType); + + game.move.use(move); + await game.toNextTurn(); + + // Tag should've been added to the opposing side of the field + expect(game).not.toHaveArenaTag(tagType, ArenaTagSide.PLAYER); + expect(game).toHaveArenaTag(tagType, ArenaTagSide.ENEMY); + }); + + // TODO: re-enable after re-fixing hazards moves + it.todo("should work when all targets fainted", async () => { + game.override.battleStyle("double"); + await game.classicMode.startBattle([SpeciesId.RAYQUAZA, SpeciesId.SHUCKLE]); + + const [enemy1, enemy2] = game.scene.getEnemyField(); + + game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER); + game.move.use(move, BattlerIndex.PLAYER_2); + await game.doKillOpponents(); + await game.toEndOfTurn(); + + expect(enemy1.isFainted()).toBe(true); + expect(enemy2.isFainted()).toBe(true); + expect(game).toHaveArenaTag(tagType, ArenaTagSide.ENEMY); + }); + + const maxLayers = tagType === ArenaTagType.SPIKES ? 3 : tagType === ArenaTagType.TOXIC_SPIKES ? 2 : 1; + const msgText = + maxLayers === 1 + ? "should fail if added while already present" + : `can be added up to ${maxLayers} times in a row before failing`; + + it(msgText, async () => { + await game.classicMode.startBattle([SpeciesId.FEEBAS]); + + const feebas = game.field.getPlayerPokemon(); + + // set up hazards until at max layers + for (let i = 0; i < maxLayers; i++) { + game.move.use(move); + await game.toNextTurn(); + + expect(feebas).toHaveUsedMove({ move, result: MoveResult.SUCCESS }); + expect(game).toHaveArenaTag({ tagType, side: ArenaTagSide.ENEMY, layers: i + 1 }); + } + + game.move.use(move); + await game.toNextTurn(); + + expect(feebas).toHaveUsedMove({ move, result: MoveResult.FAIL }); + expect(game).toHaveArenaTag({ tagType, side: ArenaTagSide.ENEMY, layers: maxLayers }); + }); + }); + + describe("Spikes", () => { + it.each<{ layers: number; damage: number }>([ + { layers: 1, damage: 12.5 }, + { layers: 2, damage: 100 / 6 }, + { layers: 3, damage: 25 }, + ])("should play message and deal $damage% of the target's max HP at $layers", async ({ layers, damage }) => { + for (let i = 0; i < layers; i++) { + game.scene.arena.addTag(ArenaTagType.SPIKES, 0, undefined, 0, ArenaTagSide.ENEMY); + } + + await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.POOCHYENA]); + + const enemy = game.field.getEnemyPokemon(); + expect(enemy).toHaveTakenDamage((enemy.getMaxHp() * damage) / 100); + expect(game.textInterceptor.logs).toContain( + i18next.t("arenaTag:spikesActivateTrap", { + pokemonNameWithAffix: getPokemonNameWithAffix(enemy), + }), + ); + }); + }); + + describe("Toxic Spikes", () => { + it.each<{ name: string; layers: number; status: StatusEffect }>([ + { name: "Poison", layers: 1, status: StatusEffect.POISON }, + { name: "Toxic", layers: 2, status: StatusEffect.TOXIC }, + ])("should apply $name at $layers without displaying neutralization msg", async ({ layers, status }) => { + for (let i = 0; i < layers; i++) { + game.scene.arena.addTag(ArenaTagType.TOXIC_SPIKES, 0, undefined, 0, ArenaTagSide.ENEMY); + } + await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.POOCHYENA]); + + const enemy = game.field.getEnemyPokemon(); + expect(enemy).toHaveStatusEffect(status); + expect(game.textInterceptor.logs).not.toContain( + i18next.t("arenaTag:toxicSpikesActivateTrapPoison", { + pokemonNameWithAffix: getPokemonNameWithAffix(enemy), + moveName: allMoves[MoveId.TOXIC_SPIKES].name, + }), + ); + }); + }); + + it("should be removed without triggering upon a grounded Poison-type switching in", async () => { + await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.EKANS]); + + game.scene.arena.addTag(ArenaTagType.TOXIC_SPIKES, 0, undefined, 0, ArenaTagSide.ENEMY); + + game.doSwitchPokemon(1); + await game.toNextTurn(); + + const ekans = game.field.getPlayerPokemon(); + expect(game).not.toHaveArenaTag(ArenaTagType.TOXIC_SPIKES, ArenaTagSide.PLAYER); + expect(game.textInterceptor.logs).not.toContain( + i18next.t("arenaTag:toxicSpikesActivateTrapPoison", { + pokemonNameWithAffix: getPokemonNameWithAffix(ekans), + moveName: allMoves[MoveId.TOXIC_SPIKES].name, + }), + ); + expect(ekans).not.toHaveStatusEffect(StatusEffect.POISON); + }); + + describe("Stealth Rock", () => { + it.each<{ multi: TypeDamageMultiplier; species: SpeciesId }>([ + { multi: 0.25, species: SpeciesId.LUCARIO }, + { multi: 0.5, species: SpeciesId.DURALUDON }, + { multi: 1, species: SpeciesId.LICKILICKY }, + { multi: 2, species: SpeciesId.DARMANITAN }, + { multi: 4, species: SpeciesId.DELIBIRD }, + ])("should deal damage based on the target's weakness to Rock - $multi", async ({ multi, species }) => { + game.override.enemySpecies(species); + game.scene.arena.addTag(ArenaTagType.STEALTH_ROCK, 0, undefined, 0, ArenaTagSide.ENEMY); + await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.EKANS]); + + const enemy = game.field.getEnemyPokemon(); + expect(enemy.getAttackTypeEffectiveness(PokemonType.ROCK, undefined, true)).toBe(multi); + expect(enemy).toHaveTakenDamage(enemy.getMaxHp() * 0.125 * multi); + expect(game.textInterceptor.logs).toContain( + i18next.t("arenaTag:stealthRockActivateTrap", { + pokemonName: getPokemonNameWithAffix(enemy), + }), + ); + }); + + it("should ignore strong winds for type effectiveness", async () => { + game.override.enemyAbility(AbilityId.DELTA_STREAM).enemySpecies(SpeciesId.RAYQUAZA); + game.scene.arena.addTag(ArenaTagType.STEALTH_ROCK, 0, undefined, 0, ArenaTagSide.ENEMY); + await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.EKANS]); + + const rayquaza = game.field.getEnemyPokemon(); + // took 25% damage despite strong winds halving effectiveness + expect(rayquaza).toHaveTakenDamage(rayquaza.getMaxHp() * 0.25); + }); + }); + + describe("Sticky Web", () => { + it("should lower the target's speed by 1 stage on entry", async () => { + game.scene.arena.addTag(ArenaTagType.STICKY_WEB, 0, undefined, 0, ArenaTagSide.ENEMY); + await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.EKANS]); + + const enemy = game.field.getEnemyPokemon(); + expect(enemy).toHaveStatStage(Stat.SPD, -1); + expect(game.textInterceptor.logs).toContain( + i18next.t("arenaTag:stickyWebActivateTrap", { + pokemonName: enemy.getNameToRender(), + }), + ); + }); + }); +}); diff --git a/test/moves/fissure.test.ts b/test/moves/fissure.test.ts index b22b81906a6..b5255d75d73 100644 --- a/test/moves/fissure.test.ts +++ b/test/moves/fissure.test.ts @@ -42,7 +42,7 @@ describe("Moves - Fissure", () => { await game.classicMode.startBattle(); - partyPokemon = game.scene.getPlayerParty()[0]; + partyPokemon = game.field.getPlayerPokemon(); enemyPokemon = game.field.getEnemyPokemon(); }); diff --git a/test/moves/fusion-flare-bolt.test.ts b/test/moves/fusion-flare-bolt.test.ts index 42cc1248325..f5d556bde48 100644 --- a/test/moves/fusion-flare-bolt.test.ts +++ b/test/moves/fusion-flare-bolt.test.ts @@ -4,10 +4,7 @@ import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; import type { Move } from "#moves/move"; -import { DamageAnimPhase } from "#phases/damage-anim-phase"; -import { MoveEffectPhase } from "#phases/move-effect-phase"; -import { MoveEndPhase } from "#phases/move-end-phase"; -import { MovePhase } from "#phases/move-phase"; +import type { MoveEffectPhase } from "#phases/move-effect-phase"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -55,14 +52,14 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => { // Force user party to act before enemy party await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]); - await game.phaseInterceptor.to(MoveEffectPhase, false); + await game.phaseInterceptor.to("MoveEffectPhase", false); expect((game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionFlare.id); - await game.phaseInterceptor.to(DamageAnimPhase, false); + await game.phaseInterceptor.to("DamageAnimPhase", false); expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(100); - await game.phaseInterceptor.to(MoveEffectPhase, false); + await game.phaseInterceptor.to("MoveEffectPhase", false); expect((game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionBolt.id); - await game.phaseInterceptor.to(DamageAnimPhase, false); + await game.phaseInterceptor.to("DamageAnimPhase", false); expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(200); }); @@ -75,14 +72,14 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => { // Force user party to act before enemy party await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]); - await game.phaseInterceptor.to(MoveEffectPhase, false); + await game.phaseInterceptor.to("MoveEffectPhase", false); expect((game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionBolt.id); - await game.phaseInterceptor.to(DamageAnimPhase, false); + await game.phaseInterceptor.to("DamageAnimPhase", false); expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(100); - await game.phaseInterceptor.to(MoveEffectPhase, false); + await game.phaseInterceptor.to("MoveEffectPhase", false); expect((game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionFlare.id); - await game.phaseInterceptor.to(DamageAnimPhase, false); + await game.phaseInterceptor.to("DamageAnimPhase", false); expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200); }); @@ -95,19 +92,19 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => { // Force first enemy to act (and fail) in between party await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY]); - await game.phaseInterceptor.to(MoveEffectPhase, false); + await game.phaseInterceptor.to("MoveEffectPhase", false); expect((game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionFlare.id); - await game.phaseInterceptor.to(DamageAnimPhase, false); + await game.phaseInterceptor.to("DamageAnimPhase", false); expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(100); - await game.phaseInterceptor.to(MoveEndPhase); + await game.phaseInterceptor.to("MoveEndPhase"); // Skip enemy move; because the enemy is at full HP, Rest should fail - await game.phaseInterceptor.runFrom(MovePhase).to(MoveEndPhase); + await game.phaseInterceptor.to("MoveEndPhase"); - await game.phaseInterceptor.to(MoveEffectPhase, false); + await game.phaseInterceptor.to("MoveEffectPhase", false); expect((game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionBolt.id); - await game.phaseInterceptor.to(DamageAnimPhase, false); + await game.phaseInterceptor.to("DamageAnimPhase", false); expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(200); }); @@ -121,18 +118,18 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => { // Force first enemy to act in between party await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY]); - await game.phaseInterceptor.to(MoveEffectPhase, false); + await game.phaseInterceptor.to("MoveEffectPhase", false); expect((game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionFlare.id); - await game.phaseInterceptor.to(DamageAnimPhase, false); + await game.phaseInterceptor.to("DamageAnimPhase", false); expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(100); - await game.phaseInterceptor.to(MoveEndPhase); + await game.phaseInterceptor.to("MoveEndPhase"); // Skip enemy move - await game.phaseInterceptor.runFrom(MovePhase).to(MoveEndPhase); + await game.phaseInterceptor.to("MoveEndPhase"); - await game.phaseInterceptor.to(MoveEffectPhase, false); + await game.phaseInterceptor.to("MoveEffectPhase", false); expect((game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionBolt.id); - await game.phaseInterceptor.to(DamageAnimPhase, false); + await game.phaseInterceptor.to("DamageAnimPhase", false); expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(100); }); @@ -145,14 +142,14 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => { // Force user party to act before enemy party await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]); - await game.phaseInterceptor.to(MoveEffectPhase, false); + await game.phaseInterceptor.to("MoveEffectPhase", false); expect((game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionBolt.id); - await game.phaseInterceptor.to(DamageAnimPhase, false); + await game.phaseInterceptor.to("DamageAnimPhase", false); expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(100); - await game.phaseInterceptor.to(MoveEffectPhase, false); + await game.phaseInterceptor.to("MoveEffectPhase", false); expect((game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionFlare.id); - await game.phaseInterceptor.to(DamageAnimPhase, false); + await game.phaseInterceptor.to("DamageAnimPhase", false); expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200); }); @@ -189,24 +186,24 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => { // Force first enemy to act in between party await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY]); - await game.phaseInterceptor.to(MoveEffectPhase, false); + await game.phaseInterceptor.to("MoveEffectPhase", false); expect((game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionBolt.id); - await game.phaseInterceptor.to(DamageAnimPhase, false); + await game.phaseInterceptor.to("DamageAnimPhase", false); expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(100); - await game.phaseInterceptor.to(MoveEffectPhase, false); + await game.phaseInterceptor.to("MoveEffectPhase", false); expect((game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionFlare.id); - await game.phaseInterceptor.to(DamageAnimPhase, false); + await game.phaseInterceptor.to("DamageAnimPhase", false); expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200); - await game.phaseInterceptor.to(MoveEffectPhase, false); + await game.phaseInterceptor.to("MoveEffectPhase", false); expect((game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionBolt.id); - await game.phaseInterceptor.to(DamageAnimPhase, false); + await game.phaseInterceptor.to("DamageAnimPhase", false); expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(200); - await game.phaseInterceptor.to(MoveEffectPhase, false); + await game.phaseInterceptor.to("MoveEffectPhase", false); expect((game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionFlare.id); - await game.phaseInterceptor.to(DamageAnimPhase, false); + await game.phaseInterceptor.to("DamageAnimPhase", false); expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200); }); @@ -243,24 +240,24 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => { // Force first enemy to act in between party await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY]); - await game.phaseInterceptor.to(MoveEffectPhase, false); + await game.phaseInterceptor.to("MoveEffectPhase", false); expect((game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionBolt.id); - await game.phaseInterceptor.to(DamageAnimPhase, false); + await game.phaseInterceptor.to("DamageAnimPhase", false); expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(100); - await game.phaseInterceptor.to(MoveEffectPhase, false); + await game.phaseInterceptor.to("MoveEffectPhase", false); expect((game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionFlare.id); - await game.phaseInterceptor.to(DamageAnimPhase, false); + await game.phaseInterceptor.to("DamageAnimPhase", false); expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200); - await game.phaseInterceptor.to(MoveEffectPhase, false); + await game.phaseInterceptor.to("MoveEffectPhase", false); expect((game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionBolt.id); - await game.phaseInterceptor.to(DamageAnimPhase, false); + await game.phaseInterceptor.to("DamageAnimPhase", false); expect(fusionBolt.calculateBattlePower).toHaveLastReturnedWith(200); - await game.phaseInterceptor.to(MoveEffectPhase, false); + await game.phaseInterceptor.to("MoveEffectPhase", false); expect((game.scene.phaseManager.getCurrentPhase() as MoveEffectPhase).move.id).toBe(fusionFlare.id); - await game.phaseInterceptor.to(DamageAnimPhase, false); + await game.phaseInterceptor.to("DamageAnimPhase", false); expect(fusionFlare.calculateBattlePower).toHaveLastReturnedWith(200); }); }); diff --git a/test/moves/fusion-flare.test.ts b/test/moves/fusion-flare.test.ts index dd8ae11683d..e5c45c8fadb 100644 --- a/test/moves/fusion-flare.test.ts +++ b/test/moves/fusion-flare.test.ts @@ -44,7 +44,7 @@ describe("Moves - Fusion Flare", () => { await game.phaseInterceptor.to(TurnStartPhase, false); // Inflict freeze quietly and check if it was properly inflicted - partyMember.trySetStatus(StatusEffect.FREEZE, false); + partyMember.doSetStatus(StatusEffect.FREEZE); expect(partyMember.status!.effect).toBe(StatusEffect.FREEZE); await game.toNextTurn(); diff --git a/test/moves/growth.test.ts b/test/moves/growth.test.ts index 4c892f0dee2..3d3b407f28b 100644 --- a/test/moves/growth.test.ts +++ b/test/moves/growth.test.ts @@ -2,8 +2,6 @@ import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; -import { EnemyCommandPhase } from "#phases/enemy-command-phase"; -import { TurnInitPhase } from "#phases/turn-init-phase"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -40,7 +38,7 @@ describe("Moves - Growth", () => { expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(0); game.move.select(MoveId.GROWTH); - await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnInitPhase); + await game.toEndOfTurn(); expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1); }); diff --git a/test/moves/parting-shot.test.ts b/test/moves/parting-shot.test.ts index 660edc4565a..e9400aef29b 100644 --- a/test/moves/parting-shot.test.ts +++ b/test/moves/parting-shot.test.ts @@ -2,10 +2,6 @@ import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; -import { BerryPhase } from "#phases/berry-phase"; -import { FaintPhase } from "#phases/faint-phase"; -import { MessagePhase } from "#phases/message-phase"; -import { TurnInitPhase } from "#phases/turn-init-phase"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, test } from "vitest"; @@ -43,7 +39,7 @@ describe("Moves - Parting Shot", () => { game.move.select(MoveId.PARTING_SHOT); - await game.phaseInterceptor.to(BerryPhase, false); + await game.phaseInterceptor.to("BerryPhase", false); expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(0); expect(game.scene.getPlayerField()[0].species.speciesId).toBe(SpeciesId.MURKROW); @@ -58,7 +54,7 @@ describe("Moves - Parting Shot", () => { game.move.select(MoveId.PARTING_SHOT); - await game.phaseInterceptor.to(BerryPhase, false); + await game.phaseInterceptor.to("BerryPhase", false); expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(0); expect(game.scene.getPlayerField()[0].species.speciesId).toBe(SpeciesId.MURKROW); @@ -79,24 +75,24 @@ describe("Moves - Parting Shot", () => { // use Memento 3 times to debuff enemy game.move.select(MoveId.MEMENTO); - await game.phaseInterceptor.to(FaintPhase); - expect(game.scene.getPlayerParty()[0].isFainted()).toBe(true); + await game.phaseInterceptor.to("FaintPhase"); + expect(game.field.getPlayerPokemon().isFainted()).toBe(true); game.doSelectPartyPokemon(1); - await game.phaseInterceptor.to(TurnInitPhase, false); + await game.phaseInterceptor.to("TurnInitPhase", false); game.move.select(MoveId.MEMENTO); - await game.phaseInterceptor.to(FaintPhase); - expect(game.scene.getPlayerParty()[0].isFainted()).toBe(true); + await game.phaseInterceptor.to("FaintPhase"); + expect(game.field.getPlayerPokemon().isFainted()).toBe(true); game.doSelectPartyPokemon(2); - await game.phaseInterceptor.to(TurnInitPhase, false); + await game.phaseInterceptor.to("TurnInitPhase", false); game.move.select(MoveId.MEMENTO); - await game.phaseInterceptor.to(FaintPhase); - expect(game.scene.getPlayerParty()[0].isFainted()).toBe(true); + await game.phaseInterceptor.to("FaintPhase"); + expect(game.field.getPlayerPokemon().isFainted()).toBe(true); game.doSelectPartyPokemon(3); // set up done - await game.phaseInterceptor.to(TurnInitPhase, false); + await game.phaseInterceptor.to("TurnInitPhase", false); const enemyPokemon = game.field.getEnemyPokemon(); expect(enemyPokemon).toBeDefined(); @@ -106,7 +102,7 @@ describe("Moves - Parting Shot", () => { // now parting shot should fail game.move.select(MoveId.PARTING_SHOT); - await game.phaseInterceptor.to(BerryPhase, false); + await game.phaseInterceptor.to("BerryPhase", false); expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-6); expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(-6); expect(game.scene.getPlayerField()[0].species.speciesId).toBe(SpeciesId.MURKROW); @@ -125,7 +121,7 @@ describe("Moves - Parting Shot", () => { game.move.select(MoveId.PARTING_SHOT); - await game.phaseInterceptor.to(BerryPhase, false); + await game.phaseInterceptor.to("BerryPhase", false); expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(0); expect(game.scene.getPlayerField()[0].species.speciesId).toBe(SpeciesId.MURKROW); @@ -144,7 +140,7 @@ describe("Moves - Parting Shot", () => { game.move.select(MoveId.PARTING_SHOT); - await game.phaseInterceptor.to(BerryPhase, false); + await game.phaseInterceptor.to("BerryPhase", false); expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(0); expect(game.scene.getPlayerField()[0].species.speciesId).toBe(SpeciesId.MURKROW); @@ -153,43 +149,24 @@ describe("Moves - Parting Shot", () => { it.todo( // TODO: fix this bug to pass the test! - "Parting shot should de-buff and not fail if no party available to switch - party size 1", - async () => { - await game.classicMode.startBattle([SpeciesId.MURKROW]); - - const enemyPokemon = game.field.getEnemyPokemon(); - expect(enemyPokemon).toBeDefined(); - - game.move.select(MoveId.PARTING_SHOT); - - await game.phaseInterceptor.to(BerryPhase, false); - expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1); - expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(-1); - expect(game.scene.getPlayerField()[0].species.speciesId).toBe(SpeciesId.MURKROW); - }, - ); - - it.todo( - // TODO: fix this bug to pass the test! - "Parting shot regularly not fail if no party available to switch - party fainted", + "should lower stats without failing if no alive party members available to switch", async () => { await game.classicMode.startBattle([SpeciesId.MURKROW, SpeciesId.MEOWTH]); + + const meowth = game.scene.getPlayerParty()[1]; + meowth.hp = 0; + game.move.select(MoveId.SPLASH); + await game.toNextTurn(); - // intentionally kill party pokemon, switch to second slot (now 1 party mon is fainted) - await game.killPokemon(game.scene.getPlayerParty()[0]); - expect(game.scene.getPlayerParty()[0].isFainted()).toBe(true); - await game.phaseInterceptor.run(MessagePhase); - game.doSelectPartyPokemon(1); - - await game.phaseInterceptor.to(TurnInitPhase, false); game.move.select(MoveId.PARTING_SHOT); + game.doSelectPartyPokemon(1); + await game.toEndOfTurn(); - await game.phaseInterceptor.to(BerryPhase, false); const enemyPokemon = game.field.getEnemyPokemon(); expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(0); - expect(game.scene.getPlayerField()[0].species.speciesId).toBe(SpeciesId.MEOWTH); + expect(game.field.getPlayerPokemon().species.speciesId).toBe(SpeciesId.MURKROW); }, ); }); diff --git a/test/moves/rest.test.ts b/test/moves/rest.test.ts new file mode 100644 index 00000000000..9f29d468e4a --- /dev/null +++ b/test/moves/rest.test.ts @@ -0,0 +1,146 @@ +import { AbilityId } from "#enums/ability-id"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { MoveId } from "#enums/move-id"; +import { MoveResult } from "#enums/move-result"; +import { SpeciesId } from "#enums/species-id"; +import { Stat } from "#enums/stat"; +import { StatusEffect } from "#enums/status-effect"; +import { GameManager } from "#test/test-utils/game-manager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Move - Rest", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .ability(AbilityId.BALL_FETCH) + .battleStyle("single") + .criticalHits(false) + .enemySpecies(SpeciesId.EKANS) + .enemyAbility(AbilityId.BALL_FETCH) + .enemyMoveset(MoveId.SPLASH); + }); + + it("should fully heal the user, cure its prior status and put it to sleep", async () => { + game.override.statusEffect(StatusEffect.POISON); + await game.classicMode.startBattle([SpeciesId.SNORLAX]); + + const snorlax = game.field.getPlayerPokemon(); + snorlax.hp = 1; + expect(snorlax.status?.effect).toBe(StatusEffect.POISON); + + game.move.use(MoveId.REST); + await game.toEndOfTurn(); + + expect(snorlax.hp).toBe(snorlax.getMaxHp()); + expect(snorlax.status?.effect).toBe(StatusEffect.SLEEP); + }); + + it("should always last 3 turns", async () => { + await game.classicMode.startBattle([SpeciesId.SNORLAX]); + + const snorlax = game.field.getPlayerPokemon(); + snorlax.hp = 1; + + // Cf https://bulbapedia.bulbagarden.net/wiki/Rest_(move): + // > The user is unable to use MoveId while asleep for 2 turns after the turn when Rest is used. + game.move.use(MoveId.REST); + await game.toNextTurn(); + + expect(snorlax.status?.effect).toBe(StatusEffect.SLEEP); + expect(snorlax.status?.sleepTurnsRemaining).toBe(3); + + game.move.use(MoveId.SWORDS_DANCE); + await game.toNextTurn(); + expect(snorlax.status?.sleepTurnsRemaining).toBe(2); + + game.move.use(MoveId.SWORDS_DANCE); + await game.toNextTurn(); + expect(snorlax.status?.sleepTurnsRemaining).toBe(1); + + game.move.use(MoveId.SWORDS_DANCE); + await game.toNextTurn(); + expect(snorlax.status?.effect).toBeUndefined(); + expect(snorlax.getStatStage(Stat.ATK)).toBe(2); + }); + + it("should preserve non-volatile status conditions", async () => { + await game.classicMode.startBattle([SpeciesId.SNORLAX]); + + const snorlax = game.field.getPlayerPokemon(); + snorlax.hp = 1; + snorlax.addTag(BattlerTagType.CONFUSED, 999); + + game.move.use(MoveId.REST); + await game.toEndOfTurn(); + + expect(snorlax.getTag(BattlerTagType.CONFUSED)).toBeDefined(); + }); + + it.each<{ name: string; status?: StatusEffect; ability?: AbilityId; dmg?: number }>([ + { name: "is at full HP", dmg: 0 }, + { name: "is grounded on Electric Terrain", ability: AbilityId.ELECTRIC_SURGE }, + { name: "is grounded on Misty Terrain", ability: AbilityId.MISTY_SURGE }, + { name: "has Comatose", ability: AbilityId.COMATOSE }, + ])("should fail if the user $name", async ({ status = StatusEffect.NONE, ability = AbilityId.NONE, dmg = 1 }) => { + game.override.ability(ability).statusEffect(status); + await game.classicMode.startBattle([SpeciesId.SNORLAX]); + + const snorlax = game.field.getPlayerPokemon(); + + snorlax.hp = snorlax.getMaxHp() - dmg; + + game.move.use(MoveId.REST); + await game.toEndOfTurn(); + + expect(snorlax.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + }); + + it("should fail if called while already asleep", async () => { + game.override.statusEffect(StatusEffect.SLEEP).moveset([MoveId.REST, MoveId.SLEEP_TALK]); + await game.classicMode.startBattle([SpeciesId.SNORLAX]); + + const snorlax = game.field.getPlayerPokemon(); + snorlax.hp = 1; + + // Need to use sleep talk here since you normally can't move while asleep + game.move.select(MoveId.SLEEP_TALK); + await game.toEndOfTurn(); + + expect(snorlax.isFullHp()).toBe(false); + expect(snorlax.status?.effect).toBe(StatusEffect.SLEEP); + expect(snorlax.getLastXMoves(-1).map(tm => tm.result)).toEqual([MoveResult.FAIL, MoveResult.SUCCESS]); + }); + + it("should succeed if called the same turn as the user wakes", async () => { + game.override.statusEffect(StatusEffect.SLEEP); + await game.classicMode.startBattle([SpeciesId.SNORLAX]); + + const snorlax = game.field.getPlayerPokemon(); + snorlax.hp = 1; + + expect(snorlax.status?.effect).toBe(StatusEffect.SLEEP); + snorlax.status!.sleepTurnsRemaining = 1; + + game.move.use(MoveId.REST); + await game.toNextTurn(); + + expect(snorlax.status!.effect).toBe(StatusEffect.SLEEP); + expect(snorlax.isFullHp()).toBe(true); + expect(snorlax.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); + expect(snorlax.status!.sleepTurnsRemaining).toBeGreaterThan(1); + }); +}); diff --git a/test/moves/revival-blessing.test.ts b/test/moves/revival-blessing.test.ts index d14fa89c738..4dc7cb97f2d 100644 --- a/test/moves/revival-blessing.test.ts +++ b/test/moves/revival-blessing.test.ts @@ -133,6 +133,6 @@ describe("Moves - Revival Blessing", () => { await game.toNextTurn(); // If there are incorrectly two switch phases into this slot, the fainted pokemon will end up in slot 3 // Make sure it's still in slot 1 - expect(game.scene.getEnemyParty()[0]).toBe(enemyFainting); + expect(game.field.getEnemyPokemon()).toBe(enemyFainting); }); }); diff --git a/test/moves/rollout.test.ts b/test/moves/rollout.test.ts index c1c66f4ab39..0e01725a188 100644 --- a/test/moves/rollout.test.ts +++ b/test/moves/rollout.test.ts @@ -44,10 +44,10 @@ describe("Moves - Rollout", () => { await game.classicMode.startBattle(); - const playerPkm = game.scene.getPlayerParty()[0]; + const playerPkm = game.field.getPlayerPokemon(); vi.spyOn(playerPkm, "stats", "get").mockReturnValue([500000, 1, 1, 1, 1, 1]); // HP, ATK, DEF, SPATK, SPDEF, SPD - const enemyPkm = game.scene.getEnemyParty()[0]; + const enemyPkm = game.field.getEnemyPokemon(); vi.spyOn(enemyPkm, "stats", "get").mockReturnValue([500000, 1, 1, 1, 1, 1]); // HP, ATK, DEF, SPATK, SPDEF, SPD vi.spyOn(enemyPkm, "getHeldItems").mockReturnValue([]); //no berries diff --git a/test/moves/sketch.test.ts b/test/moves/sketch.test.ts index ed010b8a883..0c2527bc09c 100644 --- a/test/moves/sketch.test.ts +++ b/test/moves/sketch.test.ts @@ -6,7 +6,6 @@ import { MoveResult } from "#enums/move-result"; import { SpeciesId } from "#enums/species-id"; import { StatusEffect } from "#enums/status-effect"; import { RandomMoveAttr } from "#moves/move"; -import { PokemonMove } from "#moves/pokemon-move"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -40,7 +39,7 @@ describe("Moves - Sketch", () => { await game.classicMode.startBattle([SpeciesId.REGIELEKI]); const playerPokemon = game.field.getPlayerPokemon(); // can't use normal moveset override because we need to check moveset changes - playerPokemon.moveset = [new PokemonMove(MoveId.SKETCH), new PokemonMove(MoveId.SKETCH)]; + game.move.changeMoveset(playerPokemon, [MoveId.SKETCH, MoveId.SKETCH]); game.move.select(MoveId.SKETCH); await game.phaseInterceptor.to("TurnEndPhase"); @@ -62,7 +61,7 @@ describe("Moves - Sketch", () => { await game.classicMode.startBattle([SpeciesId.REGIELEKI]); const playerPokemon = game.field.getPlayerPokemon(); const enemyPokemon = game.field.getEnemyPokemon(); - playerPokemon.moveset = [new PokemonMove(MoveId.SKETCH), new PokemonMove(MoveId.GROWL)]; + game.move.changeMoveset(playerPokemon, [MoveId.SKETCH, MoveId.GROWL]); game.move.select(MoveId.GROWL); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); @@ -88,8 +87,9 @@ describe("Moves - Sketch", () => { game.override.enemyMoveset([MoveId.METRONOME]); await game.classicMode.startBattle([SpeciesId.REGIELEKI]); + const playerPokemon = game.field.getPlayerPokemon(); - playerPokemon.moveset = [new PokemonMove(MoveId.SKETCH)]; + game.move.changeMoveset(playerPokemon, MoveId.SKETCH); // Opponent uses Metronome -> False Swipe, then player uses Sketch, which should sketch Metronome game.move.select(MoveId.SKETCH); diff --git a/test/moves/sleep-talk.test.ts b/test/moves/sleep-talk.test.ts index 9e8db2e3615..56dc7ba2121 100644 --- a/test/moves/sleep-talk.test.ts +++ b/test/moves/sleep-talk.test.ts @@ -1,6 +1,7 @@ import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { MoveResult } from "#enums/move-result"; +import { MoveUseMode } from "#enums/move-use-mode"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; import { StatusEffect } from "#enums/status-effect"; @@ -31,13 +32,36 @@ describe("Moves - Sleep Talk", () => { .battleStyle("single") .criticalHits(false) .enemySpecies(SpeciesId.MAGIKARP) - .enemyAbility(AbilityId.BALL_FETCH) + .enemyAbility(AbilityId.NO_GUARD) .enemyMoveset(MoveId.SPLASH) .enemyLevel(100); }); - it("should fail when the user is not asleep", async () => { - game.override.statusEffect(StatusEffect.NONE); + it("should call a random valid move if the user is asleep", async () => { + game.override.moveset([MoveId.SLEEP_TALK, MoveId.DIG, MoveId.FLY, MoveId.SWORDS_DANCE]); // Dig and Fly are invalid moves, Swords Dance should always be called + await game.classicMode.startBattle([SpeciesId.FEEBAS]); + + game.move.select(MoveId.SLEEP_TALK); + await game.toNextTurn(); + + const feebas = game.field.getPlayerPokemon(); + expect(feebas.getStatStage(Stat.ATK)).toBe(2); + expect(feebas.getLastXMoves(2)).toEqual([ + expect.objectContaining({ + move: MoveId.SWORDS_DANCE, + result: MoveResult.SUCCESS, + useMode: MoveUseMode.FOLLOW_UP, + }), + expect.objectContaining({ + move: MoveId.SLEEP_TALK, + result: MoveResult.SUCCESS, + useMode: MoveUseMode.NORMAL, + }), + ]); + }); + + it("should fail if the user is not asleep", async () => { + game.override.statusEffect(StatusEffect.POISON); await game.classicMode.startBattle([SpeciesId.FEEBAS]); game.move.select(MoveId.SLEEP_TALK); @@ -45,6 +69,19 @@ describe("Moves - Sleep Talk", () => { expect(game.field.getPlayerPokemon().getLastXMoves()[0].result).toBe(MoveResult.FAIL); }); + it("should fail the turn the user wakes up from Sleep", async () => { + await game.classicMode.startBattle([SpeciesId.FEEBAS]); + + const feebas = game.field.getPlayerPokemon(); + expect(feebas.status?.effect).toBe(StatusEffect.SLEEP); + feebas.status!.sleepTurnsRemaining = 1; + + game.move.select(MoveId.SLEEP_TALK); + await game.toNextTurn(); + + expect(feebas).toHaveUsedMove({ result: MoveResult.FAIL }); + }); + it("should fail if the user has no valid moves", async () => { game.override.moveset([MoveId.SLEEP_TALK, MoveId.DIG, MoveId.METRONOME, MoveId.SOLAR_BEAM]); await game.classicMode.startBattle([SpeciesId.FEEBAS]); @@ -54,22 +91,15 @@ describe("Moves - Sleep Talk", () => { expect(game.field.getPlayerPokemon().getLastXMoves()[0].result).toBe(MoveResult.FAIL); }); - it("should call a random valid move if the user is asleep", async () => { - game.override.moveset([MoveId.SLEEP_TALK, MoveId.DIG, MoveId.FLY, MoveId.SWORDS_DANCE]); // Dig and Fly are invalid moves, Swords Dance should always be called + it("should apply secondary effects of the called move", async () => { + game.override.moveset([MoveId.SLEEP_TALK, MoveId.SCALE_SHOT]); await game.classicMode.startBattle([SpeciesId.FEEBAS]); game.move.select(MoveId.SLEEP_TALK); await game.toNextTurn(); - expect(game.field.getPlayerPokemon().getStatStage(Stat.ATK)); - }); - it("should apply secondary effects of a move", async () => { - game.override.moveset([MoveId.SLEEP_TALK, MoveId.DIG, MoveId.FLY, MoveId.WOOD_HAMMER]); // Dig and Fly are invalid moves, Wood Hammer should always be called - await game.classicMode.startBattle(); - - game.move.select(MoveId.SLEEP_TALK); - await game.toNextTurn(); - - expect(game.field.getPlayerPokemon().isFullHp()).toBeFalsy(); // Wood Hammer recoil effect should be applied + const feebas = game.field.getPlayerPokemon(); + expect(feebas.getStatStage(Stat.SPD)).toBe(1); + expect(feebas.getStatStage(Stat.DEF)).toBe(-1); }); }); diff --git a/test/moves/spikes.test.ts b/test/moves/spikes.test.ts deleted file mode 100644 index 0055945cef9..00000000000 --- a/test/moves/spikes.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { ArenaTrapTag } from "#data/arena-tag"; -import { AbilityId } from "#enums/ability-id"; -import { ArenaTagSide } from "#enums/arena-tag-side"; -import { BattlerIndex } from "#enums/battler-index"; -import { MoveId } from "#enums/move-id"; -import { SpeciesId } from "#enums/species-id"; -import { GameManager } from "#test/test-utils/game-manager"; -import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; - -describe("Moves - Spikes", () => { - let phaserGame: Phaser.Game; - let game: GameManager; - - beforeAll(() => { - phaserGame = new Phaser.Game({ - type: Phaser.HEADLESS, - }); - }); - - afterEach(() => { - game.phaseInterceptor.restoreOg(); - }); - - beforeEach(() => { - game = new GameManager(phaserGame); - game.override - .battleStyle("single") - .enemySpecies(SpeciesId.MAGIKARP) - .enemyAbility(AbilityId.BALL_FETCH) - .ability(AbilityId.BALL_FETCH) - .enemyMoveset(MoveId.SPLASH) - .moveset([MoveId.SPIKES, MoveId.SPLASH, MoveId.ROAR]); - }); - - it("should not damage the team that set them", async () => { - await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.POOCHYENA]); - - game.move.select(MoveId.SPIKES); - await game.toNextTurn(); - - game.move.select(MoveId.SPLASH); - await game.toNextTurn(); - - game.doSwitchPokemon(1); - await game.toNextTurn(); - - game.doSwitchPokemon(1); - await game.toNextTurn(); - - const player = game.scene.getPlayerParty()[0]; - expect(player.hp).toBe(player.getMaxHp()); - }); - - it("should damage opposing pokemon that are forced to switch in", async () => { - game.override.startingWave(5); - await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.POOCHYENA]); - - game.move.select(MoveId.SPIKES); - await game.toNextTurn(); - - game.move.select(MoveId.ROAR); - await game.toNextTurn(); - - const enemy = game.scene.getEnemyParty()[0]; - expect(enemy.hp).toBeLessThan(enemy.getMaxHp()); - }); - - it("should damage opposing pokemon that choose to switch in", async () => { - game.override.startingWave(5); - await game.classicMode.startBattle([SpeciesId.MIGHTYENA, SpeciesId.POOCHYENA]); - - game.move.select(MoveId.SPIKES); - await game.toNextTurn(); - - game.move.select(MoveId.SPLASH); - game.forceEnemyToSwitch(); - await game.toNextTurn(); - - const enemy = game.scene.getEnemyParty()[0]; - expect(enemy.hp).toBeLessThan(enemy.getMaxHp()); - }); - - // TODO: re-enable after re-fixing hazards moves - it.todo("should work when all targets fainted", async () => { - game.override.enemySpecies(SpeciesId.DIGLETT).battleStyle("double").startingLevel(1000); - await game.classicMode.startBattle([SpeciesId.RAYQUAZA, SpeciesId.SHUCKLE]); - - const [enemy1, enemy2] = game.scene.getEnemyField(); - - game.move.use(MoveId.HYPER_VOICE, BattlerIndex.PLAYER); - game.move.use(MoveId.SPIKES, BattlerIndex.PLAYER_2); - await game.toEndOfTurn(); - - expect(enemy1.isFainted()).toBe(true); - expect(enemy2.isFainted()).toBe(true); - expect(game.scene.arena.getTagOnSide(ArenaTrapTag, ArenaTagSide.ENEMY)).toBeDefined(); - }); -}); diff --git a/test/moves/tackle.test.ts b/test/moves/tackle.test.ts index 23abd650e55..5418b16bba8 100644 --- a/test/moves/tackle.test.ts +++ b/test/moves/tackle.test.ts @@ -1,8 +1,6 @@ import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; -import { EnemyCommandPhase } from "#phases/enemy-command-phase"; -import { TurnEndPhase } from "#phases/turn-end-phase"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -41,7 +39,7 @@ describe("Moves - Tackle", () => { await game.classicMode.startBattle([SpeciesId.MIGHTYENA]); const hpOpponent = game.scene.currentBattle.enemyParty[0].hp; game.move.select(moveToUse); - await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnEndPhase); + await game.toEndOfTurn(); const hpLost = hpOpponent - game.scene.currentBattle.enemyParty[0].hp; expect(hpLost).toBe(0); }); @@ -50,12 +48,12 @@ describe("Moves - Tackle", () => { const moveToUse = MoveId.TACKLE; await game.classicMode.startBattle([SpeciesId.MIGHTYENA]); game.scene.currentBattle.enemyParty[0].stats[Stat.DEF] = 50; - game.scene.getPlayerParty()[0].stats[Stat.ATK] = 50; + game.field.getPlayerPokemon().stats[Stat.ATK] = 50; const hpOpponent = game.scene.currentBattle.enemyParty[0].hp; game.move.select(moveToUse); - await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnEndPhase); + await game.toEndOfTurn(); const hpLost = hpOpponent - game.scene.currentBattle.enemyParty[0].hp; expect(hpLost).toBeGreaterThan(0); expect(hpLost).toBeLessThan(4); diff --git a/test/moves/tail-whip.test.ts b/test/moves/tail-whip.test.ts index 8d2dfbda096..70476179b03 100644 --- a/test/moves/tail-whip.test.ts +++ b/test/moves/tail-whip.test.ts @@ -2,8 +2,6 @@ import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; -import { EnemyCommandPhase } from "#phases/enemy-command-phase"; -import { TurnInitPhase } from "#phases/turn-init-phase"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -43,7 +41,7 @@ describe("Moves - Tail whip", () => { expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0); game.move.select(moveToUse); - await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnInitPhase); + await game.toEndOfTurn(); expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(-1); }); diff --git a/test/moves/tera-starstorm.test.ts b/test/moves/tera-starstorm.test.ts index 869cf597dde..178827c6b0a 100644 --- a/test/moves/tera-starstorm.test.ts +++ b/test/moves/tera-starstorm.test.ts @@ -72,7 +72,7 @@ describe("Moves - Tera Starstorm", () => { it("targets both opponents in a double battle when used by Terapagos immediately after terastallizing", async () => { await game.classicMode.startBattle([SpeciesId.TERAPAGOS]); - const terapagos = game.scene.getPlayerParty()[0]; + const terapagos = game.field.getPlayerPokemon(); terapagos.isTerastallized = false; game.move.selectWithTera(MoveId.TERA_STARSTORM, 0); @@ -89,7 +89,7 @@ describe("Moves - Tera Starstorm", () => { it("targets only one opponent in a double battle when used by Terapagos without terastallizing", async () => { await game.classicMode.startBattle([SpeciesId.TERAPAGOS]); - const terapagos = game.scene.getPlayerParty()[0]; + const terapagos = game.field.getPlayerPokemon(); terapagos.isTerastallized = false; game.move.select(MoveId.TERA_STARSTORM, 0, BattlerIndex.ENEMY); @@ -106,8 +106,7 @@ describe("Moves - Tera Starstorm", () => { it("applies the effects when Terapagos in Stellar Form is fused with another Pokemon", async () => { await game.classicMode.startBattle([SpeciesId.TERAPAGOS, SpeciesId.CHARMANDER, SpeciesId.MAGIKARP]); - const fusionedMon = game.scene.getPlayerParty()[0]; - const magikarp = game.scene.getPlayerParty()[2]; + const [fusionedMon, , magikarp] = game.scene.getPlayerParty(); // Fuse party members (taken from PlayerPokemon.fuse(...) function) fusionedMon.fusionSpecies = magikarp.species; diff --git a/test/moves/toxic-spikes.test.ts b/test/moves/toxic-spikes.test.ts deleted file mode 100644 index 0a0bf8baefc..00000000000 --- a/test/moves/toxic-spikes.test.ts +++ /dev/null @@ -1,143 +0,0 @@ -import type { ArenaTrapTag } from "#data/arena-tag"; -import { AbilityId } from "#enums/ability-id"; -import { ArenaTagSide } from "#enums/arena-tag-side"; -import { ArenaTagType } from "#enums/arena-tag-type"; -import { MoveId } from "#enums/move-id"; -import { SpeciesId } from "#enums/species-id"; -import { StatusEffect } from "#enums/status-effect"; -import type { SessionSaveData } from "#system/game-data"; -import { GameData } from "#system/game-data"; -import { GameManager } from "#test/test-utils/game-manager"; -import { decrypt, encrypt } from "#utils/data"; -import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; - -describe("Moves - Toxic Spikes", () => { - let phaserGame: Phaser.Game; - let game: GameManager; - - beforeAll(() => { - phaserGame = new Phaser.Game({ - type: Phaser.HEADLESS, - }); - }); - - afterEach(() => { - game.phaseInterceptor.restoreOg(); - }); - - beforeEach(() => { - game = new GameManager(phaserGame); - game.override - .battleStyle("single") - .startingWave(5) - .enemySpecies(SpeciesId.RATTATA) - .enemyAbility(AbilityId.BALL_FETCH) - .ability(AbilityId.BALL_FETCH) - .enemyMoveset(MoveId.SPLASH) - .moveset([MoveId.TOXIC_SPIKES, MoveId.SPLASH, MoveId.ROAR, MoveId.COURT_CHANGE]); - }); - - it("should not affect the opponent if they do not switch", async () => { - await game.classicMode.runToSummon([SpeciesId.MIGHTYENA, SpeciesId.POOCHYENA]); - - const enemy = game.scene.getEnemyField()[0]; - - game.move.select(MoveId.TOXIC_SPIKES); - await game.phaseInterceptor.to("TurnEndPhase"); - game.move.select(MoveId.SPLASH); - await game.phaseInterceptor.to("TurnEndPhase"); - game.doSwitchPokemon(1); - await game.phaseInterceptor.to("TurnEndPhase"); - - expect(enemy.hp).toBe(enemy.getMaxHp()); - expect(enemy.status?.effect).toBeUndefined(); - }); - - it("should poison the opponent if they switch into 1 layer", async () => { - await game.classicMode.runToSummon([SpeciesId.MIGHTYENA]); - - game.move.select(MoveId.TOXIC_SPIKES); - await game.phaseInterceptor.to("TurnEndPhase"); - game.move.select(MoveId.ROAR); - await game.phaseInterceptor.to("TurnEndPhase"); - - const enemy = game.scene.getEnemyField()[0]; - - expect(enemy.hp).toBeLessThan(enemy.getMaxHp()); - expect(enemy.status?.effect).toBe(StatusEffect.POISON); - }); - - it("should badly poison the opponent if they switch into 2 layers", async () => { - await game.classicMode.runToSummon([SpeciesId.MIGHTYENA]); - - game.move.select(MoveId.TOXIC_SPIKES); - await game.phaseInterceptor.to("TurnEndPhase"); - game.move.select(MoveId.TOXIC_SPIKES); - await game.phaseInterceptor.to("TurnEndPhase"); - game.move.select(MoveId.ROAR); - await game.phaseInterceptor.to("TurnEndPhase"); - - const enemy = game.scene.getEnemyField()[0]; - expect(enemy.hp).toBeLessThan(enemy.getMaxHp()); - expect(enemy.status?.effect).toBe(StatusEffect.TOXIC); - }); - - it("should be removed if a grounded poison pokemon switches in", async () => { - await game.classicMode.runToSummon([SpeciesId.MUK, SpeciesId.PIDGEY]); - - const muk = game.field.getPlayerPokemon(); - - game.move.select(MoveId.TOXIC_SPIKES); - await game.toNextTurn(); - // also make sure the toxic spikes are removed even if the pokemon - // that set them up is the one switching in (https://github.com/pagefaultgames/pokerogue/issues/935) - game.move.select(MoveId.COURT_CHANGE); - await game.toNextTurn(); - game.doSwitchPokemon(1); - await game.toNextTurn(); - game.doSwitchPokemon(1); - await game.toNextTurn(); - game.move.select(MoveId.SPLASH); - await game.toNextTurn(); - - expect(muk.isFullHp()).toBe(true); - expect(muk.status?.effect).toBeUndefined(); - expect(game.scene.arena.tags.length).toBe(0); - }); - - it("shouldn't create multiple layers per use in doubles", async () => { - await game.classicMode.runToSummon([SpeciesId.MIGHTYENA, SpeciesId.POOCHYENA]); - - game.move.select(MoveId.TOXIC_SPIKES); - await game.phaseInterceptor.to("TurnEndPhase"); - - const arenaTags = game.scene.arena.getTagOnSide(ArenaTagType.TOXIC_SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag; - expect(arenaTags.tagType).toBe(ArenaTagType.TOXIC_SPIKES); - expect(arenaTags.layers).toBe(1); - }); - - it("should persist through reload", async () => { - game.override.startingWave(1); - const gameData = new GameData(); - - await game.classicMode.runToSummon([SpeciesId.MIGHTYENA]); - - game.move.select(MoveId.TOXIC_SPIKES); - await game.phaseInterceptor.to("TurnEndPhase"); - game.move.select(MoveId.SPLASH); - await game.doKillOpponents(); - await game.phaseInterceptor.to("BattleEndPhase"); - await game.toNextWave(); - - const sessionData: SessionSaveData = gameData.getSessionSaveData(); - localStorage.setItem("sessionTestData", encrypt(JSON.stringify(sessionData), true)); - const recoveredData: SessionSaveData = gameData.parseSessionData( - decrypt(localStorage.getItem("sessionTestData")!, true), - ); - await gameData.loadSession(0, recoveredData); - - expect(sessionData.arena.tags).toEqual(recoveredData.arena.tags); - localStorage.removeItem("sessionTestData"); - }); -}); diff --git a/test/moves/whirlwind.test.ts b/test/moves/whirlwind.test.ts index bbb2afe621a..61c05a30322 100644 --- a/test/moves/whirlwind.test.ts +++ b/test/moves/whirlwind.test.ts @@ -1,4 +1,3 @@ -import { globalScene } from "#app/global-scene"; import { Status } from "#data/status-effect"; import { AbilityId } from "#enums/ability-id"; import { BattleType } from "#enums/battle-type"; @@ -179,18 +178,13 @@ describe("Moves - Whirlwind", () => { const eligibleEnemy = enemyParty.filter(p => p.hp > 0 && p.isAllowedInBattle()); expect(eligibleEnemy.length).toBe(1); - // Spy on the queueMessage function - const queueSpy = vi.spyOn(globalScene.phaseManager, "queueMessage"); - // Player uses Whirlwind; opponent uses Splash game.move.select(MoveId.WHIRLWIND); await game.move.selectEnemyMove(MoveId.SPLASH); await game.toNextTurn(); - // Verify that the failure message is displayed for Whirlwind - expect(queueSpy).toHaveBeenCalledWith(expect.stringContaining("But it failed")); - // Verify the opponent's Splash message - expect(queueSpy).toHaveBeenCalledWith(expect.stringContaining("But nothing happened!")); + const player = game.field.getPlayerPokemon(); + expect(player).toHaveUsedMove({ move: MoveId.WHIRLWIND, result: MoveResult.FAIL }); }); it("should not pull in the other trainer's pokemon in a partner trainer battle", async () => { diff --git a/test/moves/wish.test.ts b/test/moves/wish.test.ts index 147c598106b..55877edbfd4 100644 --- a/test/moves/wish.test.ts +++ b/test/moves/wish.test.ts @@ -39,15 +39,6 @@ describe("Move - Wish", () => { .enemyLevel(100); }); - /** - * Expect that wish is active with the specified number of attacks. - * @param numAttacks - The number of wish instances that should be queued; default `1` - */ - function expectWishActive(numAttacks = 1) { - const wishes = game.scene.arena.positionalTagManager["tags"].filter(t => t.tagType === PositionalTagType.WISH); - expect(wishes).toHaveLength(numAttacks); - } - it("should heal the Pokemon in the current slot for 50% of the user's maximum HP", async () => { await game.classicMode.startBattle([SpeciesId.ALOMOMOLA, SpeciesId.BLISSEY]); @@ -58,19 +49,19 @@ describe("Move - Wish", () => { game.move.use(MoveId.WISH); await game.toNextTurn(); - expectWishActive(); + expect(game).toHavePositionalTag(PositionalTagType.WISH); game.doSwitchPokemon(1); await game.toEndOfTurn(); - expectWishActive(0); + expect(game).toHavePositionalTag(PositionalTagType.WISH, 0); expect(game.textInterceptor.logs).toContain( i18next.t("arenaTag:wishTagOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(alomomola), }), ); - expect(alomomola.hp).toBe(1); - expect(blissey.hp).toBe(toDmgValue(alomomola.getMaxHp() / 2) + 1); + expect(alomomola).toHaveHp(1); + expect(blissey).toHaveHp(toDmgValue(alomomola.getMaxHp() / 2) + 1); }); it("should work if the user has full HP, but not if it already has an active Wish", async () => { @@ -82,13 +73,13 @@ describe("Move - Wish", () => { game.move.use(MoveId.WISH); await game.toNextTurn(); - expectWishActive(); + expect(game).toHavePositionalTag(PositionalTagType.WISH); game.move.use(MoveId.WISH); await game.toEndOfTurn(); expect(alomomola.hp).toBe(toDmgValue(alomomola.getMaxHp() / 2) + 1); - expect(alomomola.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + expect(alomomola).toHaveUsedMove({ result: MoveResult.FAIL }); }); it("should function independently of Future Sight", async () => { @@ -103,7 +94,8 @@ describe("Move - Wish", () => { await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.toNextTurn(); - expectWishActive(1); + expect(game).toHavePositionalTag(PositionalTagType.WISH); + expect(game).toHavePositionalTag(PositionalTagType.DELAYED_ATTACK); }); it("should work in double battles and trigger in order of creation", async () => { @@ -127,7 +119,7 @@ describe("Move - Wish", () => { await game.setTurnOrder(oldOrder.map(p => p.getBattlerIndex())); await game.toNextTurn(); - expectWishActive(4); + expect(game).toHavePositionalTag(PositionalTagType.WISH, 4); // Lower speed to change turn order alomomola.setStatStage(Stat.SPD, 6); @@ -141,7 +133,7 @@ describe("Move - Wish", () => { await game.phaseInterceptor.to("PositionalTagPhase"); // all wishes have activated and added healing phases - expectWishActive(0); + expect(game).toHavePositionalTag(PositionalTagType.WISH, 0); const healPhases = game.scene.phaseManager.phaseQueue.filter(p => p.is("PokemonHealPhase")); expect(healPhases).toHaveLength(4); @@ -165,14 +157,14 @@ describe("Move - Wish", () => { game.move.use(MoveId.WISH, BattlerIndex.PLAYER_2); await game.toNextTurn(); - expectWishActive(); + expect(game).toHavePositionalTag(PositionalTagType.WISH); game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER); game.move.use(MoveId.MEMENTO, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2); await game.toEndOfTurn(); // Wish went away without doing anything - expectWishActive(0); + expect(game).toHavePositionalTag(PositionalTagType.WISH, 0); expect(game.textInterceptor.logs).not.toContain( i18next.t("arenaTag:wishTagOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(blissey), diff --git a/test/mystery-encounter/encounter-test-utils.ts b/test/mystery-encounter/encounter-test-utils.ts index 784e8ae4950..7b2dbfc9aeb 100644 --- a/test/mystery-encounter/encounter-test-utils.ts +++ b/test/mystery-encounter/encounter-test-utils.ts @@ -9,7 +9,6 @@ import { MessagePhase } from "#phases/message-phase"; import { MysteryEncounterBattlePhase, MysteryEncounterOptionSelectedPhase, - MysteryEncounterPhase, MysteryEncounterRewardsPhase, } from "#phases/mystery-encounter-phases"; import { VictoryPhase } from "#phases/victory-phase"; @@ -89,9 +88,9 @@ export async function runMysteryEncounterToEnd( uiHandler.processInput(Button.ACTION); }); - await game.phaseInterceptor.to(CommandPhase); + await game.toNextTurn(); } else { - await game.phaseInterceptor.to(MysteryEncounterRewardsPhase); + await game.phaseInterceptor.to("MysteryEncounterRewardsPhase"); } } @@ -112,7 +111,7 @@ export async function runSelectMysteryEncounterOption( ); if (game.isCurrentPhase(MessagePhase)) { - await game.phaseInterceptor.run(MessagePhase); + await game.phaseInterceptor.to("MessagePhase"); } // dispose of intro messages @@ -126,7 +125,7 @@ export async function runSelectMysteryEncounterOption( () => game.isCurrentPhase(MysteryEncounterOptionSelectedPhase), ); - await game.phaseInterceptor.to(MysteryEncounterPhase, true); + await game.phaseInterceptor.to("MysteryEncounterPhase", true); // select the desired option const uiHandler = game.scene.ui.getHandler(); @@ -205,7 +204,7 @@ export async function skipBattleRunMysteryEncounterRewardsPhase(game: GameManage game.scene.field.remove(p); }); game.scene.phaseManager.pushPhase(new VictoryPhase(0)); - game.phaseInterceptor.superEndPhase(); + game.endPhase(); game.setMode(UiMode.MESSAGE); - await game.phaseInterceptor.to(MysteryEncounterRewardsPhase, runRewardsPhase); + await game.phaseInterceptor.to("MysteryEncounterRewardsPhase", runRewardsPhase); } diff --git a/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts b/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts index d903568785a..9cd1679c411 100644 --- a/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts +++ b/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts @@ -7,7 +7,6 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; import { ShinyRateBoosterModifier } from "#modifiers/modifier"; -import { PokemonMove } from "#moves/pokemon-move"; import { AnOfferYouCantRefuseEncounter } from "#mystery-encounters/an-offer-you-cant-refuse-encounter"; import * as EncounterPhaseUtils from "#mystery-encounters/encounter-phase-utils"; import * as MysteryEncounters from "#mystery-encounters/mystery-encounters"; @@ -66,7 +65,7 @@ describe("An Offer You Can't Refuse - Mystery Encounter", () => { expect(AnOfferYouCantRefuseEncounter.dialogue).toBeDefined(); expect(AnOfferYouCantRefuseEncounter.dialogue.intro).toStrictEqual([ { text: `${namespace}:intro` }, - { speaker: `${namespace}:speaker`, text: `${namespace}:intro_dialogue` }, + { speaker: `${namespace}:speaker`, text: `${namespace}:introDialogue` }, ]); expect(AnOfferYouCantRefuseEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}:title`); expect(AnOfferYouCantRefuseEncounter.dialogue.encounterOptionsDialogue?.description).toBe( @@ -180,7 +179,7 @@ describe("An Offer You Can't Refuse - Mystery Encounter", () => { expect(option.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option.2.label`, buttonTooltip: `${namespace}:option.2.tooltip`, - disabledButtonTooltip: `${namespace}:option.2.tooltip_disabled`, + disabledButtonTooltip: `${namespace}:option.2.tooltipDisabled`, selected: [ { speaker: `${namespace}:speaker`, @@ -207,9 +206,8 @@ describe("An Offer You Can't Refuse - Mystery Encounter", () => { it("should award EXP to a pokemon with a move in EXTORTION_MOVES", async () => { game.override.ability(AbilityId.SYNCHRONIZE); // Not an extortion ability, so we can test extortion move await game.runToMysteryEncounter(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, [SpeciesId.ABRA]); - const party = scene.getPlayerParty(); - const abra = party.find(pkm => pkm.species.speciesId === SpeciesId.ABRA)!; - abra.moveset = [new PokemonMove(MoveId.BEAT_UP)]; + const abra = game.field.getPlayerPokemon(); + game.move.changeMoveset(abra, MoveId.BEAT_UP); const expBefore = abra.exp; await runMysteryEncounterToEnd(game, 2); diff --git a/test/mystery-encounter/encounters/berries-abound-encounter.test.ts b/test/mystery-encounter/encounters/berries-abound-encounter.test.ts index 25116a89ec5..12c5a6515bc 100644 --- a/test/mystery-encounter/encounters/berries-abound-encounter.test.ts +++ b/test/mystery-encounter/encounters/berries-abound-encounter.test.ts @@ -134,7 +134,7 @@ describe("Berries Abound - Mystery Encounter", () => { await runMysteryEncounterToEnd(game, 1, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); - await game.phaseInterceptor.to(SelectModifierPhase, false); + await game.phaseInterceptor.to("SelectModifierPhase", false); expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); const berriesAfter = scene.findModifiers(m => m instanceof BerryModifier) as BerryModifier[]; @@ -147,9 +147,7 @@ describe("Berries Abound - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); await runMysteryEncounterToEnd(game, 1, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); - await game.phaseInterceptor.to(SelectModifierPhase, false); - expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find( @@ -194,7 +192,7 @@ describe("Berries Abound - Mystery Encounter", () => { // Should be enraged expect(enemyField[0].summonData.statStages).toEqual([0, 1, 0, 1, 1, 0, 0]); - expect(encounterTextSpy).toHaveBeenCalledWith(`${namespace}:option.2.selected_bad`); + expect(encounterTextSpy).toHaveBeenCalledWith(`${namespace}:option.2.selectedBad`); }); it("should start battle if fastest pokemon is slower than boss above wave 50", async () => { @@ -218,7 +216,7 @@ describe("Berries Abound - Mystery Encounter", () => { // Should be enraged expect(enemyField[0].summonData.statStages).toEqual([1, 1, 1, 1, 1, 0, 0]); - expect(encounterTextSpy).toHaveBeenCalledWith(`${namespace}:option.2.selected_bad`); + expect(encounterTextSpy).toHaveBeenCalledWith(`${namespace}:option.2.selectedBad`); }); it("Should skip battle when fastest pokemon is faster than boss", async () => { @@ -232,9 +230,9 @@ describe("Berries Abound - Mystery Encounter", () => { }); await runMysteryEncounterToEnd(game, 2); - await game.phaseInterceptor.to(SelectModifierPhase, false); + await game.phaseInterceptor.to("SelectModifierPhase", false); expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find( diff --git a/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts b/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts index bed9d48d063..13d3c030c63 100644 --- a/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts +++ b/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts @@ -181,7 +181,7 @@ describe("Bug-Type Superfan - Mystery Encounter", () => { }, { speaker: `${namespace}:speaker`, - text: `${namespace}:intro_dialogue`, + text: `${namespace}:introDialogue`, }, ]); expect(BugTypeSuperfanEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}:title`); @@ -368,9 +368,9 @@ describe("Bug-Type Superfan - Mystery Encounter", () => { expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterRewardsPhase.name); game.phaseInterceptor["prompts"] = []; // Clear out prompt handlers game.onNextPrompt("MysteryEncounterRewardsPhase", UiMode.OPTION_SELECT, () => { - game.phaseInterceptor.superEndPhase(); + game.endPhase(); }); - await game.phaseInterceptor.run(MysteryEncounterRewardsPhase); + await game.phaseInterceptor.to("MysteryEncounterRewardsPhase"); expect(selectOptionSpy).toHaveBeenCalledTimes(1); const optionData = selectOptionSpy.mock.calls[0][0]; @@ -389,13 +389,13 @@ describe("Bug-Type Superfan - Mystery Encounter", () => { expect(option.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option.2.label`, buttonTooltip: `${namespace}:option.2.tooltip`, - disabledButtonTooltip: `${namespace}:option.2.disabled_tooltip`, + disabledButtonTooltip: `${namespace}:option.2.disabledTooltip`, }); }); it("should NOT be selectable if the player doesn't have any Bug types", async () => { await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, [SpeciesId.ABRA]); - await game.phaseInterceptor.to(MysteryEncounterPhase, false); + await game.phaseInterceptor.to("MysteryEncounterPhase", false); const encounterPhase = scene.phaseManager.getCurrentPhase(); expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); @@ -417,7 +417,7 @@ describe("Bug-Type Superfan - Mystery Encounter", () => { await runMysteryEncounterToEnd(game, 2); expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find( @@ -436,7 +436,7 @@ describe("Bug-Type Superfan - Mystery Encounter", () => { await runMysteryEncounterToEnd(game, 2); expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find( @@ -458,7 +458,7 @@ describe("Bug-Type Superfan - Mystery Encounter", () => { await runMysteryEncounterToEnd(game, 2); expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find( @@ -482,7 +482,7 @@ describe("Bug-Type Superfan - Mystery Encounter", () => { await runMysteryEncounterToEnd(game, 2); expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find( @@ -513,24 +513,24 @@ describe("Bug-Type Superfan - Mystery Encounter", () => { expect(option.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option.3.label`, buttonTooltip: `${namespace}:option.3.tooltip`, - disabledButtonTooltip: `${namespace}:option.3.disabled_tooltip`, + disabledButtonTooltip: `${namespace}:option.3.disabledTooltip`, selected: [ { text: `${namespace}:option.3.selected`, }, { speaker: `${namespace}:speaker`, - text: `${namespace}:option.3.selected_dialogue`, + text: `${namespace}:option.3.selectedDialogue`, }, ], - secondOptionPrompt: `${namespace}:option.3.select_prompt`, + secondOptionPrompt: `${namespace}:option.3.selectPrompt`, }); }); it("should NOT be selectable if the player doesn't have any Bug items", async () => { game.scene.modifiers = []; await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty); - await game.phaseInterceptor.to(MysteryEncounterPhase, false); + await game.phaseInterceptor.to("MysteryEncounterPhase", false); game.scene.modifiers = []; const encounterPhase = scene.phaseManager.getCurrentPhase(); @@ -558,7 +558,7 @@ describe("Bug-Type Superfan - Mystery Encounter", () => { await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find( diff --git a/test/mystery-encounter/encounters/clowning-around-encounter.test.ts b/test/mystery-encounter/encounters/clowning-around-encounter.test.ts index b573701d568..f02a5c623af 100644 --- a/test/mystery-encounter/encounters/clowning-around-encounter.test.ts +++ b/test/mystery-encounter/encounters/clowning-around-encounter.test.ts @@ -25,7 +25,6 @@ import * as MysteryEncounters from "#mystery-encounters/mystery-encounters"; import { CommandPhase } from "#phases/command-phase"; import { MovePhase } from "#phases/move-phase"; import { PostMysteryEncounterPhase } from "#phases/mystery-encounter-phases"; -import { NewBattlePhase } from "#phases/new-battle-phase"; import { SelectModifierPhase } from "#phases/select-modifier-phase"; import { runMysteryEncounterToEnd, @@ -80,7 +79,7 @@ describe("Clowning Around - Mystery Encounter", () => { { text: `${namespace}:intro` }, { speaker: `${namespace}:speaker`, - text: `${namespace}:intro_dialogue`, + text: `${namespace}:introDialogue`, }, ]); expect(ClowningAroundEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}:title`); @@ -200,9 +199,9 @@ describe("Clowning Around - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty); await runMysteryEncounterToEnd(game, 1, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); - await game.phaseInterceptor.to(SelectModifierPhase, false); + await game.phaseInterceptor.to("SelectModifierPhase", false); expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); const abilityToTrain = scene.currentBattle.mysteryEncounter?.misc.ability; game.onNextPrompt("PostMysteryEncounterPhase", UiMode.MESSAGE, () => { @@ -215,7 +214,7 @@ describe("Clowning Around - Mystery Encounter", () => { const partyUiHandler = game.scene.ui.handlers[UiMode.PARTY] as PartyUiHandler; vi.spyOn(partyUiHandler, "show"); game.endPhase(); - await game.phaseInterceptor.to(PostMysteryEncounterPhase); + await game.phaseInterceptor.to("PostMysteryEncounterPhase"); expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(PostMysteryEncounterPhase.name); // Wait for Yes/No confirmation to appear @@ -228,9 +227,9 @@ describe("Clowning Around - Mystery Encounter", () => { // Click "Select" on Pokemon partyUiHandler.processInput(Button.ACTION); // Stop next battle before it runs - await game.phaseInterceptor.to(NewBattlePhase, false); + await game.phaseInterceptor.to("NewBattlePhase", false); - const leadPokemon = scene.getPlayerParty()[0]; + const leadPokemon = game.field.getPlayerPokemon(); expect(leadPokemon.customPokemonData?.ability).toBe(abilityToTrain); }); }); @@ -249,11 +248,11 @@ describe("Clowning Around - Mystery Encounter", () => { text: `${namespace}:option.2.selected`, }, { - text: `${namespace}:option.2.selected_2`, + text: `${namespace}:option.2.selected2`, }, { speaker: `${namespace}:speaker`, - text: `${namespace}:option.2.selected_3`, + text: `${namespace}:option.2.selected3`, }, ], }); @@ -263,30 +262,30 @@ describe("Clowning Around - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty); // Set some moves on party for attack type booster generation - scene.getPlayerParty()[0].moveset = [new PokemonMove(MoveId.TACKLE), new PokemonMove(MoveId.THIEF)]; + game.move.changeMoveset(game.field.getPlayerPokemon(), [MoveId.TACKLE, MoveId.THIEF]); // 2 Sitrus Berries on lead scene.modifiers = []; let itemType = generateModifierType(modifierTypes.BERRY, [BerryType.SITRUS]) as PokemonHeldItemModifierType; - await addItemToPokemon(scene, scene.getPlayerParty()[0], 2, itemType); + await addItemToPokemon(scene, game.field.getPlayerPokemon(), 2, itemType); // 2 Ganlon Berries on lead itemType = generateModifierType(modifierTypes.BERRY, [BerryType.GANLON]) as PokemonHeldItemModifierType; - await addItemToPokemon(scene, scene.getPlayerParty()[0], 2, itemType); + await addItemToPokemon(scene, game.field.getPlayerPokemon(), 2, itemType); // 5 Golden Punch on lead (ultra) itemType = generateModifierType(modifierTypes.GOLDEN_PUNCH) as PokemonHeldItemModifierType; - await addItemToPokemon(scene, scene.getPlayerParty()[0], 5, itemType); + await addItemToPokemon(scene, game.field.getPlayerPokemon(), 5, itemType); // 5 Lucky Egg on lead (ultra) itemType = generateModifierType(modifierTypes.LUCKY_EGG) as PokemonHeldItemModifierType; - await addItemToPokemon(scene, scene.getPlayerParty()[0], 5, itemType); + await addItemToPokemon(scene, game.field.getPlayerPokemon(), 5, itemType); // 3 Soothe Bell on lead (great tier, but counted as ultra by this ME) itemType = generateModifierType(modifierTypes.SOOTHE_BELL) as PokemonHeldItemModifierType; - await addItemToPokemon(scene, scene.getPlayerParty()[0], 3, itemType); + await addItemToPokemon(scene, game.field.getPlayerPokemon(), 3, itemType); // 5 Soul Dew on lead (rogue) itemType = generateModifierType(modifierTypes.SOUL_DEW) as PokemonHeldItemModifierType; - await addItemToPokemon(scene, scene.getPlayerParty()[0], 5, itemType); + await addItemToPokemon(scene, game.field.getPlayerPokemon(), 5, itemType); // 2 Golden Egg on lead (rogue) itemType = generateModifierType(modifierTypes.GOLDEN_EGG) as PokemonHeldItemModifierType; - await addItemToPokemon(scene, scene.getPlayerParty()[0], 2, itemType); + await addItemToPokemon(scene, game.field.getPlayerPokemon(), 2, itemType); // 5 Soul Dew on second party pokemon (these should not change) itemType = generateModifierType(modifierTypes.SOUL_DEW) as PokemonHeldItemModifierType; @@ -294,7 +293,7 @@ describe("Clowning Around - Mystery Encounter", () => { await runMysteryEncounterToEnd(game, 2); - const leadItemsAfter = scene.getPlayerParty()[0].getHeldItems(); + const leadItemsAfter = game.field.getPlayerPokemon().getHeldItems(); const ultraCountAfter = leadItemsAfter .filter(m => m.type.tier === ModifierTier.ULTRA) .reduce((a, b) => a + b.stackCount, 0); @@ -334,11 +333,11 @@ describe("Clowning Around - Mystery Encounter", () => { text: `${namespace}:option.3.selected`, }, { - text: `${namespace}:option.3.selected_2`, + text: `${namespace}:option.3.selected2`, }, { speaker: `${namespace}:speaker`, - text: `${namespace}:option.3.selected_3`, + text: `${namespace}:option.3.selected3`, }, ], }); @@ -348,14 +347,14 @@ describe("Clowning Around - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty); // Same type moves on lead - scene.getPlayerParty()[0].moveset = [new PokemonMove(MoveId.ICE_BEAM), new PokemonMove(MoveId.SURF)]; + game.move.changeMoveset(game.field.getPlayerPokemon(), [MoveId.ICE_BEAM, MoveId.SURF]); // Different type moves on second - scene.getPlayerParty()[1].moveset = [new PokemonMove(MoveId.GRASS_KNOT), new PokemonMove(MoveId.ELECTRO_BALL)]; + game.move.changeMoveset(scene.getPlayerParty()[1], [MoveId.GRASS_KNOT, MoveId.ELECTRO_BALL]); // No moves on third scene.getPlayerParty()[2].moveset = []; await runMysteryEncounterToEnd(game, 3); - const leadTypesAfter = scene.getPlayerParty()[0].getTypes(); + const leadTypesAfter = game.field.getPlayerPokemon().getTypes(); const secondaryTypesAfter = scene.getPlayerParty()[1].getTypes(); const thirdTypesAfter = scene.getPlayerParty()[2].getTypes(); diff --git a/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts b/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts index 97d0ce31367..de47b074089 100644 --- a/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts +++ b/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts @@ -6,7 +6,6 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; import { UiMode } from "#enums/ui-mode"; -import { PokemonMove } from "#moves/pokemon-move"; import { DancingLessonsEncounter } from "#mystery-encounters/dancing-lessons-encounter"; import * as EncounterPhaseUtils from "#mystery-encounters/encounter-phase-utils"; import * as MysteryEncounters from "#mystery-encounters/mystery-encounters"; @@ -100,7 +99,7 @@ describe("Dancing Lessons - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); // Make party lead's level arbitrarily high to not get KOed by move - const partyLead = scene.getPlayerParty()[0]; + const partyLead = game.field.getPlayerPokemon(); partyLead.level = 1000; partyLead.calculateStats(); await runMysteryEncounterToEnd(game, 1, undefined, true); @@ -121,14 +120,14 @@ describe("Dancing Lessons - Mystery Encounter", () => { it("should have a Baton in the rewards after battle", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); // Make party lead's level arbitrarily high to not get KOed by move - const partyLead = scene.getPlayerParty()[0]; + const partyLead = game.field.getPlayerPokemon(); partyLead.level = 1000; partyLead.calculateStats(); await runMysteryEncounterToEnd(game, 1, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); - await game.phaseInterceptor.to(SelectModifierPhase, false); + await game.phaseInterceptor.to("SelectModifierPhase", false); expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find( @@ -159,7 +158,7 @@ describe("Dancing Lessons - Mystery Encounter", () => { const phaseSpy = vi.spyOn(scene.phaseManager, "unshiftPhase"); await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); - scene.getPlayerParty()[0].moveset = []; + game.field.getPlayerPokemon().moveset = []; await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1 }); const movePhases = phaseSpy.mock.calls.filter(p => p[0] instanceof LearnMovePhase).map(p => p[0]); @@ -171,7 +170,7 @@ describe("Dancing Lessons - Mystery Encounter", () => { const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); - scene.getPlayerParty()[0].moveset = []; + game.field.getPlayerPokemon().moveset = []; await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1 }); expect(leaveEncounterWithoutBattleSpy).toBeCalled(); @@ -186,8 +185,8 @@ describe("Dancing Lessons - Mystery Encounter", () => { expect(option.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option.3.label`, buttonTooltip: `${namespace}:option.3.tooltip`, - disabledButtonTooltip: `${namespace}:option.3.disabled_tooltip`, - secondOptionPrompt: `${namespace}:option.3.select_prompt`, + disabledButtonTooltip: `${namespace}:option.3.disabledTooltip`, + secondOptionPrompt: `${namespace}:option.3.selectPrompt`, selected: [ { text: `${namespace}:option.3.selected`, @@ -199,7 +198,7 @@ describe("Dancing Lessons - Mystery Encounter", () => { it("should add Oricorio to the party", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); const partyCountBefore = scene.getPlayerParty().length; - scene.getPlayerParty()[0].moveset = [new PokemonMove(MoveId.DRAGON_DANCE)]; + game.move.changeMoveset(game.field.getPlayerPokemon(), MoveId.DRAGON_DANCE); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); const partyCountAfter = scene.getPlayerParty().length; @@ -215,7 +214,7 @@ describe("Dancing Lessons - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); const partyCountBefore = scene.getPlayerParty().length; scene.getPlayerParty().forEach(p => (p.moveset = [])); - await game.phaseInterceptor.to(MysteryEncounterPhase, false); + await game.phaseInterceptor.to("MysteryEncounterPhase", false); const encounterPhase = scene.phaseManager.getCurrentPhase(); expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); @@ -238,7 +237,7 @@ describe("Dancing Lessons - Mystery Encounter", () => { const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); - scene.getPlayerParty()[0].moveset = [new PokemonMove(MoveId.DRAGON_DANCE)]; + game.move.changeMoveset(game.field.getPlayerPokemon(), MoveId.DRAGON_DANCE); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); expect(leaveEncounterWithoutBattleSpy).toBeCalled(); diff --git a/test/mystery-encounter/encounters/delibirdy-encounter.test.ts b/test/mystery-encounter/encounters/delibirdy-encounter.test.ts index 16c726f1de6..7398b639f1c 100644 --- a/test/mystery-encounter/encounters/delibirdy-encounter.test.ts +++ b/test/mystery-encounter/encounters/delibirdy-encounter.test.ts @@ -186,7 +186,7 @@ describe("Delibird-y - Mystery Encounter", () => { expect(option.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option.2.label`, buttonTooltip: `${namespace}:option.2.tooltip`, - secondOptionPrompt: `${namespace}:option.2.select_prompt`, + secondOptionPrompt: `${namespace}:option.2.selectPrompt`, selected: [ { text: `${namespace}:option.2.selected`, @@ -201,7 +201,7 @@ describe("Delibird-y - Mystery Encounter", () => { // Set 2 Sitrus berries on party lead scene.modifiers = []; const sitrus = generateModifierType(modifierTypes.BERRY, [BerryType.SITRUS])!; - const sitrusMod = sitrus.newModifier(scene.getPlayerParty()[0]) as BerryModifier; + const sitrusMod = sitrus.newModifier(game.field.getPlayerPokemon()) as BerryModifier; sitrusMod.stackCount = 2; scene.addModifier(sitrusMod, true, false, false, true); await scene.updateModifiers(true); @@ -222,7 +222,7 @@ describe("Delibird-y - Mystery Encounter", () => { // Set 1 Reviver Seed on party lead scene.modifiers = []; const revSeed = generateModifierType(modifierTypes.REVIVER_SEED)!; - const modifier = revSeed.newModifier(scene.getPlayerParty()[0]) as PokemonInstantReviveModifier; + const modifier = revSeed.newModifier(game.field.getPlayerPokemon()) as PokemonInstantReviveModifier; modifier.stackCount = 1; scene.addModifier(modifier, true, false, false, true); await scene.updateModifiers(true); @@ -248,7 +248,7 @@ describe("Delibird-y - Mystery Encounter", () => { const sitrus = generateModifierType(modifierTypes.BERRY, [BerryType.SITRUS])!; // Sitrus berries on party - const sitrusMod = sitrus.newModifier(scene.getPlayerParty()[0]) as BerryModifier; + const sitrusMod = sitrus.newModifier(game.field.getPlayerPokemon()) as BerryModifier; sitrusMod.stackCount = 2; scene.addModifier(sitrusMod, true, false, false, true); await scene.updateModifiers(true); @@ -277,7 +277,7 @@ describe("Delibird-y - Mystery Encounter", () => { // Set 1 Reviver Seed on party lead const revSeed = generateModifierType(modifierTypes.REVIVER_SEED)!; - const modifier = revSeed.newModifier(scene.getPlayerParty()[0]) as PokemonInstantReviveModifier; + const modifier = revSeed.newModifier(game.field.getPlayerPokemon()) as PokemonInstantReviveModifier; modifier.stackCount = 1; scene.addModifier(modifier, true, false, false, true); await scene.updateModifiers(true); @@ -301,7 +301,7 @@ describe("Delibird-y - Mystery Encounter", () => { // Set 1 Soul Dew on party lead scene.modifiers = []; const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!; - const modifier = soulDew.newModifier(scene.getPlayerParty()[0]); + const modifier = soulDew.newModifier(game.field.getPlayerPokemon()); scene.addModifier(modifier, true, false, false, true); await scene.updateModifiers(true); @@ -329,7 +329,7 @@ describe("Delibird-y - Mystery Encounter", () => { // Set 1 Reviver Seed on party lead const revSeed = generateModifierType(modifierTypes.REVIVER_SEED)!; - const modifier = revSeed.newModifier(scene.getPlayerParty()[0]) as PokemonInstantReviveModifier; + const modifier = revSeed.newModifier(game.field.getPlayerPokemon()) as PokemonInstantReviveModifier; modifier.stackCount = 1; scene.addModifier(modifier, true, false, false, true); await scene.updateModifiers(true); @@ -348,7 +348,7 @@ describe("Delibird-y - Mystery Encounter", () => { expect(option.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option.3.label`, buttonTooltip: `${namespace}:option.3.tooltip`, - secondOptionPrompt: `${namespace}:option.3.select_prompt`, + secondOptionPrompt: `${namespace}:option.3.selectPrompt`, selected: [ { text: `${namespace}:option.3.selected`, @@ -363,7 +363,7 @@ describe("Delibird-y - Mystery Encounter", () => { // Set 2 Soul Dew on party lead scene.modifiers = []; const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!; - const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; + const modifier = soulDew.newModifier(game.field.getPlayerPokemon()) as PokemonNatureWeightModifier; modifier.stackCount = 2; scene.addModifier(modifier, true, false, false, true); await scene.updateModifiers(true); @@ -384,7 +384,7 @@ describe("Delibird-y - Mystery Encounter", () => { // Set 1 Soul Dew on party lead scene.modifiers = []; const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!; - const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; + const modifier = soulDew.newModifier(game.field.getPlayerPokemon()) as PokemonNatureWeightModifier; modifier.stackCount = 1; scene.addModifier(modifier, true, false, false, true); await scene.updateModifiers(true); @@ -410,7 +410,7 @@ describe("Delibird-y - Mystery Encounter", () => { // Set 1 Soul Dew on party lead const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!; - const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; + const modifier = soulDew.newModifier(game.field.getPlayerPokemon()) as PokemonNatureWeightModifier; modifier.stackCount = 1; scene.addModifier(modifier, true, false, false, true); await scene.updateModifiers(true); @@ -434,7 +434,7 @@ describe("Delibird-y - Mystery Encounter", () => { // Set 1 Reviver Seed on party lead scene.modifiers = []; const revSeed = generateModifierType(modifierTypes.REVIVER_SEED)!; - const modifier = revSeed.newModifier(scene.getPlayerParty()[0]); + const modifier = revSeed.newModifier(game.field.getPlayerPokemon()); scene.addModifier(modifier, true, false, false, true); await scene.updateModifiers(true); @@ -463,7 +463,7 @@ describe("Delibird-y - Mystery Encounter", () => { // Set 1 Soul Dew on party lead scene.modifiers = []; const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!; - const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; + const modifier = soulDew.newModifier(game.field.getPlayerPokemon()) as PokemonNatureWeightModifier; modifier.stackCount = 1; scene.addModifier(modifier, true, false, false, true); await scene.updateModifiers(true); diff --git a/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts b/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts index 3d84d70b47e..3c19d458049 100644 --- a/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts +++ b/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts @@ -61,7 +61,7 @@ describe("Department Store Sale - Mystery Encounter", () => { { text: `${namespace}:intro` }, { speaker: `${namespace}:speaker`, - text: `${namespace}:intro_dialogue`, + text: `${namespace}:introDialogue`, }, ]); expect(DepartmentStoreSaleEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}:title`); @@ -94,7 +94,7 @@ describe("Department Store Sale - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); await runMysteryEncounterToEnd(game, 1); expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find( @@ -131,7 +131,7 @@ describe("Department Store Sale - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); await runMysteryEncounterToEnd(game, 2); expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find( @@ -171,7 +171,7 @@ describe("Department Store Sale - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); await runMysteryEncounterToEnd(game, 3); expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find( @@ -211,7 +211,7 @@ describe("Department Store Sale - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); await runMysteryEncounterToEnd(game, 4); expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find( diff --git a/test/mystery-encounter/encounters/field-trip-encounter.test.ts b/test/mystery-encounter/encounters/field-trip-encounter.test.ts index 8502137cc6e..fd3e20012b1 100644 --- a/test/mystery-encounter/encounters/field-trip-encounter.test.ts +++ b/test/mystery-encounter/encounters/field-trip-encounter.test.ts @@ -61,7 +61,7 @@ describe("Field Trip - Mystery Encounter", () => { }, { speaker: `${namespace}:speaker`, - text: `${namespace}:intro_dialogue`, + text: `${namespace}:introDialogue`, }, ]); expect(FieldTripEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}:title`); @@ -78,7 +78,7 @@ describe("Field Trip - Mystery Encounter", () => { expect(option.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option.1.label`, buttonTooltip: `${namespace}:option.1.tooltip`, - secondOptionPrompt: `${namespace}:second_option_prompt`, + secondOptionPrompt: `${namespace}:secondOptionPrompt`, }); }); @@ -139,7 +139,7 @@ describe("Field Trip - Mystery Encounter", () => { expect(option.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option.2.label`, buttonTooltip: `${namespace}:option.2.tooltip`, - secondOptionPrompt: `${namespace}:second_option_prompt`, + secondOptionPrompt: `${namespace}:secondOptionPrompt`, }); }); @@ -200,7 +200,7 @@ describe("Field Trip - Mystery Encounter", () => { expect(option.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option.3.label`, buttonTooltip: `${namespace}:option.3.tooltip`, - secondOptionPrompt: `${namespace}:second_option_prompt`, + secondOptionPrompt: `${namespace}:secondOptionPrompt`, }); }); diff --git a/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts b/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts index a48422c459f..54f790ca207 100644 --- a/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts +++ b/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts @@ -253,7 +253,7 @@ describe("Fiery Fallout - Mystery Encounter", () => { expect(option1.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option.3.label`, buttonTooltip: `${namespace}:option.3.tooltip`, - disabledButtonTooltip: `${namespace}:option.3.disabled_tooltip`, + disabledButtonTooltip: `${namespace}:option.3.disabledTooltip`, selected: [ { text: `${namespace}:option.3.selected`, diff --git a/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts b/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts index 8149212f00f..8650b42ce4d 100644 --- a/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts +++ b/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts @@ -6,7 +6,6 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; import { UiMode } from "#enums/ui-mode"; -import { PokemonMove } from "#moves/pokemon-move"; import * as EncounterPhaseUtils from "#mystery-encounters/encounter-phase-utils"; import { FightOrFlightEncounter } from "#mystery-encounters/fight-or-flight-encounter"; import * as MysteryEncounters from "#mystery-encounters/mystery-encounters"; @@ -122,9 +121,9 @@ describe("Fight or Flight - Mystery Encounter", () => { await runMysteryEncounterToEnd(game, 1, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); - await game.phaseInterceptor.to(SelectModifierPhase, false); + await game.phaseInterceptor.to("SelectModifierPhase", false); expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find( @@ -143,7 +142,7 @@ describe("Fight or Flight - Mystery Encounter", () => { expect(option.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option.2.label`, buttonTooltip: `${namespace}:option.2.tooltip`, - disabledButtonTooltip: `${namespace}:option.2.disabled_tooltip`, + disabledButtonTooltip: `${namespace}:option.2.disabledTooltip`, selected: [ { text: `${namespace}:option.2.selected`, @@ -155,7 +154,7 @@ describe("Fight or Flight - Mystery Encounter", () => { it("should NOT be selectable if the player doesn't have a Stealing move", async () => { await game.runToMysteryEncounter(MysteryEncounterType.FIGHT_OR_FLIGHT, defaultParty); scene.getPlayerParty().forEach(p => (p.moveset = [])); - await game.phaseInterceptor.to(MysteryEncounterPhase, false); + await game.phaseInterceptor.to("MysteryEncounterPhase", false); const encounterPhase = scene.phaseManager.getCurrentPhase(); expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); @@ -178,13 +177,13 @@ describe("Fight or Flight - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.FIGHT_OR_FLIGHT, defaultParty); // Mock moveset - scene.getPlayerParty()[0].moveset = [new PokemonMove(MoveId.KNOCK_OFF)]; + game.move.changeMoveset(game.field.getPlayerPokemon(), MoveId.KNOCK_OFF); const item = game.scene.currentBattle.mysteryEncounter!.misc; await runMysteryEncounterToEnd(game, 2); - await game.phaseInterceptor.to(SelectModifierPhase, false); + await game.phaseInterceptor.to("SelectModifierPhase", false); expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find( diff --git a/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts b/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts index 3025b08b8b6..7bfaaac1141 100644 --- a/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts +++ b/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts @@ -71,7 +71,7 @@ describe("Fun And Games! - Mystery Encounter", () => { expect(FunAndGamesEncounter.dialogue.intro).toStrictEqual([ { speaker: `${namespace}:speaker`, - text: `${namespace}:intro_dialogue`, + text: `${namespace}:introDialogue`, }, ]); expect(FunAndGamesEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}:title`); @@ -120,7 +120,7 @@ describe("Fun And Games! - Mystery Encounter", () => { it("should NOT be selectable if the player doesn't have enough money", async () => { game.scene.money = 0; await game.runToMysteryEncounter(MysteryEncounterType.FUN_AND_GAMES, defaultParty); - await game.phaseInterceptor.to(MysteryEncounterPhase, false); + await game.phaseInterceptor.to("MysteryEncounterPhase", false); const encounterPhase = scene.phaseManager.getCurrentPhase(); expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); @@ -162,7 +162,7 @@ describe("Fun And Games! - Mystery Encounter", () => { // Turn 3 (game.scene.phaseManager.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, MoveUseMode.NORMAL); - await game.phaseInterceptor.to(SelectModifierPhase, false); + await game.phaseInterceptor.to("SelectModifierPhase", false); // Rewards expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); @@ -181,11 +181,11 @@ describe("Fun And Games! - Mystery Encounter", () => { // Skip minigame scene.currentBattle.mysteryEncounter!.misc.turnsRemaining = 0; (game.scene.phaseManager.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, MoveUseMode.NORMAL); - await game.phaseInterceptor.to(SelectModifierPhase, false); + await game.phaseInterceptor.to("SelectModifierPhase", false); // Rewards expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find( @@ -210,11 +210,11 @@ describe("Fun And Games! - Mystery Encounter", () => { wobbuffet.hp = Math.floor(0.2 * wobbuffet.getMaxHp()); scene.currentBattle.mysteryEncounter!.misc.turnsRemaining = 0; (game.scene.phaseManager.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, MoveUseMode.NORMAL); - await game.phaseInterceptor.to(SelectModifierPhase, false); + await game.phaseInterceptor.to("SelectModifierPhase", false); // Rewards expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find( @@ -240,11 +240,11 @@ describe("Fun And Games! - Mystery Encounter", () => { wobbuffet.hp = Math.floor(0.1 * wobbuffet.getMaxHp()); scene.currentBattle.mysteryEncounter!.misc.turnsRemaining = 0; (game.scene.phaseManager.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, MoveUseMode.NORMAL); - await game.phaseInterceptor.to(SelectModifierPhase, false); + await game.phaseInterceptor.to("SelectModifierPhase", false); // Rewards expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find( @@ -270,11 +270,11 @@ describe("Fun And Games! - Mystery Encounter", () => { wobbuffet.hp = 1; scene.currentBattle.mysteryEncounter!.misc.turnsRemaining = 0; (game.scene.phaseManager.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, MoveUseMode.NORMAL); - await game.phaseInterceptor.to(SelectModifierPhase, false); + await game.phaseInterceptor.to("SelectModifierPhase", false); // Rewards expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find( diff --git a/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts b/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts index 867a33f6ab6..bb56505ac48 100644 --- a/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts +++ b/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts @@ -98,21 +98,21 @@ describe("Global Trade System - Mystery Encounter", () => { expect(option.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option.1.label`, buttonTooltip: `${namespace}:option.1.tooltip`, - secondOptionPrompt: `${namespace}:option.1.trade_options_prompt`, + secondOptionPrompt: `${namespace}:option.1.tradeOptionsPrompt`, }); }); it("Should trade a Pokemon from the player's party for the first of 3 Pokemon options", async () => { await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, defaultParty); - const speciesBefore = scene.getPlayerParty()[0].species.speciesId; + const speciesBefore = game.field.getPlayerPokemon().species.speciesId; await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1, optionNo: 1 }); const speciesAfter = scene.getPlayerParty().at(-1)?.species.speciesId; expect(speciesAfter).toBeDefined(); expect(speciesBefore).not.toBe(speciesAfter); - expect(defaultParty.includes(speciesAfter!)).toBeFalsy(); + expect(defaultParty).not.toContain(speciesAfter); }); it("Should trade a Pokemon from the player's party for the second of 3 Pokemon options", async () => { @@ -210,7 +210,7 @@ describe("Global Trade System - Mystery Encounter", () => { expect(option.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option.3.label`, buttonTooltip: `${namespace}:option.3.tooltip`, - secondOptionPrompt: `${namespace}:option.3.trade_options_prompt`, + secondOptionPrompt: `${namespace}:option.3.tradeOptionsPrompt`, }); }); @@ -220,14 +220,14 @@ describe("Global Trade System - Mystery Encounter", () => { // Set 2 Soul Dew on party lead scene.modifiers = []; const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!; - const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; + const modifier = soulDew.newModifier(game.field.getPlayerPokemon()) as PokemonNatureWeightModifier; modifier.stackCount = 2; scene.addModifier(modifier, true, false, false, true); await scene.updateModifiers(true); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find( @@ -247,7 +247,7 @@ describe("Global Trade System - Mystery Encounter", () => { // Set 1 Soul Dew on party lead scene.modifiers = []; const soulDew = generateModifierType(modifierTypes.SOUL_DEW)!; - const modifier = soulDew.newModifier(scene.getPlayerParty()[0]) as PokemonNatureWeightModifier; + const modifier = soulDew.newModifier(game.field.getPlayerPokemon()) as PokemonNatureWeightModifier; modifier.stackCount = 1; scene.addModifier(modifier, true, false, false, true); await scene.updateModifiers(true); diff --git a/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts b/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts index d29f8fe6a82..73134381553 100644 --- a/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts +++ b/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts @@ -99,9 +99,9 @@ describe("Lost at Sea - Mystery Encounter", () => { expect(option1.dialogue).toBeDefined(); expect(option1.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option.1.label`, - disabledButtonLabel: `${namespace}:option.1.label_disabled`, + disabledButtonLabel: `${namespace}:option.1.labelDisabled`, buttonTooltip: `${namespace}:option.1.tooltip`, - disabledButtonTooltip: `${namespace}:option.1.tooltip_disabled`, + disabledButtonTooltip: `${namespace}:option.1.tooltipDisabled`, selected: [ { text: `${namespace}:option.1.selected`, @@ -162,9 +162,9 @@ describe("Lost at Sea - Mystery Encounter", () => { expect(option2.dialogue).toBeDefined(); expect(option2.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option.2.label`, - disabledButtonLabel: `${namespace}:option.2.label_disabled`, + disabledButtonLabel: `${namespace}:option.2.labelDisabled`, buttonTooltip: `${namespace}:option.2.tooltip`, - disabledButtonTooltip: `${namespace}:option.2.tooltip_disabled`, + disabledButtonTooltip: `${namespace}:option.2.tooltipDisabled`, selected: [ { text: `${namespace}:option.2.selected`, diff --git a/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts b/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts index 5412f269122..0c4e3044bbd 100644 --- a/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts +++ b/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts @@ -161,9 +161,9 @@ describe("Mysterious Challengers - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, defaultParty); await runMysteryEncounterToEnd(game, 1, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); - await game.phaseInterceptor.to(SelectModifierPhase, false); + await game.phaseInterceptor.to("SelectModifierPhase", false); expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find( @@ -205,9 +205,9 @@ describe("Mysterious Challengers - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, defaultParty); await runMysteryEncounterToEnd(game, 2, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); - await game.phaseInterceptor.to(SelectModifierPhase, false); + await game.phaseInterceptor.to("SelectModifierPhase", false); expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find( @@ -262,9 +262,9 @@ describe("Mysterious Challengers - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, defaultParty); await runMysteryEncounterToEnd(game, 3, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); - await game.phaseInterceptor.to(SelectModifierPhase, false); + await game.phaseInterceptor.to("SelectModifierPhase", false); expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find( diff --git a/test/mystery-encounter/encounters/part-timer-encounter.test.ts b/test/mystery-encounter/encounters/part-timer-encounter.test.ts index 63eea8bbca2..36a92b2b6bf 100644 --- a/test/mystery-encounter/encounters/part-timer-encounter.test.ts +++ b/test/mystery-encounter/encounters/part-timer-encounter.test.ts @@ -5,7 +5,6 @@ import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; -import { PokemonMove } from "#moves/pokemon-move"; import * as EncounterPhaseUtils from "#mystery-encounters/encounter-phase-utils"; import * as MysteryEncounters from "#mystery-encounters/mystery-encounters"; import { CIVILIZATION_ENCOUNTER_BIOMES } from "#mystery-encounters/mystery-encounters"; @@ -65,7 +64,7 @@ describe("Part-Timer - Mystery Encounter", () => { { text: `${namespace}:intro` }, { speaker: `${namespace}:speaker`, - text: `${namespace}:intro_dialogue`, + text: `${namespace}:introDialogue`, }, ]); expect(PartTimerEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}:title`); @@ -110,7 +109,7 @@ describe("Part-Timer - Mystery Encounter", () => { expect(EncounterPhaseUtils.updatePlayerMoney).toHaveBeenCalledWith(scene.getWaveMoneyAmount(1), true, false); // Expect PP of mon's moves to have been reduced to 2 - const moves = scene.getPlayerParty()[0].moveset; + const moves = game.field.getPlayerPokemon().moveset; for (const move of moves) { expect((move?.getMovePp() ?? 0) - (move?.ppUsed ?? 0)).toBe(2); } @@ -219,7 +218,7 @@ describe("Part-Timer - Mystery Encounter", () => { expect(option.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option.3.label`, buttonTooltip: `${namespace}:option.3.tooltip`, - disabledButtonTooltip: `${namespace}:option.3.disabled_tooltip`, + disabledButtonTooltip: `${namespace}:option.3.disabledTooltip`, selected: [ { text: `${namespace}:option.3.selected`, @@ -233,7 +232,9 @@ describe("Part-Timer - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty); // Mock movesets - scene.getPlayerParty().forEach(p => (p.moveset = [])); + scene.getPlayerParty().forEach(p => { + p.moveset = []; + }); await game.phaseInterceptor.to(MysteryEncounterPhase, false); const encounterPhase = scene.phaseManager.getCurrentPhase(); @@ -257,14 +258,14 @@ describe("Part-Timer - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty); // Mock moveset - scene.getPlayerParty()[0].moveset = [new PokemonMove(MoveId.ATTRACT)]; + game.move.changeMoveset(game.field.getPlayerPokemon(), MoveId.ATTRACT); await runMysteryEncounterToEnd(game, 3); expect(EncounterPhaseUtils.updatePlayerMoney).toHaveBeenCalledWith(scene.getWaveMoneyAmount(2.5), true, false); // Expect PP of mon's moves to have been reduced to 2 - const moves = scene.getPlayerParty()[0].moveset; + const moves = game.field.getPlayerPokemon().moveset; for (const move of moves) { - expect((move?.getMovePp() ?? 0) - (move?.ppUsed ?? 0)).toBe(2); + expect(move.getMovePp() - move.ppUsed).toBe(2); } }); diff --git a/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts b/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts index ff4f73cfbde..4d006abc636 100644 --- a/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts +++ b/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts @@ -146,7 +146,7 @@ describe("Teleporting Hijinks - Mystery Encounter", () => { it("should NOT be selectable if the player doesn't have enough money", async () => { game.scene.money = 0; await game.runToMysteryEncounter(MysteryEncounterType.TELEPORTING_HIJINKS, defaultParty); - await game.phaseInterceptor.to(MysteryEncounterPhase, false); + await game.phaseInterceptor.to("MysteryEncounterPhase", false); const encounterPhase = scene.phaseManager.getCurrentPhase(); expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); @@ -207,7 +207,7 @@ describe("Teleporting Hijinks - Mystery Encounter", () => { expect(option.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option.2.label`, buttonTooltip: `${namespace}:option.2.tooltip`, - disabledButtonTooltip: `${namespace}:option.2.disabled_tooltip`, + disabledButtonTooltip: `${namespace}:option.2.disabledTooltip`, selected: [ { text: `${namespace}:option.2.selected`, @@ -218,7 +218,7 @@ describe("Teleporting Hijinks - Mystery Encounter", () => { it("should NOT be selectable if the player doesn't the right type pokemon", async () => { await game.runToMysteryEncounter(MysteryEncounterType.TELEPORTING_HIJINKS, [SpeciesId.BLASTOISE]); - await game.phaseInterceptor.to(MysteryEncounterPhase, false); + await game.phaseInterceptor.to("MysteryEncounterPhase", false); const encounterPhase = scene.phaseManager.getCurrentPhase(); expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); @@ -299,9 +299,9 @@ describe("Teleporting Hijinks - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.TELEPORTING_HIJINKS, defaultParty); await runMysteryEncounterToEnd(game, 3, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); - await game.phaseInterceptor.to(SelectModifierPhase, false); + await game.phaseInterceptor.to("SelectModifierPhase", false); expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find( diff --git a/test/mystery-encounter/encounters/the-expert-breeder-encounter.test.ts b/test/mystery-encounter/encounters/the-expert-breeder-encounter.test.ts index 4556f7a7f45..ade98bfa99f 100644 --- a/test/mystery-encounter/encounters/the-expert-breeder-encounter.test.ts +++ b/test/mystery-encounter/encounters/the-expert-breeder-encounter.test.ts @@ -13,7 +13,6 @@ import * as MysteryEncounters from "#mystery-encounters/mystery-encounters"; import { HUMAN_TRANSITABLE_BIOMES } from "#mystery-encounters/mystery-encounters"; import { TheExpertPokemonBreederEncounter } from "#mystery-encounters/the-expert-pokemon-breeder-encounter"; import { CommandPhase } from "#phases/command-phase"; -import { PostMysteryEncounterPhase } from "#phases/mystery-encounter-phases"; import { SelectModifierPhase } from "#phases/select-modifier-phase"; import { runMysteryEncounterToEnd, @@ -70,8 +69,8 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => { text: `${namespace}:intro`, }, { - speaker: "trainerNames:expert_pokemon_breeder", - text: `${namespace}:intro_dialogue`, + speaker: "trainerNames:expertPokemonBreeder", + text: `${namespace}:introDialogue`, }, ]); expect(TheExpertPokemonBreederEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}:title`); @@ -123,7 +122,7 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => { buttonTooltip: expect.any(String), // Varies based on pokemon selected: [ { - speaker: "trainerNames:expert_pokemon_breeder", + speaker: "trainerNames:expertPokemonBreeder", text: `${namespace}:option.selected`, }, ], @@ -176,7 +175,7 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => { await runMysteryEncounterToEnd(game, 1, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); - await game.phaseInterceptor.to(SelectModifierPhase, false); + await game.phaseInterceptor.to("SelectModifierPhase", false); expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); const eggsAfter = scene.gameData.eggs; @@ -187,8 +186,8 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => { expect(eggsAfter.filter(egg => egg.tier === EggTier.COMMON).length).toBe(commonEggs); expect(eggsAfter.filter(egg => egg.tier === EggTier.RARE).length).toBe(rareEggs); - game.phaseInterceptor.superEndPhase(); - await game.phaseInterceptor.to(PostMysteryEncounterPhase); + game.endPhase(); + await game.phaseInterceptor.to("PostMysteryEncounterPhase"); const friendshipAfter = scene.currentBattle.mysteryEncounter!.misc.pokemon1.friendship; // 20 from ME + extra from winning battle (that extra is not accurate to what happens in game. @@ -208,7 +207,7 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => { buttonTooltip: expect.any(String), // Varies based on pokemon selected: [ { - speaker: "trainerNames:expert_pokemon_breeder", + speaker: "trainerNames:expertPokemonBreeder", text: `${namespace}:option.selected`, }, ], @@ -261,7 +260,7 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => { await runMysteryEncounterToEnd(game, 2, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); - await game.phaseInterceptor.to(SelectModifierPhase, false); + await game.phaseInterceptor.to("SelectModifierPhase", false); expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); const eggsAfter = scene.gameData.eggs; @@ -272,8 +271,8 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => { expect(eggsAfter.filter(egg => egg.tier === EggTier.COMMON).length).toBe(commonEggs); expect(eggsAfter.filter(egg => egg.tier === EggTier.RARE).length).toBe(rareEggs); - game.phaseInterceptor.superEndPhase(); - await game.phaseInterceptor.to(PostMysteryEncounterPhase); + game.endPhase(); + await game.phaseInterceptor.to("PostMysteryEncounterPhase"); const friendshipAfter = scene.currentBattle.mysteryEncounter!.misc.pokemon2.friendship; expect(friendshipAfter).toBe(friendshipBefore + 20 + FRIENDSHIP_GAIN_FROM_BATTLE); // 20 from ME + extra for friendship gained from winning battle @@ -290,7 +289,7 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => { buttonTooltip: expect.any(String), // Varies based on pokemon selected: [ { - speaker: "trainerNames:expert_pokemon_breeder", + speaker: "trainerNames:expertPokemonBreeder", text: `${namespace}:option.selected`, }, ], @@ -343,7 +342,7 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => { await runMysteryEncounterToEnd(game, 3, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); - await game.phaseInterceptor.to(SelectModifierPhase, false); + await game.phaseInterceptor.to("SelectModifierPhase", false); expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); const eggsAfter = scene.gameData.eggs; @@ -354,8 +353,8 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => { expect(eggsAfter.filter(egg => egg.tier === EggTier.COMMON).length).toBe(commonEggs); expect(eggsAfter.filter(egg => egg.tier === EggTier.RARE).length).toBe(rareEggs); - game.phaseInterceptor.superEndPhase(); - await game.phaseInterceptor.to(PostMysteryEncounterPhase); + game.endPhase(); + await game.phaseInterceptor.to("PostMysteryEncounterPhase"); const friendshipAfter = scene.currentBattle.mysteryEncounter!.misc.pokemon3.friendship; expect(friendshipAfter).toBe(friendshipBefore + 20 + FRIENDSHIP_GAIN_FROM_BATTLE); // 20 + extra for friendship gained from winning battle diff --git a/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts b/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts index 611a103dab2..3880c07c312 100644 --- a/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts +++ b/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts @@ -67,11 +67,11 @@ describe("The Pokemon Salesman - Mystery Encounter", () => { expect(dialogue).toBeDefined(); expect(dialogue.intro).toStrictEqual([ { text: `${namespace}:intro` }, - { speaker: `${namespace}:speaker`, text: `${namespace}:intro_dialogue` }, + { speaker: `${namespace}:speaker`, text: `${namespace}:introDialogue` }, ]); const { title, description, query } = dialogue.encounterOptionsDialogue!; expect(title).toBe(`${namespace}:title`); - expect(description).toMatch(new RegExp(`^${namespace}\\:description(_shiny)?$`)); + expect(description).toMatch(new RegExp(`^${namespace}\\:description(Shiny)?$`)); expect(query).toBe(`${namespace}:query`); expect(options.length).toBe(2); }); @@ -117,10 +117,10 @@ describe("The Pokemon Salesman - Mystery Encounter", () => { expect(dialogue).toBeDefined(); expect(dialogue).toStrictEqual({ buttonLabel: `${namespace}:option.1.label`, - buttonTooltip: expect.stringMatching(new RegExp(`^${namespace}\\:option\\.1\\.tooltip(_shiny)?$`)), + buttonTooltip: expect.stringMatching(new RegExp(`^${namespace}\\:option\\.1\\.tooltip(Shiny)?$`)), selected: [ { - text: `${namespace}:option.1.selected_message`, + text: `${namespace}:option.1.selectedMessage`, }, ], }); diff --git a/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts b/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts index a314a14485f..3592e2dc774 100644 --- a/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts +++ b/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts @@ -229,9 +229,9 @@ describe("The Strong Stuff - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.THE_STRONG_STUFF, defaultParty); await runMysteryEncounterToEnd(game, 2, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); - await game.phaseInterceptor.to(SelectModifierPhase, false); + await game.phaseInterceptor.to("SelectModifierPhase", false); expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find( diff --git a/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts b/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts index ae2f9fd79ff..cf0ff7a94bd 100644 --- a/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts +++ b/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts @@ -16,7 +16,6 @@ import * as MysteryEncounters from "#mystery-encounters/mystery-encounters"; import { HUMAN_TRANSITABLE_BIOMES } from "#mystery-encounters/mystery-encounters"; import { TheWinstrateChallengeEncounter } from "#mystery-encounters/the-winstrate-challenge-encounter"; import { CommandPhase } from "#phases/command-phase"; -import { MysteryEncounterRewardsPhase } from "#phases/mystery-encounter-phases"; import { PartyHealPhase } from "#phases/party-heal-phase"; import { SelectModifierPhase } from "#phases/select-modifier-phase"; import { VictoryPhase } from "#phases/victory-phase"; @@ -73,7 +72,7 @@ describe("The Winstrate Challenge - Mystery Encounter", () => { { text: `${namespace}:intro` }, { speaker: `${namespace}:speaker`, - text: `${namespace}:intro_dialogue`, + text: `${namespace}:introDialogue`, }, ]); expect(TheWinstrateChallengeEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}:title`); @@ -295,9 +294,9 @@ describe("The Winstrate Challenge - Mystery Encounter", () => { // Should have Macho Brace in the rewards await skipBattleToNextBattle(game, true); - await game.phaseInterceptor.to(SelectModifierPhase, false); + await game.phaseInterceptor.to("SelectModifierPhase", false); expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find( @@ -339,7 +338,7 @@ describe("The Winstrate Challenge - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.THE_WINSTRATE_CHALLENGE, defaultParty); await runMysteryEncounterToEnd(game, 2); expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find( @@ -366,11 +365,10 @@ async function skipBattleToNextBattle(game: GameManager, isFinalBattle = false) p.status = new Status(StatusEffect.FAINT); game.scene.field.remove(p); }); - game.phaseInterceptor["onHold"] = []; game.scene.phaseManager.pushPhase(new VictoryPhase(0)); - game.phaseInterceptor.superEndPhase(); + game.endPhase(); if (isFinalBattle) { - await game.phaseInterceptor.to(MysteryEncounterRewardsPhase); + await game.phaseInterceptor.to("MysteryEncounterRewardsPhase"); } else { await game.toNextTurn(); } diff --git a/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts b/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts index 133fbfb10ba..b32dcddadb8 100644 --- a/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts +++ b/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts @@ -172,7 +172,7 @@ describe("Trash to Treasure - Mystery Encounter", () => { it("should give 2 Leftovers, 1 Shell Bell, and Black Sludge", async () => { await game.runToMysteryEncounter(MysteryEncounterType.TRASH_TO_TREASURE, defaultParty); await runMysteryEncounterToEnd(game, 1); - await game.phaseInterceptor.to(SelectModifierPhase, false); + await game.phaseInterceptor.to("SelectModifierPhase", false); expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); const leftovers = scene.findModifier(m => m instanceof TurnHealModifier) as TurnHealModifier; @@ -242,9 +242,9 @@ describe("Trash to Treasure - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.TRASH_TO_TREASURE, defaultParty); await runMysteryEncounterToEnd(game, 2, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); - await game.phaseInterceptor.to(SelectModifierPhase, false); + await game.phaseInterceptor.to("SelectModifierPhase", false); expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find( diff --git a/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts b/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts index 24d7960049e..5aadaf5c29a 100644 --- a/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts +++ b/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts @@ -11,7 +11,6 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; import type { BerryModifier } from "#modifiers/modifier"; -import { PokemonMove } from "#moves/pokemon-move"; import * as EncounterPhaseUtils from "#mystery-encounters/encounter-phase-utils"; import { generateModifierType } from "#mystery-encounters/encounter-phase-utils"; import * as MysteryEncounters from "#mystery-encounters/mystery-encounters"; @@ -172,7 +171,7 @@ describe("Uncommon Breed - Mystery Encounter", () => { expect(option.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option.2.label`, buttonTooltip: `${namespace}:option.2.tooltip`, - disabledButtonTooltip: `${namespace}:option.2.disabled_tooltip`, + disabledButtonTooltip: `${namespace}:option.2.disabledTooltip`, selected: [ { text: `${namespace}:option.2.selected`, @@ -214,11 +213,11 @@ describe("Uncommon Breed - Mystery Encounter", () => { // Berries on party lead const sitrus = generateModifierType(modifierTypes.BERRY, [BerryType.SITRUS])!; - const sitrusMod = sitrus.newModifier(scene.getPlayerParty()[0]) as BerryModifier; + const sitrusMod = sitrus.newModifier(game.field.getPlayerPokemon()) as BerryModifier; sitrusMod.stackCount = 2; scene.addModifier(sitrusMod, true, false, false, true); const ganlon = generateModifierType(modifierTypes.BERRY, [BerryType.GANLON])!; - const ganlonMod = ganlon.newModifier(scene.getPlayerParty()[0]) as BerryModifier; + const ganlonMod = ganlon.newModifier(game.field.getPlayerPokemon()) as BerryModifier; ganlonMod.stackCount = 3; scene.addModifier(ganlonMod, true, false, false, true); await scene.updateModifiers(true); @@ -237,7 +236,7 @@ describe("Uncommon Breed - Mystery Encounter", () => { expect(option.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option.3.label`, buttonTooltip: `${namespace}:option.3.tooltip`, - disabledButtonTooltip: `${namespace}:option.3.disabled_tooltip`, + disabledButtonTooltip: `${namespace}:option.3.disabledTooltip`, selected: [ { text: `${namespace}:option.3.selected`, @@ -270,7 +269,7 @@ describe("Uncommon Breed - Mystery Encounter", () => { const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); await game.runToMysteryEncounter(MysteryEncounterType.UNCOMMON_BREED, defaultParty); // Mock moveset - scene.getPlayerParty()[0].moveset = [new PokemonMove(MoveId.CHARM)]; + game.move.changeMoveset(game.field.getPlayerPokemon(), MoveId.CHARM); await runMysteryEncounterToEnd(game, 3); expect(leaveEncounterWithoutBattleSpy).toBeCalled(); diff --git a/test/mystery-encounter/encounters/weird-dream-encounter.test.ts b/test/mystery-encounter/encounters/weird-dream-encounter.test.ts index ed0d612e967..e2ec7ae514a 100644 --- a/test/mystery-encounter/encounters/weird-dream-encounter.test.ts +++ b/test/mystery-encounter/encounters/weird-dream-encounter.test.ts @@ -68,7 +68,7 @@ describe("Weird Dream - Mystery Encounter", () => { }, { speaker: `${namespace}:speaker`, - text: `${namespace}:intro_dialogue`, + text: `${namespace}:introDialogue`, }, ]); expect(WeirdDreamEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}:title`); @@ -112,11 +112,11 @@ describe("Weird Dream - Mystery Encounter", () => { it("should transform the new party into new species, 2 at +90/+110, the rest at +40/50 BST", async () => { await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty); - const pokemonPrior = scene.getPlayerParty().map(pokemon => pokemon); + const pokemonPrior = scene.getPlayerParty().slice(); const bstsPrior = pokemonPrior.map(species => species.getSpeciesForm().getBaseStatTotal()); await runMysteryEncounterToEnd(game, 1); - await game.phaseInterceptor.to(SelectModifierPhase, false); + await game.phaseInterceptor.to("SelectModifierPhase", false); expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); const pokemonAfter = scene.getPlayerParty(); @@ -139,9 +139,9 @@ describe("Weird Dream - Mystery Encounter", () => { it("should have 1 Memory Mushroom, 5 Rogue Balls, and 3 Mints in rewards", async () => { await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty); await runMysteryEncounterToEnd(game, 1); - await game.phaseInterceptor.to(SelectModifierPhase, false); + await game.phaseInterceptor.to("SelectModifierPhase", false); expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find( @@ -196,9 +196,9 @@ describe("Weird Dream - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty); await runMysteryEncounterToEnd(game, 2, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); - await game.phaseInterceptor.to(SelectModifierPhase, false); + await game.phaseInterceptor.to("SelectModifierPhase", false); expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find( diff --git a/test/mystery-encounter/mystery-encounter-utils.test.ts b/test/mystery-encounter/mystery-encounter-utils.test.ts index adcc3111319..361c3f7480e 100644 --- a/test/mystery-encounter/mystery-encounter-utils.test.ts +++ b/test/mystery-encounter/mystery-encounter-utils.test.ts @@ -63,7 +63,7 @@ describe("Mystery Encounter Utils", () => { // Both pokemon fainted scene.getPlayerParty().forEach(p => { p.hp = 0; - p.trySetStatus(StatusEffect.FAINT); + p.doSetStatus(StatusEffect.FAINT); void p.updateInfo(); }); @@ -83,7 +83,7 @@ describe("Mystery Encounter Utils", () => { // Only faint 1st pokemon const party = scene.getPlayerParty(); party[0].hp = 0; - party[0].trySetStatus(StatusEffect.FAINT); + party[0].doSetStatus(StatusEffect.FAINT); await party[0].updateInfo(); // Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal) @@ -102,7 +102,7 @@ describe("Mystery Encounter Utils", () => { // Only faint 1st pokemon const party = scene.getPlayerParty(); party[0].hp = 0; - party[0].trySetStatus(StatusEffect.FAINT); + party[0].doSetStatus(StatusEffect.FAINT); await party[0].updateInfo(); // Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal) @@ -121,7 +121,7 @@ describe("Mystery Encounter Utils", () => { // Only faint 1st pokemon const party = scene.getPlayerParty(); party[0].hp = 0; - party[0].trySetStatus(StatusEffect.FAINT); + party[0].doSetStatus(StatusEffect.FAINT); await party[0].updateInfo(); // Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal) @@ -167,7 +167,7 @@ describe("Mystery Encounter Utils", () => { const party = scene.getPlayerParty(); party[0].level = 100; party[0].hp = 0; - party[0].trySetStatus(StatusEffect.FAINT); + party[0].doSetStatus(StatusEffect.FAINT); await party[0].updateInfo(); party[1].level = 10; @@ -206,7 +206,7 @@ describe("Mystery Encounter Utils", () => { const party = scene.getPlayerParty(); party[0].level = 10; party[0].hp = 0; - party[0].trySetStatus(StatusEffect.FAINT); + party[0].doSetStatus(StatusEffect.FAINT); await party[0].updateInfo(); party[1].level = 100; diff --git a/test/phases/form-change-phase.test.ts b/test/phases/form-change-phase.test.ts index 3caf824b252..17fe7f4449c 100644 --- a/test/phases/form-change-phase.test.ts +++ b/test/phases/form-change-phase.test.ts @@ -38,7 +38,7 @@ describe("Form Change Phase", () => { await game.classicMode.startBattle([SpeciesId.ZACIAN]); // Before the form change: Should be Hero form - const zacian = game.scene.getPlayerParty()[0]; + const zacian = game.field.getPlayerPokemon(); expect(zacian.getFormKey()).toBe("hero-of-many-battles"); expect(zacian.getTypes()).toStrictEqual([PokemonType.FAIRY]); expect(zacian.calculateBaseStats()).toStrictEqual([92, 120, 115, 80, 115, 138]); diff --git a/test/phases/mystery-encounter-phase.test.ts b/test/phases/mystery-encounter-phase.test.ts index 2b6105c7034..a3dc779b02c 100644 --- a/test/phases/mystery-encounter-phase.test.ts +++ b/test/phases/mystery-encounter-phase.test.ts @@ -37,7 +37,7 @@ describe("Mystery Encounter Phases", () => { SpeciesId.VOLCARONA, ]); - await game.phaseInterceptor.to(MysteryEncounterPhase, false); + await game.phaseInterceptor.to("MysteryEncounterPhase", false); expect(game.scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); }); @@ -49,9 +49,9 @@ describe("Mystery Encounter Phases", () => { game.onNextPrompt("MysteryEncounterPhase", UiMode.MYSTERY_ENCOUNTER, () => { // End phase early for test - game.phaseInterceptor.superEndPhase(); + game.endPhase(); }); - await game.phaseInterceptor.run(MysteryEncounterPhase); + await game.phaseInterceptor.to("MysteryEncounterPhase"); expect(game.scene.mysteryEncounterSaveData.encounteredEvents.length).toBeGreaterThan(0); expect(game.scene.mysteryEncounterSaveData.encounteredEvents[0].type).toEqual( @@ -75,7 +75,7 @@ describe("Mystery Encounter Phases", () => { handler.processInput(Button.ACTION); }); - await game.phaseInterceptor.run(MysteryEncounterPhase); + await game.phaseInterceptor.to("MysteryEncounterPhase"); // Select option 1 for encounter const handler = game.scene.ui.getHandler() as MysteryEncounterUiHandler; diff --git a/test/phases/select-modifier-phase.test.ts b/test/phases/select-modifier-phase.test.ts index ae4cebb1866..b77e31e931f 100644 --- a/test/phases/select-modifier-phase.test.ts +++ b/test/phases/select-modifier-phase.test.ts @@ -241,7 +241,7 @@ describe("SelectModifierPhase", () => { const selectModifierPhase = new SelectModifierPhase(0, undefined, customModifiers); scene.phaseManager.unshiftPhase(selectModifierPhase); game.move.select(MoveId.SPLASH); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find( @@ -265,7 +265,7 @@ describe("SelectModifierPhase", () => { const selectModifierPhase = new SelectModifierPhase(0, undefined, customModifiers); scene.phaseManager.unshiftPhase(selectModifierPhase); game.move.select(MoveId.SPLASH); - await game.phaseInterceptor.run(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); expect(scene.ui.getMode()).to.equal(UiMode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find( diff --git a/test/system/rename-run.test.ts b/test/system/rename-run.test.ts new file mode 100644 index 00000000000..5031d84245f --- /dev/null +++ b/test/system/rename-run.test.ts @@ -0,0 +1,82 @@ +import * as account from "#app/account"; +import * as bypassLoginModule from "#app/global-vars/bypass-login"; +import { pokerogueApi } from "#app/plugins/api/pokerogue-api"; +import type { SessionSaveData } from "#app/system/game-data"; +import { AbilityId } from "#enums/ability-id"; +import { MoveId } from "#enums/move-id"; +import { GameManager } from "#test/test-utils/game-manager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("System - Rename Run", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([MoveId.SPLASH]) + .battleStyle("single") + .enemyAbility(AbilityId.BALL_FETCH) + .enemyMoveset(MoveId.SPLASH); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + describe("renameSession", () => { + beforeEach(() => { + vi.spyOn(bypassLoginModule, "bypassLogin", "get").mockReturnValue(false); + vi.spyOn(account, "updateUserInfo").mockImplementation(async () => [true, 1]); + }); + + it("should return false if slotId < 0", async () => { + const result = await game.scene.gameData.renameSession(-1, "Named Run"); + + expect(result).toEqual(false); + }); + + it("should return false if getSession returns null", async () => { + vi.spyOn(game.scene.gameData, "getSession").mockResolvedValue(null as unknown as SessionSaveData); + + const result = await game.scene.gameData.renameSession(-1, "Named Run"); + + expect(result).toEqual(false); + }); + + it("should return true if bypassLogin is true", async () => { + vi.spyOn(bypassLoginModule, "bypassLogin", "get").mockReturnValue(true); + vi.spyOn(game.scene.gameData, "getSession").mockResolvedValue({} as SessionSaveData); + + const result = await game.scene.gameData.renameSession(0, "Named Run"); + + expect(result).toEqual(true); + }); + + it("should return false if api returns error", async () => { + vi.spyOn(game.scene.gameData, "getSession").mockResolvedValue({} as SessionSaveData); + vi.spyOn(pokerogueApi.savedata.session, "update").mockResolvedValue("Unknown Error!"); + + const result = await game.scene.gameData.renameSession(0, "Named Run"); + + expect(result).toEqual(false); + }); + + it("should return true if api is succesfull", async () => { + vi.spyOn(game.scene.gameData, "getSession").mockResolvedValue({} as SessionSaveData); + vi.spyOn(pokerogueApi.savedata.session, "update").mockResolvedValue(""); + + const result = await game.scene.gameData.renameSession(0, "Named Run"); + + expect(result).toEqual(true); + expect(account.updateUserInfo).toHaveBeenCalled(); + }); + }); +}); diff --git a/test/test-utils/game-manager.ts b/test/test-utils/game-manager.ts index 49446c906ca..d1c311b8e80 100644 --- a/test/test-utils/game-manager.ts +++ b/test/test-utils/game-manager.ts @@ -20,13 +20,11 @@ import { ModifierTypeOption } from "#modifiers/modifier-type"; import { CheckSwitchPhase } from "#phases/check-switch-phase"; import { CommandPhase } from "#phases/command-phase"; import { EncounterPhase } from "#phases/encounter-phase"; -import { LoginPhase } from "#phases/login-phase"; import { MovePhase } from "#phases/move-phase"; import { MysteryEncounterPhase } from "#phases/mystery-encounter-phases"; import { NewBattlePhase } from "#phases/new-battle-phase"; import { SelectStarterPhase } from "#phases/select-starter-phase"; import type { SelectTargetPhase } from "#phases/select-target-phase"; -import { TitlePhase } from "#phases/title-phase"; import { TurnEndPhase } from "#phases/turn-end-phase"; import { TurnInitPhase } from "#phases/turn-init-phase"; import { TurnStartPhase } from "#phases/turn-start-phase"; @@ -188,10 +186,12 @@ export class GameManager { * @returns A promise that resolves when the title phase is reached. */ async runToTitle(): Promise { - await this.phaseInterceptor.whenAboutToRun(LoginPhase); - this.phaseInterceptor.pop(); - await this.phaseInterceptor.run(TitlePhase); + // Go to login phase and skip past it + await this.phaseInterceptor.to("LoginPhase", false); + this.phaseInterceptor.shiftPhase(true); + await this.phaseInterceptor.to("TitlePhase"); + // TODO: This should be moved to a separate initialization method this.scene.gameSpeed = 5; this.scene.moveAnimations = false; this.scene.showLevelUpStats = false; @@ -224,7 +224,7 @@ export class GameManager { // This will consider all battle entry dialog as seens and skip them vi.spyOn(this.scene.ui, "shouldSkipDialogue").mockReturnValue(true); - if (overrides.OPP_HELD_ITEMS_OVERRIDE.length === 0) { + if (overrides.ENEMY_HELD_ITEMS_OVERRIDE.length === 0) { this.removeEnemyHeldItems(); } @@ -270,7 +270,7 @@ export class GameManager { true, ); - await this.phaseInterceptor.run(EncounterPhase); + await this.phaseInterceptor.to("EncounterPhase"); if (!isNullOrUndefined(encounterType)) { expect(this.scene.currentBattle?.mysteryEncounter?.encounterType).toBe(encounterType); } @@ -548,7 +548,7 @@ export class GameManager { * This *does not* account for priority and will override Trick Room's effect. */ async setTurnOrder(order: BattlerIndex[]): Promise { - await this.phaseInterceptor.to(TurnStartPhase, false); + await this.phaseInterceptor.to("TurnStartPhase", false); vi.spyOn(this.scene.phaseManager.getCurrentPhase() as TurnStartPhase, "getSpeedOrder").mockReturnValue(order); } diff --git a/test/test-utils/helpers/challenge-mode-helper.ts b/test/test-utils/helpers/challenge-mode-helper.ts index 3952685a560..a6d4b5c3b5a 100644 --- a/test/test-utils/helpers/challenge-mode-helper.ts +++ b/test/test-utils/helpers/challenge-mode-helper.ts @@ -49,8 +49,8 @@ export class ChallengeModeHelper extends GameManagerHelper { selectStarterPhase.initBattle(starters); }); - await this.game.phaseInterceptor.run(EncounterPhase); - if (overrides.OPP_HELD_ITEMS_OVERRIDE.length === 0 && this.game.override.removeEnemyStartingItems) { + await this.game.phaseInterceptor.to("EncounterPhase"); + if (overrides.ENEMY_HELD_ITEMS_OVERRIDE.length === 0 && this.game.override.removeEnemyStartingItems) { this.game.removeEnemyHeldItems(); } } diff --git a/test/test-utils/helpers/classic-mode-helper.ts b/test/test-utils/helpers/classic-mode-helper.ts index 5d73dc07615..008648fcd0d 100644 --- a/test/test-utils/helpers/classic-mode-helper.ts +++ b/test/test-utils/helpers/classic-mode-helper.ts @@ -53,7 +53,7 @@ export class ClassicModeHelper extends GameManagerHelper { }); await this.game.phaseInterceptor.to(EncounterPhase); - if (overrides.OPP_HELD_ITEMS_OVERRIDE.length === 0 && this.game.override.removeEnemyStartingItems) { + if (overrides.ENEMY_HELD_ITEMS_OVERRIDE.length === 0 && this.game.override.removeEnemyStartingItems) { this.game.removeEnemyHeldItems(); } } diff --git a/test/test-utils/helpers/daily-mode-helper.ts b/test/test-utils/helpers/daily-mode-helper.ts index 7aa1e699118..ca882eaf548 100644 --- a/test/test-utils/helpers/daily-mode-helper.ts +++ b/test/test-utils/helpers/daily-mode-helper.ts @@ -37,7 +37,7 @@ export class DailyModeHelper extends GameManagerHelper { await this.game.phaseInterceptor.to(EncounterPhase); - if (overrides.OPP_HELD_ITEMS_OVERRIDE.length === 0 && this.game.override.removeEnemyStartingItems) { + if (overrides.ENEMY_HELD_ITEMS_OVERRIDE.length === 0 && this.game.override.removeEnemyStartingItems) { this.game.removeEnemyHeldItems(); } } diff --git a/test/test-utils/helpers/move-helper.ts b/test/test-utils/helpers/move-helper.ts index 6a01e4110da..3d5e9ae6af9 100644 --- a/test/test-utils/helpers/move-helper.ts +++ b/test/test-utils/helpers/move-helper.ts @@ -228,8 +228,8 @@ export class MoveHelper extends GameManagerHelper { console.warn("Player moveset override disabled due to use of `game.move.changeMoveset`!"); } } else { - if (coerceArray(Overrides.OPP_MOVESET_OVERRIDE).length > 0) { - vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([]); + if (coerceArray(Overrides.ENEMY_MOVESET_OVERRIDE).length > 0) { + vi.spyOn(Overrides, "ENEMY_MOVESET_OVERRIDE", "get").mockReturnValue([]); console.warn("Enemy moveset override disabled due to use of `game.move.changeMoveset`!"); } } @@ -302,8 +302,8 @@ export class MoveHelper extends GameManagerHelper { (this.game.scene.phaseManager.getCurrentPhase() as EnemyCommandPhase).getFieldIndex() ]; - if ([Overrides.OPP_MOVESET_OVERRIDE].flat().length > 0) { - vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([]); + if ([Overrides.ENEMY_MOVESET_OVERRIDE].flat().length > 0) { + vi.spyOn(Overrides, "ENEMY_MOVESET_OVERRIDE", "get").mockReturnValue([]); console.warn( "Warning: `forceEnemyMove` overwrites the Pokemon's moveset and disables the enemy moveset override!", ); diff --git a/test/test-utils/helpers/overrides-helper.ts b/test/test-utils/helpers/overrides-helper.ts index d67ceedf891..93b89688935 100644 --- a/test/test-utils/helpers/overrides-helper.ts +++ b/test/test-utils/helpers/overrides-helper.ts @@ -406,7 +406,7 @@ export class OverridesHelper extends GameManagerHelper { * @returns `this` */ public enemySpecies(species: SpeciesId | number): this { - vi.spyOn(Overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(species); + vi.spyOn(Overrides, "ENEMY_SPECIES_OVERRIDE", "get").mockReturnValue(species); this.log(`Enemy Pokemon species set to ${SpeciesId[species]} (=${species})!`); return this; } @@ -416,7 +416,7 @@ export class OverridesHelper extends GameManagerHelper { * @returns `this` */ public enableEnemyFusion(): this { - vi.spyOn(Overrides, "OPP_FUSION_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(Overrides, "ENEMY_FUSION_OVERRIDE", "get").mockReturnValue(true); this.log("Enemy Pokemon is a random fusion!"); return this; } @@ -427,7 +427,7 @@ export class OverridesHelper extends GameManagerHelper { * @returns `this` */ public enemyFusionSpecies(species: SpeciesId | number): this { - vi.spyOn(Overrides, "OPP_FUSION_SPECIES_OVERRIDE", "get").mockReturnValue(species); + vi.spyOn(Overrides, "ENEMY_FUSION_SPECIES_OVERRIDE", "get").mockReturnValue(species); this.log(`Enemy Pokemon fusion species set to ${SpeciesId[species]} (=${species})!`); return this; } @@ -438,7 +438,7 @@ export class OverridesHelper extends GameManagerHelper { * @returns `this` */ public enemyAbility(ability: AbilityId): this { - vi.spyOn(Overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(ability); + vi.spyOn(Overrides, "ENEMY_ABILITY_OVERRIDE", "get").mockReturnValue(ability); this.log(`Enemy Pokemon ability set to ${AbilityId[ability]} (=${ability})!`); return this; } @@ -449,7 +449,7 @@ export class OverridesHelper extends GameManagerHelper { * @returns `this` */ public enemyPassiveAbility(passiveAbility: AbilityId): this { - vi.spyOn(Overrides, "OPP_PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(passiveAbility); + vi.spyOn(Overrides, "ENEMY_PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(passiveAbility); this.log(`Enemy Pokemon PASSIVE ability set to ${AbilityId[passiveAbility]} (=${passiveAbility})!`); return this; } @@ -460,7 +460,7 @@ export class OverridesHelper extends GameManagerHelper { * @returns `this` */ public enemyHasPassiveAbility(hasPassiveAbility: boolean | null): this { - vi.spyOn(Overrides, "OPP_HAS_PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(hasPassiveAbility); + vi.spyOn(Overrides, "ENEMY_HAS_PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(hasPassiveAbility); if (hasPassiveAbility === null) { this.log("Enemy Pokemon PASSIVE ability no longer force enabled or disabled!"); } else { @@ -475,7 +475,7 @@ export class OverridesHelper extends GameManagerHelper { * @returns `this` */ public enemyMoveset(moveset: MoveId | MoveId[]): this { - vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue(moveset); + vi.spyOn(Overrides, "ENEMY_MOVESET_OVERRIDE", "get").mockReturnValue(moveset); moveset = coerceArray(moveset); const movesetStr = moveset.map(moveId => MoveId[moveId]).join(", "); this.log(`Enemy Pokemon moveset set to ${movesetStr} (=[${moveset.join(", ")}])!`); @@ -488,7 +488,7 @@ export class OverridesHelper extends GameManagerHelper { * @returns `this` */ public enemyLevel(level: number): this { - vi.spyOn(Overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(level); + vi.spyOn(Overrides, "ENEMY_LEVEL_OVERRIDE", "get").mockReturnValue(level); this.log(`Enemy Pokemon level set to ${level}!`); return this; } @@ -499,7 +499,7 @@ export class OverridesHelper extends GameManagerHelper { * @returns `this` */ public enemyStatusEffect(statusEffect: StatusEffect): this { - vi.spyOn(Overrides, "OPP_STATUS_OVERRIDE", "get").mockReturnValue(statusEffect); + vi.spyOn(Overrides, "ENEMY_STATUS_OVERRIDE", "get").mockReturnValue(statusEffect); this.log(`Enemy Pokemon status-effect set to ${StatusEffect[statusEffect]} (=${statusEffect})!`); return this; } @@ -510,7 +510,7 @@ export class OverridesHelper extends GameManagerHelper { * @returns `this` */ public enemyHeldItems(items: ModifierOverride[]): this { - vi.spyOn(Overrides, "OPP_HELD_ITEMS_OVERRIDE", "get").mockReturnValue(items); + vi.spyOn(Overrides, "ENEMY_HELD_ITEMS_OVERRIDE", "get").mockReturnValue(items); this.log("Enemy Pokemon held items set to:", items); return this; } @@ -571,7 +571,7 @@ export class OverridesHelper extends GameManagerHelper { * @param variant - (Optional) The enemy's shiny {@linkcode Variant}. */ enemyShiny(shininess: boolean | null, variant?: Variant): this { - vi.spyOn(Overrides, "OPP_SHINY_OVERRIDE", "get").mockReturnValue(shininess); + vi.spyOn(Overrides, "ENEMY_SHINY_OVERRIDE", "get").mockReturnValue(shininess); if (shininess === null) { this.log("Disabled enemy Pokemon shiny override!"); } else { @@ -579,7 +579,7 @@ export class OverridesHelper extends GameManagerHelper { } if (variant !== undefined) { - vi.spyOn(Overrides, "OPP_VARIANT_OVERRIDE", "get").mockReturnValue(variant); + vi.spyOn(Overrides, "ENEMY_VARIANT_OVERRIDE", "get").mockReturnValue(variant); this.log(`Set enemy shiny variant to be ${variant}!`); } return this; @@ -594,7 +594,7 @@ export class OverridesHelper extends GameManagerHelper { * @returns `this` */ public enemyHealthSegments(healthSegments: number): this { - vi.spyOn(Overrides, "OPP_HEALTH_SEGMENTS_OVERRIDE", "get").mockReturnValue(healthSegments); + vi.spyOn(Overrides, "ENEMY_HEALTH_SEGMENTS_OVERRIDE", "get").mockReturnValue(healthSegments); this.log("Enemy Pokemon health segments set to:", healthSegments); return this; } diff --git a/test/test-utils/helpers/reload-helper.ts b/test/test-utils/helpers/reload-helper.ts index a8ed0e21307..7166f1b6cf9 100644 --- a/test/test-utils/helpers/reload-helper.ts +++ b/test/test-utils/helpers/reload-helper.ts @@ -57,7 +57,7 @@ export class ReloadHelper extends GameManagerHelper { this.game.scene.modifiers = []; } titlePhase.loadSaveSlot(-1); // Load the desired session data - this.game.phaseInterceptor.shift(); // Loading the save slot also ended TitlePhase, clean it up + this.game.phaseInterceptor.shiftPhase(); // Loading the save slot also ended TitlePhase, clean it up // Run through prompts for switching Pokemon, copied from classicModeHelper.ts if (this.game.scene.battleStyle === BattleStyle.SWITCH) { diff --git a/test/test-utils/matchers/to-equal-array-unsorted.ts b/test/test-utils/matchers/to-equal-array-unsorted.ts index 846ea9e7779..97398689032 100644 --- a/test/test-utils/matchers/to-equal-array-unsorted.ts +++ b/test/test-utils/matchers/to-equal-array-unsorted.ts @@ -1,4 +1,5 @@ import { getOnelineDiffStr } from "#test/test-utils/string-utils"; +import { receivedStr } from "#test/test-utils/test-utils"; import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; /** @@ -14,22 +15,22 @@ export function toEqualArrayUnsorted( ): SyncExpectationResult { if (!Array.isArray(received)) { return { - pass: false, - message: () => `Expected an array, but got ${this.utils.stringify(received)}!`, + pass: this.isNot, + message: () => `Expected to receive an array, but got ${receivedStr(received)}!`, }; } if (received.length !== expected.length) { return { pass: false, - message: () => `Expected to receive array of length ${received.length}, but got ${expected.length} instead!`, - actual: received, + message: () => `Expected to receive an array of length ${received.length}, but got ${expected.length} instead!`, expected, + actual: received, }; } - const actualSorted = received.slice().sort(); - const expectedSorted = expected.slice().sort(); + const actualSorted = received.toSorted(); + const expectedSorted = expected.toSorted(); const pass = this.equals(actualSorted, expectedSorted, [...this.customTesters, this.utils.iterableEquality]); const actualStr = getOnelineDiffStr.call(this, actualSorted); diff --git a/test/test-utils/matchers/to-have-ability-applied.ts b/test/test-utils/matchers/to-have-ability-applied.ts index a3921e6371c..1ed74410de0 100644 --- a/test/test-utils/matchers/to-have-ability-applied.ts +++ b/test/test-utils/matchers/to-have-ability-applied.ts @@ -21,8 +21,8 @@ export function toHaveAbilityApplied( ): SyncExpectationResult { if (!isPokemonInstance(received)) { return { - pass: false, - message: () => `Expected to recieve a Pokemon, but got ${receivedStr(received)}!`, + pass: this.isNot, + message: () => `Expected to receive a Pokemon, but got ${receivedStr(received)}!`, }; } diff --git a/test/test-utils/matchers/to-have-arena-tag.ts b/test/test-utils/matchers/to-have-arena-tag.ts new file mode 100644 index 00000000000..e2a4a71ffd5 --- /dev/null +++ b/test/test-utils/matchers/to-have-arena-tag.ts @@ -0,0 +1,82 @@ +import type { ArenaTag, ArenaTagTypeMap } from "#data/arena-tag"; +import { ArenaTagSide } from "#enums/arena-tag-side"; +import type { ArenaTagType } from "#enums/arena-tag-type"; +import type { OneOther } from "#test/@types/test-helpers"; +// biome-ignore lint/correctness/noUnusedImports: TSDoc +import type { GameManager } from "#test/test-utils/game-manager"; +import { getOnelineDiffStr } from "#test/test-utils/string-utils"; +import { isGameManagerInstance, receivedStr } from "#test/test-utils/test-utils"; +import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; + +// intersection required to preserve T for inferences +export type toHaveArenaTagOptions = OneOther & { + tagType: T; +}; + +/** + * Matcher to check if the {@linkcode Arena} has a given {@linkcode ArenaTag} active. + * @param received - The object to check. Should be the current {@linkcode GameManager}. + * @param expectedTag - The `ArenaTagType` of the desired tag, or a partially-filled object + * containing the desired properties + * @param side - The {@linkcode ArenaTagSide | side of the field} the tag should affect, or + * {@linkcode ArenaTagSide.BOTH} to check both sides + * @returns The result of the matching + */ +export function toHaveArenaTag( + this: MatcherState, + received: unknown, + expectedTag: T | toHaveArenaTagOptions, + side: ArenaTagSide = ArenaTagSide.BOTH, +): SyncExpectationResult { + if (!isGameManagerInstance(received)) { + return { + pass: this.isNot, + message: () => `Expected to receive a GameManager, but got ${receivedStr(received)}!`, + }; + } + + if (!received.scene?.arena) { + return { + pass: this.isNot, + message: () => `Expected GameManager.${received.scene ? "scene.arena" : "scene"} to be defined!`, + }; + } + + // Coerce lone `tagType`s into objects + // Bangs are ok as we enforce safety via overloads + // @ts-expect-error - Typescript is being stupid as tag type and side will always exist + const etag: Partial & { tagType: T; side: ArenaTagSide } = + typeof expectedTag === "object" ? expectedTag : { tagType: expectedTag, side }; + + // If checking only tag type/side OR no tags were found, break out early. + // We need to get all tags for the case of checking properties of a tag present on both sides of the arena + const tags = received.scene.arena.findTagsOnSide(t => t.tagType === etag.tagType, etag.side); + if (typeof expectedTag !== "object" || tags.length === 0) { + const pass = tags.length > 0; + return { + pass, + message: () => + pass + ? `Expected the Arena to NOT have a tag of type ${etag.tagType}, but it did!` + : `Expected the Arena to have a tag of type ${etag.tagType}, but it didn't!`, + expected: etag, + actual: received.scene.arena.tags.map(t => ({ tagType: t.tagType, side: t.side })), + }; + } + + // Pass if any of the matching tags meet our criteria + const pass = tags.some(tag => + this.equals(tag, etag, [...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality]), + ); + + const expectedStr = getOnelineDiffStr.call(this, expectedTag); + return { + pass, + message: () => + pass + ? `Expected the Arena to NOT have a tag matching ${expectedStr}, but it did!` + : `Expected the Arena to have a tag matching ${expectedStr}, but it didn't!`, + expected: expectedTag, + actual: tags, + }; +} diff --git a/test/test-utils/matchers/to-have-effective-stat.ts b/test/test-utils/matchers/to-have-effective-stat.ts index bc10a646c02..dda6bc7e91e 100644 --- a/test/test-utils/matchers/to-have-effective-stat.ts +++ b/test/test-utils/matchers/to-have-effective-stat.ts @@ -6,7 +6,7 @@ import { getStatName } from "#test/test-utils/string-utils"; import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils"; import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; -export interface ToHaveEffectiveStatMatcherOptions { +export interface toHaveEffectiveStatOptions { /** * The target {@linkcode Pokemon} * @see {@linkcode Pokemon.getEffectiveStat} @@ -30,7 +30,7 @@ export interface ToHaveEffectiveStatMatcherOptions { * @param received - The object to check. Should be a {@linkcode Pokemon} * @param stat - The {@linkcode EffectiveStat} to check * @param expectedValue - The expected value of the {@linkcode stat} - * @param options - The {@linkcode ToHaveEffectiveStatMatcherOptions} + * @param options - The {@linkcode toHaveEffectiveStatOptions} * @returns Whether the matcher passed */ export function toHaveEffectiveStat( @@ -38,11 +38,11 @@ export function toHaveEffectiveStat( received: unknown, stat: EffectiveStat, expectedValue: number, - { enemy, move, isCritical = false }: ToHaveEffectiveStatMatcherOptions = {}, + { enemy, move, isCritical = false }: toHaveEffectiveStatOptions = {}, ): SyncExpectationResult { if (!isPokemonInstance(received)) { return { - pass: false, + pass: this.isNot, message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`, }; } diff --git a/test/test-utils/matchers/to-have-fainted.ts b/test/test-utils/matchers/to-have-fainted.ts index 73ca96a31b5..f3e84e7a425 100644 --- a/test/test-utils/matchers/to-have-fainted.ts +++ b/test/test-utils/matchers/to-have-fainted.ts @@ -12,7 +12,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; export function toHaveFainted(this: MatcherState, received: unknown): SyncExpectationResult { if (!isPokemonInstance(received)) { return { - pass: false, + pass: this.isNot, message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`, }; } diff --git a/test/test-utils/matchers/to-have-full-hp.ts b/test/test-utils/matchers/to-have-full-hp.ts index 3d7c8f9458d..893bb647283 100644 --- a/test/test-utils/matchers/to-have-full-hp.ts +++ b/test/test-utils/matchers/to-have-full-hp.ts @@ -12,7 +12,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; export function toHaveFullHp(this: MatcherState, received: unknown): SyncExpectationResult { if (!isPokemonInstance(received)) { return { - pass: false, + pass: this.isNot, message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`, }; } diff --git a/test/test-utils/matchers/to-have-hp.ts b/test/test-utils/matchers/to-have-hp.ts index 20d171b23ce..e6463383ac2 100644 --- a/test/test-utils/matchers/to-have-hp.ts +++ b/test/test-utils/matchers/to-have-hp.ts @@ -13,7 +13,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; export function toHaveHp(this: MatcherState, received: unknown, expectedHp: number): SyncExpectationResult { if (!isPokemonInstance(received)) { return { - pass: false, + pass: this.isNot, message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`, }; } diff --git a/test/test-utils/matchers/to-have-positional-tag.ts b/test/test-utils/matchers/to-have-positional-tag.ts new file mode 100644 index 00000000000..448339d6a8d --- /dev/null +++ b/test/test-utils/matchers/to-have-positional-tag.ts @@ -0,0 +1,107 @@ +// biome-ignore-start lint/correctness/noUnusedImports: TSDoc +import type { GameManager } from "#test/test-utils/game-manager"; +// biome-ignore-end lint/correctness/noUnusedImports: TSDoc + +import type { serializedPosTagMap } from "#data/positional-tags/load-positional-tag"; +import type { PositionalTagType } from "#enums/positional-tag-type"; +import type { OneOther } from "#test/@types/test-helpers"; +import { getOnelineDiffStr } from "#test/test-utils/string-utils"; +import { isGameManagerInstance, receivedStr } from "#test/test-utils/test-utils"; +import { toTitleCase } from "#utils/strings"; +import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; + +export type toHavePositionalTagOptions

= OneOther & { + tagType: P; +}; + +/** + * Matcher to check if the {@linkcode Arena} has a certain number of {@linkcode PositionalTag}s active. + * @param received - The object to check. Should be the current {@linkcode GameManager} + * @param expectedTag - The {@linkcode PositionalTagType} of the desired tag, or a partially-filled {@linkcode PositionalTag} + * containing the desired properties + * @param count - The number of tags that should be active; defaults to `1` and must be within the range `[0, 4]` + * @returns The result of the matching + */ +export function toHavePositionalTag

( + this: MatcherState, + received: unknown, + expectedTag: P | toHavePositionalTagOptions

, + count = 1, +): SyncExpectationResult { + if (!isGameManagerInstance(received)) { + return { + pass: this.isNot, + message: () => `Expected to receive a GameManager, but got ${receivedStr(received)}!`, + }; + } + + if (!received.scene?.arena?.positionalTagManager) { + return { + pass: this.isNot, + message: () => + `Expected GameManager.${received.scene?.arena ? "scene.arena.positionalTagManager" : received.scene ? "scene.arena" : "scene"} to be defined!`, + }; + } + + // TODO: Increase limit if triple battles are added + if (count < 0 || count > 4) { + return { + pass: this.isNot, + message: () => `Expected count to be between 0 and 4, but got ${count} instead!`, + }; + } + + const allTags = received.scene.arena.positionalTagManager.tags; + const tagType = typeof expectedTag === "string" ? expectedTag : expectedTag.tagType; + const matchingTags = allTags.filter(t => t.tagType === tagType); + + // If checking exclusively tag type, check solely the number of matching tags on field + if (typeof expectedTag === "string") { + const pass = matchingTags.length === count; + const expectedStr = getPosTagStr(expectedTag); + + return { + pass, + message: () => + pass + ? `Expected the Arena to NOT have ${count} ${expectedStr} active, but it did!` + : `Expected the Arena to have ${count} ${expectedStr} active, but got ${matchingTags.length} instead!`, + expected: expectedTag, + actual: allTags, + }; + } + + // Check for equality with the provided object + if (matchingTags.length === 0) { + return { + pass: false, + message: () => `Expected the Arena to have a tag of type ${expectedTag.tagType}, but it didn't!`, + expected: expectedTag.tagType, + actual: received.scene.arena.tags.map(t => t.tagType), + }; + } + + // Pass if any of the matching tags meet the criteria + const pass = matchingTags.some(tag => + this.equals(tag, expectedTag, [...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality]), + ); + + const expectedStr = getOnelineDiffStr.call(this, expectedTag); + return { + pass, + message: () => + pass + ? `Expected the Arena to NOT have a tag matching ${expectedStr}, but it did!` + : `Expected the Arena to have a tag matching ${expectedStr}, but it didn't!`, + expected: expectedTag, + actual: matchingTags, + }; +} + +function getPosTagStr(pType: PositionalTagType, count = 1): string { + let ret = toTitleCase(pType) + "Tag"; + if (count > 1) { + ret += "s"; + } + return ret; +} diff --git a/test/test-utils/matchers/to-have-stat-stage.ts b/test/test-utils/matchers/to-have-stat-stage.ts index feecd650bef..a9ae910aece 100644 --- a/test/test-utils/matchers/to-have-stat-stage.ts +++ b/test/test-utils/matchers/to-have-stat-stage.ts @@ -23,14 +23,14 @@ export function toHaveStatStage( ): SyncExpectationResult { if (!isPokemonInstance(received)) { return { - pass: false, + pass: this.isNot, message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`, }; } if (expectedStage < -6 || expectedStage > 6) { return { - pass: false, + pass: this.isNot, message: () => `Expected ${expectedStage} to be within the range [-6, 6]!`, }; } diff --git a/test/test-utils/matchers/to-have-status-effect.ts b/test/test-utils/matchers/to-have-status-effect.ts index a46800632f3..fa5f0346ebd 100644 --- a/test/test-utils/matchers/to-have-status-effect.ts +++ b/test/test-utils/matchers/to-have-status-effect.ts @@ -28,7 +28,7 @@ export function toHaveStatusEffect( ): SyncExpectationResult { if (!isPokemonInstance(received)) { return { - pass: false, + pass: this.isNot, message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`, }; } @@ -37,10 +37,8 @@ export function toHaveStatusEffect( const actualEffect = received.status?.effect ?? StatusEffect.NONE; // Check exclusively effect equality first, coercing non-matching status effects to numbers. - if (actualEffect !== (expectedStatus as Exclude)?.effect) { - // This is actually 100% safe as `expectedStatus?.effect` will evaluate to `undefined` if a StatusEffect was passed, - // which will never match actualEffect by definition - expectedStatus = (expectedStatus as Exclude).effect; + if (typeof expectedStatus === "object" && actualEffect !== expectedStatus.effect) { + expectedStatus = expectedStatus.effect; } if (typeof expectedStatus === "number") { diff --git a/test/test-utils/matchers/to-have-taken-damage.ts b/test/test-utils/matchers/to-have-taken-damage.ts index 77c60ae836a..55c163a2dc7 100644 --- a/test/test-utils/matchers/to-have-taken-damage.ts +++ b/test/test-utils/matchers/to-have-taken-damage.ts @@ -24,7 +24,7 @@ export function toHaveTakenDamage( ): SyncExpectationResult { if (!isPokemonInstance(received)) { return { - pass: false, + pass: this.isNot, message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`, }; } diff --git a/test/test-utils/matchers/to-have-terrain.ts b/test/test-utils/matchers/to-have-terrain.ts index 292c32abafc..f951abed0b3 100644 --- a/test/test-utils/matchers/to-have-terrain.ts +++ b/test/test-utils/matchers/to-have-terrain.ts @@ -20,15 +20,15 @@ export function toHaveTerrain( ): SyncExpectationResult { if (!isGameManagerInstance(received)) { return { - pass: false, - message: () => `Expected GameManager, but got ${receivedStr(received)}!`, + pass: this.isNot, + message: () => `Expected to receive a GameManager, but got ${receivedStr(received)}!`, }; } if (!received.scene?.arena) { return { - pass: false, - message: () => `Expected GameManager.${received.scene ? "scene" : "scene.arena"} to be defined!`, + pass: this.isNot, + message: () => `Expected GameManager.${received.scene ? "scene.arena" : "scene"} to be defined!`, }; } @@ -41,8 +41,8 @@ export function toHaveTerrain( pass, message: () => pass - ? `Expected Arena to NOT have ${expectedStr} active, but it did!` - : `Expected Arena to have ${expectedStr} active, but got ${actualStr} instead!`, + ? `Expected the Arena to NOT have ${expectedStr} active, but it did!` + : `Expected the Arena to have ${expectedStr} active, but got ${actualStr} instead!`, expected: expectedTerrainType, actual, }; diff --git a/test/test-utils/matchers/to-have-types.ts b/test/test-utils/matchers/to-have-types.ts index 3f16f740583..1c13fc083ae 100644 --- a/test/test-utils/matchers/to-have-types.ts +++ b/test/test-utils/matchers/to-have-types.ts @@ -7,10 +7,16 @@ import { isPokemonInstance, receivedStr } from "../test-utils"; export interface toHaveTypesOptions { /** - * Whether to enforce exact matches (`true`) or superset matches (`false`). - * @defaultValue `true` + * Value dictating the strength of the enforced typing match. + * + * Possible values (in ascending order of strength) are: + * - `"ordered"`: Enforce that the {@linkcode Pokemon}'s types are identical **and in the same order** + * - `"unordered"`: Enforce that the {@linkcode Pokemon}'s types are identical **without checking order** + * - `"superset"`: Enforce that the {@linkcode Pokemon}'s types are **a superset of** the expected types + * (all must be present, but extras can be there) + * @defaultValue `"unordered"` */ - exact?: boolean; + mode?: "ordered" | "unordered" | "superset"; /** * Optional arguments to pass to {@linkcode Pokemon.getTypes}. */ @@ -18,35 +24,54 @@ export interface toHaveTypesOptions { } /** - * Matcher that checks if an array contains exactly the given items, disregarding order. - * @param received - The object to check. Should be an array of one or more {@linkcode PokemonType}s. - * @param options - The {@linkcode toHaveTypesOptions | options} for this matcher + * Matcher that checks if a Pokemon's typing is as expected. + * @param received - The object to check. Should be a {@linkcode Pokemon} + * @param expectedTypes - An array of one or more {@linkcode PokemonType}s to compare against. + * @param mode - The mode to perform the matching in. + * Possible values (in ascending order of strength) are: + * - `"ordered"`: Enforce that the {@linkcode Pokemon}'s types are identical **and in the same order** + * - `"unordered"`: Enforce that the {@linkcode Pokemon}'s types are identical **without checking order** + * - `"superset"`: Enforce that the {@linkcode Pokemon}'s types are **a superset of** the expected types + * (all must be present, but extras can be there) + * + * Default `unordered` + * @param args - Extra arguments passed to {@linkcode Pokemon.getTypes} * @returns The result of the matching */ export function toHaveTypes( this: MatcherState, received: unknown, - expected: [PokemonType, ...PokemonType[]], - options: toHaveTypesOptions = {}, + expectedTypes: [PokemonType, ...PokemonType[]], + { mode = "unordered", args = [] }: toHaveTypesOptions = {}, ): SyncExpectationResult { if (!isPokemonInstance(received)) { return { - pass: false, - message: () => `Expected to recieve a Pokémon, but got ${receivedStr(received)}!`, + pass: this.isNot, + message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`, }; } - const actualTypes = received.getTypes(...(options.args ?? [])).sort(); - const expectedTypes = expected.slice().sort(); + // Return early if no types were passed in + if (expectedTypes.length === 0) { + return { + pass: this.isNot, + message: () => "Expected to receive a non-empty array of PokemonTypes!", + }; + } + + // Avoid sorting the types if strict ordering is desired + const actualSorted = mode === "ordered" ? received.getTypes(...args) : received.getTypes(...args).toSorted(); + const expectedSorted = mode === "ordered" ? expectedTypes : expectedTypes.toSorted(); // Exact matches do not care about subset equality - const matchers = options.exact - ? [...this.customTesters, this.utils.iterableEquality] - : [...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality]; - const pass = this.equals(actualTypes, expectedTypes, matchers); + const matchers = + mode === "superset" + ? [...this.customTesters, this.utils.iterableEquality] + : [...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality]; + const pass = this.equals(actualSorted, expectedSorted, matchers); - const actualStr = stringifyEnumArray(PokemonType, actualTypes); - const expectedStr = stringifyEnumArray(PokemonType, expectedTypes); + const actualStr = stringifyEnumArray(PokemonType, actualSorted); + const expectedStr = stringifyEnumArray(PokemonType, expectedSorted); const pkmName = getPokemonNameWithAffix(received); return { @@ -55,7 +80,7 @@ export function toHaveTypes( pass ? `Expected ${pkmName} to NOT have types ${expectedStr}, but it did!` : `Expected ${pkmName} to have types ${expectedStr}, but got ${actualStr} instead!`, - expected: expectedTypes, - actual: actualTypes, + expected: expectedSorted, + actual: actualSorted, }; } diff --git a/test/test-utils/matchers/to-have-used-move.ts b/test/test-utils/matchers/to-have-used-move.ts index ef90e4dbad9..3697b3e0bc6 100644 --- a/test/test-utils/matchers/to-have-used-move.ts +++ b/test/test-utils/matchers/to-have-used-move.ts @@ -13,7 +13,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; /** * Matcher to check the contents of a {@linkcode Pokemon}'s move history. * @param received - The actual value received. Should be a {@linkcode Pokemon} - * @param expectedValue - The {@linkcode MoveId} the Pokemon is expected to have used, + * @param expectedMove - The {@linkcode MoveId} the Pokemon is expected to have used, * or a partially filled {@linkcode TurnMove} containing the desired properties to check * @param index - The index of the move history entry to check, in order from most recent to least recent. * Default `0` (last used move) @@ -22,12 +22,12 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; export function toHaveUsedMove( this: MatcherState, received: unknown, - expectedResult: MoveId | AtLeastOne, + expectedMove: MoveId | AtLeastOne, index = 0, ): SyncExpectationResult { if (!isPokemonInstance(received)) { return { - pass: false, + pass: this.isNot, message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`, }; } @@ -37,34 +37,33 @@ export function toHaveUsedMove( if (move === undefined) { return { - pass: false, + pass: this.isNot, message: () => `Expected ${pkmName} to have used ${index + 1} moves, but it didn't!`, actual: received.getLastXMoves(-1), }; } // Coerce to a `TurnMove` - if (typeof expectedResult === "number") { - expectedResult = { move: expectedResult }; + if (typeof expectedMove === "number") { + expectedMove = { move: expectedMove }; } const moveIndexStr = index === 0 ? "last move" : `${getOrdinal(index)} most recent move`; - const pass = this.equals(move, expectedResult, [ + const pass = this.equals(move, expectedMove, [ ...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality, ]); - const expectedStr = getOnelineDiffStr.call(this, expectedResult); + const expectedStr = getOnelineDiffStr.call(this, expectedMove); return { pass, message: () => pass ? `Expected ${pkmName}'s ${moveIndexStr} to NOT match ${expectedStr}, but it did!` - : // Replace newlines with spaces to preserve one-line ness - `Expected ${pkmName}'s ${moveIndexStr} to match ${expectedStr}, but it didn't!`, - expected: expectedResult, + : `Expected ${pkmName}'s ${moveIndexStr} to match ${expectedStr}, but it didn't!`, + expected: expectedMove, actual: move, }; } diff --git a/test/test-utils/matchers/to-have-used-pp.ts b/test/test-utils/matchers/to-have-used-pp.ts index 3b606a535bc..4815cfcadab 100644 --- a/test/test-utils/matchers/to-have-used-pp.ts +++ b/test/test-utils/matchers/to-have-used-pp.ts @@ -13,7 +13,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; /** * Matcher to check the amount of PP consumed by a {@linkcode Pokemon}. * @param received - The actual value received. Should be a {@linkcode Pokemon} - * @param expectedValue - The {@linkcode MoveId} that should have consumed PP + * @param moveId - The {@linkcode MoveId} that should have consumed PP * @param ppUsed - The numerical amount of PP that should have been consumed, * or `all` to indicate the move should be _out_ of PP * @returns Whether the matcher passed @@ -23,35 +23,35 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; export function toHaveUsedPP( this: MatcherState, received: unknown, - expectedMove: MoveId, + moveId: MoveId, ppUsed: number | "all", ): SyncExpectationResult { if (!isPokemonInstance(received)) { return { - pass: false, + pass: this.isNot, message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`, }; } - const override = received.isPlayer() ? Overrides.MOVESET_OVERRIDE : Overrides.OPP_MOVESET_OVERRIDE; + const override = received.isPlayer() ? Overrides.MOVESET_OVERRIDE : Overrides.ENEMY_MOVESET_OVERRIDE; if (coerceArray(override).length > 0) { return { - pass: false, + pass: this.isNot, message: () => `Cannot test for PP consumption with ${received.isPlayer() ? "player" : "enemy"} moveset overrides active!`, }; } const pkmName = getPokemonNameWithAffix(received); - const moveStr = getEnumStr(MoveId, expectedMove); + const moveStr = getEnumStr(MoveId, moveId); - const movesetMoves = received.getMoveset().filter(pm => pm.moveId === expectedMove); + const movesetMoves = received.getMoveset().filter(pm => pm.moveId === moveId); if (movesetMoves.length !== 1) { return { - pass: false, + pass: this.isNot, message: () => `Expected MoveId.${moveStr} to appear in ${pkmName}'s moveset exactly once, but got ${movesetMoves.length} times!`, - expected: expectedMove, + expected: moveId, actual: received.getMoveset(), }; } diff --git a/test/test-utils/matchers/to-have-weather.ts b/test/test-utils/matchers/to-have-weather.ts index 49433b2137b..ffb1e0aad97 100644 --- a/test/test-utils/matchers/to-have-weather.ts +++ b/test/test-utils/matchers/to-have-weather.ts @@ -20,15 +20,15 @@ export function toHaveWeather( ): SyncExpectationResult { if (!isGameManagerInstance(received)) { return { - pass: false, - message: () => `Expected GameManager, but got ${receivedStr(received)}!`, + pass: this.isNot, + message: () => `Expected to receive a GameManager, but got ${receivedStr(received)}!`, }; } if (!received.scene?.arena) { return { - pass: false, - message: () => `Expected GameManager.${received.scene ? "scene" : "scene.arena"} to be defined!`, + pass: this.isNot, + message: () => `Expected GameManager.${received.scene ? "scene.arena" : "scene"} to be defined!`, }; } @@ -41,8 +41,8 @@ export function toHaveWeather( pass, message: () => pass - ? `Expected Arena to NOT have ${expectedStr} weather active, but it did!` - : `Expected Arena to have ${expectedStr} weather active, but got ${actualStr} instead!`, + ? `Expected the Arena to NOT have ${expectedStr} weather active, but it did!` + : `Expected the Arena to have ${expectedStr} weather active, but got ${actualStr} instead!`, expected: expectedWeatherType, actual, }; diff --git a/test/test-utils/phase-interceptor.ts b/test/test-utils/phase-interceptor.ts index 50de7e9f047..0d357a75557 100644 --- a/test/test-utils/phase-interceptor.ts +++ b/test/test-utils/phase-interceptor.ts @@ -1,3 +1,4 @@ +import type { BattleScene } from "#app/battle-scene"; import { Phase } from "#app/phase"; import { UiMode } from "#enums/ui-mode"; import { AttemptRunPhase } from "#phases/attempt-run-phase"; @@ -64,6 +65,7 @@ import { UnlockPhase } from "#phases/unlock-phase"; import { VictoryPhase } from "#phases/victory-phase"; import { ErrorInterceptor } from "#test/test-utils/error-interceptor"; import type { PhaseClass, PhaseString } from "#types/phase-types"; +import type { AwaitableUiHandler } from "#ui/awaitable-ui-handler"; import { UI } from "#ui/ui"; export interface PromptHandler { @@ -76,20 +78,39 @@ export interface PromptHandler { type PhaseInterceptorPhase = PhaseClass | PhaseString; +interface PhaseStub { + start(): void; + endBySetMode: boolean; +} + +interface InProgressStub { + name: string; + callback(): void; + onError(error: any): void; +} + +interface onHoldStub { + name: string; + call(): void; +} + export class PhaseInterceptor { - public scene; - public phases = {}; - public log: string[]; - private onHold; - private interval; - private promptInterval; - private intervalRun; + public scene: BattleScene; + // @ts-expect-error: initialized in `initPhases` + public phases: Record = {}; + public log: PhaseString[]; + /** + * TODO: This should not be an array; + * Our linear phase system means only 1 phase is ever started at once (if any) + */ + private onHold: onHoldStub[]; + private interval: NodeJS.Timeout; + private promptInterval: NodeJS.Timeout; + private intervalRun: NodeJS.Timeout; private prompts: PromptHandler[]; - private phaseFrom; - private inProgress; - private originalSetMode; - private originalSetOverlayMode; - private originalSuperEnd; + private inProgress?: InProgressStub; + private originalSetMode: UI["setMode"]; + private originalSuperEnd: Phase["end"]; /** * List of phases with their corresponding start methods. @@ -100,72 +121,73 @@ export class PhaseInterceptor { * `initPhases()` so that its subclasses can use `super.start()` properly. */ private PHASES = [ - [LoginPhase, this.startPhase], - [TitlePhase, this.startPhase], - [SelectGenderPhase, this.startPhase], - [NewBiomeEncounterPhase, this.startPhase], - [SelectStarterPhase, this.startPhase], - [PostSummonPhase, this.startPhase], - [SummonPhase, this.startPhase], - [ToggleDoublePositionPhase, this.startPhase], - [CheckSwitchPhase, this.startPhase], - [ShowAbilityPhase, this.startPhase], - [MessagePhase, this.startPhase], - [TurnInitPhase, this.startPhase], - [CommandPhase, this.startPhase], - [EnemyCommandPhase, this.startPhase], - [TurnStartPhase, this.startPhase], - [MovePhase, this.startPhase], - [MoveEffectPhase, this.startPhase], - [DamageAnimPhase, this.startPhase], - [FaintPhase, this.startPhase], - [BerryPhase, this.startPhase], - [TurnEndPhase, this.startPhase], - [BattleEndPhase, this.startPhase], - [EggLapsePhase, this.startPhase], - [SelectModifierPhase, this.startPhase], - [NextEncounterPhase, this.startPhase], - [NewBattlePhase, this.startPhase], - [VictoryPhase, this.startPhase], - [LearnMovePhase, this.startPhase], - [MoveEndPhase, this.startPhase], - [StatStageChangePhase, this.startPhase], - [ShinySparklePhase, this.startPhase], - [SelectTargetPhase, this.startPhase], - [UnavailablePhase, this.startPhase], - [QuietFormChangePhase, this.startPhase], - [SwitchPhase, this.startPhase], - [SwitchSummonPhase, this.startPhase], - [PartyHealPhase, this.startPhase], - [FormChangePhase, this.startPhase], - [EvolutionPhase, this.startPhase], - [EndEvolutionPhase, this.startPhase], - [LevelCapPhase, this.startPhase], - [AttemptRunPhase, this.startPhase], - [SelectBiomePhase, this.startPhase], - [PositionalTagPhase, this.startPhase], - [PokemonTransformPhase, this.startPhase], - [MysteryEncounterPhase, this.startPhase], - [MysteryEncounterOptionSelectedPhase, this.startPhase], - [MysteryEncounterBattlePhase, this.startPhase], - [MysteryEncounterRewardsPhase, this.startPhase], - [PostMysteryEncounterPhase, this.startPhase], - [RibbonModifierRewardPhase, this.startPhase], - [GameOverModifierRewardPhase, this.startPhase], - [ModifierRewardPhase, this.startPhase], - [PartyExpPhase, this.startPhase], - [ExpPhase, this.startPhase], - [EncounterPhase, this.startPhase], - [GameOverPhase, this.startPhase], - [UnlockPhase, this.startPhase], - [PostGameOverPhase, this.startPhase], - [RevivalBlessingPhase, this.startPhase], + LoginPhase, + TitlePhase, + SelectGenderPhase, + NewBiomeEncounterPhase, + SelectStarterPhase, + PostSummonPhase, + SummonPhase, + ToggleDoublePositionPhase, + CheckSwitchPhase, + ShowAbilityPhase, + MessagePhase, + TurnInitPhase, + CommandPhase, + EnemyCommandPhase, + TurnStartPhase, + MovePhase, + MoveEffectPhase, + DamageAnimPhase, + FaintPhase, + BerryPhase, + TurnEndPhase, + BattleEndPhase, + EggLapsePhase, + SelectModifierPhase, + NextEncounterPhase, + NewBattlePhase, + VictoryPhase, + LearnMovePhase, + MoveEndPhase, + StatStageChangePhase, + ShinySparklePhase, + SelectTargetPhase, + UnavailablePhase, + QuietFormChangePhase, + SwitchPhase, + SwitchSummonPhase, + PartyHealPhase, + FormChangePhase, + EvolutionPhase, + EndEvolutionPhase, + LevelCapPhase, + AttemptRunPhase, + SelectBiomePhase, + PositionalTagPhase, + PokemonTransformPhase, + MysteryEncounterPhase, + MysteryEncounterOptionSelectedPhase, + MysteryEncounterBattlePhase, + MysteryEncounterRewardsPhase, + PostMysteryEncounterPhase, + RibbonModifierRewardPhase, + GameOverModifierRewardPhase, + ModifierRewardPhase, + PartyExpPhase, + ExpPhase, + EncounterPhase, + GameOverPhase, + UnlockPhase, + PostGameOverPhase, + RevivalBlessingPhase, ]; private endBySetMode = [ TitlePhase, SelectGenderPhase, CommandPhase, + SelectStarterPhase, SelectModifierPhase, MysteryEncounterPhase, PostMysteryEncounterPhase, @@ -175,7 +197,7 @@ export class PhaseInterceptor { * Constructor to initialize the scene and properties, and to start the phase handling. * @param scene - The scene to be managed. */ - constructor(scene) { + constructor(scene: BattleScene) { this.scene = scene; this.onHold = []; this.prompts = []; @@ -200,16 +222,6 @@ export class PhaseInterceptor { } } - /** - * Method to set the starting phase. - * @param phaseFrom - The phase to start from. - * @returns The instance of the PhaseInterceptor. - */ - runFrom(phaseFrom: PhaseInterceptorPhase): PhaseInterceptor { - this.phaseFrom = phaseFrom; - return this; - } - /** * Method to transition to a target phase. * @param phaseTo - The phase to transition to. @@ -219,59 +231,50 @@ export class PhaseInterceptor { async to(phaseTo: PhaseInterceptorPhase, runTarget = true): Promise { return new Promise(async (resolve, reject) => { ErrorInterceptor.getInstance().add(this); - if (this.phaseFrom) { - await this.run(this.phaseFrom).catch(e => reject(e)); - this.phaseFrom = null; - } const targetName = typeof phaseTo === "string" ? phaseTo : phaseTo.name; this.intervalRun = setInterval(async () => { const currentPhase = this.onHold?.length && this.onHold[0]; - if (currentPhase && currentPhase.name === targetName) { - clearInterval(this.intervalRun); - if (!runTarget) { - return resolve(); - } - await this.run(currentPhase).catch(e => { + if (!currentPhase) { + // No current phase means the manager either hasn't started yet + // or we were interrupted by prompt; wait for phase to finish + return; + } + + // If current phase is different, run it and wait for it to finish. + if (currentPhase.name !== targetName) { + await this.run().catch(e => { clearInterval(this.intervalRun); return reject(e); }); + return; + } + + // Hit target phase; run it and resolve + clearInterval(this.intervalRun); + if (!runTarget) { return resolve(); } - if (currentPhase && currentPhase.name !== targetName) { - await this.run(currentPhase).catch(e => { - clearInterval(this.intervalRun); - return reject(e); - }); - } + await this.run().catch(e => { + clearInterval(this.intervalRun); + return reject(e); + }); + return resolve(); }); }); } /** - * Method to run a phase with an optional skip function. - * @param phaseTarget - The phase to run. - * @param skipFn - Optional skip function. + * Method to run the current phase with an optional skip function. * @returns A promise that resolves when the phase is run. */ - run(phaseTarget: PhaseInterceptorPhase, skipFn?: (className: PhaseClass) => boolean): Promise { - const targetName = typeof phaseTarget === "string" ? phaseTarget : phaseTarget.name; - this.scene.moveAnimations = null; // Mandatory to avoid crash + private run(): Promise { + // @ts-expect-error: This is apparently mandatory to avoid a crash; review if this is needed + this.scene.moveAnimations = null; return new Promise(async (resolve, reject) => { ErrorInterceptor.getInstance().add(this); const interval = setInterval(async () => { const currentPhase = this.onHold.shift(); if (currentPhase) { - if (currentPhase.name !== targetName) { - clearInterval(interval); - const skip = skipFn?.(currentPhase.name); - if (skip) { - this.onHold.unshift(currentPhase); - ErrorInterceptor.getInstance().remove(this); - return resolve(); - } - clearInterval(interval); - return reject(`Wrong phase: this is ${currentPhase.name} and not ${targetName}`); - } clearInterval(interval); this.inProgress = { name: currentPhase.name, @@ -287,26 +290,6 @@ export class PhaseInterceptor { }); } - whenAboutToRun(phaseTarget: PhaseInterceptorPhase, _skipFn?: (className: PhaseClass) => boolean): Promise { - const targetName = typeof phaseTarget === "string" ? phaseTarget : phaseTarget.name; - this.scene.moveAnimations = null; // Mandatory to avoid crash - return new Promise(async (resolve, _reject) => { - ErrorInterceptor.getInstance().add(this); - const interval = setInterval(async () => { - const currentPhase = this.onHold[0]; - if (currentPhase?.name === targetName) { - clearInterval(interval); - resolve(); - } - }); - }); - } - - pop() { - this.onHold.pop(); - this.scene.phaseManager.shiftPhase(); - } - /** * Remove the current phase from the phase interceptor. * @@ -316,7 +299,7 @@ export class PhaseInterceptor { * * @param shouldRun Whether or not the current scene should also be run. */ - shift(shouldRun = false): void { + shiftPhase(shouldRun = false): void { this.onHold.shift(); if (shouldRun) { this.scene.phaseManager.shiftPhase(); @@ -328,17 +311,16 @@ export class PhaseInterceptor { */ initPhases() { this.originalSetMode = UI.prototype.setMode; - this.originalSetOverlayMode = UI.prototype.setOverlayMode; this.originalSuperEnd = Phase.prototype.end; UI.prototype.setMode = (mode, ...args) => this.setMode.call(this, mode, ...args); Phase.prototype.end = () => this.superEndPhase.call(this); - for (const [phase, methodStart] of this.PHASES) { + for (const phase of this.PHASES) { const originalStart = phase.prototype.start; this.phases[phase.name] = { start: originalStart, endBySetMode: this.endBySetMode.some(elm => elm.name === phase.name), }; - phase.prototype.start = () => methodStart.call(this, phase); + phase.prototype.start = () => this.startPhase.call(this, phase); } } @@ -347,7 +329,7 @@ export class PhaseInterceptor { * @param phase - The phase to start. */ startPhase(phase: PhaseClass) { - this.log.push(phase.name); + this.log.push(phase.name as PhaseString); const instance = this.scene.phaseManager.getCurrentPhase(); this.onHold.push({ name: phase.name, @@ -357,16 +339,11 @@ export class PhaseInterceptor { }); } - unlock() { - this.inProgress?.callback(); - this.inProgress = undefined; - } - /** * Method to end a phase and log it. * @param phase - The phase to start. */ - superEndPhase() { + private superEndPhase() { const instance = this.scene.phaseManager.getCurrentPhase(); this.originalSuperEnd.apply(instance); this.inProgress?.callback(); @@ -379,7 +356,8 @@ export class PhaseInterceptor { * @param args - Additional arguments to pass to the original method. */ setMode(mode: UiMode, ...args: unknown[]): Promise { - const currentPhase = this.scene.phaseManager.getCurrentPhase(); + // TODO: remove the `!` in PR 6243 / after PR 6243 is merged + const currentPhase = this.scene.phaseManager.getCurrentPhase()!; const instance = this.scene.ui; console.log("setMode", `${UiMode[mode]} (=${mode})`, args); const ret = this.originalSetMode.apply(instance, [mode, ...args]); @@ -395,18 +373,6 @@ export class PhaseInterceptor { return ret; } - /** - * mock to set overlay mode - * @param mode - The {@linkcode Mode} to set. - * @param args - Additional arguments to pass to the original method. - */ - setOverlayMode(mode: UiMode, ...args: unknown[]): Promise { - const instance = this.scene.ui; - console.log("setOverlayMode", `${UiMode[mode]} (=${mode})`, args); - const ret = this.originalSetOverlayMode.apply(instance, [mode, ...args]); - return ret; - } - /** * Method to start the prompt handler. */ @@ -425,7 +391,7 @@ export class PhaseInterceptor { currentPhase === actionForNextPrompt.phaseTarget && currentHandler.active && (!actionForNextPrompt.awaitingActionInput || - (actionForNextPrompt.awaitingActionInput && currentHandler.awaitingActionInput)) + (actionForNextPrompt.awaitingActionInput && (currentHandler as AwaitableUiHandler)["awaitingActionInput"])) ) { const prompt = this.prompts.shift(); if (prompt?.callback) { @@ -467,11 +433,10 @@ export class PhaseInterceptor { * function stored in `this.phases`. Additionally, it clears the `promptInterval` and `interval`. */ restoreOg() { - for (const [phase] of this.PHASES) { + for (const phase of this.PHASES) { phase.prototype.start = this.phases[phase.name].start; } UI.prototype.setMode = this.originalSetMode; - UI.prototype.setOverlayMode = this.originalSetOverlayMode; Phase.prototype.end = this.originalSuperEnd; clearInterval(this.promptInterval); clearInterval(this.interval); diff --git a/test/test-utils/string-utils.ts b/test/test-utils/string-utils.ts index bd3dd7c2fa9..6c29c04c107 100644 --- a/test/test-utils/string-utils.ts +++ b/test/test-utils/string-utils.ts @@ -34,10 +34,10 @@ interface getEnumStrOptions { * @returns The stringified representation of `val` as dictated by the options. * @example * ```ts - * enum fakeEnum { - * ONE: 1, - * TWO: 2, - * THREE: 3, + * enum testEnum { + * ONE = 1, + * TWO = 2, + * THREE = 3, * } * getEnumStr(fakeEnum, fakeEnum.ONE); // Output: "ONE (=1)" * getEnumStr(fakeEnum, fakeEnum.TWO, {casing: "Title", prefix: "fakeEnum.", suffix: "!!!"}); // Output: "fakeEnum.TWO!!! (=2)" @@ -174,10 +174,14 @@ export function getStatName(s: Stat): string { * Convert an object into a oneline diff to be shown in an error message. * @param obj - The object to return the oneline diff of * @returns The updated diff + * @example + * ```ts + * const diff = getOnelineDiffStr.call(this, obj) + * ``` */ export function getOnelineDiffStr(this: MatcherState, obj: unknown): string { return this.utils .stringify(obj, undefined, { maxLength: 35, indent: 0, printBasicPrototype: false }) .replace(/\n/g, " ") // Replace newlines with spaces - .replace(/,(\s*)}$/g, "$1}"); + .replace(/,(\s*)}$/g, "$1}"); // Trim trailing commas } diff --git a/test/ui/pokedex.test.ts b/test/ui/pokedex.test.ts index 217c1f09a3b..edd9fa879d0 100644 --- a/test/ui/pokedex.test.ts +++ b/test/ui/pokedex.test.ts @@ -69,7 +69,7 @@ describe("UI - Pokedex", () => { // Open the pokedex UI. await game.runToTitle(); - await game.phaseInterceptor.setOverlayMode(UiMode.POKEDEX); + await game.scene.ui.setOverlayMode(UiMode.POKEDEX); // Get the handler for the current UI. const handler = game.scene.ui.getHandler(); @@ -89,7 +89,7 @@ describe("UI - Pokedex", () => { // Open the pokedex UI. await game.runToTitle(); - await game.phaseInterceptor.setOverlayMode(UiMode.POKEDEX_PAGE, species, starterAttributes); + await game.scene.ui.setOverlayMode(UiMode.POKEDEX_PAGE, species, starterAttributes); // Get the handler for the current UI. const handler = game.scene.ui.getHandler(); diff --git a/test/ui/starter-select.test.ts b/test/ui/starter-select.test.ts index 6dc9603c8b3..397f3d6086f 100644 --- a/test/ui/starter-select.test.ts +++ b/test/ui/starter-select.test.ts @@ -6,8 +6,6 @@ import { GameModes } from "#enums/game-modes"; import { Nature } from "#enums/nature"; import { SpeciesId } from "#enums/species-id"; import { UiMode } from "#enums/ui-mode"; -import { EncounterPhase } from "#phases/encounter-phase"; -import { SelectStarterPhase } from "#phases/select-starter-phase"; import type { TitlePhase } from "#phases/title-phase"; import { GameManager } from "#test/test-utils/game-manager"; import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; @@ -54,9 +52,8 @@ describe("UI - Starter select", () => { handler.processInput(Button.RIGHT); handler.processInput(Button.LEFT); handler.processInput(Button.ACTION); - game.phaseInterceptor.unlock(); }); - await game.phaseInterceptor.run(SelectStarterPhase); + await game.phaseInterceptor.to("SelectStarterPhase"); let options: OptionSelectItem[] = []; let optionSelectUiHandler: OptionSelectUiHandler | undefined; await new Promise(resolve => { @@ -88,12 +85,12 @@ describe("UI - Starter select", () => { resolve(); }); }); - await game.phaseInterceptor.whenAboutToRun(EncounterPhase); + await game.phaseInterceptor.to("EncounterPhase", false); - expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(SpeciesId.BULBASAUR); - expect(game.scene.getPlayerParty()[0].shiny).toBe(true); - expect(game.scene.getPlayerParty()[0].variant).toBe(2); - expect(game.scene.getPlayerParty()[0].gender).toBe(Gender.MALE); + expect(game.field.getPlayerPokemon().species.speciesId).toBe(SpeciesId.BULBASAUR); + expect(game.field.getPlayerPokemon().shiny).toBe(true); + expect(game.field.getPlayerPokemon().variant).toBe(2); + expect(game.field.getPlayerPokemon().gender).toBe(Gender.MALE); }); it("Bulbasaur - shiny - variant 2 female hardy overgrow", async () => { @@ -115,9 +112,8 @@ describe("UI - Starter select", () => { handler.processInput(Button.LEFT); handler.processInput(Button.CYCLE_GENDER); handler.processInput(Button.ACTION); - game.phaseInterceptor.unlock(); }); - await game.phaseInterceptor.run(SelectStarterPhase); + await game.phaseInterceptor.to("SelectStarterPhase"); let options: OptionSelectItem[] = []; let optionSelectUiHandler: OptionSelectUiHandler | undefined; await new Promise(resolve => { @@ -149,13 +145,13 @@ describe("UI - Starter select", () => { resolve(); }); }); - await game.phaseInterceptor.whenAboutToRun(EncounterPhase); + await game.phaseInterceptor.to("EncounterPhase", false); - expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(SpeciesId.BULBASAUR); - expect(game.scene.getPlayerParty()[0].shiny).toBe(true); - expect(game.scene.getPlayerParty()[0].variant).toBe(2); - expect(game.scene.getPlayerParty()[0].nature).toBe(Nature.HARDY); - expect(game.scene.getPlayerParty()[0].getAbility().id).toBe(AbilityId.OVERGROW); + expect(game.field.getPlayerPokemon().species.speciesId).toBe(SpeciesId.BULBASAUR); + expect(game.field.getPlayerPokemon().shiny).toBe(true); + expect(game.field.getPlayerPokemon().variant).toBe(2); + expect(game.field.getPlayerPokemon().nature).toBe(Nature.HARDY); + expect(game.field.getPlayerPokemon().getAbility().id).toBe(AbilityId.OVERGROW); }); it("Bulbasaur - shiny - variant 2 female lonely chlorophyl", async () => { @@ -179,9 +175,8 @@ describe("UI - Starter select", () => { handler.processInput(Button.CYCLE_NATURE); handler.processInput(Button.CYCLE_ABILITY); handler.processInput(Button.ACTION); - game.phaseInterceptor.unlock(); }); - await game.phaseInterceptor.run(SelectStarterPhase); + await game.phaseInterceptor.to("SelectStarterPhase"); let options: OptionSelectItem[] = []; let optionSelectUiHandler: OptionSelectUiHandler | undefined; await new Promise(resolve => { @@ -213,14 +208,14 @@ describe("UI - Starter select", () => { resolve(); }); }); - await game.phaseInterceptor.whenAboutToRun(EncounterPhase); + await game.phaseInterceptor.to("EncounterPhase", false); - expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(SpeciesId.BULBASAUR); - expect(game.scene.getPlayerParty()[0].shiny).toBe(true); - expect(game.scene.getPlayerParty()[0].variant).toBe(2); - expect(game.scene.getPlayerParty()[0].gender).toBe(Gender.FEMALE); - expect(game.scene.getPlayerParty()[0].nature).toBe(Nature.LONELY); - expect(game.scene.getPlayerParty()[0].getAbility().id).toBe(AbilityId.CHLOROPHYLL); + expect(game.field.getPlayerPokemon().species.speciesId).toBe(SpeciesId.BULBASAUR); + expect(game.field.getPlayerPokemon().shiny).toBe(true); + expect(game.field.getPlayerPokemon().variant).toBe(2); + expect(game.field.getPlayerPokemon().gender).toBe(Gender.FEMALE); + expect(game.field.getPlayerPokemon().nature).toBe(Nature.LONELY); + expect(game.field.getPlayerPokemon().getAbility().id).toBe(AbilityId.CHLOROPHYLL); }); it("Bulbasaur - shiny - variant 2 female", async () => { @@ -242,9 +237,8 @@ describe("UI - Starter select", () => { handler.processInput(Button.LEFT); handler.processInput(Button.CYCLE_GENDER); handler.processInput(Button.ACTION); - game.phaseInterceptor.unlock(); }); - await game.phaseInterceptor.run(SelectStarterPhase); + await game.phaseInterceptor.to("SelectStarterPhase"); let options: OptionSelectItem[] = []; let optionSelectUiHandler: OptionSelectUiHandler | undefined; await new Promise(resolve => { @@ -276,12 +270,12 @@ describe("UI - Starter select", () => { resolve(); }); }); - await game.phaseInterceptor.whenAboutToRun(EncounterPhase); + await game.phaseInterceptor.to("EncounterPhase", false); - expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(SpeciesId.BULBASAUR); - expect(game.scene.getPlayerParty()[0].shiny).toBe(true); - expect(game.scene.getPlayerParty()[0].variant).toBe(2); - expect(game.scene.getPlayerParty()[0].gender).toBe(Gender.FEMALE); + expect(game.field.getPlayerPokemon().species.speciesId).toBe(SpeciesId.BULBASAUR); + expect(game.field.getPlayerPokemon().shiny).toBe(true); + expect(game.field.getPlayerPokemon().variant).toBe(2); + expect(game.field.getPlayerPokemon().gender).toBe(Gender.FEMALE); }); it("Bulbasaur - not shiny", async () => { @@ -303,9 +297,8 @@ describe("UI - Starter select", () => { handler.processInput(Button.LEFT); handler.processInput(Button.ACTION); handler.processInput(Button.CYCLE_SHINY); - game.phaseInterceptor.unlock(); }); - await game.phaseInterceptor.run(SelectStarterPhase); + await game.phaseInterceptor.to("SelectStarterPhase"); let options: OptionSelectItem[] = []; let optionSelectUiHandler: OptionSelectUiHandler | undefined; await new Promise(resolve => { @@ -337,11 +330,11 @@ describe("UI - Starter select", () => { resolve(); }); }); - await game.phaseInterceptor.whenAboutToRun(EncounterPhase); + await game.phaseInterceptor.to("EncounterPhase", false); - expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(SpeciesId.BULBASAUR); - expect(game.scene.getPlayerParty()[0].shiny).toBe(false); - expect(game.scene.getPlayerParty()[0].variant).toBe(0); + expect(game.field.getPlayerPokemon().species.speciesId).toBe(SpeciesId.BULBASAUR); + expect(game.field.getPlayerPokemon().shiny).toBe(false); + expect(game.field.getPlayerPokemon().variant).toBe(0); }); it("Bulbasaur - shiny - variant 1", async () => { @@ -365,9 +358,8 @@ describe("UI - Starter select", () => { handler.processInput(Button.CYCLE_SHINY); handler.processInput(Button.CYCLE_SHINY); handler.processInput(Button.ACTION); - game.phaseInterceptor.unlock(); }); - await game.phaseInterceptor.run(SelectStarterPhase); + await game.phaseInterceptor.to("SelectStarterPhase"); let options: OptionSelectItem[] = []; let optionSelectUiHandler: OptionSelectUiHandler | undefined; await new Promise(resolve => { @@ -399,11 +391,11 @@ describe("UI - Starter select", () => { resolve(); }); }); - await game.phaseInterceptor.whenAboutToRun(EncounterPhase); + await game.phaseInterceptor.to("EncounterPhase", false); - expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(SpeciesId.BULBASAUR); - expect(game.scene.getPlayerParty()[0].shiny).toBe(true); - expect(game.scene.getPlayerParty()[0].variant).toBe(1); + expect(game.field.getPlayerPokemon().species.speciesId).toBe(SpeciesId.BULBASAUR); + expect(game.field.getPlayerPokemon().shiny).toBe(true); + expect(game.field.getPlayerPokemon().variant).toBe(1); }); it("Bulbasaur - shiny - variant 0", async () => { @@ -426,9 +418,8 @@ describe("UI - Starter select", () => { handler.processInput(Button.CYCLE_SHINY); handler.processInput(Button.CYCLE_SHINY); handler.processInput(Button.ACTION); - game.phaseInterceptor.unlock(); }); - await game.phaseInterceptor.run(SelectStarterPhase); + await game.phaseInterceptor.to("SelectStarterPhase"); let options: OptionSelectItem[] = []; let optionSelectUiHandler: OptionSelectUiHandler | undefined; await new Promise(resolve => { @@ -460,11 +451,11 @@ describe("UI - Starter select", () => { resolve(); }); }); - await game.phaseInterceptor.whenAboutToRun(EncounterPhase); + await game.phaseInterceptor.to("EncounterPhase", false); - expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(SpeciesId.BULBASAUR); - expect(game.scene.getPlayerParty()[0].shiny).toBe(true); - expect(game.scene.getPlayerParty()[0].variant).toBe(0); + expect(game.field.getPlayerPokemon().species.speciesId).toBe(SpeciesId.BULBASAUR); + expect(game.field.getPlayerPokemon().shiny).toBe(true); + expect(game.field.getPlayerPokemon().variant).toBe(0); }); it("Check if first pokemon in party is caterpie from gen 1 and 1rd row, 3rd column", async () => { @@ -486,9 +477,8 @@ describe("UI - Starter select", () => { handler.processInput(Button.RIGHT); handler.processInput(Button.RIGHT); handler.processInput(Button.ACTION); - game.phaseInterceptor.unlock(); }); - await game.phaseInterceptor.run(SelectStarterPhase); + await game.phaseInterceptor.to("SelectStarterPhase"); let options: OptionSelectItem[] = []; let optionSelectUiHandler: OptionSelectUiHandler | undefined; await new Promise(resolve => { @@ -527,8 +517,8 @@ describe("UI - Starter select", () => { const saveSlotSelectUiHandler = game.scene.ui.getHandler() as SaveSlotSelectUiHandler; saveSlotSelectUiHandler.processInput(Button.ACTION); }); - await game.phaseInterceptor.whenAboutToRun(EncounterPhase); - expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(SpeciesId.CATERPIE); + await game.phaseInterceptor.to("EncounterPhase", false); + expect(game.field.getPlayerPokemon().species.speciesId).toBe(SpeciesId.CATERPIE); }); it("Check if first pokemon in party is nidoran_m from gen 1 and 2nd row, 4th column (cursor (9+4)-1)", async () => { @@ -551,9 +541,8 @@ describe("UI - Starter select", () => { handler.processInput(Button.RIGHT); handler.processInput(Button.DOWN); handler.processInput(Button.ACTION); - game.phaseInterceptor.unlock(); }); - await game.phaseInterceptor.run(SelectStarterPhase); + await game.phaseInterceptor.to("SelectStarterPhase"); let options: OptionSelectItem[] = []; let optionSelectUiHandler: OptionSelectUiHandler | undefined; await new Promise(resolve => { @@ -593,7 +582,7 @@ describe("UI - Starter select", () => { const saveSlotSelectUiHandler = game.scene.ui.getHandler() as SaveSlotSelectUiHandler; saveSlotSelectUiHandler.processInput(Button.ACTION); }); - await game.phaseInterceptor.whenAboutToRun(EncounterPhase); - expect(game.scene.getPlayerParty()[0].species.speciesId).toBe(SpeciesId.NIDORAN_M); + await game.phaseInterceptor.to("EncounterPhase", false); + expect(game.field.getPlayerPokemon().species.speciesId).toBe(SpeciesId.NIDORAN_M); }); }); diff --git a/test/ui/transfer-item.test.ts b/test/ui/transfer-item.test.ts index 0d101b5b4ef..8e42149acc3 100644 --- a/test/ui/transfer-item.test.ts +++ b/test/ui/transfer-item.test.ts @@ -72,8 +72,6 @@ describe("UI - Transfer Items", () => { expect( handler.optionsContainer.list.some(option => RegExp(/Lum Berry\[color.*(2)/).exec((option as BBCodeText).text)), ).toBe(true); - - game.phaseInterceptor.unlock(); }); await game.phaseInterceptor.to("SelectModifierPhase"); @@ -93,8 +91,6 @@ describe("UI - Transfer Items", () => { expect(handler.optionsContainer.list.some(option => (option as BBCodeText).text?.includes("Transfer"))).toBe( true, ); - - game.phaseInterceptor.unlock(); }); await game.phaseInterceptor.to("SelectModifierPhase"); diff --git a/test/ui/type-hints.test.ts b/test/ui/type-hints.test.ts index f1f27322a64..b5fe0d9585a 100644 --- a/test/ui/type-hints.test.ts +++ b/test/ui/type-hints.test.ts @@ -2,7 +2,6 @@ import { Button } from "#enums/buttons"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { UiMode } from "#enums/ui-mode"; -import { CommandPhase } from "#phases/command-phase"; import { GameManager } from "#test/test-utils/game-manager"; import type { MockText } from "#test/test-utils/mocks/mocks-container/mock-text"; import { FightUiHandler } from "#ui/fight-ui-handler"; @@ -46,7 +45,6 @@ describe("UI - Type Hints", () => { const { ui } = game.scene; const handler = ui.getHandler(); handler.processInput(Button.ACTION); // select "Fight" - game.phaseInterceptor.unlock(); }); game.onNextPrompt("CommandPhase", UiMode.FIGHT, () => { @@ -59,7 +57,7 @@ describe("UI - Type Hints", () => { expect.soft(dragonClawText.color).toBe("#929292"); ui.getHandler().processInput(Button.ACTION); }); - await game.phaseInterceptor.to(CommandPhase); + await game.phaseInterceptor.to("CommandPhase"); }); it("check status move color", async () => { @@ -71,7 +69,6 @@ describe("UI - Type Hints", () => { const { ui } = game.scene; const handler = ui.getHandler(); handler.processInput(Button.ACTION); // select "Fight" - game.phaseInterceptor.unlock(); }); game.onNextPrompt("CommandPhase", UiMode.FIGHT, () => { @@ -84,7 +81,7 @@ describe("UI - Type Hints", () => { expect.soft(growlText.color).toBe(undefined); ui.getHandler().processInput(Button.ACTION); }); - await game.phaseInterceptor.to(CommandPhase); + await game.phaseInterceptor.to("CommandPhase"); }); it("should show the proper hint for a move in doubles after one of the enemy pokemon flees", async () => { @@ -107,7 +104,6 @@ describe("UI - Type Hints", () => { const { ui } = game.scene; const handler = ui.getHandler(); handler.processInput(Button.ACTION); // select "Fight" - game.phaseInterceptor.unlock(); }); game.onNextPrompt("CommandPhase", UiMode.FIGHT, () => { @@ -121,6 +117,6 @@ describe("UI - Type Hints", () => { expect.soft(shadowBallText.color).toBe(undefined); ui.getHandler().processInput(Button.ACTION); }); - await game.phaseInterceptor.to(CommandPhase); + await game.phaseInterceptor.to("CommandPhase"); }); });