Minor Update 1.11.4 to main

Minor Update 1.11.4
This commit is contained in:
damocleas 2025-12-20 19:00:03 -05:00 committed by GitHub
commit a0328020a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
278 changed files with 7628 additions and 7068 deletions

View File

@ -5,7 +5,9 @@ module.exports = {
name: "no-non-type-@type-exports",
severity: "error",
comment:
"Files in @types should not export anything but types and interfaces. The folder is intended to house imports that are removed at runtime, and thus should not contain anything with a bearing on runtime code.",
"Files in `@types/` should not export anything but types and interfaces. "
+ "The folder is intended to house imports that are removed at runtime, "
+ "and thus should not contain anything with a bearing on runtime code.",
from: {},
to: {
path: "(^|/)src/@types",
@ -18,6 +20,7 @@ module.exports = {
comment: "Files in 'enums/' and '@types/' must only use type imports.",
from: {
path: ["(^|/)src/@types", "(^|/)src/enums"],
pathNot: ["(^|/)src/@types/phaser[.]d[.]ts"],
},
to: {
dependencyTypesNot: ["type-only"],
@ -27,7 +30,8 @@ module.exports = {
name: "no-circular-at-runtime",
severity: "error",
comment:
"This dependency is part of a circular relationship. You might want to revise your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ",
"This dependency is part of a circular relationship. You might want to revise "
+ "your solution (i.e. use dependency inversion, make sure the modules have a single responsibility).",
from: {},
to: {
circular: true,
@ -39,7 +43,11 @@ module.exports = {
{
name: "no-orphans",
comment:
"This is an orphan module - it's likely not used (anymore?). Either use it or remove it. If it's logical this module is an orphan (i.e. it's a config file), add an exception for it in your dependency-cruiser configuration. By default this rule does not scrutinize dot-files (e.g. .eslintrc.js), TypeScript declaration files (.d.ts), tsconfig.json and some of the babel and webpack configs.",
"This is an orphan module - it's likely not used [anymore]. Either use it or "
+ "remove it. If it's logical this module is an orphan (i.e. it's a config file), "
+ "add an exception for it in your dependency-cruiser configuration. By default "
+ "this rule does not scrutinize dot-files (e.g. .eslintrc.js), TypeScript declaration "
+ "files (.d.ts), tsconfig.json and some of the babel and webpack configs.",
severity: "error",
from: {
orphan: true,
@ -56,7 +64,8 @@ module.exports = {
{
name: "no-deprecated-core",
comment:
"A module depends on a node core module that has been deprecated. Find an alternative - these are bound to exist - node doesn't deprecate lightly.",
"A module depends on a node core module that has been deprecated. "
+ "Find an alternative - these are bound to exist - node doesn't deprecate lightly.",
severity: "error",
from: {},
to: {
@ -88,7 +97,9 @@ module.exports = {
{
name: "not-to-deprecated",
comment:
"This module uses a (version of an) npm module that has been deprecated. Either upgrade to a later version of that module, or find an alternative. Deprecated modules are a security risk.",
"This module uses a (version of an) npm module that has been deprecated. "
+ "Either upgrade to a later version of that module, or find an alternative. "
+ "Deprecated modules are a security risk.",
severity: "error",
from: {},
to: {
@ -99,7 +110,10 @@ module.exports = {
name: "no-non-package-json",
severity: "error",
comment:
"This module depends on an npm package that isn't in the 'dependencies' section of your package.json. That's problematic as the package either (1) won't be available on live (2 - worse) will be available on live with an non-guaranteed version. Fix it by adding the package to the dependencies in your package.json.",
"This module depends on an npm package that isn't in the 'dependencies' section of your package.json. "
+ "That's problematic as the package either (1) won't be available on live (2 - worse) will be "
+ "available on live with an non-guaranteed version. Fix it by adding the package to the dependencies "
+ "in your package.json.",
from: {},
to: {
dependencyTypes: ["npm-no-pkg", "npm-unknown"],
@ -108,7 +122,8 @@ module.exports = {
{
name: "not-to-unresolvable",
comment:
"This module depends on a module that cannot be found ('resolved to disk'). If it's an npm module: add it to your package.json. In all other cases you likely already know what to do.",
"This module depends on a module that cannot be found ('resolved to disk'). "
+ "If it's an npm module: add it to your package.json. In all other cases you likely already know what to do.",
severity: "error",
from: {},
to: {
@ -118,7 +133,9 @@ module.exports = {
{
name: "no-duplicate-dep-types",
comment:
"Likely this module depends on an external ('npm') package that occurs more than once in your package.json i.e. bot as a devDependencies and in dependencies. This will cause maintenance problems later on.",
"Likely this module depends on an external ('npm') package that occurs more than once "
+ "in your package.json (i.e. both in `devDependencies` and in `dependencies`). "
+ "This will cause maintenance problems later on.",
severity: "error",
from: {},
to: {
@ -135,7 +152,9 @@ module.exports = {
{
name: "not-to-spec",
comment:
"This module depends on a spec (test) file. The sole responsibility of a spec file is to test code. If there's something in a spec that's of use to other modules, it doesn't have that single responsibility anymore. Factor it out into (e.g.) a separate utility/ helper or a mock.",
"This module depends on a spec (test) file. The sole responsibility of a spec file is to test code. "
+ "If there's something in a spec that's of use to other modules, it doesn't have that single "
+ "responsibility anymore. Factor it out into (e.g.) a separate utility/helper or a mock.",
severity: "error",
from: {},
to: {
@ -146,7 +165,11 @@ module.exports = {
name: "not-to-dev-dep",
severity: "error",
comment:
"This module depends on an npm package from the 'devDependencies' section of your package.json. It looks like something that ships to production, though. To prevent problems with npm packages that aren't there on production declare it (only!) in the 'dependencies'section of your package.json. If this module is development only - add it to the from.pathNot re of the not-to-dev-dep rule in the dependency-cruiser configuration",
"This module depends on an npm package from the 'devDependencies' section of your "
+ "package.json. It looks like something that ships to production, though. To prevent problems "
+ "with npm packages that aren't there on production declare it (only!) in the 'dependencies'"
+ "section of your package.json. If this module is development only - add it to the "
+ "from.pathNot re of the not-to-dev-dep rule in the dependency-cruiser configuration",
from: {
path: "^(src)",
pathNot: ["[.](?:spec|test|setup|script)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$", "./test"],
@ -163,7 +186,10 @@ module.exports = {
name: "optional-deps-used",
severity: "info",
comment:
"This module depends on an npm package that is declared as an optional dependency in your package.json. As this makes sense in limited situations only, it's flagged here. If you're using an optional dependency here by design - add an exception to yourdependency-cruiser configuration.",
"This module depends on an npm package that is declared as an optional dependency "
+ "in your package.json. As this makes sense in limited situations only, it's flagged here. "
+ "If you're using an optional dependency here by design - add an exception to your"
+ "dependency-cruiser configuration.",
from: {},
to: {
dependencyTypes: ["npm-optional"],
@ -172,7 +198,10 @@ module.exports = {
{
name: "peer-deps-used",
comment:
"This module depends on an npm package that is declared as a peer dependency in your package.json. This makes sense if your package is e.g. a plugin, but in other cases - maybe not so much. If the use of a peer dependency is intentional add an exception to your dependency-cruiser configuration.",
"This module depends on an npm package that is declared as a peer dependency "
+ "in your package.json. This makes sense if your package is e.g. a plugin, but in "
+ "other cases - maybe not so much. If the use of a peer dependency is intentional "
+ "add an exception to your dependency-cruiser configuration.",
severity: "error",
from: {},
to: {

View File

@ -9,7 +9,7 @@
{
"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",
"image": "mcr.microsoft.com/devcontainers/typescript-node:4-24-bookworm",
"features": {
"ghcr.io/devcontainers/features/github-cli:1": {
"installDirectlyFromGitHubRelease": true,
@ -19,6 +19,9 @@
"version": "latest"
}
},
"containerEnv": {
"PORT": "8000"
},
"customizations": {
"vscode": {
"settings": {

87
.git-blame-ignore-revs Normal file
View File

@ -0,0 +1,87 @@
# SPDX-FileCopyrightText: 2024-2025 NONE
#
# SPDX-License-Identifier: CC0-1.0
# This file lists revisions of large-scale formatting, style and code movement changes so that
# they can be excluded from git blame results.
#
# To set this file as the default ignore file for git blame, run:
# $ git config --local blame.ignoreRevsFile .git-blame-ignore-revs
# ESLint - The Essential Linter and Formatter for JavaScript and TypeScript (#1224)
bac6c22973fedff868e73a989443035c672e8f57
# [Refactor] use typescript strict-null (#3259)
a07d2c57a44361f4ad48e024067c9b7638c0d84d
# [Test] Replace `doAttack()` with `move.select()` in tests (#3567)
828897316e089ba390bc5fd3503e9175c7f45e8e
# [Misc] Add eslint rule to enforce indenting of `case` statements (#4692)
39abac65be26c2004ea1ca9a686cc8d7b35efb65
# [Refactor] Create global scene variable (#4766)
0107b1d47ea4a898f39dd3534fcccd4241c03470
# [Misc][Refactor][GitHub] Ditch eslint for biome, and add a formatter (#5495)
408b66f9135004e128d7d737904facc30529c771
# [Biome] Add and apply `lint/style/noNamespaceImport` (#5650)
6f56dce7712c609dc78f7ff11650eb6a34f8f661
# [Test] Remove deprecated test funcs (#5906)
a33638a7a36b01935e40ea876532c770b53f0994
# [Test] Remove redundant entries and `Array.fill()` in moveset overrides (#5907)
cdda539ac5609c1f0fa5da0114ee51d5bff82bf5
# [Test] Condensed all `game.override` calls into 1 chained line where possible (#5926)
061c9872658011c1eefba3c10b2de33bdff8a0b0
# [Test] Remove unneeded `mockRestore` and `testTimeout` calls in tests (#5927)
0918985a63cc8795bc9a45d8ceacdfaca5f12ee7
# [Misc] Improve enum naming (#5933)
9dcb904649474b9ccec52b7d1251ee731b5edabf
# [Dev] Enable Biome checking of `ability.ts` (#5948)
75beec12a892d28b51d8cbd859a28fc793617736
# [Dev] Enable Biome import sorting (#6052)
8cf1b9f766051bd610ac803222272d6e7287571d
# [Misc] Standardize-file-names (#6137)
51d4c33de056dad78990b82aedd9f1b527d05d6d
# [Test] Cleaned up tests to use updated test utils; part 1 (#6176)
87340624011bc8a68a9181ac2917cddb81eb073c
# [Test] Replaced all instances of `game.scene.getXXXPokemon()!` inside tests with `game.field.getXXXPokemon()` (#6178)
8da02bad50995077243c8036aeef215ef42b42d4
# [Refactor] Remove `null` from `PhaseManager.currentPhase` signature (#6243)
d5e6670456acf3af98fac5bf7a20dc6cb3854c98
# [Dev] Migrated to Biome 2.2.3, added more rules (#6259)
c0da686ba0da1b08b56e4117839d9d45ed95e69a
# [Misc] Make the repo REUSE compliant (#6474)
6766940fa15202c9995f2fed7287bf85939d9816
# [Test] Updated more uses of `game.scene.getEnemyField` and `game.scene.getPlayerField` to use updated test utils (#6524)
3d9e493e5f550ca933413c015bbb644398b1a8a7
# [Refactor] Remove `isNullOrUndefined` in favor of loose check against null (#6549)
c7a2c666af0009bb347616cefd9a767fb66b4b46
# [Refactor][Dev] Move public to its own submodule (#6590)
c695df777c041e9a2e747fd2767249cbb1235868
# [Dev] Improve typescript performance and version bump node and dependencies (#6627)
f4456f6c7cd77ecdb680517364b97fde9c07293f
# [Dev] Enable linting of `move.ts` and update Biome to 2.3.2 (#6688)
d0ddcaa7a3a9dd262577abf60e40dcc8834faf7b
# [Dev] Update Biome from `2.3.2` to `2.3.8` (#6799)
b6bd9566e24729e4677f1afb60da159425a9f60d

1
.github/REUSE.toml vendored
View File

@ -9,6 +9,7 @@ version = 1
[[annotations]]
path = [
"workflows/**/*.yml",
"actions/**/*.yml",
"ISSUE_TEMPLATE/**/*.yml",
"FUNDING.yml",
"CODEOWNERS",

17
.github/actions/setup-deps/action.yml vendored Normal file
View File

@ -0,0 +1,17 @@
name: "Setup pnpm and Node.js"
description: "Setup pnpm and Node.js environment for workflows. Requires the repo to be cloned to at least have .nvmrc and package.json"
runs:
using: "composite"
steps:
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: "pnpm"
- name: Install dependencies
run: pnpm i
shell: bash

View File

@ -45,7 +45,7 @@ jobs:
private-key: ${{ secrets.PAGEFAULT_APP_PRIVATE_KEY }}
- name: Check out code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
submodules: "recursive"
# Always base off of beta branch, regardless of the branch the workflow was triggered from.

View File

@ -15,20 +15,12 @@ jobs:
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
submodules: "recursive"
ref: ${{ vars.BETA_DEPLOY_BRANCH || 'beta'}}
- name: Install pnpm
uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
- name: Install dependencies
run: pnpm i
- uses: ./.github/actions/setup-deps
- name: Build
run: pnpm build:beta

View File

@ -14,19 +14,11 @@ jobs:
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
submodules: 'recursive'
- name: Install pnpm
uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- name: Install dependencies
run: pnpm i
- uses: ./.github/actions/setup-deps
- name: Build
run: pnpm build

View File

@ -29,7 +29,7 @@ jobs:
steps:
- name: Checkout repository for Typedoc
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
path: pokerogue_docs
@ -42,7 +42,7 @@ jobs:
rm -rf assets
- name: Checkout asset submodule
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
repository: 'pagefaultgames/pokerogue-assets'
ref: ${{ steps.asset-submodule-ref.asset_ref }}
@ -59,13 +59,13 @@ jobs:
version: 10
- name: Setup Node
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version-file: "pokerogue_docs/.nvmrc"
- name: Checkout repository for Github Pages
if: github.event_name == 'push'
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
path: pokerogue_gh
ref: gh-pages

View File

@ -25,21 +25,12 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
submodules: "recursive"
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
cache: "pnpm"
- name: Install Node modules
run: pnpm i
- uses: ./.github/actions/setup-deps
# Lint files with Biome-Lint - https://biomejs.dev/linter/
- name: Lint with Biome
@ -64,13 +55,6 @@ jobs:
id: typecheck-scripts
if: ${{ !cancelled() }}
# NOTE: These steps *must* be ran last for the moment due to deleting files in `assets/`.
# Some asset files do not yet have full licensing information, and thus must be removed
# before checking for REUSE compliance
- name: Prepare for REUSE compliance
run: rm -rf assets/* LICENSES/LicenseRef-*
if: ${{ !cancelled() }}
- name: Check for REUSE compliance
id: reuse-lint
uses: fsfe/reuse-action@v5

View File

@ -3,44 +3,42 @@ name: Test Template
on:
workflow_call:
inputs:
project:
required: true
type: string
shard:
required: true
type: number
totalShards:
required: true
type: number
skip:
required: true
type: boolean
default: false
jobs:
test:
# We can't use dynmically named jobs until https://github.com/orgs/community/discussions/13261 is implemented
# We can't use dynamically named jobs until https://github.com/orgs/community/discussions/13261 is implemented
name: Shard
timeout-minutes: 10
runs-on: ubuntu-latest
if: ${{ !inputs.skip }}
steps:
- name: Check out Git repository
uses: actions/checkout@v4.2.2
uses: actions/checkout@v6
with:
submodules: "recursive"
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
cache: "pnpm"
- name: Install Node.js dependencies
run: pnpm i
- uses: ./.github/actions/setup-deps
- name: Run tests
run: pnpm test:silent --shard=${{ inputs.shard }}/${{ inputs.totalShards }}
run: >
pnpm test:silent
--shard=${{ inputs.shard }}/${{ inputs.totalShards }}
--reporter=blob
--outputFile=test-results/blob-${{ inputs.shard }}.json || true
# NB: This CANNOT be made into a job output due to not being extractable from the matrix:
# https://github.com/orgs/community/discussions/17245
- name: Upload test result blobs
uses: actions/upload-artifact@v4
with:
name: shard-${{ inputs.shard }}-blob
path: test-results/blob-${{ inputs.shard }}.json
retention-days: 1 # don't need to keep for very long since they get merged afterwards
overwrite: true
if-no-files-found: error
if: ${{ !cancelled() }}

View File

@ -26,8 +26,8 @@ jobs:
outputs:
all: ${{ steps.filter.outputs.all }}
steps:
- name: checkout
uses: actions/checkout@v4
- name: Checkout GitHub repository
uses: actions/checkout@v6
with:
sparse-checkout: |
.github/test-filters.yml
@ -41,6 +41,7 @@ jobs:
run-tests:
name: Run Tests
needs: check-path-change-filter
if: ${{ needs.check-path-change-filter.outputs.all == 'true'}}
strategy:
# don't stop upon 1 shard failing
fail-fast: false
@ -48,7 +49,33 @@ jobs:
shard: [1, 2, 3, 4, 5]
uses: ./.github/workflows/test-shard-template.yml
with:
project: main
shard: ${{ matrix.shard }}
totalShards: 5
skip: ${{ needs.check-path-change-filter.outputs.all != 'true'}}
check-results:
name: Check Test Results
timeout-minutes: 8
needs: run-tests
runs-on: ubuntu-latest
if: ${{ needs.run-tests.result != 'skipped' && needs.run-tests.result != 'cancelled' }}
steps:
- name: Check out Git repository
uses: actions/checkout@v6
with:
sparse-checkout: |
.github/actions/setup-deps/action.yml
test/setup
test/test-utils/reporters/custom-default-reporter.ts
- uses: ./.github/actions/setup-deps
- name: Download blob artifacts
uses: actions/download-artifact@v6
with:
pattern: shard-?-blob
path: test-results
merge-multiple: true
- name: Merge blobs
run: pnpm test:merge-reports

6
.gitignore vendored
View File

@ -16,10 +16,12 @@ dist
dist-ssr
*.local
build
.pnpm-store
# Editor directories and files (excluding `extensions.json` for devcontainer)
*.code-workspace
.vscode/*
.zed
!.vscode/extensions.json
!.vscode/spdx.code-snippets
.idea
@ -39,7 +41,6 @@ assets/images/pokemon/icons/input/output/*
assets/images/character/*/
src/data/battle-anim-raw-data*.ts
src/data/battle-anim-data.ts
src/overrides.ts
coverage
/.vs
@ -50,6 +51,5 @@ coverage
/dependency-graph.svg
/.vs
# Script outputs
./*.csv
./*.csv

View File

@ -25,6 +25,7 @@ ls:
<<: *cfg
test: *src
ignore:
- .pnpm-store
- node_modules
- .vscode
- .github

View File

@ -26,9 +26,9 @@ If you use any external code, please make sure to follow its licensing informati
## 🛠️ 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
@ -47,11 +47,18 @@ This Linux environment comes with all required dependencies needed to start work
[codespaces-link]: <https://github.com/codespaces/new?hide_repo_select=true&repo=620476224&ref=beta>
[devcontainer-ext]: <https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers>
> [!IMPORTANT]
> Due to quirks of devcontainer port forwarding, you must use **`pnpm start:podman`** to start a local dev server from within a devcontainer.
> All other instructions remain the same as local development.
### Podman
For those who prefer Docker containers, see [this instructions page](./docs/podman.md) for information on how to setup a development environment with Podman.
### 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/)
- node: >=24.9.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
@ -90,7 +97,7 @@ Notable topics include:
- [Commenting your code](./docs/comments.md)
- [Linting & Formatting](./docs/linting.md)
- [Localization](./docs/localization.md)
- [Enemy AI move selection](./docs/enemy-ai.md)
- [Enemy AI move selection](./docs/enemy-ai.md)
- [Running with Podman](./docs/podman.md)
Again, if you have unanswered questions please feel free to ask!
@ -103,7 +110,7 @@ You've just made a change - how can you check if it works? You have two areas to
> This will likely be your first stop. After making a change, you'll want to spin the game up and make sure everything is as you expect. To do this, you will need a way to manipulate the game to produce the situation you're looking to test.
[src/overrides.ts](../src/overrides.ts) contains overrides for most values you'll need to change for testing, controlled through the `overrides` object.
[src/overrides.ts](./src/overrides.ts) contains overrides for most values you'll need to change for testing, controlled through the `overrides` object.
For example, here is how you could test a scenario where the player Pokemon has the ability Drought and the enemy Pokemon has the move Water Gun:
```typescript
@ -135,4 +142,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.

View File

@ -4,7 +4,7 @@ SPDX-FileCopyrightText: 2024-2025 Pagefault Games
SPDX-License-Identifier: CC-BY-NC-SA-4.0
-->
<div align="center"><picture><img src="https://github.com/pagefaultgames/pokerogue-assets/blob/beta/images/logo.png" width="300" alt="PokéRogue"></picture>
<div align="center"><picture><img src="https://github.com/pagefaultgames/pokerogue-assets/blob/beta/images/logo.png?raw=true" width="300" alt="PokéRogue"></picture>
[![Discord Static Badge](https://img.shields.io/badge/Community_Discord-blurple?style=flat&logo=discord&logoSize=auto&labelColor=white&color=5865F2)](https://discord.gg/pokerogue)
[![Docs Coverage Static Badge](https://pagefaultgames.github.io/pokerogue/beta/coverage.svg)](https://pagefaultgames.github.io/pokerogue/beta)

2
assets

@ -1 +1 @@
Subproject commit 9d391bd666f339c31db3d48a9907139950c14d1e
Subproject commit a36741a2112217eaf067248d7d1917266339a56d

View File

@ -5,7 +5,7 @@
*/
{
"$schema": "https://biomejs.dev/schemas/2.2.5/schema.json",
"$schema": "https://biomejs.dev/schemas/2.3.8/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
@ -16,7 +16,7 @@
"enabled": true,
"useEditorconfig": true,
"indentStyle": "space",
"includes": ["**", "!**/src/data/balance/**"],
"includes": ["**", "!src/data/balance/**"], // TODO: enable formatting of balance folder
"lineWidth": 120
},
"files": {
@ -25,20 +25,23 @@
// and having to verify whether each individual file is ignored
"includes": [
"**",
"!**/dist",
"!**/coverage",
"!**/assets",
"!**/locales",
"!**/.github",
"!**/node_modules",
"!**/typedoc",
"!!.github",
"!!assets",
"!!coverage",
"!!dist",
"!!docs",
"!!locales",
"!!typedoc",
// TODO: lint css and html?
"!**/*.css",
"!**/*.html",
// TODO: enable linting this file
"!**/src/data/moves/move.ts",
"!!*.css",
"!!*.html",
// this file is too big
"!**/src/data/balance/tm-species-map.ts"
"!src/data/balance/tm-species-map.ts",
// there's some sort of bug when Biome parses this file
// relating to recursive variable assignment
// cf https://github.com/biomejs/biome/issues/8204
// TODO: remove this exclusion when the bug is fixed
"!!test/test-utils/setup/test-end-log.ts"
]
},
"assist": {
@ -116,7 +119,17 @@
},
"useCollapsedIf": "error",
"useCollapsedElseIf": "error",
"useDeprecatedReason": "error",
"useConsistentArrayType": {
"level": "error",
"fix": "safe",
"options": {}
},
"useShorthandAssign": {
"level": "error",
"fix": "safe",
"options": {}
},
"noSubstr": "error",
"noYodaExpression": "error",
"useForOf": "error",
@ -194,7 +207,8 @@
"noDocumentCookie": "off", // Firefox has minimal support for the "Cookie Store API"
"noConstantBinaryExpressions": "error",
"noTsIgnore": "error",
"useIterableCallbackReturn": "warn" // TODO: Refactor and change to error
"useIterableCallbackReturn": "warn", // TODO: Refactor and change to error
"noNonNullAssertedOptionalChain": "warn" // TODO: Refactor and change to error
},
"complexity": {
"useWhile": "error",
@ -205,7 +219,7 @@
"noForEach": "off", // Foreach vs for of is not that simple.
"noUselessSwitchCase": "off", // Explicit > Implicit
"noUselessConstructor": "error",
"noBannedTypes": "warn", // TODO: Refactor and make this an error
"noBannedTypes": "error",
"noThisInStatic": "error",
"noUselessThisAlias": "error",
"noUselessTernary": "error",
@ -217,13 +231,14 @@
"noBarrelFile": "error"
},
"nursery": {
"noProto": "error",
"useFind": "error",
"noUselessUndefined": "error",
"useMaxParams": {
"level": "info", // TODO: Change to "error"... eventually...
"options": { "max": 7 }
},
"noShadow": "warn", // TODO: refactor and make "error"
"noNonNullAssertedOptionalChain": "warn", // TODO: refactor and make "error"
"noDuplicateDependencies": "error",
"noImportCycles": "error",
// TODO: Change to error once promises are used properly
@ -243,6 +258,16 @@
}
},
"overrides": [
{
"includes": ["**/scripts/**/*.js"],
"linter": {
"rules": {
"nursery": {
"noFloatingPromises": "error"
}
}
}
},
{
"includes": ["**/test/**/*.test.ts"],
"linter": {

View File

@ -38,28 +38,37 @@ For an example of how TSDoc comments work, here are some TSDoc comments taken fr
* Attribute to put in a {@link https://bulbapedia.bulbagarden.net/wiki/Substitute_(doll) | Substitute Doll} for the user.
*/
export class AddSubstituteAttr extends MoveEffectAttr {
/** The ratio of the user's max HP that is required to apply this effect */
private hpCost: number;
/** Whether the damage taken should be rounded up (Shed Tail rounds up) */
private roundUp: boolean;
/** The percentage of the user's maximum HP that is required to apply this effect. */
private readonly hpCost: number;
/** Whether the damage taken should be rounded up (Shed Tail rounds up). */
private readonly roundUp: boolean;
constructor(hpCost: number, roundUp: boolean) {
// code removed
}
/**
* Removes 1/4 of the user's maximum HP (rounded down) to create a substitute for the user
* @param user - The {@linkcode Pokemon} that used the move.
* @param target - n/a
* @param move - The {@linkcode Move} with this attribute.
* @param args - n/a
* @returns `true` if the attribute successfully applies, `false` otherwise
* Helper function to compute the amount of HP required to create a substitute.
* @param user - The {@linkcode Pokemon} using the move
* @returns The amount of HP that required to create a substitute.
*/
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
private getHpCost(user: Pokemon): number {
// code removed
}
getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
/**
* Remove a fraction of the user's maximum HP to create a 25% HP substitute doll.
* @param user - The {@linkcode Pokemon} using the move
* @param target - n/a
* @param move - The {@linkcode Move} being used
* @param args - n/a
* @returns Whether the attribute successfully applied.
*/
public override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
// code removed
}
public override getUserBenefitScore(user: Pokemon, _target: Pokemon, _move: Move): number {
// code removed
}
@ -67,12 +76,7 @@ export class AddSubstituteAttr extends MoveEffectAttr {
// code removed
}
/**
* Get the substitute-specific failure message if one should be displayed.
* @param user - The pokemon using the move.
* @returns The substitute-specific failure message if the conditions apply, otherwise `undefined`
*/
getFailedText(user: Pokemon, _target: Pokemon, _move: Move): string | undefined {
public override getFailedText(user: Pokemon): string | undefined {
// code removed
}
}

View File

@ -18,7 +18,7 @@ For the most part, Biome attempts to stay "out of your hair", letting you write
On the other hand, if Biome complains about a piece of code, **there's probably a good reason why**. Disable comments should be used sparingly or when readabilty demands it - your first instinct should be to fix the code in question, not disable the rule.
## Editor Integration
Biome has integration with many popular code editors. See [these](https://biomejs.dev/guides/editors/first-party-extensions/) [pages](https://biomejs.dev/guides/editors/third-party-extensions/) for information about enabling Biome in your editor of choice.
Biome has integrations with many popular code editors. See [these](https://biomejs.dev/guides/editors/first-party-extensions/) [pages](https://biomejs.dev/guides/editors/third-party-extensions/) for information about enabling Biome in your editor of choice.
## Automated Runs
Generally speaking, most users shouldn't need to run Biome directly; in addition to editor integration, a [pre-commit hook](../lefthook.yml) will automatically format and lint all staged files before each commit.
@ -32,7 +32,7 @@ We also have a [Github Action](../.github/workflows/linting.yml) to verify code
These are effectively the same commands as run by Lefthook, merely on a project-wide scale.
## Running Biome via CLI
To run you Biome on your files manually, you have 2 main options:
To run Biome on your files manually, you have 2 main options:
1. Run the scripts included in `package.json` (`pnpm biome` and `pnpm biome:all`). \
These have sensible defaults for command-line options, but do not allow altering certain flags (as some cannot be specified twice in the same command)
@ -50,14 +50,11 @@ A full list of flags and options can be found on [their website](https://biomejs
## Linting Rules
We primarily use Biome's [recommended ruleset](https://biomejs.dev/linter/rules/) for linting JS/TS files, with some customizations to better suit our project's needs[^1].
We primarily use Biome's [recommended ruleset](https://biomejs.dev/linter/rules/) for linting JS/TS files, with some customizations to better suit our project's needs. \
A complete list of rules can be found in the [`biome.jsonc`](../biome.jsonc) file in the project root. Most rules are accompanied by comments explaining the reasons for their inclusion/exclusion.
Some things to consider:
- We have disabled rules that prioritize style over performance, such as `useTemplate`.
- Some rules are currently marked as warnings (`warn`) to allow for gradual refactoring without blocking development. **Do not write new code that triggers these rules!**
- The linter is configured to ignore specific files and folders (such as excessively large files or ones in need of refactoring) to improve performance and focus on actionable areas.
> [!IMPORTANT]
> Certain lint rules may be marked as `info` or `warn` to allow for gradual refactoring without blocking development.
> **Do not write new code that triggers these rules!**
Any questions about linting rules can be brought up in the `#dev-corner` channel in the community Discord.
[^1]: A complete list of rules can be found in the [`biome.jsonc`](../biome.jsonc) file in the project root. Many rules are accompanied by comments explaining the reasons for their inclusion (or lack thereof).

View File

@ -28,7 +28,7 @@ This repository is integrated into the main one as a [git submodule](https://git
In essence, a submodule is a way for one repository (i.e. `pokerogue`) to use another repository (i.e. `pokerogue-locales`) internally.
The parent repo (the "superproject") houses a cloned version of the 2nd repository (the "submodule") inside it, making locales effectively a "repository within a repository", so to speak.
>[!TIP]
> [!TIP]
> Many popular IDEs have integrated `git` support with special handling around submodules:
>
> ![Image showing Visual Studio Code's `git` integration in the File Explorer. A blue "S" in the top right hand corner indicates the `locales` folder is a submodule.](https://github.com/user-attachments/assets/00674c3f-72ee-42f7-8b09-4008d466b263 "What the `locales` submodule looks like in VS Code's File Explorer")
@ -97,17 +97,23 @@ 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. 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 locales 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.
3. At this point, you may begin [testing locales integration in your main PR](#documenting-locales-changes).
4. The Translation Team will approve the locales 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.
[^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.
> [!IMPORTANT]
> The Dev and Translation teams have strict requirements for ensuring consistency of newly added locales entries.
> PRs failing these requirements **will not be mergeable into `locales`**!
> - File names should be in `kebab-case`. Example: `trainer-names.json`
> - Key names should be in `camelCase`. Example: `aceTrainer`
> - Keys making use of i18next's inbuilt [context support](https://www.i18next.com/translation-function/context) must use `snake_case` for the context extension[^3]. Example: `aceTrainer_male`
[^3]: If your PR introduces a new context extension not already used in the codebase, the validation workflow will be unable to detect it and flag it as invalid. \
To fix this, update the [`i18nextKeyExtensions`](https://github.com/pagefaultgames/pokerogue-locales/blob/main/.github/scripts/locales-format-checker/constants.js#L30) array with the new entries.
### Requirements for Modifying Translated Text
PRs that modify existing text have different risks with respect to coordination between development and translation, so their requirements are slightly different:
@ -130,9 +136,9 @@ The basic procedure is roughly as follows:
```
2. Set some of the [in-game overrides](../CONTRIBUTING.md#1---manual-testing) inside `overrides.ts` to values corresponding to the interactions being tested.
3. Start a local dev server (`pnpm start:dev`) and open localhost in your browser.
4. Take screenshots or record a video of the locales changes being displayed in-game using the software of your choice[^2].
4. Take screenshots or record a video of the locales changes being displayed in-game using the software of your choice[^3].
[^2]: For those lacking a screen capture device, [OBS Studio](https://obsproject.com) is a popular open-source option.
[^3]: For those lacking a screen capture device, [OBS Studio](https://obsproject.com) is a popular open-source option.
> [!NOTE]
> For those aiming to film their changes, bear in mind that GitHub has a hard **10mB limit** on uploaded media content.

View File

@ -8,9 +8,9 @@ pre-commit:
- rebase
commands:
biome-lint:
# Disable colors as certain IDEs don't support it in the output pane.
# Disable colors as certain IDEs (such as VSCode) don't support it in the output pane.
# Summary mode looks decent in plain ASCII anyhow
run: pnpm exec biome check --write --colors=off --reporter=summary --staged --no-errors-on-unmatched --diagnostic-level=error
run: pnpm biome:staged --colors=off --reporter=summary
stage_fixed: true
ls-lint:
run: pnpm exec ls-lint
@ -18,4 +18,13 @@ pre-commit:
post-merge:
commands:
update-submodules:
run: pnpm update-submodules
run: pnpm update-submodules
update-packages:
run: pnpm i
post-checkout:
commands:
update-packages:
# cf https://git-scm.com/docs/githooks#_post_checkout:
# The 3rd argument is 1 for branch checkouts and 0 for file checkouts.
run: if test {3} -eq "1"; then pnpm i; fi

@ -1 +1 @@
Subproject commit b5b0d94eee7cbcf0e055f8074ca1ebedb920e59e
Subproject commit 6b5e2130256dd521908f15a485d045fb36baca41

View File

@ -1,7 +1,7 @@
{
"name": "pokemon-rogue-battle",
"private": true,
"version": "1.11.3",
"version": "1.11.4",
"type": "module",
"scripts": {
"start:prod": "vite --mode production",
@ -17,17 +17,19 @@
"test:cov": "vitest run --coverage --no-isolate",
"test:watch": "vitest watch --coverage --no-isolate",
"test:silent": "vitest run --silent='passed-only' --no-isolate",
"test:merge-reports": "MERGE_REPORTS=1 vitest run --merge-reports=test-results --silent=passed-only --configLoader runner",
"test:create": "node scripts/create-test/create-test.js",
"eggMoves:parse": "node scripts/parse-egg-moves/main.js",
"scrape-trainers": "node scripts/scrape-trainer-names/main.js",
"typecheck": "tsc --noEmit",
"typecheck:scripts": "tsc -p scripts/jsconfig.json",
"biome": "biome check --write --changed --no-errors-on-unmatched --diagnostic-level=error",
"biome:staged": "biome check --write --staged --no-errors-on-unmatched --diagnostic-level=error",
"biome:all": "biome check --write --no-errors-on-unmatched --diagnostic-level=error",
"biome-ci": "biome ci --diagnostic-level=error --reporter=github --no-errors-on-unmatched",
"typedoc": "typedoc",
"depcruise": "depcruise src test",
"postinstall": "lefthook install; git config --local fetch.recurseSubmodules true",
"postinstall": "lefthook install; git config --local fetch.recurseSubmodules true; git config --local blame.ignoreRevsFile .git-blame-ignore-revs",
"update-version:patch": "pnpm version patch --force --no-git-tag-version",
"update-version:minor": "pnpm version minor --force --no-git-tag-version",
"update-locales": "git submodule update --progress --init --recursive --depth 1 locales",
@ -38,47 +40,48 @@
"update-submodules:remote": "pnpm update-locales:remote && pnpm update-assets:remote"
},
"devDependencies": {
"@biomejs/biome": "2.2.5",
"@biomejs/biome": "2.3.8",
"@inquirer/prompts": "^8.0.2",
"@ls-lint/ls-lint": "2.3.1",
"@types/crypto-js": "^4.2.0",
"@types/crypto-js": "^4.2.2",
"@types/jsdom": "^27.0.0",
"@types/node": "^24",
"@vitest/coverage-istanbul": "^3.2.4",
"@vitest/expect": "^3.2.4",
"@vitest/utils": "^3.2.4",
"chalk": "^5.4.1",
"dependency-cruiser": "^17.0.2",
"inquirer": "^12.8.2",
"jsdom": "^27.0.0",
"lefthook": "^1.12.2",
"msw": "^2.10.4",
"@types/node": "^24.10.2",
"@vitest/coverage-istanbul": "^4.0.15",
"@vitest/expect": "^4.0.15",
"@vitest/utils": "^4.0.15",
"chalk": "^5.6.2",
"dependency-cruiser": "^17.3.2",
"jsdom": "^27.3.0",
"lefthook": "^2.0.9",
"msw": "^2.12.4",
"phaser3spectorjs": "^0.0.8",
"typedoc": "^0.28.13",
"type-fest": "^5.3.1",
"typedoc": "^0.28.15",
"typedoc-github-theme": "^0.3.1",
"typedoc-plugin-coverage": "^4.0.1",
"typedoc-plugin-mdn-links": "^5.0.9",
"typescript": "^5.9.2",
"vite": "^7.0.7",
"typedoc-plugin-coverage": "^4.0.2",
"typedoc-plugin-mdn-links": "^5.0.10",
"typescript": "^5.9.3",
"vite": "^7.2.7",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.2.4",
"vitest-canvas-mock": "^0.3.3"
"vitest": "^4.0.15",
"vitest-canvas-mock": "^1.1.3"
},
"dependencies": {
"@material/material-color-utilities": "^0.3.0",
"compare-versions": "^6.1.1",
"core-js": "^3.46.0",
"core-js": "^3.47.0",
"crypto-js": "^4.2.0",
"i18next": "^25.5.3",
"i18next": "^25.7.2",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"i18next-korean-postposition-processor": "^1.0.0",
"json-stable-stringify": "^1.3.0",
"jszip": "^3.10.1",
"phaser": "^3.90.0",
"phaser3-rex-plugins": "^1.80.16"
"phaser3-rex-plugins": "^1.80.17"
},
"engines": {
"node": ">=24.9.0"
},
"packageManager": "pnpm@10.19.0"
"packageManager": "pnpm@10.24.0"
}

File diff suppressed because it is too large Load Diff

View File

@ -5,3 +5,5 @@ onlyBuiltDependencies:
- msw
shellEmulator: true
minimumReleaseAge: 1440

View File

@ -0,0 +1,80 @@
/*
* SPDX-FileCopyrightText: 2024-2025 Pagefault Games
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import chalk from "chalk";
import { cliAliases, validTestTypes } from "./constants.js";
import { promptFileName, promptTestType } from "./interactive.js";
/**
* @import {testType} from "./constants.js"
*/
/**
* Parse `process.argv` to retrieve the test type if it exists, otherwise prompting input from the user.
* @param {string | undefined} arg - The argument from `process.argv`
* @returns {Promise<testType | undefined>}
* A Promise that resolves with the type of test to be created, or `undefined` if the user interactively selects "Exit".
* Will set `process.exitCode` to a non-zero integer if args are invalid.
*/
export async function getTestType(arg) {
if (arg == null) {
return await promptTestType();
}
const testType = getCliTestType(arg);
if (testType) {
console.log(chalk.blue(`Using ${testType} as test type from CLI...`));
return testType;
}
console.error(
chalk.red.bold(
`✗ Invalid type of test file specified: ${arg}!\nValid types: ${chalk.blue(validTestTypes.join(", "))}`,
),
);
process.exitCode = 1;
return;
}
/**
* Parse a test type from command-line args.
* @param {string} arg
* @returns {testType | undefined} The resulting test type.
* Will return `undefined` if no valid match was found.
*/
function getCliTestType(arg) {
// Check for a direct match, falling back to alias checking if none work
const testTypeName = validTestTypes.find(c => c.toLowerCase() === arg.toLowerCase());
if (testTypeName) {
return testTypeName;
}
const alias = /** @type {(keyof typeof cliAliases)[]} */ (Object.keys(cliAliases)).find(aliasKey =>
cliAliases[aliasKey].some(alias => alias.toLowerCase() === arg.toLowerCase()),
);
return alias;
}
/**
* Obtain the file name for a given file
* @param {testType} testType - The chosen test type
* @param {string | undefined} arg - The contents of `process.argv[3]`, if it exists
* @returns {Promise<string | undefined>} A promise that resolves with the name of the file to create.
*/
export async function getFileName(testType, arg) {
if (arg == null) {
return await promptFileName(testType);
}
const nameTrimmed = arg.trim().replace(".test.ts", "");
if (nameTrimmed.length === 0) {
console.error(chalk.red.bold("✗ Cannot use an empty string as a file name!"));
process.exitCode = 1;
return;
}
console.log(chalk.blue(`Using ${nameTrimmed} as file name from CLI...`));
return nameTrimmed;
}

View File

@ -0,0 +1,47 @@
/*
* SPDX-FileCopyrightText: 2024-2025 Pagefault Games
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
/**
* Array containing all valid options for the type of test file to create.
* @package
*/
export const validTestTypes = /** @type {const} */ ([
"Move",
"Ability",
"Item",
"Reward",
"Mystery Encounter",
"Utils",
"UI",
]);
/**
* @typedef {typeof validTestTypes[number]}
* testType
* Union type representing a single valid choice of test type.
*/
/**
* Const object mapping each test type to any additional names they can be used with from CLI.
* @satisfies {Partial<Record<testType, readonly string[]>>}
*/
export const cliAliases = /** @type {const} */ ({
"Mystery Encounter": ["ME"],
});
/**
* Const object matching all test types to the directories in which their tests reside.
* @satisfies {Record<testType, string>}
*/
export const testTypesToDirs = /** @type {const} */ ({
Move: "moves",
Ability: "abilities",
Item: "items",
Reward: "rewards",
"Mystery Encounter": "mystery-encounter/encounters",
Utils: "utils",
UI: "ui",
});

View File

@ -11,146 +11,25 @@
*/
import fs from "node:fs";
import path, { join } from "node:path";
import { join } from "node:path";
import chalk from "chalk";
import inquirer from "inquirer";
import { writeFileSafe } from "../helpers/file.js";
import { toKebabCase, toTitleCase } from "../helpers/strings.js";
import { getFileName, getTestType } from "./cli.js";
import { getBoilerplatePath, getTestFileFullPath } from "./dirs.js";
import { HELP_FLAGS, showHelpText } from "./help-message.js";
/**
* @import {testType} from "./constants.js"
*/
//#region Constants
const version = "2.0.2";
// Get the directory name of the current module file
const version = "2.1.0";
const __dirname = import.meta.dirname;
const projectRoot = path.join(__dirname, "..", "..");
const choices = /** @type {const} */ (["Move", "Ability", "Item", "Reward", "Mystery Encounter", "Utils", "UI"]);
/** @typedef {typeof choices[number]} choiceType */
/**
* Object mapping choice types to extra names they can be used with from CLI.
* @satisfies {Partial<Record<choiceType, readonly string[]>>}
*/
const cliAliases = {
"Mystery Encounter": ["ME"],
};
/** @satisfies {{[k in choiceType]: string}} */
const choicesToDirs = /** @type {const} */ ({
Move: "moves",
Ability: "abilities",
Item: "items",
Reward: "rewards",
"Mystery Encounter": "mystery-encounter/encounters",
Utils: "utils",
UI: "ui",
});
const projectRoot = join(__dirname, "..", "..");
//#endregion
//#region Functions
/**
* Get the path to a given folder in the test directory
* @param {...string} folders the subfolders to append to the base path
* @returns {string} the path to the requested folder
*/
function getTestFolderPath(...folders) {
return path.join(projectRoot, "test", ...folders);
}
/**
* Prompts the user to select a type via list.
* @returns {Promise<choiceType>} the selected type
*/
async function promptTestType() {
/** @type {choiceType | "EXIT"} */
const choice = (
await inquirer.prompt([
{
type: "list",
name: "selectedOption",
message: "What type of test would you like to create?",
choices: [...choices, "EXIT"],
},
])
).selectedOption;
if (choice === "EXIT") {
console.log("Exiting...");
return process.exit(0);
}
return choice;
}
/**
* Prompts the user to provide a file name.
* @param {choiceType} selectedType The chosen string (used to display console logs)
* @returns {Promise<string>} the selected file name
*/
async function promptFileName(selectedType) {
/** @type {string} */
const fileNameAnswer = (
await inquirer.prompt([
{
type: "input",
name: "userInput",
message: `Please provide the name of the ${selectedType}.`,
},
])
).userInput;
if (fileNameAnswer.trim().length === 0) {
console.error("Please provide a valid file name!");
return await promptFileName(selectedType);
}
return fileNameAnswer;
}
/**
* Obtain the path to the boilerplate file based on the current option.
* @param {choiceType} choiceType The choice selected
* @returns {string} The path to the boilerplate file
*/
function getBoilerplatePath(choiceType) {
switch (choiceType) {
// case "Reward":
// return path.join(__dirname, "boilerplates/reward.boilerplate.ts");
default:
return path.join(__dirname, "boilerplates/default.boilerplate.ts");
}
}
/**
* Parse `process.argv` and get the test type if it exists.
* @returns {choiceType | undefined}
* The type of choice the CLI args corresponds to, or `undefined` if none were specified.
* Will set `process.exitCode` to a non-zero integer if args are invalid.
*/
function convertArgsToTestType() {
// If the first argument is a test name, use that as the test name
const args = process.argv.slice(2);
if (args[0] == null) {
return;
}
// Check for a direct match, falling back to alias checking.
const choiceName = choices.find(c => c.toLowerCase() === args[0].toLowerCase());
if (choiceName) {
return choiceName;
}
const alias = /** @type {(keyof cliAliases)[]} */ (Object.keys(cliAliases)).find(k =>
cliAliases[k].some(a => a.toLowerCase() === args[0].toLowerCase()),
);
if (alias) {
return alias;
}
console.error(
chalk.red.bold(`✗ Invalid type of test file specified: ${args[0]}!
Valid types: ${chalk.blue(choices.join(", "))}`),
);
process.exitCode = 1;
return;
}
//#region Main
/**
* Run the interactive `test:create` CLI.
@ -159,65 +38,49 @@ Valid types: ${chalk.blue(choices.join(", "))}`),
async function runInteractive() {
console.group(chalk.grey(`🧪 Create Test - v${version}\n`));
const cliTestType = convertArgsToTestType();
if (process.exitCode) {
const args = process.argv.slice(2);
if (HELP_FLAGS.some(h => args.includes(h))) {
return showHelpText();
}
const testType = await getTestType(args[0]);
if (process.exitCode || !testType) {
return;
}
// TODO: Add a help command
const fileNameAnswer = await getFileName(testType, args[1]);
if (process.exitCode || !fileNameAnswer) {
return;
}
try {
let choice;
if (cliTestType) {
console.log(chalk.blue(`Using ${cliTestType} as test type from CLI...`));
choice = cliTestType;
} else {
choice = await promptTestType();
}
const fileNameAnswer = await promptFileName(choice);
// Convert fileName from snake_case or camelCase to kebab-case
const fileName = fileNameAnswer
.replace(/_+/g, "-") // Convert snake_case (underscore) to kebab-case (dashes)
.replace(/([a-z])([A-Z])/g, "$1-$2") // Convert camelCase to kebab-case
.replace(/\s+/g, "-") // Replace spaces with dashes
.toLowerCase(); // Ensure all lowercase
// Format the description for the test case in Title Case
const formattedName = fileName.replace(/-/g, " ").replace(/\b\w/g, char => char.toUpperCase());
const description = `${choice} - ${formattedName}`;
// Determine the directory based on the type
const localDir = choicesToDirs[choice];
const absoluteDir = getTestFolderPath(localDir);
// Define the content template
const content = fs.readFileSync(getBoilerplatePath(choice), "utf8").replace("{{description}}", description);
// Ensure the directory exists
if (!fs.existsSync(absoluteDir)) {
fs.mkdirSync(absoluteDir, { recursive: true });
}
// Create the file with the given name
const filePath = path.join(absoluteDir, `${fileName}.test.ts`);
if (fs.existsSync(filePath)) {
console.error(chalk.red.bold(`✗ File "${fileName}.test.ts" already exists!\n`));
process.exit(1);
}
// Write the template content to the file
fs.writeFileSync(filePath, content, "utf8");
console.log(chalk.green.bold(`✔ File created at: ${join("test", localDir, fileName)}.test.ts\n`));
console.groupEnd();
doCreateFile(testType, fileNameAnswer);
} catch (err) {
console.error(chalk.red("✗ Error: ", err));
}
console.groupEnd();
}
/**
* Helper function to create the test file.
* @param {testType} testType - The type of test to create
* @param {string} fileNameAnswer - The name of the file to create
* @returns {void}
*/
function doCreateFile(testType, fileNameAnswer) {
// Convert file name to kebab-case, formatting the description in Title Case
const fileName = toKebabCase(fileNameAnswer);
const formattedName = toTitleCase(fileNameAnswer);
const description = `${testType} - ${formattedName}`;
const content = fs.readFileSync(getBoilerplatePath(testType), "utf8").replace("{{description}}", description);
const filePath = getTestFileFullPath(testType, fileName);
writeFileSafe(filePath, content, "utf8");
console.log(chalk.green.bold(`✔ File created at: ${filePath.replace(`${projectRoot}/`, "")}\n`));
}
//#endregion
//#region Run
runInteractive();
//#endregion
await runInteractive();

View File

@ -0,0 +1,56 @@
/*
* SPDX-FileCopyrightText: 2024-2025 Pagefault Games
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { join } from "path";
import { testTypesToDirs } from "./constants.js";
/**
* @import { testType } from "./constants.js";
*/
// Get the directory name of the current module file
const __dirname = import.meta.dirname;
const projectRoot = join(__dirname, "..", "..");
/**
* Const object matching all {@linkcode testType}s to any custom boilerplate files
* they may be associated with.
* @type {Readonly<Partial<Record<testType, string>>>}
*/
const customBoilerplates = {
// Reward: "boilerplates/reward.boilerplate.ts", // Todo: Boilerplate is added in the modifier rework
};
const DEFAULT_BOILERPLATE_PATH = "boilerplates/default.boilerplate.ts";
/**
* Retrieve the path to the boilerplate file used for the given test type.
* @param {testType} testType - The type of test file to create
* @returns {string} The path to the boilerplate file.
*/
export function getBoilerplatePath(testType) {
return join(import.meta.dirname, customBoilerplates[testType] ?? DEFAULT_BOILERPLATE_PATH);
}
/**
* Get the path to a given folder in the test directory
* @param {...string} folders the subfolders to append to the base path
* @returns {string} the path to the requested folder
*/
function getTestFolderPath(...folders) {
return join(projectRoot, "test", ...folders);
}
/**
* Helper function to convert the test file name into an absolute path.
* @param {testType} testType - The type of test being created (used to look up folder)
* @param {string} fileName - The name of the test file (without suffix)
* @returns {string}
*/
export function getTestFileFullPath(testType, fileName) {
const absoluteDir = getTestFolderPath(testTypesToDirs[testType]);
return join(absoluteDir, `${fileName}.test.ts`);
}

View File

@ -0,0 +1,32 @@
/*
* SPDX-FileCopyrightText: 2025 Pagefault Games
* SPDX-FileContributor: Bertie690
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import chalk from "chalk";
/**
* Array containing all valid ways of showing the help message.
*/
export const HELP_FLAGS = /** @type {const} */ (["-h", "-help", "--help"]);
/**
* Show help/usage text for the `test:create` CLI.
* @package
*/
export function showHelpText() {
console.log(`
Usage: ${chalk.cyan("pnpm test:create [options] [testType] [fileName]")}
If either ${chalk.hex("#7fff00")("testType")} or ${chalk.hex("#7fff00")("fileName")} are omitted,
they will be selected interactively.
${chalk.hex("#8a2be2")("Arguments:")}
${chalk.hex("#7fff00")("testType")} The type/category of test file to create.
${chalk.hex("#7fff00")("fileName")} The name of the test file to create.
${chalk.hex("#ffa500")("Options:")}
${chalk.blue("-h, -help, --help")} Show this help message.
`);
}

View File

@ -0,0 +1,54 @@
/*
* SPDX-FileCopyrightText: 2024-2025 Pagefault Games
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { input, select } from "@inquirer/prompts";
import chalk from "chalk";
import { validTestTypes } from "./constants.js";
/**
* @import {testType} from "./constants.js"
*/
/**
* Prompt the user to select a test type via list.
* @returns {Promise<testType | undefined>} The selected type, or `undefined` if "Exit" was pressed.
*/
export async function promptTestType() {
/** @type {testType | "EXIT"} */
const choice = await select({
message: "What type of test would you like to create?",
choices: [...validTestTypes, "EXIT"],
});
if (choice === "EXIT") {
console.log("Exiting...");
process.exitCode = 0;
return;
}
return choice;
}
/**
* Prompt the user to provide a file name.
* @param {testType} selectedType - The chosen string (used for logging & validation)
* @returns {Promise<string>} The selected file name
*/
export async function promptFileName(selectedType) {
/** @type {string} */
const fileNameAnswer = await input({
message: `Please provide the name of the ${selectedType}.`,
validate: name => {
const nameProcessed = name.trim().replace(".test.ts", "");
if (nameProcessed.length === 0) {
return chalk.red.bold("✗ Cannot use an empty string as a file name!");
}
return true;
},
});
// Trim whitespace and any extension suffixes
return fileNameAnswer.trim().replace(".test.ts", "");
}

36
scripts/helpers/file.js Normal file
View File

@ -0,0 +1,36 @@
/*
* SPDX-FileCopyrightText: 2025 Pagefault Games
* SPDX-FileContributor: Bertie690
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { dirname } from "node:path";
/**
* @import {PathOrFileDescriptor, WriteFileOptions} from "node:fs"
*/
/**
* "Safely" write to a file, creating any parent directories as required.
* @param {PathOrFileDescriptor} file - The filename or file descriptor to open
* @param {string | NodeJS.ArrayBufferView<ArrayBufferLike>} content - The content which will be written
* @param {WriteFileOptions} [options]
* @returns {void}
* @remarks
* If `file` is a file descriptor, this method will simply return the result of
* {@linkcode writeFileSync} verbatim.
*/
export function writeFileSafe(file, content, options) {
if (typeof file === "number") {
return writeFileSync(file, content, options);
}
const parentDir = dirname(file.toString("utf-8"));
if (!existsSync(parentDir)) {
mkdirSync(parentDir, { recursive: true });
}
writeFileSync(file, content, options);
}

View File

@ -1,3 +1,9 @@
/*
* SPDX-FileCopyrightText: 2024-2025 Pagefault Games
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
//! DO NOT EDIT THIS FILE - CREATED BY THE `eggMoves:parse` script automatically
import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id";

View File

@ -1,4 +0,0 @@
SPDX-FileCopyrightText: 2025 Pagefault Games
SPDX-FileContributor: Bertie690
SPDX-License-Identifier: AGPL-3.0-only

View File

@ -5,8 +5,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import fs from "fs";
import { input, select } from "@inquirer/prompts";
import chalk from "chalk";
import inquirer from "inquirer";
import { showHelpText } from "./help-message.js";
/**
@ -19,20 +19,14 @@ import { showHelpText } from "./help-message.js";
*/
export async function runInteractive() {
/** @type {"Console" | "File" | "Help" | "Exit"} */
const answer = await inquirer
.prompt([
{
type: "list",
name: "type",
message: "Select the method to obtain egg moves.",
choices: ["Console", "File", "Help", "Exit"],
},
])
.then(a => a.type);
const answer = await select({
message: "Select the method to obtain egg moves.",
choices: ["Console", "File", "Help", "Exit"],
});
if (answer === "Exit") {
console.log("Exiting...");
process.exitCode = 1;
process.exitCode = 0;
return { type: "Exit" };
}
@ -68,24 +62,18 @@ function promptForValue(type) {
* @returns {Promise<string>} The file path inputted by the user.
*/
async function getFilePath() {
return await inquirer
.prompt([
{
type: "input",
name: "path",
message: "Please enter the path to the egg move CSV file.",
validate: input => {
if (input.trim() === "") {
return "File path cannot be empty!";
}
if (!fs.existsSync(input)) {
return "File does not exist!";
}
return true;
},
},
])
.then(answer => answer.path);
return await input({
message: "Please enter the path to the egg move CSV file.",
validate: filePath => {
if (filePath.trim() === "") {
return "File path cannot be empty!";
}
if (!fs.existsSync(filePath)) {
return "File does not exist!";
}
return true;
},
});
}
/**
@ -93,22 +81,16 @@ async function getFilePath() {
* @returns {Promise<string>} The CSV input from the user.
*/
async function doPromptConsole() {
return await inquirer
.prompt([
{
type: "input",
name: "csv",
message: "Please enter the egg move CSV text.",
validate: input => {
if (input.trim() === "") {
return "CSV text cannot be empty!";
}
if (!input.match(/^[^,]+(,[^,]+){4}$/gm)) {
return "CSV text malformed - should contain 5 consecutive comma-separated values per line!";
}
return true;
},
},
])
.then(answer => answer.csv);
return await input({
message: "Please enter the egg move CSV text.",
validate: value => {
if (value.trim() === "") {
return "CSV text cannot be empty!";
}
if (!value.match(/^[^,]+(,[^,]+){4}$/gm)) {
return "CSV text malformed - should contain 5 consecutive comma-separated values per line!";
}
return true;
},
});
}

View File

@ -18,13 +18,13 @@ import { showHelpText } from "./help-message.js";
import { runInteractive } from "./interactive.js";
import { parseEggMoves } from "./parse.js";
const version = "1.0.0";
const version = "1.0.1";
// Get the directory name of the current module file
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.join(__dirname, "..", "..");
const templatePath = path.join(__dirname, "egg-move-template.ts");
const templatePath = path.join(__dirname, "egg-move-template.boilerplate.ts");
// TODO: Do we want this to be configurable?
const eggMoveTargetPath = path.join(projectRoot, "src/data/balance/egg-moves.ts");
@ -54,8 +54,8 @@ async function start() {
let csv = "";
const inputType = await parseArguments();
if (process.exitCode) {
// If exit code is non-zero, return to allow it to propagate up the chain.
// If exit code was set, return to allow it to propagate it up the chain.
if (process.exitCode != null) {
return;
}
switch (inputType.type) {
@ -66,7 +66,6 @@ async function start() {
csv = await fs.promises.readFile(inputType.value, "utf-8");
break;
case "Exit":
// Help screen triggered; break out
return;
}
@ -80,8 +79,10 @@ async function start() {
async function parseArguments() {
const args = process.argv.slice(2); // first 2 args are node and script name (irrelevant)
// Yoink everything up to the first "=" to get the raw command, using nullish coaclescing to convert
// "no args" into "undefined"
/** @type {string | undefined} */
const arg = args[0].split("=")[0]; // Yoink everything up to the first "=" to get the raw command
const arg = args[0]?.split("=")[0];
switch (arg) {
case "-f":
case "--file":
@ -170,4 +171,4 @@ function badArgs() {
process.exitCode = 1;
}
start();
await start();

View File

@ -7,8 +7,8 @@
import { existsSync, writeFileSync } from "node:fs";
import { format, inspect } from "node:util";
import { confirm } from "@inquirer/prompts";
import chalk from "chalk";
import inquirer from "inquirer";
import { JSDOM } from "jsdom";
import { toCamelCase, toPascalSnakeCase, toTitleCase } from "../helpers/strings.js";
import { checkGenderAndType } from "./check-gender.js";
@ -285,16 +285,10 @@ async function tryWriteFile(outFile, output) {
* @returns {Promise<boolean>} Whether "Yes" or "No" was selected.
*/
async function promptExisting(outFile) {
return (
await inquirer.prompt([
{
type: "confirm",
name: "continue",
message: `File ${chalk.blue(outFile)} already exists!\nDo you want to replace it?`,
default: false,
},
])
).continue;
return await confirm({
message: `File ${chalk.blue(outFile)} already exists!\nDo you want to replace it?`,
default: false,
});
}
main();
await main();

View File

@ -1,4 +1,5 @@
import type { AbAttrConstructorMap } from "#abilities/ability";
import type { applyAbAttrs } from "#abilities/apply-ab-attrs";
import type { BattleStat } from "#enums/stat";
import type { Pokemon } from "#field/pokemon";
import type { Move } from "#moves/move";
@ -6,11 +7,8 @@ import type { Move } from "#moves/move";
// intentionally re-export all types from abilities to have this be the centralized place to import ability types
export type * from "#abilities/ability";
// biome-ignore lint/correctness/noUnusedImports: Used in a tsdoc comment
import type { applyAbAttrs } from "#abilities/apply-ab-attrs";
export type AbAttrCondition = (pokemon: Pokemon) => boolean;
export type PokemonAttackCondition = (user: Pokemon | null, target: Pokemon | null, move: Move) => boolean;
export type PokemonAttackCondition = (user: Pokemon, target: Pokemon | null, move: Move) => boolean;
export type PokemonDefendCondition = (target: Pokemon, user: Pokemon, move: Move) => boolean;
export type PokemonStatStageChangeCondition = (
target: Pokemon,

147
src/@types/api.ts Normal file
View File

@ -0,0 +1,147 @@
import type { SessionSaveData, SystemSaveData } from "#types/save-data";
export interface UserInfo {
username: string;
lastSessionSlot: number;
discordId: string;
googleId: string;
hasAdminRole: boolean;
}
export interface TitleStatsResponse {
playerCount: number;
battleCount: number;
}
// #region Account API
export interface AccountInfoResponse extends UserInfo {}
export interface AccountLoginRequest {
username: string;
password: string;
}
export interface AccountLoginResponse {
token: string;
}
export interface AccountRegisterRequest {
username: string;
password: string;
}
export interface AccountChangePwRequest {
password: string;
}
export interface AccountChangePwResponse {
success: boolean;
}
// #endregion
// #region Admin API
export interface SearchAccountRequest {
username: string;
}
export interface DiscordRequest extends SearchAccountRequest {
discordId: string;
}
export interface GoogleRequest extends SearchAccountRequest {
googleId: string;
}
export interface SearchAccountResponse {
username: string;
discordId: string;
googleId: string;
lastLoggedIn: string;
registered: string;
systemData?: SystemSaveData;
}
/** Third party login services */
export type AdminUiHandlerService = "discord" | "google";
/** Mode for the admin UI handler */
export type AdminUiHandlerServiceMode = "Link" | "Unlink";
export interface PokerogueAdminApiParams extends Record<AdminUiHandlerService, SearchAccountRequest> {
discord: DiscordRequest;
google: GoogleRequest;
}
// #endregion
export interface UpdateAllSavedataRequest {
system: SystemSaveData;
session: SessionSaveData;
sessionSlotId: number;
clientSessionId: string;
}
// #region Session Save API
export interface UpdateSessionSavedataRequest {
slot: number;
trainerId: number;
secretId: number;
clientSessionId: string;
}
/** This is **NOT** related to {@linkcode ClearSessionSavedataRequest} */
export interface NewClearSessionSavedataRequest {
slot: number;
isVictory: boolean;
clientSessionId: string;
}
export interface GetSessionSavedataRequest {
slot: number;
clientSessionId: string;
}
export interface DeleteSessionSavedataRequest {
slot: number;
clientSessionId: string;
}
/** This is **NOT** related to {@linkcode NewClearSessionSavedataRequest} */
export interface ClearSessionSavedataRequest {
slot: number;
trainerId: number;
clientSessionId: string;
}
/** Pokerogue API response for path: `/savedata/session/clear` */
export interface ClearSessionSavedataResponse {
/** Contains the error message if any occured */
error?: string;
/** Is `true` if the request was successfully processed */
success?: boolean;
}
// #endregion
// #region System Save API
export interface GetSystemSavedataRequest {
clientSessionId: string;
}
export interface UpdateSystemSavedataRequest {
clientSessionId: string;
trainerId?: number;
secretId?: number;
}
export interface VerifySystemSavedataRequest {
clientSessionId: string;
}
export interface VerifySystemSavedataResponse {
valid: boolean;
systemData: SystemSaveData;
}
// #endregion

View File

@ -1,24 +0,0 @@
import type { UserInfo } from "#types/user-info";
export interface AccountInfoResponse extends UserInfo {}
export interface AccountLoginRequest {
username: string;
password: string;
}
export interface AccountLoginResponse {
token: string;
}
export interface AccountRegisterRequest {
username: string;
password: string;
}
export interface AccountChangePwRequest {
password: string;
}
export interface AccountChangePwResponse {
success: boolean;
}

View File

@ -1,32 +0,0 @@
import type { SystemSaveData } from "#types/save-data";
export interface SearchAccountRequest {
username: string;
}
export interface DiscordRequest extends SearchAccountRequest {
discordId: string;
}
export interface GoogleRequest extends SearchAccountRequest {
googleId: string;
}
export interface SearchAccountResponse {
username: string;
discordId: string;
googleId: string;
lastLoggedIn: string;
registered: string;
systemData?: SystemSaveData;
}
/** Third party login services */
export type AdminUiHandlerService = "discord" | "google";
/** Mode for the admin UI handler */
export type AdminUiHandlerServiceMode = "Link" | "Unlink";
export interface PokerogueAdminApiParams extends Record<AdminUiHandlerService, SearchAccountRequest> {
discord: DiscordRequest;
google: GoogleRequest;
}

View File

@ -1,4 +0,0 @@
export interface TitleStatsResponse {
playerCount: number;
battleCount: number;
}

View File

@ -1,8 +0,0 @@
import type { SessionSaveData, SystemSaveData } from "#types/save-data";
export interface UpdateAllSavedataRequest {
system: SystemSaveData;
session: SessionSaveData;
sessionSlotId: number;
clientSessionId: string;
}

View File

@ -1,40 +0,0 @@
export interface UpdateSessionSavedataRequest {
slot: number;
trainerId: number;
secretId: number;
clientSessionId: string;
}
/** This is **NOT** similar to {@linkcode ClearSessionSavedataRequest} */
export interface NewClearSessionSavedataRequest {
slot: number;
isVictory: boolean;
clientSessionId: string;
}
export interface GetSessionSavedataRequest {
slot: number;
clientSessionId: string;
}
export interface DeleteSessionSavedataRequest {
slot: number;
clientSessionId: string;
}
/** This is **NOT** similar to {@linkcode NewClearSessionSavedataRequest} */
export interface ClearSessionSavedataRequest {
slot: number;
trainerId: number;
clientSessionId: string;
}
/**
* Pokerogue API response for path: `/savedata/session/clear`
*/
export interface ClearSessionSavedataResponse {
/** Contains the error message if any occured */
error?: string;
/** Is `true` if the request was successfully processed */
success?: boolean;
}

View File

@ -1,20 +0,0 @@
import type { SystemSaveData } from "#types/save-data";
export interface GetSystemSavedataRequest {
clientSessionId: string;
}
export interface UpdateSystemSavedataRequest {
clientSessionId: string;
trainerId?: number;
secretId?: number;
}
export interface VerifySystemSavedataRequest {
clientSessionId: string;
}
export interface VerifySystemSavedataResponse {
valid: boolean;
systemData: SystemSaveData;
}

View File

@ -1,6 +1,5 @@
import type { ArenaTagTypeMap } from "#data/arena-tag";
import type { ArenaTagType } from "#enums/arena-tag-type";
// biome-ignore lint/correctness/noUnusedImports: TSDocs
import type { SessionSaveData } from "#types/save-data";
import type { ObjectValues } from "#types/type-helpers";

View File

@ -1,10 +1,7 @@
// biome-ignore-start lint/correctness/noUnusedImports: Used in a TSDoc comment
import type { AbilityBattlerTag, BattlerTagTypeMap, SerializableBattlerTag, TypeBoostTag } from "#data/battler-tags";
import type { AbilityId } from "#enums/ability-id";
import type { SessionSaveData } from "#types/save-data";
// biome-ignore-end lint/correctness/noUnusedImports: Used in a TSDoc comment
import type { BattlerTagType } from "#enums/battler-tag-type";
import type { SessionSaveData } from "#types/save-data";
import type { InferKeys, ObjectValues } from "#types/type-helpers";
/**

27
src/@types/phaser.d.ts vendored Normal file
View File

@ -0,0 +1,27 @@
import "phaser";
declare module "phaser" {
namespace Math {
interface RandomDataGenerator {
pick<T>(array: ArrayLike<T>): T;
weightedPick<T>(array: ArrayLike<T>): T;
}
}
namespace Input {
namespace Gamepad {
interface GamepadPlugin {
/**
* Refreshes the list of connected Gamepads.
* This is called automatically when a gamepad is connected or disconnected, and during the update loop.
*/
refreshPads(): void;
}
}
}
interface Game {
/** A manifest used to cache various files requested from the server. */
manifest?: Record<string, string>;
}
}

View File

@ -1,10 +1,12 @@
import type { ScoreboardCategory } from "#ui/daily-run-scoreboard";
/** @deprecated */
export interface GetDailyRankingsRequest {
category: ScoreboardCategory;
page?: number;
}
/** @deprecated */
export interface GetDailyRankingsPageCountRequest {
category: ScoreboardCategory;
}

View File

@ -131,7 +131,7 @@ export type RunHistoryData = Record<number, RunEntry>;
export interface RunEntry {
entry: SessionSaveData;
isVictory: boolean;
/*Automatically set to false at the moment - implementation TBD*/
/** Automatically set to false at the moment - implementation TBD */
isFavorite: boolean;
}

View File

@ -0,0 +1,16 @@
import type { SessionSaveData, SystemSaveData } from "#types/save-data";
export interface SessionSaveMigrator {
version: string;
migrate: (data: SessionSaveData) => void;
}
export interface SettingsSaveMigrator {
version: string;
migrate: (data: object) => void;
}
export interface SystemSaveMigrator {
version: string;
migrate: (data: SystemSaveData) => void;
}

View File

@ -1,6 +0,0 @@
import type { SessionSaveData } from "./save-data";
export interface SessionSaveMigrator {
version: string;
migrate: (data: SessionSaveData) => void;
}

View File

@ -1,5 +0,0 @@
export interface SettingsSaveMigrator {
version: string;
// biome-ignore lint/complexity/noBannedTypes: TODO - refactor settings
migrate: (data: Object) => void;
}

View File

@ -1,4 +1,3 @@
// biome-ignore lint/correctness/noUnusedImports: Used in TSDoc comment
import type { EvoLevelThresholdKind } from "#enums/evo-level-threshold-kind";
import type { SpeciesId } from "#enums/species-id";

View File

@ -1,6 +0,0 @@
import type { SystemSaveData } from "./save-data";
export interface SystemSaveMigrator {
version: string;
migrate: (data: SystemSaveData) => void;
}

View File

@ -2,9 +2,7 @@
* A collection of custom utility types that aid in type checking and ensuring strict type conformity
*/
// biome-ignore-start lint/correctness/noUnusedImports: Used in a tsdoc comment
import type { AbAttr } from "#abilities/ability";
// biome-ignore-end lint/correctness/noUnusedImports: Used in a tsdoc comment
/**
* Exactly matches the type of the argument, preventing adding additional properties.

View File

@ -1,7 +0,0 @@
export interface UserInfo {
username: string;
lastSessionSlot: number;
discordId: string;
googleId: string;
hasAdminRole: boolean;
}

View File

@ -1,6 +1,6 @@
import { pokerogueApi } from "#api/pokerogue-api";
import { bypassLogin } from "#constants/app-constants";
import type { UserInfo } from "#types/user-info";
import type { UserInfo } from "#types/api";
import { randomString } from "#utils/common";
export let loggedInUser: UserInfo | null = null;

View File

@ -22,7 +22,6 @@ import { InvertPostFX } from "#app/pipelines/invert";
import { SpritePipeline } from "#app/pipelines/sprite";
import { SceneBase } from "#app/scene-base";
import { startingWave } from "#app/starting-wave";
import { TimedEventManager } from "#app/timed-event-manager";
import { UiInputs } from "#app/ui-inputs";
import { pokemonPrevolutions } from "#balance/pokemon-evolutions";
import { FRIENDSHIP_GAIN_FROM_BATTLE } from "#balance/starters";
@ -310,7 +309,7 @@ export class BattleScene extends SceneBase {
private bgm: AnySound;
private bgmResumeTimer: Phaser.Time.TimerEvent | null;
private bgmCache: Set<string> = new Set();
private readonly bgmCache: Set<string> = new Set();
private playTimeTimer: Phaser.Time.TimerEvent;
public rngCounter = 0;
@ -318,9 +317,7 @@ export class BattleScene extends SceneBase {
public rngOffset = 0;
public inputMethod: string;
private infoToggles: InfoToggle[] = [];
public eventManager: TimedEventManager;
private readonly infoToggles: InfoToggle[] = [];
/**
* Allows subscribers to listen for events
@ -336,7 +333,6 @@ export class BattleScene extends SceneBase {
constructor() {
super("battle");
this.phaseManager = new PhaseManager();
this.eventManager = new TimedEventManager();
this.updateGameInfo();
initGlobalScene(this);
}
@ -720,12 +716,10 @@ export class BattleScene extends SceneBase {
}
cachedFetch(url: string, init?: RequestInit): Promise<Response> {
const manifest = this.game["manifest"];
if (manifest) {
const timestamp = manifest[`/${url.replace("./", "")}`];
if (timestamp) {
url += `?t=${timestamp}`;
}
const { manifest } = this.game;
const timestamp = manifest?.[`/${url.replace("./", "")}`];
if (timestamp) {
url += `?t=${timestamp}`;
}
return fetch(url, init);
}
@ -2378,6 +2372,8 @@ export class BattleScene extends SceneBase {
switch (bgmName) {
case "title": //Firel PokéRogue Title
return 46.5;
case "winter_title": //Andr06 Winter Title
return 20.57;
case "battle_kanto_champion": //B2W2 Kanto Champion Battle
return 13.95;
case "battle_johto_champion": //B2W2 Johto Champion Battle
@ -3625,9 +3621,9 @@ export class BattleScene extends SceneBase {
// biome-ignore format: biome sucks at formatting this line
for (const seenEncounterData of this.mysteryEncounterSaveData.encounteredEvents) {
if (seenEncounterData.tier === MysteryEncounterTier.COMMON) {
tierWeights[0] = tierWeights[0] - 6;
tierWeights[0] -= 6;
} else if (seenEncounterData.tier === MysteryEncounterTier.GREAT) {
tierWeights[1] = tierWeights[1] - 4;
tierWeights[1] -= 4;
}
}
@ -3651,7 +3647,7 @@ export class BattleScene extends SceneBase {
let availableEncounters: MysteryEncounter[] = [];
const previousEncounter = this.mysteryEncounterSaveData.encounteredEvents.at(-1)?.type ?? null; // TODO: This being `null` is a bit weird
const disabledEncounters = this.eventManager.getEventMysteryEncountersDisabled();
const disabledEncounters = timedEventManager.getEventMysteryEncountersDisabled();
const biomeMysteryEncounters =
mysteryEncountersByBiome.get(this.arena.biomeType)?.filter(enc => !disabledEncounters.includes(enc)) ?? [];
// If no valid encounters exist at tier, checks next tier down, continuing until there are some encounters available
@ -3663,7 +3659,7 @@ export class BattleScene extends SceneBase {
return false;
}
if (
this.eventManager.getMysteryEncounterTierForEvent(encounterType, encounterCandidate.encounterTier) !== tier
timedEventManager.getMysteryEncounterTierForEvent(encounterType, encounterCandidate.encounterTier) !== tier
) {
return false;
}

View File

@ -4,6 +4,7 @@ import { ArenaTagType } from "#enums/arena-tag-type";
import { BattleSpec } from "#enums/battle-spec";
import { BattleType } from "#enums/battle-type";
import { BattlerIndex } from "#enums/battler-index";
import { BiomeId } from "#enums/biome-id";
import type { Command } from "#enums/command";
import type { MoveId } from "#enums/move-id";
import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
@ -18,11 +19,11 @@ import { Trainer } from "#field/trainer";
import { MoneyMultiplierModifier, type PokemonHeldItemModifier } from "#modifiers/modifier";
import type { CustomModifierSettings } from "#modifiers/modifier-type";
import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
import i18next from "#plugins/i18n";
import { MusicPreference } from "#system/settings";
import { trainerConfigs } from "#trainers/trainer-config";
import type { TurnMove } from "#types/turn-move";
import {
isBetween,
NumberHolder,
randInt,
randomString,
@ -33,6 +34,7 @@ import {
} from "#utils/common";
import { getEnumValues } from "#utils/enums";
import { randSeedUniqueItem } from "#utils/random";
import i18next from "i18next";
export interface TurnCommand {
command: Command;
@ -249,8 +251,13 @@ export class Battle {
}
return this.trainer?.getMixedBattleBgm() ?? null;
}
if (this.gameMode.isClassic && this.waveIndex > 195 && this.battleSpec !== BattleSpec.FINAL_BOSS) {
return "end_summit";
if (this.gameMode.isClassic) {
if (isBetween(this.waveIndex, 191, 194)) {
return "end";
}
if (isBetween(this.waveIndex, 196, 199)) {
return "end_summit";
}
}
const wildOpponents = globalScene.getEnemyParty();
for (const pokemon of wildOpponents) {
@ -260,7 +267,12 @@ export class Battle {
}
return "battle_final_encounter";
}
if (pokemon.species.legendary || pokemon.species.subLegendary || pokemon.species.mythical) {
if (
pokemon.species.legendary
|| pokemon.species.subLegendary
|| pokemon.species.mythical
|| (pokemon.species.category.startsWith("Paradox") && globalScene.arena.biomeType !== BiomeId.END)
) {
if (globalScene.musicPreference === MusicPreference.GENFIVE) {
switch (pokemon.species.speciesId) {
case SpeciesId.REGIROCK:
@ -401,6 +413,26 @@ export class Battle {
case SpeciesId.TING_LU:
case SpeciesId.CHI_YU:
return "battle_legendary_ruinous";
case SpeciesId.GREAT_TUSK:
case SpeciesId.SCREAM_TAIL:
case SpeciesId.BRUTE_BONNET:
case SpeciesId.FLUTTER_MANE:
case SpeciesId.SLITHER_WING:
case SpeciesId.SANDY_SHOCKS:
case SpeciesId.IRON_TREADS:
case SpeciesId.IRON_BUNDLE:
case SpeciesId.IRON_HANDS:
case SpeciesId.IRON_JUGULIS:
case SpeciesId.IRON_MOTH:
case SpeciesId.IRON_THORNS:
case SpeciesId.ROARING_MOON:
case SpeciesId.IRON_VALIANT:
case SpeciesId.WALKING_WAKE:
case SpeciesId.IRON_LEAVES:
case SpeciesId.GOUGING_FIRE:
case SpeciesId.RAGING_BOLT:
case SpeciesId.IRON_BOULDER:
case SpeciesId.IRON_CROWN:
case SpeciesId.KORAIDON:
case SpeciesId.MIRAIDON:
return "battle_legendary_kor_mir";

View File

@ -1,9 +1,5 @@
/* biome-ignore-start lint/correctness/noUnusedImports: tsdoc imports */
import type { BattleScene } from "#app/battle-scene";
import type { SpeciesFormChangeRevertWeatherFormTrigger } from "#data/form-change-triggers";
/* biome-ignore-end lint/correctness/noUnusedImports: tsdoc imports */
import { applyAbAttrs } from "#abilities/apply-ab-attrs";
import type { BattleScene } from "#app/battle-scene";
import { globalScene } from "#app/global-scene";
import { getPokemonNameWithAffix } from "#app/messages";
import type { EntryHazardTag, SuppressAbilitiesTag } from "#data/arena-tag";
@ -11,6 +7,7 @@ import type { BattlerTag } from "#data/battler-tags";
import { GroundedTag } from "#data/battler-tags";
import { getBerryEffectFunc } from "#data/berry";
import { allAbilities, allMoves } from "#data/data-lists";
import type { SpeciesFormChangeRevertWeatherFormTrigger } from "#data/form-change-triggers";
import { SpeciesFormChangeAbilityTrigger, SpeciesFormChangeWeatherTrigger } from "#data/form-change-triggers";
import { Gender } from "#data/gender";
import { getPokeballName } from "#data/pokeball";
@ -71,32 +68,40 @@ import { inSpeedOrder } from "#utils/speed-order-generator";
import { toCamelCase } from "#utils/strings";
import i18next from "i18next";
//#region Bit sets
/** Bit set for an ability's `bypass faint` flag */
const AB_FLAG_BYPASS_FAINT = 1;
const AB_FLAG_BYPASS_FAINT = 1 << 0;
/** Bit set for an ability's `ignorable` flag */
const AB_FLAG_IGNORABLE = 2;
const AB_FLAG_IGNORABLE = 1 << 1;
/** Bit set for an ability's `suppressable` flag */
const AB_FLAG_UNSUPPRESSABLE = 4;
const AB_FLAG_UNSUPPRESSABLE = 1 << 2;
/** Bit set for an ability's `uncopiable` flag */
const AB_FLAG_UNCOPIABLE = 8;
const AB_FLAG_UNCOPIABLE = 1 << 3;
/** Bit set for an ability's `unreplaceable` flag */
const AB_FLAG_UNREPLACEABLE = 16;
const AB_FLAG_UNREPLACEABLE = 1 << 4;
/** Bit set for an ability's `unimplemented` flag */
const AB_FLAG_UNIMPLEMENTED = 32;
const AB_FLAG_UNIMPLEMENTED = 1 << 5;
/** Bit set for an ability's `partial` flag */
const AB_FLAG_PARTIAL = 64;
/** Bits set for a swappable ability */
const AB_FLAG_PARTIAL = 1 << 6;
/** Bit set for an unswappable ability */
const AB_FLAG_UNSWAPPABLE = AB_FLAG_UNCOPIABLE | AB_FLAG_UNREPLACEABLE;
//#endregion Bit sets
/**
* An Ability is a class representing the various Abilities Pokemon may have. \
* Each has one or more {@linkcode AbAttr | attributes} that can apply independently
* of one another.
*/
export class Ability {
/** The ability's unique identifier */
public readonly id: AbilityId;
/** Key used to localize the ability's name */
/** The locales key for the ability's name. */
private readonly i18nKey: string;
/** The localized ability name
/**
* The localized ability name.
* @remarks
* Includes The (P) or (N) suffix, if the ability is partial/unimplemented
* Includes the `"(P)"` or `"(N)"` suffix if the ability is partial/unimplemented
*/
public get name(): string {
if (this.id === AbilityId.NONE) {
@ -124,35 +129,65 @@ export class Ability {
}
return i18next.t(`ability:${this.i18nKey}.description`);
}
/** Whether the retains its effects through a faint */
/**
* Whether this ability can activate even if the user faints.
* @remarks
* If `true`, the ability will also activate when revived via Reviver Seed.
*/
public get bypassFaint(): boolean {
return (this.flags & AB_FLAG_BYPASS_FAINT) !== 0;
}
/** Whether the ability is ignorable by mold breaker like effects */
/**
* Whether this ability can be ignored by effects like
* {@linkcode MoveId.SUNSTEEL_STRIKE | Sunsteel Strike} or {@linkcode AbilityId.MOLD_BREAKER | Mold Breaker}.
*/
public get ignorable(): boolean {
return (this.flags & AB_FLAG_IGNORABLE) !== 0;
}
/** Whether the ability can be suppressed by gastro acid and neutralizing gas */
/**
* Whether this ability can be suppressed by effects like
* {@linkcode MoveId.GASTRO_ACID | Gastro Acid} or {@linkcode AbilityId.NEUTRALIZING_GAS | Neutralizing Gas}.
*/
public get suppressable(): boolean {
return !(this.flags & AB_FLAG_UNSUPPRESSABLE);
}
/** Whether the ability can be copied, such as via trace */
/**
* Whether this ability can be copied by effects like
* {@linkcode MoveId.ROLE_PLAY | Role Play} or {@linkcode AbilityId.TRACE | Trace}.
*/
public get copiable(): boolean {
return !(this.flags & AB_FLAG_UNCOPIABLE);
}
/** Whether the ability can be replaced, such as via entrainment */
/**
* Whether this ability can be replaced by effects like
* {@linkcode MoveId.SIMPLE_BEAM | Simple Beam} or {@linkcode MoveId.ENTRAINMENT | Entrainment}.
*/
public get replaceable(): boolean {
return !(this.flags & AB_FLAG_UNREPLACEABLE);
}
/** Whether the ability is partially implemented. Mutually exclusive with {@linkcode unimplemented} */
/**
* Whether this ability is partially implemented.
* @remarks
* Mutually exclusive with {@linkcode unimplemented}
*/
public get partial(): boolean {
return (this.flags & AB_FLAG_PARTIAL) !== 0;
}
/** Whether the ability is unimplemented. Mutually exclusive with {@linkcode partial} */
/**
* Whether this ability is unimplemented.
* @remarks
* Mutually exclusive with {@linkcode partial}
*/
public get unimplemented(): boolean {
return (this.flags & AB_FLAG_UNIMPLEMENTED) !== 0;
}
/** Whether this ability can be swapped via moves like skill swap */
/**
* Whether this ability can be swapped via effects like {@linkcode MoveId.SKILL_SWAP | Skill Swap}.
* @remarks
* Logically equivalent to `this.copiable && this.replaceable`, albeit slightly faster
* due to using a pre-computed bitmask.
*/
public get swappable(): boolean {
return !(this.flags & AB_FLAG_UNSWAPPABLE);
}
@ -238,8 +273,8 @@ class AbBuilder {
* @param args - The arguments needed to instantiate the given class.
* @returns `this`
*/
attr<T extends Constructor<AbAttr>>(AttrType: T, ...args: ConstructorParameters<T>): this {
const attr = new AttrType(...args);
attr<T extends Constructor<AbAttr>>(attrType: T, ...args: ConstructorParameters<T>): this {
const attr = new attrType(...args);
this.attrs.push(attr);
return this;
@ -2317,7 +2352,7 @@ export class PostAttackApplyBattlerTagAbAttr extends PostAttackAbAttr {
override canApply(params: PostMoveInteractionAbAttrParams): boolean {
const { pokemon, move, opponent } = params;
/**Battler tags inflicted by abilities post attacking are also considered additional effects.*/
// Battler tags inflicted by abilities post attacking are also considered additional effects.
return (
super.canApply(params)
&& !opponent.hasAbilityWithAttr("IgnoreMoveEffectsAbAttr")
@ -2707,6 +2742,7 @@ export class PostSummonAddArenaTagAbAttr extends PostSummonAbAttr {
private readonly turnCount: number;
private readonly side?: ArenaTagSide;
private readonly quiet?: boolean;
// TODO: This should not need to track the source ID in a tempvar
private sourceId: number;
constructor(showAbility: boolean, tagType: ArenaTagType, turnCount: number, side?: ArenaTagSide, quiet?: boolean) {
@ -2741,6 +2777,7 @@ export class PostSummonMessageAbAttr extends PostSummonAbAttr {
}
}
// TODO: This should be merged with message func
export class PostSummonUnnamedMessageAbAttr extends PostSummonAbAttr {
//Attr doesn't force pokemon name on the message
private readonly message: string;
@ -2811,13 +2848,13 @@ export class PostSummonStatStageChangeAbAttr extends PostSummonAbAttr {
private readonly selfTarget: boolean;
private readonly intimidate: boolean;
constructor(stats: readonly BattleStat[], stages: number, selfTarget?: boolean, intimidate?: boolean) {
constructor(stats: readonly BattleStat[], stages: number, selfTarget = false, intimidate = true) {
super(true);
this.stats = stats;
this.stages = stages;
this.selfTarget = !!selfTarget;
this.intimidate = !!intimidate;
this.selfTarget = selfTarget;
this.intimidate = intimidate;
}
override apply({ pokemon, simulated }: AbAttrBaseParams): void {
@ -3324,6 +3361,7 @@ export class CommanderAbAttr extends AbAttr {
/**
* Base class for ability attributes that apply their effect when their user switches out.
*/
// TODO: Clarify the differences between this and `PreLeaveFieldAbAttr`
export abstract class PreSwitchOutAbAttr extends AbAttr {
constructor(showAbility = true) {
super(showAbility);
@ -3356,65 +3394,6 @@ export class PreSwitchOutResetStatusAbAttr extends PreSwitchOutAbAttr {
}
}
/**
* Clears Desolate Land/Primordial Sea/Delta Stream upon the Pokemon switching out.
*/
export class PreSwitchOutClearWeatherAbAttr extends PreSwitchOutAbAttr {
override apply({ pokemon, simulated }: AbAttrBaseParams): boolean {
// TODO: Evaluate why this is returning a boolean rather than relay
const weatherType = globalScene.arena.weather?.weatherType;
let turnOffWeather = false;
// Clear weather only if user's ability matches the weather and no other pokemon has the ability.
switch (weatherType) {
case WeatherType.HARSH_SUN:
if (
pokemon.hasAbility(AbilityId.DESOLATE_LAND)
&& globalScene
.getField(true)
.filter(p => p !== pokemon)
.filter(p => p.hasAbility(AbilityId.DESOLATE_LAND)).length === 0
) {
turnOffWeather = true;
}
break;
case WeatherType.HEAVY_RAIN:
if (
pokemon.hasAbility(AbilityId.PRIMORDIAL_SEA)
&& globalScene
.getField(true)
.filter(p => p !== pokemon)
.filter(p => p.hasAbility(AbilityId.PRIMORDIAL_SEA)).length === 0
) {
turnOffWeather = true;
}
break;
case WeatherType.STRONG_WINDS:
if (
pokemon.hasAbility(AbilityId.DELTA_STREAM)
&& globalScene
.getField(true)
.filter(p => p !== pokemon)
.filter(p => p.hasAbility(AbilityId.DELTA_STREAM)).length === 0
) {
turnOffWeather = true;
}
break;
}
if (simulated) {
return turnOffWeather;
}
if (turnOffWeather) {
globalScene.arena.trySetWeather(WeatherType.NONE);
return true;
}
return false;
}
}
export class PreSwitchOutHealAbAttr extends PreSwitchOutAbAttr {
override canApply({ pokemon }: AbAttrBaseParams): boolean {
return !pokemon.isFullHp();
@ -3648,8 +3627,6 @@ export class ProtectStatAbAttr extends PreStatStageChangeAbAttr {
export interface ConfusionOnStatusEffectAbAttrParams extends AbAttrBaseParams {
/** The status effect that was applied */
effect: StatusEffect;
/** The move that applied the status effect */
move: Move;
/** The opponent that was inflicted with the status effect */
opponent: Pokemon;
}
@ -3678,9 +3655,9 @@ export class ConfusionOnStatusEffectAbAttr extends AbAttr {
/**
* Applies confusion to the target pokemon.
*/
override apply({ opponent, simulated, pokemon, move }: ConfusionOnStatusEffectAbAttrParams): void {
override apply({ opponent, simulated, pokemon }: ConfusionOnStatusEffectAbAttrParams): void {
if (!simulated) {
opponent.addTag(BattlerTagType.CONFUSED, pokemon.randBattleSeedIntRange(2, 5), move.id, opponent.id);
opponent.addTag(BattlerTagType.CONFUSED, pokemon.randBattleSeedIntRange(2, 5), undefined, opponent.id);
}
}
}
@ -5012,25 +4989,26 @@ export class PostTurnHurtIfSleepingAbAttr extends PostTurnAbAttr {
*/
export class FetchBallAbAttr extends PostTurnAbAttr {
override canApply({ simulated, pokemon }: AbAttrBaseParams): boolean {
return !simulated && globalScene.currentBattle.lastUsedPokeball != null && !!pokemon.isPlayer;
return !simulated && globalScene.currentBattle.lastUsedPokeball != null && pokemon.isPlayer();
}
/**
* Adds the last used Pokeball back into the player's inventory
*/
override apply({ pokemon }: AbAttrBaseParams): void {
const lastUsed = globalScene.currentBattle.lastUsedPokeball;
globalScene.pokeballCounts[lastUsed!]++;
const lastUsed = globalScene.currentBattle.lastUsedPokeball!;
globalScene.pokeballCounts[lastUsed]++;
globalScene.currentBattle.lastUsedPokeball = null;
globalScene.phaseManager.queueMessage(
i18next.t("abilityTriggers:fetchBall", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
pokeballName: getPokeballName(lastUsed!),
pokeballName: getPokeballName(lastUsed),
}),
);
}
}
// TODO: Remove this and just replace it with applying `PostSummonChangeTerrainAbAttr` again
export class PostBiomeChangeAbAttr extends AbAttr {
private declare readonly _: never;
}
@ -5055,6 +5033,7 @@ export class PostBiomeChangeWeatherChangeAbAttr extends PostBiomeChangeAbAttr {
}
}
// TODO: Remove this and just replace it with applying `PostSummonChangeTerrainAbAttr` again
/** @sealed */
export class PostBiomeChangeTerrainChangeAbAttr extends PostBiomeChangeAbAttr {
private readonly terrainType: TerrainType;
@ -5121,7 +5100,6 @@ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr {
*/
override apply({ source, pokemon, move, targets, simulated }: PostMoveUsedAbAttrParams): void {
if (!simulated) {
pokemon.turnData.extraTurns++;
// If the move is an AttackMove or a StatusMove the Dancer must replicate the move on the source of the Dance
if (move.getMove().is("AttackMove") || move.getMove().is("StatusMove")) {
const target = this.getTarget(pokemon, source, targets);
@ -5813,10 +5791,10 @@ export interface MoveAbilityBypassAbAttrParams extends AbAttrBaseParams {
export class MoveAbilityBypassAbAttr extends AbAttr {
private readonly moveIgnoreFunc: (pokemon: Pokemon, move: Move) => boolean;
constructor(moveIgnoreFunc?: (pokemon: Pokemon, move: Move) => boolean) {
constructor(moveIgnoreFunc: (pokemon: Pokemon, move: Move) => boolean = () => true) {
super(false);
this.moveIgnoreFunc = moveIgnoreFunc || ((_pokemon, _move) => true);
this.moveIgnoreFunc = moveIgnoreFunc;
}
override canApply({ pokemon, move, cancelled }: MoveAbilityBypassAbAttrParams): boolean {
@ -6331,7 +6309,6 @@ class ForceSwitchOutHelper {
}
if (switchOutTarget.hp > 0) {
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
globalScene.phaseManager.queueDeferred(
"SwitchPhase",
this.switchType,
@ -6350,7 +6327,6 @@ class ForceSwitchOutHelper {
return false;
}
if (switchOutTarget.hp > 0) {
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
const summonIndex = globalScene.currentBattle.trainer
? globalScene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot)
: 0;
@ -6691,7 +6667,6 @@ const AbilityAttrs = Object.freeze({
CommanderAbAttr,
PreSwitchOutAbAttr,
PreSwitchOutResetStatusAbAttr,
PreSwitchOutClearWeatherAbAttr,
PreSwitchOutHealAbAttr,
PreSwitchOutFormChangeAbAttr,
PreLeaveFieldAbAttr,
@ -7155,8 +7130,8 @@ export function initAbilities() {
.ignorable()
.build(),
new AbBuilder(AbilityId.RIVALRY, 4)
.attr(MovePowerBoostAbAttr, (user, target, _move) => user?.gender !== Gender.GENDERLESS && target?.gender !== Gender.GENDERLESS && user?.gender === target?.gender, 1.25)
.attr(MovePowerBoostAbAttr, (user, target, _move) => user?.gender !== Gender.GENDERLESS && target?.gender !== Gender.GENDERLESS && user?.gender !== target?.gender, 0.75)
.attr(MovePowerBoostAbAttr, (user, target, _move) => user.gender !== Gender.GENDERLESS && target?.gender !== Gender.GENDERLESS && user.gender === target?.gender, 1.25)
.attr(MovePowerBoostAbAttr, (user, target, _move) => user.gender !== Gender.GENDERLESS && target?.gender !== Gender.GENDERLESS && user.gender !== target?.gender, 0.75)
.build(),
new AbBuilder(AbilityId.STEADFAST, 4)
.attr(FlinchStatStageChangeAbAttr, [ Stat.SPD ], 1)
@ -7389,10 +7364,10 @@ export function initAbilities() {
.ignorable()
.build(),
new AbBuilder(AbilityId.TOXIC_BOOST, 5)
.attr(MovePowerBoostAbAttr, (user, _target, move) => move.category === MoveCategory.PHYSICAL && (user?.status?.effect === StatusEffect.POISON || user?.status?.effect === StatusEffect.TOXIC), 1.5)
.attr(MovePowerBoostAbAttr, (user, _target, move) => move.category === MoveCategory.PHYSICAL && (user.status?.effect === StatusEffect.POISON || user.status?.effect === StatusEffect.TOXIC), 1.5)
.build(),
new AbBuilder(AbilityId.FLARE_BOOST, 5)
.attr(MovePowerBoostAbAttr, (user, _target, move) => move.category === MoveCategory.SPECIAL && user?.status?.effect === StatusEffect.BURN, 1.5)
.attr(MovePowerBoostAbAttr, (user, _target, move) => move.category === MoveCategory.SPECIAL && user.status?.effect === StatusEffect.BURN, 1.5)
.build(),
new AbBuilder(AbilityId.HARVEST, 5)
.attr(
@ -7436,7 +7411,7 @@ export function initAbilities() {
new AbBuilder(AbilityId.ANALYTIC, 5)
.attr(MovePowerBoostAbAttr, (user) =>
// Boost power if all other Pokemon have already moved (no other moves are slated to execute)
!globalScene.phaseManager.hasPhaseOfType("MovePhase", phase => phase.pokemon.id !== user?.id),
!globalScene.phaseManager.hasPhaseOfType("MovePhase", phase => phase.pokemon.id !== user.id),
1.3)
.build(),
new AbBuilder(AbilityId.ILLUSION, 5)
@ -7618,8 +7593,7 @@ export function initAbilities() {
.attr(AddSecondStrikeAbAttr)
// Only multiply damage on the last strike of multi-strike moves
.attr(MoveDamageBoostAbAttr, 0.25, (user, target, move) => (
!!user
&& user.turnData.hitCount > 1 // move was originally multi hit
user.turnData.hitCount > 1 // move was originally multi hit
&& user.turnData.hitsLeft === 1 // move is on its final strike
&& move.canBeMultiStrikeEnhanced(user, true, target)
)
@ -7879,7 +7853,7 @@ export function initAbilities() {
.attr(ReceivedMoveDamageMultiplierAbAttr, (target, user, move) => target.getMoveEffectiveness(user, move) >= 2, 0.75)
.build(),
new AbBuilder(AbilityId.NEUROFORCE, 7)
.attr(MovePowerBoostAbAttr, (user, target, move) => (target?.getMoveEffectiveness(user!, move) ?? 1) >= 2, 1.25)
.attr(MovePowerBoostAbAttr, (user, target, move) => (target?.getMoveEffectiveness(user, move) ?? 1) >= 2, 1.25)
.build(),
new AbBuilder(AbilityId.INTREPID_SWORD, 8)
.attr(PostSummonStatStageChangeAbAttr, [ Stat.ATK ], 1, true)

View File

@ -44,11 +44,8 @@
* @module
*/
// biome-ignore-start lint/correctness/noUnusedImports: TSDoc imports
import type { BattlerTag } from "#app/data/battler-tags";
// biome-ignore-end lint/correctness/noUnusedImports: TSDoc imports
import { applyAbAttrs, applyOnGainAbAttrs, applyOnLoseAbAttrs } from "#abilities/apply-ab-attrs";
import type { BattlerTag } from "#app/data/battler-tags";
import { globalScene } from "#app/global-scene";
import { getPokemonNameWithAffix } from "#app/messages";
import { CommonBattleAnim } from "#data/battle-anims";
@ -995,7 +992,7 @@ class ToxicSpikesTag extends EntryHazardTag {
// 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);
return pokemon.trySetStatus(effect, undefined, 0, this.getMoveName(), false, true);
}
getMatchupScoreMultiplier(pokemon: Pokemon): number {
@ -1535,7 +1532,7 @@ export class SuppressAbilitiesTag extends SerializableArenaTag {
// Could have a custom message that plays when a specific pokemon's NG ends? This entire thing exists due to passives after all
const setter = globalScene
.getField(true)
.filter(p => p.hasAbilityWithAttr("PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr", false))[0];
.find(p => p.hasAbilityWithAttr("PreLeaveFieldRemoveSuppressAbilitiesSourceAbAttr", false));
// Setter may not exist if both NG Pokemon faint simultaneously
if (setter == null) {
return;

View File

@ -1,3 +1,9 @@
/*
* SPDX-FileCopyrightText: 2024-2025 Pagefault Games
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
//! DO NOT EDIT THIS FILE - CREATED BY THE `eggMoves:parse` script automatically
import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id";

View File

@ -1,4 +1,3 @@
// biome-ignore lint/correctness/noUnusedImports: Used in TSDoc comments
import type { determineEnemySpecies } from "#app/ai/ai-species-gen";
import { defaultStarterSpecies } from "#app/constants";
import { globalScene } from "#app/global-scene";
@ -655,10 +654,18 @@ export const pokemonEvolutions: PokemonEvolutions = {
new SpeciesEvolution(SpeciesId.GARDEVOIR, 30, null, null),
new SpeciesEvolution(SpeciesId.GALLADE, 1, EvolutionItem.DAWN_STONE, {key: EvoCondKey.GENDER, gender: Gender.MALE}, [30, 30, 30]),
],
[SpeciesId.SURSKIT]: [new SpeciesEvolution(SpeciesId.MASQUERAIN, 22, null, null)],
[SpeciesId.SHROOMISH]: [new SpeciesEvolution(SpeciesId.BRELOOM, 23, null, null)],
[SpeciesId.SLAKOTH]: [new SpeciesEvolution(SpeciesId.VIGOROTH, 18, null, null)],
[SpeciesId.VIGOROTH]: [new SpeciesEvolution(SpeciesId.SLAKING, 36, null, null)],
[SpeciesId.SURSKIT]: [
new SpeciesEvolution(SpeciesId.MASQUERAIN, 22, null, null)
],
[SpeciesId.SHROOMISH]: [
new SpeciesEvolution(SpeciesId.BRELOOM, 23, null, null)
],
[SpeciesId.SLAKOTH]: [
new SpeciesEvolution(SpeciesId.VIGOROTH, 18, null, null)
],
[SpeciesId.VIGOROTH]: [
new SpeciesEvolution(SpeciesId.SLAKING, 36, null, null)
],
[SpeciesId.NINCADA]: [
new SpeciesEvolution(SpeciesId.NINJASK, 20, null, null),
new SpeciesEvolution(SpeciesId.SHEDINJA, 20, null, {key: EvoCondKey.SHEDINJA})
@ -736,26 +743,66 @@ export const pokemonEvolutions: PokemonEvolutions = {
new SpeciesEvolution(SpeciesId.GLALIE, 42, null, null),
new SpeciesEvolution(SpeciesId.FROSLASS, 1, EvolutionItem.DAWN_STONE, {key: EvoCondKey.GENDER, gender: Gender.FEMALE}, [42, 42, 42]),
],
[SpeciesId.SPHEAL]: [new SpeciesEvolution(SpeciesId.SEALEO, 32, null, null)],
[SpeciesId.SEALEO]: [new SpeciesEvolution(SpeciesId.WALREIN, 44, null, null)],
[SpeciesId.BAGON]: [new SpeciesEvolution(SpeciesId.SHELGON, 30, null, null)],
[SpeciesId.SHELGON]: [new SpeciesEvolution(SpeciesId.SALAMENCE, 50, null, null)],
[SpeciesId.BELDUM]: [new SpeciesEvolution(SpeciesId.METANG, 20, null, null)],
[SpeciesId.METANG]: [new SpeciesEvolution(SpeciesId.METAGROSS, 45, null, null)],
[SpeciesId.TURTWIG]: [new SpeciesEvolution(SpeciesId.GROTLE, 18, null, null)],
[SpeciesId.GROTLE]: [new SpeciesEvolution(SpeciesId.TORTERRA, 32, null, null)],
[SpeciesId.CHIMCHAR]: [new SpeciesEvolution(SpeciesId.MONFERNO, 14, null, null)],
[SpeciesId.MONFERNO]: [new SpeciesEvolution(SpeciesId.INFERNAPE, 36, null, null)],
[SpeciesId.PIPLUP]: [new SpeciesEvolution(SpeciesId.PRINPLUP, 16, null, null)],
[SpeciesId.PRINPLUP]: [new SpeciesEvolution(SpeciesId.EMPOLEON, 36, null, null)],
[SpeciesId.STARLY]: [new SpeciesEvolution(SpeciesId.STARAVIA, 14, null, null)],
[SpeciesId.STARAVIA]: [new SpeciesEvolution(SpeciesId.STARAPTOR, 34, null, null)],
[SpeciesId.BIDOOF]: [new SpeciesEvolution(SpeciesId.BIBAREL, 15, null, null)],
[SpeciesId.KRICKETOT]: [new SpeciesEvolution(SpeciesId.KRICKETUNE, 10, null, null)],
[SpeciesId.SHINX]: [new SpeciesEvolution(SpeciesId.LUXIO, 15, null, null)],
[SpeciesId.LUXIO]: [new SpeciesEvolution(SpeciesId.LUXRAY, 30, null, null)],
[SpeciesId.CRANIDOS]: [new SpeciesEvolution(SpeciesId.RAMPARDOS, 30, null, null)],
[SpeciesId.SHIELDON]: [new SpeciesEvolution(SpeciesId.BASTIODON, 30, null, null)],
[SpeciesId.SPHEAL]: [
new SpeciesEvolution(SpeciesId.SEALEO, 32, null, null)
],
[SpeciesId.SEALEO]: [
new SpeciesEvolution(SpeciesId.WALREIN, 44, null, null)
],
[SpeciesId.BAGON]: [
new SpeciesEvolution(SpeciesId.SHELGON, 30, null, null)
],
[SpeciesId.SHELGON]: [
new SpeciesEvolution(SpeciesId.SALAMENCE, 50, null, null)
],
[SpeciesId.BELDUM]: [
new SpeciesEvolution(SpeciesId.METANG, 20, null, null)
],
[SpeciesId.METANG]: [
new SpeciesEvolution(SpeciesId.METAGROSS, 45, null, null)
],
[SpeciesId.TURTWIG]: [
new SpeciesEvolution(SpeciesId.GROTLE, 18, null, null)
],
[SpeciesId.GROTLE]: [
new SpeciesEvolution(SpeciesId.TORTERRA, 32, null, null)
],
[SpeciesId.CHIMCHAR]: [
new SpeciesEvolution(SpeciesId.MONFERNO, 14, null, null)
],
[SpeciesId.MONFERNO]: [
new SpeciesEvolution(SpeciesId.INFERNAPE, 36, null, null)
],
[SpeciesId.PIPLUP]: [
new SpeciesEvolution(SpeciesId.PRINPLUP, 16, null, null)
],
[SpeciesId.PRINPLUP]: [
new SpeciesEvolution(SpeciesId.EMPOLEON, 36, null, null)
],
[SpeciesId.STARLY]: [
new SpeciesEvolution(SpeciesId.STARAVIA, 14, null, null)
],
[SpeciesId.STARAVIA]: [
new SpeciesEvolution(SpeciesId.STARAPTOR, 34, null, null)
],
[SpeciesId.BIDOOF]: [
new SpeciesEvolution(SpeciesId.BIBAREL, 15, null, null)
],
[SpeciesId.KRICKETOT]: [
new SpeciesEvolution(SpeciesId.KRICKETUNE, 10, null, null)
],
[SpeciesId.SHINX]: [
new SpeciesEvolution(SpeciesId.LUXIO, 15, null, null)
],
[SpeciesId.LUXIO]: [
new SpeciesEvolution(SpeciesId.LUXRAY, 30, null, null)
],
[SpeciesId.CRANIDOS]: [
new SpeciesEvolution(SpeciesId.RAMPARDOS, 30, null, null)
],
[SpeciesId.SHIELDON]: [
new SpeciesEvolution(SpeciesId.BASTIODON, 30, null, null)
],
[SpeciesId.BURMY]: [
new SpeciesEvolution(SpeciesId.MOTHIM, 20, null, {key: EvoCondKey.GENDER, gender: Gender.MALE}),
new SpeciesEvolution(SpeciesId.WORMADAM, 20, null, {key: EvoCondKey.GENDER, gender: Gender.FEMALE})

View File

@ -7,10 +7,11 @@ export const CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER = 3;
export const FRIENDSHIP_GAIN_FROM_BATTLE = 3;
export const FRIENDSHIP_GAIN_FROM_RARE_CANDY = 6;
export const FRIENDSHIP_LOSS_FROM_FAINT = 5;
// #endregion
/**
* Function to get the cumulative friendship threshold at which a candy is earned
* @param starterCost The cost of the starter, found in {@linkcode speciesStarterCosts}
* @param starterCost - The cost of the starter, found in {@linkcode speciesStarterCosts}
* @returns aforementioned threshold
*/
export function getStarterValueFriendshipCap(starterCost: number): number {
@ -618,43 +619,72 @@ export const speciesStarterCosts = {
[SpeciesId.BLOODMOON_URSALUNA]: 5,
};
const starterCandyCosts: { passive: number; costReduction: [number, number]; egg: number; }[] = [
{ passive: 40, costReduction: [ 25, 60 ], egg: 30 }, // 1 Cost
{ passive: 40, costReduction: [ 25, 60 ], egg: 30 }, // 2 Cost
{ passive: 35, costReduction: [ 20, 50 ], egg: 25 }, // 3 Cost
{ passive: 30, costReduction: [ 15, 40 ], egg: 20 }, // 4 Cost
{ passive: 25, costReduction: [ 12, 35 ], egg: 18 }, // 5 Cost
{ passive: 20, costReduction: [ 10, 30 ], egg: 15 }, // 6 Cost
{ passive: 15, costReduction: [ 8, 20 ], egg: 12 }, // 7 Cost
{ passive: 10, costReduction: [ 5, 15 ], egg: 10 }, // 8 Cost
{ passive: 10, costReduction: [ 5, 15 ], egg: 10 }, // 9 Cost
{ passive: 10, costReduction: [ 5, 15 ], egg: 10 }, // 10 Cost
interface StarterCandyCosts {
/** The candy cost to unlock the starter's passive ability */
readonly passive: number;
/** The candy costs to reduce the starter's point cost */
readonly costReduction: readonly [number, number];
/** The costs to buy a same-species egg */
readonly eggCosts: readonly [number, ...number[]];
/** The number of eggs required to hatch to reduce the cost for buying more eggs */
readonly eggCostReductionThresholds: readonly number[];
}
const allStarterCandyCosts: readonly StarterCandyCosts[] = [
{ passive: 40, costReduction: [25, 60], eggCosts: [30, 27, 22, 15], eggCostReductionThresholds: [20, 40, 80] }, // 1 Cost
{ passive: 40, costReduction: [25, 60], eggCosts: [30, 27, 22, 15], eggCostReductionThresholds: [20, 40, 80] }, // 2 Cost
{ passive: 35, costReduction: [20, 50], eggCosts: [25, 22, 18, 12], eggCostReductionThresholds: [20, 40, 80] }, // 3 Cost
{ passive: 30, costReduction: [15, 40], eggCosts: [20, 18, 15, 10], eggCostReductionThresholds: [15, 30, 60] }, // 4 Cost
{ passive: 25, costReduction: [12, 35], eggCosts: [18, 16, 13, 9], eggCostReductionThresholds: [15, 30, 60] }, // 5 Cost
{ passive: 20, costReduction: [10, 30], eggCosts: [15, 13, 11, 7], eggCostReductionThresholds: [15, 30, 60] }, // 6 Cost
{ passive: 15, costReduction: [8, 20], eggCosts: [12, 10, 9, 6], eggCostReductionThresholds: [10, 20, 40] }, // 7 Cost
{ passive: 10, costReduction: [5, 15], eggCosts: [10, 9, 7, 5], eggCostReductionThresholds: [10, 20, 40] }, // 8 Cost
{ passive: 10, costReduction: [5, 15], eggCosts: [10, 9, 7, 5], eggCostReductionThresholds: [10, 20, 40] }, // 9 Cost
{ passive: 10, costReduction: [5, 15], eggCosts: [10, 9, 7, 5], eggCostReductionThresholds: [8, 16, 32] }, // 10 Cost
];
/**
* Getter for {@linkcode starterCandyCosts} for passive unlock candy cost based on initial point cost
* @param starterCost the default point cost of the starter found in {@linkcode speciesStarterCosts}
* Getter for {@linkcode allStarterCandyCosts} for passive unlock candy cost based on initial point cost
* @param starterCost - The default point cost of the starter found in {@linkcode speciesStarterCosts}
* @returns the candy cost for passive unlock
*/
export function getPassiveCandyCount(starterCost: number): number {
return starterCandyCosts[starterCost - 1].passive;
return allStarterCandyCosts[starterCost - 1].passive;
}
/**
* Getter for {@linkcode starterCandyCosts} for value reduction unlock candy cost based on initial point cost
* @param starterCost the default point cost of the starter found in {@linkcode speciesStarterCosts}
* Getter for {@linkcode allStarterCandyCosts} for value reduction unlock candy cost based on initial point cost
* @param starterCost - The default point cost of the starter found in {@linkcode speciesStarterCosts}
* @returns respective candy cost for the two cost reductions as an array 2 numbers
*/
export function getValueReductionCandyCounts(starterCost: number): [number, number] {
return starterCandyCosts[starterCost - 1].costReduction;
export function getValueReductionCandyCounts(starterCost: number): readonly [number, number] {
return allStarterCandyCosts[starterCost - 1].costReduction;
}
/**
* Getter for {@linkcode starterCandyCosts} for egg purchase candy cost based on initial point cost
* @param starterCost the default point cost of the starter found in {@linkcode speciesStarterCosts}
* Getter for {@linkcode allStarterCandyCosts} for egg purchase candy cost based on initial point cost
* @param starterCost - The default point cost of the starter found in {@linkcode speciesStarterCosts}
* @param hatchCount - The number of eggs hatched of the starter
* @returns the candy cost for the purchasable egg
*/
export function getSameSpeciesEggCandyCounts(starterCost: number): number {
return starterCandyCosts[starterCost - 1].egg;
export function getSameSpeciesEggCandyCounts(starterCost: number, hatchCount: number): number {
const starterCandyCosts = allStarterCandyCosts[starterCost - 1];
let eggCostIndex = 0;
while (hatchCount >= starterCandyCosts.eggCostReductionThresholds[eggCostIndex]) {
eggCostIndex++;
}
return starterCandyCosts.eggCosts[eggCostIndex];
}
/**
* This is used for internal testing purposes only and will not be populated outside of the test environment.
* @internal
*/
export const __TEST_allStarterCandyCosts: readonly StarterCandyCosts[] = [];
if (import.meta.env.NODE_ENV === "test") {
for (const starterCandyCosts of allStarterCandyCosts) {
// @ts-expect-error: done this way to keep it `readonly`
__TEST_allStarterCandyCosts.push(starterCandyCosts);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -86,7 +86,6 @@ export const tmPoolTiers: TmPoolTiers = {
[MoveId.ICE_PUNCH]: ModifierTier.GREAT,
[MoveId.THUNDER_PUNCH]: ModifierTier.GREAT,
[MoveId.SWORDS_DANCE]: ModifierTier.GREAT,
[MoveId.CUT]: ModifierTier.COMMON,
[MoveId.FLY]: ModifierTier.GREAT,
[MoveId.MEGA_KICK]: ModifierTier.GREAT,
[MoveId.BODY_SLAM]: ModifierTier.GREAT,
@ -160,7 +159,6 @@ export const tmPoolTiers: TmPoolTiers = {
[MoveId.SLEEP_TALK]: ModifierTier.COMMON,
[MoveId.HEAL_BELL]: ModifierTier.COMMON,
[MoveId.RETURN]: ModifierTier.ULTRA,
[MoveId.FRUSTRATION]: ModifierTier.COMMON,
[MoveId.SAFEGUARD]: ModifierTier.COMMON,
[MoveId.PAIN_SPLIT]: ModifierTier.COMMON,
[MoveId.MEGAHORN]: ModifierTier.ULTRA,

View File

@ -14,6 +14,8 @@ import { getEnumKeys, getEnumValues } from "#utils/enums";
import { toKebabCase } from "#utils/strings";
import Phaser from "phaser";
// TODO: Split up this entire file - it has way WAY too much stuff for its own good.
// (Also happens to be positively spaghetti, but that's besides the point)
export class AnimConfig {
public id: number;
public graphic: string;
@ -821,7 +823,7 @@ export abstract class BattleAnim {
frame.target === AnimFrameTarget.GRAPHIC
&& isReversed(this.srcLine[0], this.srcLine[2], this.dstLine[0], this.dstLine[2])
) {
scaleX = scaleX * -1;
scaleX *= -1;
}
}
break;
@ -835,7 +837,7 @@ export abstract class BattleAnim {
}
// biome-ignore lint/complexity/noBannedTypes: callback is used liberally
play(onSubstitute?: boolean, callback?: Function) {
play(onSubstitute?: boolean, callback?: () => void) {
const isOppAnim = this.isOppAnim();
const user = isOppAnim ? this.target! : this.user!;
const target = isOppAnim ? this.user! : this.target!; // TODO: These bangs are LITERALLY not correct at all
@ -1179,7 +1181,7 @@ export abstract class BattleAnim {
frameTimeMult: number,
frameTimedEventPriority?: 0 | 1 | 3 | 5,
// biome-ignore lint/complexity/noBannedTypes: callback is used liberally
callback?: Function,
callback?: () => void,
) {
const spriteCache: SpriteCache = {
[AnimFrameTarget.GRAPHIC]: [],

View File

@ -82,7 +82,6 @@ import type { Move } from "#moves/move";
import type { MoveEffectPhase } from "#phases/move-effect-phase";
import type { MovePhase } from "#phases/move-phase";
import type { StatStageChangeCallback } from "#phases/stat-stage-change-phase";
import i18next from "#plugins/i18n";
import type {
AbilityBattlerTagType,
BattlerTagData,
@ -103,6 +102,7 @@ import type { Mutable } from "#types/type-helpers";
import { coerceArray } from "#utils/array";
import { BooleanHolder, getFrameMs, NumberHolder, toDmgValue } from "#utils/common";
import { toCamelCase } from "#utils/strings";
import i18next from "i18next";
/** Interface containing the serializable fields of `BattlerTag` */
interface BaseBattlerTag {

View File

@ -790,22 +790,14 @@ export class SingleTypeChallenge extends Challenge {
applyStarterSelectModify(speciesId: SpeciesId, dexEntry: DexEntry, _starterDataEntry: StarterDataEntry): boolean {
const type = this.value - 1;
if (speciesId === SpeciesId.RALTS) {
if (type === PokemonType.FIGHTING) {
dexEntry.caughtAttr &= ~DexAttr.FEMALE;
}
if (type === PokemonType.FAIRY) {
dexEntry.caughtAttr &= ~DexAttr.MALE;
}
}
if (speciesId === SpeciesId.SNORUNT && type === PokemonType.GHOST) {
if (speciesId === SpeciesId.RALTS && type === PokemonType.FIGHTING) {
dexEntry.caughtAttr &= ~DexAttr.FEMALE;
} else if (speciesId === SpeciesId.SNORUNT && type === PokemonType.GHOST) {
dexEntry.caughtAttr &= ~DexAttr.MALE;
}
if (speciesId === SpeciesId.BURMY) {
} else if (speciesId === SpeciesId.BURMY) {
if (type === PokemonType.FLYING) {
dexEntry.caughtAttr &= ~DexAttr.FEMALE;
}
if ([PokemonType.GRASS, PokemonType.GROUND, PokemonType.STEEL].includes(type)) {
} else if ([PokemonType.GRASS, PokemonType.GROUND, PokemonType.STEEL].includes(type)) {
dexEntry.caughtAttr &= ~DexAttr.MALE;
}
}
@ -933,7 +925,7 @@ export class FreshStartChallenge extends Challenge {
}
applyStarterModify(pokemon: Pokemon): boolean {
pokemon.abilityIndex = pokemon.abilityIndex % 2; // Always base ability, if you set it to hidden it wraps to first ability
pokemon.abilityIndex %= 2; // Always base ability, if you set it to hidden it wraps to first ability
pokemon.passive = false; // Passive isn't unlocked
let validMoves = pokemon.species
.getLevelMoves()

View File

@ -10,7 +10,7 @@ export interface TrainerTypeMessages {
}
export interface TrainerTypeDialogue {
[key: number]: TrainerTypeMessages | Array<TrainerTypeMessages>;
[key: number]: TrainerTypeMessages | TrainerTypeMessages[];
}
export function getTrainerTypeDialogue(): TrainerTypeDialogue {

View File

@ -259,15 +259,10 @@ export const noAbilityTypeOverrideMoves: ReadonlySet<MoveId> = new Set([
/** Set of all moves that cannot be copied by {@linkcode MoveId.SKETCH}. */
export const invalidSketchMoves: ReadonlySet<MoveId> = new Set([
MoveId.NONE,
MoveId.CHATTER,
MoveId.MIRROR_MOVE,
MoveId.SLEEP_TALK,
MoveId.STRUGGLE,
MoveId.SKETCH,
MoveId.REVIVAL_BLESSING,
MoveId.TERA_STARSTORM,
MoveId.BREAKNECK_BLITZ__PHYSICAL,
MoveId.BREAKNECK_BLITZ__SPECIAL,
]);
/** Set of all moves that cannot be locked into by {@linkcode MoveId.ENCORE}. */

View File

@ -1,4 +1,3 @@
// biome-ignore lint/correctness/noUnusedImports: Used in a TSDoc comment
import type { GameMode } from "#app/game-mode";
import { globalScene } from "#app/global-scene";
import { getPokemonNameWithAffix } from "#app/messages";

File diff suppressed because it is too large Load Diff

View File

@ -55,7 +55,7 @@ export class PokemonMove {
// TODO: Add Sky Drop's 1 turn stall
if (this.moveId === MoveId.NONE || move.name.endsWith(" (N)")) {
return [false, i18next.t("battle:moveNotImplemented", moveName.replace(" (N)", ""))];
return [false, i18next.t("battle:moveNotImplemented", { moveName: moveName.replace(" (N)", "") })];
}
if (!ignorePp && move.pp !== -1 && this.ppUsed >= this.getMovePp()) {

View File

@ -567,7 +567,7 @@ function doBerryBounce(berrySprites: Phaser.GameObjects.Sprite[], yd: number, ba
bouncePower = bouncePower > 0.01 ? bouncePower * 0.5 : 0;
if (bouncePower) {
bounceYOffset = bounceYOffset * bouncePower;
bounceYOffset *= bouncePower;
globalScene.tweens.add({
targets: berrySprites,

View File

@ -34,10 +34,10 @@ import {
import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option";
import i18next from "#plugins/i18n";
import { PokemonData } from "#system/pokemon-data";
import { randSeedItem } from "#utils/common";
import { getEnumValues } from "#utils/enums";
import i18next from "i18next";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/berriesAbound";

View File

@ -34,7 +34,6 @@ import {
setEncounterRewards,
transitionMysteryEncounterIntroVisuals,
} from "#mystery-encounters/encounter-phase-utils";
import { getSpriteKeysFromSpecies } from "#mystery-encounters/encounter-pokemon-utils";
import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option";
@ -177,7 +176,26 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde
.withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES)
.withScenePartySizeRequirement(3, 6)
.withMaxAllowedEncounters(1)
.withIntroSpriteConfigs([]) // These are set in onInit()
.withIntroSpriteConfigs([
{
species: SpeciesId.VESPIQUEN,
spriteKey: "",
fileRoot: "",
hasShadow: true,
repeat: true,
x: 35,
y: -2,
yShadow: -2,
},
{
spriteKey: "bug_type_superfan",
fileRoot: "trainer",
hasShadow: true,
x: -20,
y: 5,
yShadow: 5,
},
])
.withAutoHideIntroVisuals(false)
.withIntroDialogue([
{
@ -194,56 +212,11 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde
// Bug type superfan trainer config
const config = getTrainerConfigForWave(globalScene.currentBattle.waveIndex);
const spriteKey = config.getSpriteKey();
encounter.enemyPartyConfigs.push({
trainerConfig: config,
female: true,
});
let beedrillKeys: { spriteKey: string; fileRoot: string };
let butterfreeKeys: { spriteKey: string; fileRoot: string };
if (globalScene.currentBattle.waveIndex < WAVE_LEVEL_BREAKPOINTS[3]) {
beedrillKeys = getSpriteKeysFromSpecies(SpeciesId.BEEDRILL, false);
butterfreeKeys = getSpriteKeysFromSpecies(SpeciesId.BUTTERFREE, false);
} else {
// Mega Beedrill/Gmax Butterfree
beedrillKeys = getSpriteKeysFromSpecies(SpeciesId.BEEDRILL, false, 1);
butterfreeKeys = getSpriteKeysFromSpecies(SpeciesId.BUTTERFREE, false, 1);
}
encounter.spriteConfigs = [
{
spriteKey: beedrillKeys.spriteKey,
fileRoot: beedrillKeys.fileRoot,
hasShadow: true,
repeat: true,
isPokemon: true,
x: -30,
tint: 0.15,
y: -4,
yShadow: -4,
},
{
spriteKey: butterfreeKeys.spriteKey,
fileRoot: butterfreeKeys.fileRoot,
hasShadow: true,
repeat: true,
isPokemon: true,
x: 30,
tint: 0.15,
y: -4,
yShadow: -4,
},
{
spriteKey,
fileRoot: "trainer",
hasShadow: true,
x: 4,
y: 7,
yShadow: 7,
},
];
const requiredItems = [
generateModifierType(modifierTypes.QUICK_CLAW),
generateModifierType(modifierTypes.GRIP_CLAW),

View File

@ -32,10 +32,10 @@ import {
HeldItemRequirement,
MoneyRequirement,
} from "#mystery-encounters/mystery-encounter-requirements";
import i18next from "#plugins/i18n";
import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler";
import { randSeedItem } from "#utils/common";
import { getPokemonSpecies } from "#utils/pokemon-utils";
import i18next from "i18next";
/** the i18n namespace for this encounter */
const namespace = "mysteryEncounters/delibirdy";
@ -80,6 +80,7 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with
MysteryEncounterType.DELIBIRDY,
)
.withEncounterTier(MysteryEncounterTier.GREAT)
.withMaxAllowedEncounters(4)
.withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES)
.withSceneRequirement(new MoneyRequirement(0, DELIBIRDY_MONEY_PRICE_MULTIPLIER)) // Must have enough money for it to spawn at the very least
.withPrimaryPokemonRequirement(

View File

@ -48,13 +48,14 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = MysteryEncounterBui
.withPrimaryPokemonHealthRatioRequirement([0.51, 1]) // At least 1 Pokemon must have above half HP
.withIntroSpriteConfigs([
{
spriteKey: SpeciesId.KROOKODILE.toString(),
spriteKey: SpeciesId.KROKOROK.toString(),
fileRoot: "pokemon",
hasShadow: true,
repeat: true,
x: 12,
y: -5,
yShadow: -5,
repeat: false,
scale: 1.1,
x: 24,
y: 0,
yShadow: 0,
},
{
spriteKey: "shady_vitamin_dealer",

View File

@ -26,9 +26,9 @@ import {
import { applyModifierTypeToPlayerPokemon } from "#mystery-encounters/encounter-pokemon-utils";
import { type MysteryEncounter, MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option";
import i18next from "#plugins/i18n";
import { randSeedInt } from "#utils/common";
import { getPokemonSpecies } from "#utils/pokemon-utils";
import i18next from "i18next";
/** the i18n namespace for this encounter */
const namespace = "mysteryEncounters/trashToTreasure";

View File

@ -35,7 +35,6 @@ import {
import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option";
import i18next from "#plugins/i18n";
import { achvs } from "#system/achv";
import { PokemonData } from "#system/pokemon-data";
import { trainerConfigs } from "#trainers/trainer-config";
@ -43,6 +42,7 @@ import { TrainerPartyTemplate } from "#trainers/trainer-party-template";
import type { HeldModifierConfig } from "#types/held-modifier-config";
import { NumberHolder, randSeedInt, randSeedShuffle } from "#utils/common";
import { getPokemonSpecies } from "#utils/pokemon-utils";
import i18next from "i18next";
/** i18n namespace for encounter */
const namespace = "mysteryEncounters/weirdDream";
@ -463,7 +463,7 @@ async function doNewTeamPostProcess(transformations: PokemonTransformation[]) {
// Any pokemon that is below 570 BST gets +20 permanent BST to 3 stats
if (shouldGetOldGateau(newPokemon)) {
const modType = modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU();
const modifier = modType?.newModifier(newPokemon);
const modifier = modType.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU).newModifier(newPokemon);
if (modifier) {
globalScene.addModifier(modifier, false, false, false, true);
}

View File

@ -347,7 +347,7 @@ export class MysteryEncounter implements IMysteryEncounter {
if (activeMon.length > 0) {
this.primaryPokemon = activeMon[0];
} else {
this.primaryPokemon = globalScene.getPlayerParty().filter(p => p.isAllowedInBattle())[0];
this.primaryPokemon = globalScene.getPlayerParty().find(p => p.isAllowedInBattle());
}
return true;
}

View File

@ -739,7 +739,7 @@ export function selectOptionThenPokemon(
export function setEncounterRewards(
customShopRewards?: CustomModifierSettings,
eggRewards?: IEggOptions[],
preRewardsCallback?: Function,
preRewardsCallback?: () => void,
): void {
globalScene.currentBattle.mysteryEncounter!.doEncounterRewards = () => {
if (preRewardsCallback) {
@ -1172,8 +1172,8 @@ export function calculateMEAggregateStats(baseSpawnWeight: number): void {
const tierWeights = [66, 40, 19, 3];
// Adjust tier weights by currently encountered events (pity system that lowers odds of multiple Common/Great)
tierWeights[0] = tierWeights[0] - 6 * numEncounters[0];
tierWeights[1] = tierWeights[1] - 4 * numEncounters[1];
tierWeights[0] -= 6 * numEncounters[0];
tierWeights[1] -= 4 * numEncounters[1];
const totalWeight = tierWeights.reduce((a, b) => a + b);
const tierValue = randSeedInt(totalWeight);

View File

@ -115,8 +115,7 @@ export function doPokeballBounceAnim(
y1: number,
y2: number,
baseBounceDuration: number,
// biome-ignore lint/complexity/noBannedTypes: TODO
callback: Function,
callback: () => void,
isCritical = false,
) {
let bouncePower = 1;

View File

@ -421,7 +421,7 @@ export abstract class PokemonSpeciesForm {
case SpeciesId.BLOODMOON_URSALUNA:
break;
default:
speciesId = speciesId % 2000;
speciesId %= 2000;
break;
}
}

View File

@ -124,12 +124,17 @@ export class PokemonSummonData {
public stats: number[] = [0, 0, 0, 0, 0, 0];
public moveset: PokemonMove[] | null;
// If not initialized this value will not be populated from save data.
public types: PokemonType[] = [];
public addedType: PokemonType | null = null;
/** Data pertaining to this pokemon's illusion. */
/** Data pertaining to this pokemon's Illusion, if it has one. */
public illusion: IllusionData | null = null;
/**
* Whether this Pokemon's illusion has been broken since switching out.
* @defaultValue `false`
*/
// TODO: Since Illusion applies on switch in, and this entire class is reset on switch-in,
// this may be replaceable with a check for `pokemon.summonData.illusionData !== null`
public illusionBroken = false;
/** Array containing all berries eaten in the last turn; used by {@linkcode AbilityId.CUD_CHEW} */
@ -139,6 +144,7 @@ export class PokemonSummonData {
* An array of all moves this pokemon has used since entering the battle.
* Used for most moves and abilities that check prior move usage or copy already-used moves.
*/
// TODO: Rework this into a sort of "global move history" that also allows checking execution order (for Fusion Bolt/Flare)
public moveHistory: TurnMove[] = [];
constructor(source?: PokemonSummonData | SerializedPokemonSummonData) {
@ -302,8 +308,10 @@ export class PokemonTurnData {
/** How many times the current move should hit the target(s) */
public hitCount = 0;
/**
* - `-1` = Calculate how many hits are left
* - `0` = Move is finished
* - `-1`: Calculate how many hits are left
* - `0`: Move is finished
* - `>0`: Move is in process of hitting targets
* @defaultValue `-1`
*/
public hitsLeft = -1;
public totalDamageDealt = 0;
@ -320,20 +328,17 @@ export class PokemonTurnData {
public summonedThisTurn = false;
public failedRunAway = false;
public joinedRound = false;
/** Tracker for a pending status effect
/**
* Tracker for a pending status effect.
*
* @remarks
* Set whenever {@linkcode Pokemon#trySetStatus} succeeds in order to prevent subsequent status effects
* from being applied. Necessary because the status is not actually set until the {@linkcode ObtainStatusEffectPhase} runs,
* from being applied. \
* Necessary because the status is not actually set until the {@linkcode ObtainStatusEffectPhase} runs,
* which may not happen before another status effect is attempted to be applied.
* @defaultValue `StatusEffect.NONE`
*/
public pendingStatus: StatusEffect = StatusEffect.NONE;
/**
* The amount of times this Pokemon has acted again and used a move in the current turn.
* Used to make sure multi-hits occur properly when the user is
* forced to act again in the same turn, and **must be incremented** by any effects that grant extra actions.
*/
public extraTurns = 0;
/**
* All berries eaten by this pokemon in this turn.
* Saved into {@linkcode PokemonSummonData | SummonData} by {@linkcode AbilityId.CUD_CHEW} on turn end.

View File

@ -14,7 +14,7 @@ import type { ObjectValues } from "#types/type-helpers";
export function loadPositionalTag<T extends PositionalTagType>({
tagType,
...args
}: serializedPosTagMap[T]): posTagInstanceMap[T];
}: toSerializedPosTag<T>): posTagInstanceMap[T];
/**
* Load the attributes of a {@linkcode PositionalTag}.
* @param tag - The {@linkcode SerializedPositionalTag} to instantiate
@ -26,7 +26,7 @@ export function loadPositionalTag(tag: SerializedPositionalTag): PositionalTag;
export function loadPositionalTag<T extends PositionalTagType>({
tagType,
...rest
}: serializedPosTagMap[T]): posTagInstanceMap[T] {
}: toSerializedPosTag<T>): posTagInstanceMap[T] {
// Note: We need 2 type assertions here:
// 1 because TS doesn't narrow the type of TagClass correctly based on `T`.
// It converts it into `new (DelayedAttackTag | WishTag) => DelayedAttackTag & WishTag`
@ -58,12 +58,19 @@ type posTagParamMap = {
[k in PositionalTagType]: ConstructorParameters<posTagMap[k]>[0];
};
/**
* Generic type to convert a {@linkcode PositionalTagType} into the serialized representation of its corresponding class instance.
*
* Used in place of a mapped type to work around Typescript deficiencies in function type signatures.
*/
export type toSerializedPosTag<T extends PositionalTagType> = posTagParamMap[T] & { readonly tagType: T };
/**
* Type mapping all positional tag types to their constructors' parameters, alongside the `tagType` selector.
* Equivalent to their serialized representations.
*/
export type serializedPosTagMap = {
[k in PositionalTagType]: posTagParamMap[k] & { tagType: k };
type serializedPosTagMap = {
[k in PositionalTagType]: toSerializedPosTag<k>;
};
/** Union type containing all serialized {@linkcode PositionalTag}s. */

View File

@ -1,4 +1,4 @@
import { loadPositionalTag } from "#data/positional-tags/load-positional-tag";
import { loadPositionalTag, type toSerializedPosTag } from "#data/positional-tags/load-positional-tag";
import type { PositionalTag } from "#data/positional-tags/positional-tag";
import type { BattlerIndex } from "#enums/battler-index";
import type { PositionalTagType } from "#enums/positional-tag-type";
@ -16,7 +16,7 @@ export class PositionalTagManager {
* @remarks
* This function does not perform any checking if the added tag is valid.
*/
public addTag<T extends PositionalTagType = never>(tag: Parameters<typeof loadPositionalTag<T>>[0]): void {
public addTag<T extends PositionalTagType>(tag: toSerializedPosTag<T>): void {
this.tags.push(loadPositionalTag(tag));
}

View File

@ -1,8 +1,6 @@
import { globalScene } from "#app/global-scene";
import { getPokemonNameWithAffix } from "#app/messages";
// biome-ignore-start lint/correctness/noUnusedImports: TSDoc
import type { ArenaTag } from "#data/arena-tag";
// biome-ignore-end lint/correctness/noUnusedImports: TSDoc
import { allMoves } from "#data/data-lists";
import type { BattlerIndex } from "#enums/battler-index";
import type { MoveId } from "#enums/move-id";
@ -20,7 +18,7 @@ import i18next from "i18next";
* and should refrain from adding extra serializable fields not contained in said interface.
* This ensures that all tags truly "become" their respective interfaces when converted to and from JSON.
*/
export interface PositionalTagBaseArgs {
interface PositionalTagBaseArgs {
/**
* The number of turns remaining until this tag's activation. \
* Decremented by 1 at the end of each turn until reaching 0, at which point it will
@ -30,16 +28,16 @@ export interface PositionalTagBaseArgs {
/**
* The {@linkcode BattlerIndex} targeted by this effect.
*/
targetIndex: BattlerIndex;
readonly targetIndex: BattlerIndex;
}
/**
* A {@linkcode PositionalTag} is a variant of an {@linkcode ArenaTag} that targets a single *slot* of the battlefield.
* Each tag can last one or more turns, triggering various effects on removal.
* Each tag can last one or more turns, triggering various effects on removal. \
* Multiple tags of the same kind can stack with one another, provided they are affecting different targets.
*/
export abstract class PositionalTag implements PositionalTagBaseArgs {
/** This tag's {@linkcode PositionalTagType | type} */
/** This tag's {@linkcode PositionalTagType | type}. */
public abstract readonly tagType: PositionalTagType;
// These arguments have to be public to implement the interface, but are functionally private
// outside this and the tag manager.
@ -76,9 +74,9 @@ interface DelayedAttackArgs extends PositionalTagBaseArgs {
/**
* The {@linkcode Pokemon.id | PID} of the {@linkcode Pokemon} having created this effect.
*/
sourceId: number;
readonly sourceId: number;
/** The {@linkcode MoveId} that created this attack. */
sourceMove: MoveId;
readonly sourceMove: MoveId;
}
/**
@ -88,6 +86,7 @@ interface DelayedAttackArgs extends PositionalTagBaseArgs {
*/
export class DelayedAttackTag extends PositionalTag implements DelayedAttackArgs {
public override readonly tagType = PositionalTagType.DELAYED_ATTACK;
public readonly sourceMove: MoveId;
public readonly sourceId: number;
@ -100,10 +99,8 @@ export class DelayedAttackTag extends PositionalTag implements DelayedAttackArgs
public override trigger(): void {
// Bangs are justified as the `shouldTrigger` method will queue the tag for removal
// if the source or target no longer exist
const source = globalScene.getPokemonById(this.sourceId)!;
const target = this.getTarget()!;
source.turnData.extraTurns++;
globalScene.phaseManager.queueMessage(
i18next.t("moveTriggers:tookMoveAttack", {
pokemonName: getPokemonNameWithAffix(target),
@ -113,7 +110,9 @@ export class DelayedAttackTag extends PositionalTag implements DelayedAttackArgs
globalScene.phaseManager.unshiftNew(
"MoveEffectPhase",
this.sourceId, // TODO: Find an alternate method of passing the source pokemon without a source ID
// TODO: Find an alternate method of passing the (currently off-field) source pokemon
// instead of relying on pokemon getter jank
this.sourceId,
[this.targetIndex],
allMoves[this.sourceMove],
MoveUseMode.DELAYED_ATTACK,
@ -135,9 +134,9 @@ export class DelayedAttackTag extends PositionalTag implements DelayedAttackArgs
/** Interface containing arguments used to construct a {@linkcode WishTag}. */
interface WishArgs extends PositionalTagBaseArgs {
/** The amount of {@linkcode Stat.HP | HP} to heal; set to 50% of the user's max HP during move usage. */
healHp: number;
readonly healHp: number;
/** The name of the {@linkcode Pokemon} having created the tag. */
pokemonName: string;
readonly pokemonName: string;
}
/**

View File

@ -158,7 +158,7 @@ export function getRandomStatus(statusA: Status | null, statusB: Status | null):
* Gets all non volatile status effects
* @returns A list containing all non volatile status effects
*/
export function getNonVolatileStatusEffects(): Array<StatusEffect> {
export function getNonVolatileStatusEffects(): StatusEffect[] {
return [
StatusEffect.POISON,
StatusEffect.TOXIC,

View File

@ -21,7 +21,6 @@ import { TrainerVariant } from "#enums/trainer-variant";
import type { EnemyPokemon } from "#field/pokemon";
import type { SpeciesStatBoosterModifier } from "#modifiers/modifier";
import { PokemonMove } from "#moves/pokemon-move";
import { getIsInitialized, initI18n } from "#plugins/i18n";
import type { EvilTeam } from "#trainers/evil-admin-trainer-pools";
import { evilAdminTrainerPools } from "#trainers/evil-admin-trainer-pools";
import {
@ -211,14 +210,8 @@ export class TrainerConfig {
setName(name: string): TrainerConfig {
if (name === "Finn") {
// Give the rival a localized name
// First check if i18n is initialized
if (!getIsInitialized()) {
initI18n();
}
// This is only the male name, because the female name is handled in a different function (setHasGenders)
if (name === "Finn") {
name = i18next.t("trainerNames:rival");
}
name = i18next.t("trainerNames:rival");
}
this.name = name;
@ -235,11 +228,6 @@ export class TrainerConfig {
}
setTitle(title: string): TrainerConfig {
// First check if i18n is initialized
if (!getIsInitialized()) {
initI18n();
}
title = toCamelCase(title);
// Get the title from the i18n file
@ -328,11 +316,6 @@ export class TrainerConfig {
setHasGenders(nameFemale?: string, femaleEncounterBgm?: TrainerType | string): TrainerConfig {
// If the female name is 'Ivy' (the rival), assign a localized name.
if (nameFemale === "Ivy") {
// Check if the internationalization (i18n) system is initialized.
if (!getIsInitialized()) {
// Initialize the i18n system if it is not already initialized.
initI18n();
}
// Set the localized name for the female rival.
this.nameFemale = i18next.t("trainerNames:rivalFemale");
} else {
@ -406,11 +389,6 @@ export class TrainerConfig {
* @returns The updated TrainerConfig instance.
*/
setDoubleTitle(titleDouble: string): TrainerConfig {
// First check if i18n is initialized
if (!getIsInitialized()) {
initI18n();
}
titleDouble = toCamelCase(titleDouble);
// Get the title from the i18n file
@ -595,10 +573,6 @@ export class TrainerConfig {
* @returns The updated TrainerConfig instance.
*/
initForEvilTeamAdmin(title: string, poolName: EvilTeam, specialtyType?: PokemonType): TrainerConfig {
if (!getIsInitialized()) {
initI18n();
}
if (specialtyType != null) {
this.setSpecialtyType(specialtyType);
}
@ -627,10 +601,6 @@ export class TrainerConfig {
* @returns The updated TrainerConfig instance.
*/
initForStatTrainer(_isMale = false): TrainerConfig {
if (!getIsInitialized()) {
initI18n();
}
this.setPartyTemplates(trainerPartyTemplates.ELITE_FOUR);
const nameForCall = toCamelCase(this.name);
@ -659,9 +629,6 @@ export class TrainerConfig {
rematch = false,
specialtyType?: PokemonType,
): TrainerConfig {
if (!getIsInitialized()) {
initI18n();
}
if (rematch) {
this.setPartyTemplates(trainerPartyTemplates.ELITE_FOUR);
} else {
@ -703,11 +670,6 @@ export class TrainerConfig {
ignoreMinTeraWave = false,
teraSlot?: number,
): TrainerConfig {
// Check if the internationalization (i18n) system is initialized.
if (!getIsInitialized()) {
initI18n();
}
// Set the function to generate the Gym Leader's party template.
this.setPartyTemplateFunc(getGymLeaderPartyTemplate);
@ -760,11 +722,6 @@ export class TrainerConfig {
specialtyType?: PokemonType,
teraSlot?: number,
): TrainerConfig {
// Check if the internationalization (i18n) system is initialized.
if (!getIsInitialized()) {
initI18n();
}
// Set the party templates for the Elite Four.
this.setPartyTemplates(trainerPartyTemplates.ELITE_FOUR);
@ -811,11 +768,6 @@ export class TrainerConfig {
* @returns The updated TrainerConfig instance.
*/
initForChampion(isMale: boolean): TrainerConfig {
// Check if the internationalization (i18n) system is initialized.
if (!getIsInitialized()) {
initI18n();
}
// Set the party templates for the Champion.
this.setPartyTemplates(trainerPartyTemplates.CHAMPION);
@ -846,10 +798,6 @@ export class TrainerConfig {
* @returns The updated TrainerConfig instance.
*/
setLocalizedName(name: string): TrainerConfig {
// Check if the internationalization (i18n) system is initialized.
if (!getIsInitialized()) {
initI18n();
}
this.name = i18next.t(`trainerNames:${toCamelCase(name)}`);
return this;
}
@ -863,33 +811,20 @@ export class TrainerConfig {
getTitle(trainerSlot: TrainerSlot = TrainerSlot.NONE, variant: TrainerVariant): string {
const ret = this.name;
// Check if the variant is double and the name for double exists
if (!trainerSlot && variant === TrainerVariant.DOUBLE && this.nameDouble) {
return this.nameDouble;
}
// Female variant
if (this.hasGenders) {
// If the name is already set
if (this.nameFemale) {
// Check if the variant is either female or this is for the partner in a double battle
if (
variant === TrainerVariant.FEMALE
|| (variant === TrainerVariant.DOUBLE && trainerSlot === TrainerSlot.TRAINER_PARTNER)
) {
return this.nameFemale;
}
}
// Check if !variant is true, if so return the name, else return the name with _female appended
else if (variant) {
if (!getIsInitialized()) {
initI18n();
}
// Check if the female version exists in the i18n file
if (i18next.exists(`trainerClasses:${toCamelCase(this.name)}Female`)) {
// If it does, return
return ret + "Female";
}
} else if (variant && i18next.exists(`trainerClasses:${toCamelCase(this.name)}Female`)) {
return ret + "Female";
}
}

View File

@ -59,6 +59,7 @@ export const MoveUseMode = {
* **cannot be reflected by other reflecting effects**.
*/
REFLECTED: 5,
/**
* This "move" was created by a transparent effect that **does not count as using a move**,
* such as {@linkcode DelayedAttackAttr | Future Sight/Doom Desire}.
@ -77,9 +78,10 @@ export type MoveUseMode = ObjectValues<typeof MoveUseMode>;
// Please update the markdown tables if any new `MoveUseMode`s get added.
/**
* Check if a given {@linkcode MoveUseMode} is virtual (i.e. called by another move or effect).
* Check if a given `MoveUseMode` is virtual (i.e. called by another move or effect).
* Virtual moves are ignored by most moveset-related effects due to not being executed directly.
* @returns Whether {@linkcode useMode} is virtual.
* @param useMode - The {@linkcode MoveUseMode} to check
* @returns Whether `useMode` is virtual.
* @remarks
* This function is equivalent to the following truth table:
*
@ -97,10 +99,10 @@ export function isVirtual(useMode: MoveUseMode): boolean {
}
/**
* Check if a given {@linkcode MoveUseMode} should ignore pre-move cancellation checks
* Check if a given `MoveUseMode` should ignore pre-move cancellation checks
* from {@linkcode StatusEffect.PARALYSIS} and {@linkcode BattlerTagLapseType.MOVE}-type effects.
* @param useMode - The {@linkcode MoveUseMode} to check.
* @returns Whether {@linkcode useMode} should ignore status and otehr cancellation checks.
* @param useMode - The {@linkcode MoveUseMode} to check
* @returns Whether `useMode` should ignore status and other cancellation checks.
* @remarks
* This function is equivalent to the following truth table:
*
@ -118,10 +120,10 @@ export function isIgnoreStatus(useMode: MoveUseMode): boolean {
}
/**
* Check if a given {@linkcode MoveUseMode} should ignore PP.
* Check if a given `MoveUseMode` should ignore PP.
* PP-ignoring moves will ignore normal PP consumption as well as associated failure checks.
* @param useMode - The {@linkcode MoveUseMode} to check.
* @returns Whether {@linkcode useMode} ignores PP.
* @param useMode - The {@linkcode MoveUseMode} to check
* @returns Whether `useMode` ignores PP consumption.
* @remarks
* This function is equivalent to the following truth table:
*
@ -139,11 +141,11 @@ export function isIgnorePP(useMode: MoveUseMode): boolean {
}
/**
* Check if a given {@linkcode MoveUseMode} is reflected.
* Check if a given `MoveUseMode` is reflected.
* Reflected moves cannot be reflected, copied, or cancelled by status effects,
* nor will they trigger {@linkcode PostDancingMoveAbAttr | Dancer}.
* @param useMode - The {@linkcode MoveUseMode} to check.
* @returns Whether {@linkcode useMode} is reflected.
* @param useMode - The {@linkcode MoveUseMode} to check
* @returns Whether `useMode` is reflected.
* @remarks
* This function is equivalent to the following truth table:
*

View File

@ -31,6 +31,7 @@ export enum UiMode {
POKEDEX,
POKEDEX_SCAN,
POKEDEX_PAGE,
LOGIN_OR_REGISTER,
LOGIN_FORM,
REGISTRATION_FORM,
LOADING,

66
src/extensions.ts Normal file
View File

@ -0,0 +1,66 @@
import "phaser";
//#region Methods/Interfaces
/**
* Interface representing an object that can be passed to {@linkcode setPositionRelative}.
*/
interface GuideObject
extends Pick<Phaser.GameObjects.Components.ComputedSize, "width" | "height">,
Pick<Phaser.GameObjects.Components.Transform, "x" | "y">,
Pick<Phaser.GameObjects.Components.Origin, "originX" | "originY"> {}
/**
* Set this object's position relative to another object with a given offset.
* @param guideObject - The object to base this object's position off of; must have defined
* x/y co-ordinates, an origin and width/height
* @param x - The X-position to set, relative to `guideObject`'s `x` value
* @param y - The Y-position to set, relative to `guideObject`'s `y` value
* @returns `this`
*/
function setPositionRelative<T extends Phaser.GameObjects.Components.Transform>(
this: T,
guideObject: GuideObject,
x: number,
y: number,
): T {
const offsetX = guideObject.width * (-0.5 + (0.5 - guideObject.originX));
const offsetY = guideObject.height * (-0.5 + (0.5 - guideObject.originY));
return this.setPosition(guideObject.x + offsetX + x, guideObject.y + offsetY + y);
}
Phaser.GameObjects.Container.prototype.setPositionRelative = setPositionRelative;
Phaser.GameObjects.Sprite.prototype.setPositionRelative = setPositionRelative;
Phaser.GameObjects.Image.prototype.setPositionRelative = setPositionRelative;
Phaser.GameObjects.NineSlice.prototype.setPositionRelative = setPositionRelative;
Phaser.GameObjects.Text.prototype.setPositionRelative = setPositionRelative;
Phaser.GameObjects.Rectangle.prototype.setPositionRelative = setPositionRelative;
//#endregion
//#region Declaration Merging
interface hasSetPositionRelative {
/**
* Set this object's position relative to another object with a given offset.
* @param guideObject - The object to base this object's position off of; must have defined
* x/y co-ordinates, an origin and width/height
* @param x - The X-position to set, relative to `guideObject`'s `x` value
* @param y - The Y-position to set, relative to `guideObject`'s `y` value
* @returns `this`
*/
setPositionRelative: typeof setPositionRelative;
}
declare module "phaser" {
namespace GameObjects {
interface Container extends hasSetPositionRelative {}
interface Sprite extends hasSetPositionRelative {}
interface Image extends hasSetPositionRelative {}
interface NineSlice extends hasSetPositionRelative {}
interface Text extends hasSetPositionRelative {}
interface Rectangle extends hasSetPositionRelative {}
}
}
//#endregion

View File

@ -134,7 +134,7 @@ function doFanOutParticle(
}
particle.x = x + sin(trigIndex, f * xSpeed);
particle.y = y + cos(trigIndex, f * ySpeed);
trigIndex = trigIndex + angle;
trigIndex += angle;
f++;
};

View File

@ -1,7 +1,3 @@
// biome-ignore-start lint/correctness/noUnusedImports: TSDoc imports
import type { PositionalTag } from "#data/positional-tags/positional-tag";
// biome-ignore-end lint/correctness/noUnusedImports: TSDoc imports
import { applyAbAttrs } from "#abilities/apply-ab-attrs";
import { globalScene } from "#app/global-scene";
import Overrides from "#app/overrides";
@ -10,6 +6,7 @@ import type { ArenaTag, ArenaTagTypeMap } 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 type { PositionalTag } from "#data/positional-tags/positional-tag";
import { PositionalTagManager } from "#data/positional-tags/positional-tag-manager";
import { getTerrainClearMessage, getTerrainStartMessage, Terrain, TerrainType } from "#data/terrain";
import {
@ -184,7 +181,7 @@ export class Arena {
ret = getPokemonSpecies(species!);
if (ret.subLegendary || ret.legendary || ret.mythical) {
const waveDifficulty = globalScene.gameMode.getWaveForDifficulty(waveIndex);
const waveDifficulty = globalScene.gameMode.getWaveForDifficulty(waveIndex, true);
if (ret.baseTotal >= 660) {
regen = waveDifficulty < 80; // Wave 50+ in daily (however, max Daily wave is 50 currently so not possible)
} else {

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