diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000000..219c096336b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +# .dockerignore +node_modules +*.log +*.md +.gitignore +Dockerfile +.env \ No newline at end of file diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index f2e17898334..fea857355a0 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -55,15 +55,15 @@ jobs: - name: Create release branch run: git checkout -b release - # In order to be able to open a PR into beta, we need the branch to have at least one change. - - name: Overwrite RELEASE file + # In order to be able to open a PR into beta, we need the branch to have at least one commit. + # The first commit is _usually_ just bumping the version number, so we can kill 2 birds with 1 stone here + - name: Bump release version run: | git config --local user.name "github-actions[bot]" git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" - echo "Release v${{ github.event.inputs.versionName }}" > RELEASE - git add RELEASE - git commit -m "Stage release v${{ github.event.inputs.versionName }}" - + pnpm --no-git-tag-version version ${{ github.events.inputs.versionName }} + git commit -am "Stage release for v${{ github.events.inputs.versionName }}" + - name: Push new branch run: git push origin release diff --git a/.github/workflows/deploy-beta.yml b/.github/workflows/deploy-beta.yml index 341999dcd45..5abba4488be 100644 --- a/.github/workflows/deploy-beta.yml +++ b/.github/workflows/deploy-beta.yml @@ -22,8 +22,6 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v4 - with: - version: 10 - uses: actions/setup-node@v4 with: diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 528906196e5..1f2c1259dd1 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -20,8 +20,6 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v4 - with: - version: 10 - uses: actions/setup-node@v4 with: diff --git a/.github/workflows/github-pages.yml b/.github/workflows/github-pages.yml index 6e056bc4032..d8482cba83c 100644 --- a/.github/workflows/github-pages.yml +++ b/.github/workflows/github-pages.yml @@ -27,20 +27,18 @@ jobs: # Only push docs when running on pushes to main/beta DRY_RUN: ${{github.event_name != 'push' || (github.ref_name != 'beta' && github.ref_name != 'main')}} - strategy: - fail-fast: false - steps: - name: Checkout repository for Typedoc uses: actions/checkout@v4 with: - submodules: 'recursive' path: pokerogue_docs - - - name: Install OS package - run: | - sudo apt update - sudo apt install -y git openssh-client + sparse-checkout: | + /* + !/public/ + /public/images/pokemon/variant/_exp_masterlist.json + /public/images/pokemon/variant/_masterlist.json + /public/images/logo.png + sparse-checkout-cone-mode: false - name: Install pnpm uses: pnpm/action-setup@v4 diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index edecae64f95..e1314c2cbd3 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -30,8 +30,6 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v4 - with: - version: 10 - name: Set up Node uses: actions/setup-node@v4 diff --git a/.github/workflows/test-shard-template.yml b/.github/workflows/test-shard-template.yml index 79aea56bbd0..6f4728863b4 100644 --- a/.github/workflows/test-shard-template.yml +++ b/.github/workflows/test-shard-template.yml @@ -32,8 +32,6 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v4 - with: - version: 10 - name: Set up Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 39506096298..e1d6e3af60e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -28,6 +28,10 @@ jobs: steps: - name: checkout uses: actions/checkout@v4 + with: + sparse-checkout: | + .github/test-filters.yml + sparse-checkout-cone-mode: false - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 id: filter diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 04ab7ff4faa..c24b648c490 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -80,7 +80,8 @@ 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! diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000000..ddb865b4831 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,47 @@ +# syntax=docker/dockerfile:1 +ARG NODE_VERSION=22.14 +ARG OS=alpine + +FROM node:${NODE_VERSION}-${OS} + +# Create non-root user +RUN addgroup -S appgroup && adduser -S appuser -G appgroup + +# Install git (for potential runtime needs) +RUN apk add --no-cache git + +# Set working directory +WORKDIR /app + +# Enable and prepare pnpm +RUN corepack enable && corepack prepare pnpm@10.14.0 --activate + +COPY . . + +# Copy package files +COPY package.json pnpm-lock.yaml ./ + +# Install all dependencies +RUN --mount=type=cache,target=/home/appuser/.pnpm-store \ + pnpm install --frozen-lockfile && \ + rm -rf /home/appuser/.pnpm-store/* + +# Change ownership +RUN chown -R appuser:appgroup /app + +# Switch to non-root user +USER appuser + +# Set environment variables +ENV VITE_BYPASS_LOGIN=1 \ + VITE_BYPASS_TUTORIAL=0 \ + NEXT_TELEMETRY_DISABLED=1 \ + PNP_HOME=/home/appuser/.shrc \ + NODE_ENV=development \ + PORT=8000 + +# Expose port +EXPOSE $PORT + +# Start the app in development mode +CMD ["pnpm", "run", "start:podman"] diff --git a/RELEASE b/RELEASE deleted file mode 100644 index a1a9f30b0e8..00000000000 --- a/RELEASE +++ /dev/null @@ -1 +0,0 @@ -Release v1.10.0 diff --git a/biome.jsonc b/biome.jsonc index e1aac032597..2433ba52010 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.2.3/schema.json", + "$schema": "https://biomejs.dev/schemas/2.2.4/schema.json", "vcs": { "enabled": true, "clientKind": "git", @@ -98,7 +98,9 @@ "useTrimStartEnd": "error", "useReadonlyClassProperties": { "level": "info", // TODO: Graduate to error eventually - "options": { "checkAllProperties": true } + // NOTE: "checkAllProperties" has an immature implementation that + // causes many false positives across files. Enable if/when maturity improves + "options": { "checkAllProperties": false } }, "useConsistentObjectDefinitions": { "level": "error", @@ -209,11 +211,15 @@ "nursery": { "noUselessUndefined": "error", "useMaxParams": { - "level": "warn", // TODO: Change to "error"... eventually... - "options": { "max": 4 } // A lot of stuff has a few params, but + "level": "info", // TODO: Change to "error"... eventually... + "options": { "max": 7 } }, "noShadow": "warn", // TODO: refactor and make "error" - "noNonNullAssertedOptionalChain": "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 + "noMisusedPromises": "info" } } }, @@ -248,16 +254,9 @@ }, // Overrides to prevent unused import removal inside `overrides.ts`, enums & `.d.ts` files (for TSDoc linkcodes), - // as well as inside script boilerplate files. + // as well as inside script boilerplate files (whose imports will _presumably_ be used in the generated file). { - // TODO: Rename existing boilerplates in the folder and remove this last alias - "includes": [ - "**/src/overrides.ts", - "**/src/enums/**/*", - "**/*.d.ts", - "scripts/**/*.boilerplate.ts", - "**/boilerplates/*.ts" - ], + "includes": ["**/src/overrides.ts", "**/src/enums/**/*", "**/*.d.ts", "scripts/**/*.boilerplate.ts"], "linter": { "rules": { "correctness": { diff --git a/docs/podman.md b/docs/podman.md new file mode 100644 index 00000000000..dea52131e92 --- /dev/null +++ b/docs/podman.md @@ -0,0 +1,27 @@ +# Using Podman + +## Requirements + +* `podman >=5.x` + +## Steps + +1. `podman build -t pokerogue -f Dockerfile .` +2. `podman create --name temp-pokerogue localhost/pokerogue` +3. `podman cp temp-pokerogue:/app/node_modules ./` +4. `podman cp temp-pokerogue:/app/public/locales ./public/` +5. `podman rm temp-pokerogue` +6. `podman run --rm -p 8000:8000 -v $(pwd):/app:Z --userns=keep-id -u $(id -u):$(id -g) localhost/pokerogue` +7. Visit `http://localhost:8000/` + +Note: + +1. Steps 2,3,4 are required because mounting working directory without installed `node_modules/` and assets locally will be empty, +this way we prevent it by copying them from the container itself to local directory + +2. `podman run` may take a couple of minutes to mount the working directory + +### Running tests inside container + +`podman run --rm -p 8000:8000 -v $(pwd):/app:Z --userns=keep-id -u $(id -u):$(id -g) localhost/pokerogue pnpm test:silent +` \ No newline at end of file diff --git a/package.json b/package.json index 65936f2599a..ac8bca50f76 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,10 @@ "start:prod": "vite --mode production", "start:beta": "vite --mode beta", "start:dev": "vite --mode development", + "start:podman": "vite --mode development --host 0.0.0.0 --port $PORT", "build": "vite build", "build:beta": "vite build --mode beta", + "build:dev": "vite build --mode development", "preview": "vite preview", "test": "vitest run --no-isolate", "test:cov": "vitest run --coverage --no-isolate", @@ -31,7 +33,7 @@ "update-locales:remote": "git submodule update --progress --init --recursive --force --remote" }, "devDependencies": { - "@biomejs/biome": "2.2.3", + "@biomejs/biome": "2.2.4", "@ls-lint/ls-lint": "2.3.1", "@types/crypto-js": "^4.2.0", "@types/jsdom": "^21.1.7", @@ -46,12 +48,12 @@ "lefthook": "^1.12.2", "msw": "^2.10.4", "phaser3spectorjs": "^0.0.8", - "typedoc": "^0.28.8", + "typedoc": "^0.28.13", "typedoc-github-theme": "^0.3.1", "typedoc-plugin-coverage": "^4.0.1", "typedoc-plugin-mdn-links": "^5.0.9", - "typescript": "^5.8.3", - "vite": "^7.0.6", + "typescript": "^5.9.2", + "vite": "^7.0.7", "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.2.4", "vitest-canvas-mock": "^0.3.3" @@ -71,5 +73,6 @@ }, "engines": { "node": ">=22.0.0" - } + }, + "packageManager": "pnpm@10.17.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 54012c3dd86..50a8b17b366 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,7 +19,7 @@ importers: version: 4.2.0 i18next: specifier: ^24.2.3 - version: 24.2.3(typescript@5.8.3) + version: 24.2.3(typescript@5.9.2) i18next-browser-languagedetector: specifier: ^8.2.0 version: 8.2.0 @@ -28,7 +28,7 @@ importers: version: 3.0.2 i18next-korean-postposition-processor: specifier: ^1.0.0 - version: 1.0.0(i18next@24.2.3(typescript@5.8.3)) + version: 1.0.0(i18next@24.2.3(typescript@5.9.2)) json-stable-stringify: specifier: ^1.3.0 version: 1.3.0 @@ -43,8 +43,8 @@ importers: version: 1.80.16(graphology-types@0.24.8) devDependencies: '@biomejs/biome': - specifier: 2.2.3 - version: 2.2.3 + specifier: 2.2.4 + version: 2.2.4 '@ls-lint/ls-lint': specifier: 2.3.1 version: 2.3.1 @@ -59,7 +59,7 @@ importers: version: 22.16.5 '@vitest/coverage-istanbul': specifier: ^3.2.4 - version: 3.2.4(vitest@3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(yaml@2.8.0)) + version: 3.2.4(vitest@3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.9.2))(yaml@2.8.1)) '@vitest/expect': specifier: ^3.2.4 version: 3.2.4 @@ -83,37 +83,37 @@ importers: version: 1.12.2 msw: specifier: ^2.10.4 - version: 2.10.4(@types/node@22.16.5)(typescript@5.8.3) + version: 2.10.4(@types/node@22.16.5)(typescript@5.9.2) phaser3spectorjs: specifier: ^0.0.8 version: 0.0.8 typedoc: - specifier: ^0.28.8 - version: 0.28.8(typescript@5.8.3) + specifier: ^0.28.13 + version: 0.28.13(typescript@5.9.2) typedoc-github-theme: specifier: ^0.3.1 - version: 0.3.1(typedoc@0.28.8(typescript@5.8.3)) + version: 0.3.1(typedoc@0.28.13(typescript@5.9.2)) typedoc-plugin-coverage: specifier: ^4.0.1 - version: 4.0.1(typedoc@0.28.8(typescript@5.8.3)) + version: 4.0.1(typedoc@0.28.13(typescript@5.9.2)) typedoc-plugin-mdn-links: specifier: ^5.0.9 - version: 5.0.9(typedoc@0.28.8(typescript@5.8.3)) + version: 5.0.9(typedoc@0.28.13(typescript@5.9.2)) typescript: - specifier: ^5.8.3 - version: 5.8.3 + specifier: ^5.9.2 + version: 5.9.2 vite: - specifier: ^7.0.6 - version: 7.0.6(@types/node@22.16.5)(yaml@2.8.0) + specifier: ^7.0.7 + version: 7.0.7(@types/node@22.16.5)(yaml@2.8.1) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.8.3)(vite@7.0.6(@types/node@22.16.5)(yaml@2.8.0)) + version: 5.1.4(typescript@5.9.2)(vite@7.0.7(@types/node@22.16.5)(yaml@2.8.1)) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(yaml@2.8.0) + version: 3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.9.2))(yaml@2.8.1) vitest-canvas-mock: specifier: ^0.3.3 - version: 0.3.3(vitest@3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(yaml@2.8.0)) + version: 0.3.3(vitest@3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.9.2))(yaml@2.8.1)) packages: @@ -195,55 +195,55 @@ packages: resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} engines: {node: '>=6.9.0'} - '@biomejs/biome@2.2.3': - resolution: {integrity: sha512-9w0uMTvPrIdvUrxazZ42Ib7t8Y2yoGLKLdNne93RLICmaHw7mcLv4PPb5LvZLJF3141gQHiCColOh/v6VWlWmg==} + '@biomejs/biome@2.2.4': + resolution: {integrity: sha512-TBHU5bUy/Ok6m8c0y3pZiuO/BZoY/OcGxoLlrfQof5s8ISVwbVBdFINPQZyFfKwil8XibYWb7JMwnT8wT4WVPg==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@2.2.3': - resolution: {integrity: sha512-OrqQVBpadB5eqzinXN4+Q6honBz+tTlKVCsbEuEpljK8ASSItzIRZUA02mTikl3H/1nO2BMPFiJ0nkEZNy3B1w==} + '@biomejs/cli-darwin-arm64@2.2.4': + resolution: {integrity: sha512-RJe2uiyaloN4hne4d2+qVj3d3gFJFbmrr5PYtkkjei1O9c+BjGXgpUPVbi8Pl8syumhzJjFsSIYkcLt2VlVLMA==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@2.2.3': - resolution: {integrity: sha512-OCdBpb1TmyfsTgBAM1kPMXyYKTohQ48WpiN9tkt9xvU6gKVKHY4oVwteBebiOqyfyzCNaSiuKIPjmHjUZ2ZNMg==} + '@biomejs/cli-darwin-x64@2.2.4': + resolution: {integrity: sha512-cFsdB4ePanVWfTnPVaUX+yr8qV8ifxjBKMkZwN7gKb20qXPxd/PmwqUH8mY5wnM9+U0QwM76CxFyBRJhC9tQwg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@2.2.3': - resolution: {integrity: sha512-q3w9jJ6JFPZPeqyvwwPeaiS/6NEszZ+pXKF+IczNo8Xj6fsii45a4gEEicKyKIytalV+s829ACZujQlXAiVLBQ==} + '@biomejs/cli-linux-arm64-musl@2.2.4': + resolution: {integrity: sha512-7TNPkMQEWfjvJDaZRSkDCPT/2r5ESFPKx+TEev+I2BXDGIjfCZk2+b88FOhnJNHtksbOZv8ZWnxrA5gyTYhSsQ==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - '@biomejs/cli-linux-arm64@2.2.3': - resolution: {integrity: sha512-g/Uta2DqYpECxG+vUmTAmUKlVhnGEcY7DXWgKP8ruLRa8Si1QHsWknPY3B/wCo0KgYiFIOAZ9hjsHfNb9L85+g==} + '@biomejs/cli-linux-arm64@2.2.4': + resolution: {integrity: sha512-M/Iz48p4NAzMXOuH+tsn5BvG/Jb07KOMTdSVwJpicmhN309BeEyRyQX+n1XDF0JVSlu28+hiTQ2L4rZPvu7nMw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - '@biomejs/cli-linux-x64-musl@2.2.3': - resolution: {integrity: sha512-y76Dn4vkP1sMRGPFlNc+OTETBhGPJ90jY3il6jAfur8XWrYBQV3swZ1Jo0R2g+JpOeeoA0cOwM7mJG6svDz79w==} + '@biomejs/cli-linux-x64-musl@2.2.4': + resolution: {integrity: sha512-m41nFDS0ksXK2gwXL6W6yZTYPMH0LughqbsxInSKetoH6morVj43szqKx79Iudkp8WRT5SxSh7qVb8KCUiewGg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - '@biomejs/cli-linux-x64@2.2.3': - resolution: {integrity: sha512-LEtyYL1fJsvw35CxrbQ0gZoxOG3oZsAjzfRdvRBRHxOpQ91Q5doRVjvWW/wepgSdgk5hlaNzfeqpyGmfSD0Eyw==} + '@biomejs/cli-linux-x64@2.2.4': + resolution: {integrity: sha512-orr3nnf2Dpb2ssl6aihQtvcKtLySLta4E2UcXdp7+RTa7mfJjBgIsbS0B9GC8gVu0hjOu021aU8b3/I1tn+pVQ==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - '@biomejs/cli-win32-arm64@2.2.3': - resolution: {integrity: sha512-Ms9zFYzjcJK7LV+AOMYnjN3pV3xL8Prxf9aWdDVL74onLn5kcvZ1ZMQswE5XHtnd/r/0bnUd928Rpbs14BzVmA==} + '@biomejs/cli-win32-arm64@2.2.4': + resolution: {integrity: sha512-NXnfTeKHDFUWfxAefa57DiGmu9VyKi0cDqFpdI+1hJWQjGJhJutHPX0b5m+eXvTKOaf+brU+P0JrQAZMb5yYaQ==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@2.2.3': - resolution: {integrity: sha512-gvCpewE7mBwBIpqk1YrUqNR4mCiyJm6UI3YWQQXkedSSEwzRdodRpaKhbdbHw1/hmTWOVXQ+Eih5Qctf4TCVOQ==} + '@biomejs/cli-win32-x64@2.2.4': + resolution: {integrity: sha512-3Y4V4zVRarVh/B/eSHczR4LYoSVyv3Dfuvm3cWs5w/HScccS0+Wt/lHOcDTRYeHjQmMYVC3rIRWqyN2EI52+zg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] @@ -285,164 +285,164 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} - '@esbuild/aix-ppc64@0.25.8': - resolution: {integrity: sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==} + '@esbuild/aix-ppc64@0.25.9': + resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.8': - resolution: {integrity: sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==} + '@esbuild/android-arm64@0.25.9': + resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.8': - resolution: {integrity: sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==} + '@esbuild/android-arm@0.25.9': + resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.8': - resolution: {integrity: sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==} + '@esbuild/android-x64@0.25.9': + resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.8': - resolution: {integrity: sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==} + '@esbuild/darwin-arm64@0.25.9': + resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.8': - resolution: {integrity: sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==} + '@esbuild/darwin-x64@0.25.9': + resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.8': - resolution: {integrity: sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==} + '@esbuild/freebsd-arm64@0.25.9': + resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.8': - resolution: {integrity: sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==} + '@esbuild/freebsd-x64@0.25.9': + resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.8': - resolution: {integrity: sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==} + '@esbuild/linux-arm64@0.25.9': + resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.8': - resolution: {integrity: sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==} + '@esbuild/linux-arm@0.25.9': + resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.8': - resolution: {integrity: sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==} + '@esbuild/linux-ia32@0.25.9': + resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.8': - resolution: {integrity: sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==} + '@esbuild/linux-loong64@0.25.9': + resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.8': - resolution: {integrity: sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==} + '@esbuild/linux-mips64el@0.25.9': + resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.8': - resolution: {integrity: sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==} + '@esbuild/linux-ppc64@0.25.9': + resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.8': - resolution: {integrity: sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==} + '@esbuild/linux-riscv64@0.25.9': + resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.8': - resolution: {integrity: sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==} + '@esbuild/linux-s390x@0.25.9': + resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.8': - resolution: {integrity: sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==} + '@esbuild/linux-x64@0.25.9': + resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.8': - resolution: {integrity: sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==} + '@esbuild/netbsd-arm64@0.25.9': + resolution: {integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.8': - resolution: {integrity: sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==} + '@esbuild/netbsd-x64@0.25.9': + resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.8': - resolution: {integrity: sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==} + '@esbuild/openbsd-arm64@0.25.9': + resolution: {integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.8': - resolution: {integrity: sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==} + '@esbuild/openbsd-x64@0.25.9': + resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.25.8': - resolution: {integrity: sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==} + '@esbuild/openharmony-arm64@0.25.9': + resolution: {integrity: sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.25.8': - resolution: {integrity: sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==} + '@esbuild/sunos-x64@0.25.9': + resolution: {integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.8': - resolution: {integrity: sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==} + '@esbuild/win32-arm64@0.25.9': + resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.8': - resolution: {integrity: sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==} + '@esbuild/win32-ia32@0.25.9': + resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.8': - resolution: {integrity: sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==} + '@esbuild/win32-x64@0.25.9': + resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==} engines: {node: '>=18'} cpu: [x64] os: [win32] - '@gerrit0/mini-shiki@3.8.1': - resolution: {integrity: sha512-HVZW+8pxoOExr5ZMPK15U79jQAZTO/S6i5byQyyZGjtNj+qaYd82cizTncwFzTQgiLo8uUBym6vh+/1tfJklTw==} + '@gerrit0/mini-shiki@3.12.2': + resolution: {integrity: sha512-HKZPmO8OSSAAo20H2B3xgJdxZaLTwtlMwxg0967scnrDlPwe6j5+ULGHyIqwgTbFCn9yv/ff8CmfWZLE9YKBzA==} '@inquirer/checkbox@4.2.0': resolution: {integrity: sha512-fdSw07FLJEU5vbpOPzXo5c6xmMGDzbZE2+niuDHX5N6mc6V0Ebso/q3xiHra4D73+PMsC8MJmcaZKuAAoaQsSA==} @@ -612,117 +612,122 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@rollup/rollup-android-arm-eabi@4.46.1': - resolution: {integrity: sha512-oENme6QxtLCqjChRUUo3S6X8hjCXnWmJWnedD7VbGML5GUtaOtAyx+fEEXnBXVf0CBZApMQU0Idwi0FmyxzQhw==} + '@rollup/rollup-android-arm-eabi@4.50.1': + resolution: {integrity: sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.46.1': - resolution: {integrity: sha512-OikvNT3qYTl9+4qQ9Bpn6+XHM+ogtFadRLuT2EXiFQMiNkXFLQfNVppi5o28wvYdHL2s3fM0D/MZJ8UkNFZWsw==} + '@rollup/rollup-android-arm64@4.50.1': + resolution: {integrity: sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.46.1': - resolution: {integrity: sha512-EFYNNGij2WllnzljQDQnlFTXzSJw87cpAs4TVBAWLdkvic5Uh5tISrIL6NRcxoh/b2EFBG/TK8hgRrGx94zD4A==} + '@rollup/rollup-darwin-arm64@4.50.1': + resolution: {integrity: sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.46.1': - resolution: {integrity: sha512-ZaNH06O1KeTug9WI2+GRBE5Ujt9kZw4a1+OIwnBHal92I8PxSsl5KpsrPvthRynkhMck4XPdvY0z26Cym/b7oA==} + '@rollup/rollup-darwin-x64@4.50.1': + resolution: {integrity: sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.46.1': - resolution: {integrity: sha512-n4SLVebZP8uUlJ2r04+g2U/xFeiQlw09Me5UFqny8HGbARl503LNH5CqFTb5U5jNxTouhRjai6qPT0CR5c/Iig==} + '@rollup/rollup-freebsd-arm64@4.50.1': + resolution: {integrity: sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.46.1': - resolution: {integrity: sha512-8vu9c02F16heTqpvo3yeiu7Vi1REDEC/yES/dIfq3tSXe6mLndiwvYr3AAvd1tMNUqE9yeGYa5w7PRbI5QUV+w==} + '@rollup/rollup-freebsd-x64@4.50.1': + resolution: {integrity: sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.46.1': - resolution: {integrity: sha512-K4ncpWl7sQuyp6rWiGUvb6Q18ba8mzM0rjWJ5JgYKlIXAau1db7hZnR0ldJvqKWWJDxqzSLwGUhA4jp+KqgDtQ==} + '@rollup/rollup-linux-arm-gnueabihf@4.50.1': + resolution: {integrity: sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.46.1': - resolution: {integrity: sha512-YykPnXsjUjmXE6j6k2QBBGAn1YsJUix7pYaPLK3RVE0bQL2jfdbfykPxfF8AgBlqtYbfEnYHmLXNa6QETjdOjQ==} + '@rollup/rollup-linux-arm-musleabihf@4.50.1': + resolution: {integrity: sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.46.1': - resolution: {integrity: sha512-kKvqBGbZ8i9pCGW3a1FH3HNIVg49dXXTsChGFsHGXQaVJPLA4f/O+XmTxfklhccxdF5FefUn2hvkoGJH0ScWOA==} + '@rollup/rollup-linux-arm64-gnu@4.50.1': + resolution: {integrity: sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.46.1': - resolution: {integrity: sha512-zzX5nTw1N1plmqC9RGC9vZHFuiM7ZP7oSWQGqpbmfjK7p947D518cVK1/MQudsBdcD84t6k70WNczJOct6+hdg==} + '@rollup/rollup-linux-arm64-musl@4.50.1': + resolution: {integrity: sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.46.1': - resolution: {integrity: sha512-O8CwgSBo6ewPpktFfSDgB6SJN9XDcPSvuwxfejiddbIC/hn9Tg6Ai0f0eYDf3XvB/+PIWzOQL+7+TZoB8p9Yuw==} + '@rollup/rollup-linux-loongarch64-gnu@4.50.1': + resolution: {integrity: sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-ppc64-gnu@4.46.1': - resolution: {integrity: sha512-JnCfFVEKeq6G3h3z8e60kAp8Rd7QVnWCtPm7cxx+5OtP80g/3nmPtfdCXbVl063e3KsRnGSKDHUQMydmzc/wBA==} + '@rollup/rollup-linux-ppc64-gnu@4.50.1': + resolution: {integrity: sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.46.1': - resolution: {integrity: sha512-dVxuDqS237eQXkbYzQQfdf/njgeNw6LZuVyEdUaWwRpKHhsLI+y4H/NJV8xJGU19vnOJCVwaBFgr936FHOnJsQ==} + '@rollup/rollup-linux-riscv64-gnu@4.50.1': + resolution: {integrity: sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.46.1': - resolution: {integrity: sha512-CvvgNl2hrZrTR9jXK1ye0Go0HQRT6ohQdDfWR47/KFKiLd5oN5T14jRdUVGF4tnsN8y9oSfMOqH6RuHh+ck8+w==} + '@rollup/rollup-linux-riscv64-musl@4.50.1': + resolution: {integrity: sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.46.1': - resolution: {integrity: sha512-x7ANt2VOg2565oGHJ6rIuuAon+A8sfe1IeUx25IKqi49OjSr/K3awoNqr9gCwGEJo9OuXlOn+H2p1VJKx1psxA==} + '@rollup/rollup-linux-s390x-gnu@4.50.1': + resolution: {integrity: sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.46.1': - resolution: {integrity: sha512-9OADZYryz/7E8/qt0vnaHQgmia2Y0wrjSSn1V/uL+zw/i7NUhxbX4cHXdEQ7dnJgzYDS81d8+tf6nbIdRFZQoQ==} + '@rollup/rollup-linux-x64-gnu@4.50.1': + resolution: {integrity: sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.46.1': - resolution: {integrity: sha512-NuvSCbXEKY+NGWHyivzbjSVJi68Xfq1VnIvGmsuXs6TCtveeoDRKutI5vf2ntmNnVq64Q4zInet0UDQ+yMB6tA==} + '@rollup/rollup-linux-x64-musl@4.50.1': + resolution: {integrity: sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.46.1': - resolution: {integrity: sha512-mWz+6FSRb82xuUMMV1X3NGiaPFqbLN9aIueHleTZCc46cJvwTlvIh7reQLk4p97dv0nddyewBhwzryBHH7wtPw==} + '@rollup/rollup-openharmony-arm64@4.50.1': + resolution: {integrity: sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.50.1': + resolution: {integrity: sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.46.1': - resolution: {integrity: sha512-7Thzy9TMXDw9AU4f4vsLNBxh7/VOKuXi73VH3d/kHGr0tZ3x/ewgL9uC7ojUKmH1/zvmZe2tLapYcZllk3SO8Q==} + '@rollup/rollup-win32-ia32-msvc@4.50.1': + resolution: {integrity: sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.46.1': - resolution: {integrity: sha512-7GVB4luhFmGUNXXJhH2jJwZCFB3pIOixv2E3s17GQHBFUOQaISlt7aGcQgqvCaDSxTZJUzlK/QJ1FN8S94MrzQ==} + '@rollup/rollup-win32-x64-msvc@4.50.1': + resolution: {integrity: sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA==} cpu: [x64] os: [win32] - '@shikijs/engine-oniguruma@3.8.1': - resolution: {integrity: sha512-KGQJZHlNY7c656qPFEQpIoqOuC4LrxjyNndRdzk5WKB/Ie87+NJCF1xo9KkOUxwxylk7rT6nhlZyTGTC4fCe1g==} + '@shikijs/engine-oniguruma@3.13.0': + resolution: {integrity: sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg==} - '@shikijs/langs@3.8.1': - resolution: {integrity: sha512-TjOFg2Wp1w07oKnXjs0AUMb4kJvujML+fJ1C5cmEj45lhjbUXtziT1x2bPQb9Db6kmPhkG5NI2tgYW1/DzhUuQ==} + '@shikijs/langs@3.13.0': + resolution: {integrity: sha512-672c3WAETDYHwrRP0yLy3W1QYB89Hbpj+pO4KhxK6FzIrDI2FoEXNiNCut6BQmEApYLfuYfpgOZaqbY+E9b8wQ==} - '@shikijs/themes@3.8.1': - resolution: {integrity: sha512-Vu3t3BBLifc0GB0UPg2Pox1naTemrrvyZv2lkiSw3QayVV60me1ujFQwPZGgUTmwXl1yhCPW8Lieesm0CYruLQ==} + '@shikijs/themes@3.13.0': + resolution: {integrity: sha512-Vxw1Nm1/Od8jyA7QuAenaV78BG2nSr3/gCGdBkLpfLscddCkzkL36Q5b67SrLLfvAJTOUzW39x4FHVCFriPVgg==} - '@shikijs/types@3.8.1': - resolution: {integrity: sha512-5C39Q8/8r1I26suLh+5TPk1DTrbY/kn3IdWA5HdizR0FhlhD05zx5nKCqhzSfDHH3p4S0ZefxWd77DLV+8FhGg==} + '@shikijs/types@3.13.0': + resolution: {integrity: sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw==} '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} @@ -1022,8 +1027,8 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} - esbuild@0.25.8: - resolution: {integrity: sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==} + esbuild@0.25.9: + resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==} engines: {node: '>=18'} hasBin: true @@ -1058,8 +1063,9 @@ packages: fast-uri@3.0.6: resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} - fdir@6.4.6: - resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} peerDependencies: picomatch: ^3 || ^4 peerDependenciesMeta: @@ -1597,8 +1603,8 @@ packages: engines: {node: '>= 0.4'} hasBin: true - rollup@4.46.1: - resolution: {integrity: sha512-33xGNBsDJAkzt0PvninskHlWnTIPgDtTwhg0U38CUoNP/7H6wI2Cz6dUeoNPbjdTdsYTGuiFFASuUOWovH0SyQ==} + rollup@4.50.1: + resolution: {integrity: sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -1734,6 +1740,10 @@ packages: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + tinypool@1.1.1: resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} engines: {node: ^18.0.0 || >=20.0.0} @@ -1818,15 +1828,15 @@ packages: peerDependencies: typedoc: 0.27.x || 0.28.x - typedoc@0.28.8: - resolution: {integrity: sha512-16GfLopc8icHfdvqZDqdGBoS2AieIRP2rpf9mU+MgN+gGLyEQvAO0QgOa6NJ5QNmQi0LFrDY9in4F2fUNKgJKA==} + typedoc@0.28.13: + resolution: {integrity: sha512-dNWY8msnYB2a+7Audha+aTF1Pu3euiE7ySp53w8kEsXoYw7dMouV5A1UsTUY345aB152RHnmRMDiovuBi7BD+w==} engines: {node: '>= 18', pnpm: '>= 10'} hasBin: true peerDependencies: - typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x + typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x - typescript@5.8.3: - resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + typescript@5.9.2: + resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} engines: {node: '>=14.17'} hasBin: true @@ -1865,8 +1875,8 @@ packages: vite: optional: true - vite@7.0.6: - resolution: {integrity: sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==} + vite@7.0.7: + resolution: {integrity: sha512-hc6LujN/EkJHmxeiDJMs0qBontZ1cdBvvoCbWhVjzUFTU329VRyOC46gHNSA8NcOC5yzCeXpwI40tieI3DEZqg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -2020,8 +2030,8 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yaml@2.8.0: - resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==} + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} engines: {node: '>= 14.6'} hasBin: true @@ -2154,39 +2164,39 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 - '@biomejs/biome@2.2.3': + '@biomejs/biome@2.2.4': optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.2.3 - '@biomejs/cli-darwin-x64': 2.2.3 - '@biomejs/cli-linux-arm64': 2.2.3 - '@biomejs/cli-linux-arm64-musl': 2.2.3 - '@biomejs/cli-linux-x64': 2.2.3 - '@biomejs/cli-linux-x64-musl': 2.2.3 - '@biomejs/cli-win32-arm64': 2.2.3 - '@biomejs/cli-win32-x64': 2.2.3 + '@biomejs/cli-darwin-arm64': 2.2.4 + '@biomejs/cli-darwin-x64': 2.2.4 + '@biomejs/cli-linux-arm64': 2.2.4 + '@biomejs/cli-linux-arm64-musl': 2.2.4 + '@biomejs/cli-linux-x64': 2.2.4 + '@biomejs/cli-linux-x64-musl': 2.2.4 + '@biomejs/cli-win32-arm64': 2.2.4 + '@biomejs/cli-win32-x64': 2.2.4 - '@biomejs/cli-darwin-arm64@2.2.3': + '@biomejs/cli-darwin-arm64@2.2.4': optional: true - '@biomejs/cli-darwin-x64@2.2.3': + '@biomejs/cli-darwin-x64@2.2.4': optional: true - '@biomejs/cli-linux-arm64-musl@2.2.3': + '@biomejs/cli-linux-arm64-musl@2.2.4': optional: true - '@biomejs/cli-linux-arm64@2.2.3': + '@biomejs/cli-linux-arm64@2.2.4': optional: true - '@biomejs/cli-linux-x64-musl@2.2.3': + '@biomejs/cli-linux-x64-musl@2.2.4': optional: true - '@biomejs/cli-linux-x64@2.2.3': + '@biomejs/cli-linux-x64@2.2.4': optional: true - '@biomejs/cli-win32-arm64@2.2.3': + '@biomejs/cli-win32-arm64@2.2.4': optional: true - '@biomejs/cli-win32-x64@2.2.3': + '@biomejs/cli-win32-x64@2.2.4': optional: true '@bundled-es-modules/cookie@2.0.1': @@ -2222,90 +2232,90 @@ snapshots: '@csstools/css-tokenizer@3.0.4': {} - '@esbuild/aix-ppc64@0.25.8': + '@esbuild/aix-ppc64@0.25.9': optional: true - '@esbuild/android-arm64@0.25.8': + '@esbuild/android-arm64@0.25.9': optional: true - '@esbuild/android-arm@0.25.8': + '@esbuild/android-arm@0.25.9': optional: true - '@esbuild/android-x64@0.25.8': + '@esbuild/android-x64@0.25.9': optional: true - '@esbuild/darwin-arm64@0.25.8': + '@esbuild/darwin-arm64@0.25.9': optional: true - '@esbuild/darwin-x64@0.25.8': + '@esbuild/darwin-x64@0.25.9': optional: true - '@esbuild/freebsd-arm64@0.25.8': + '@esbuild/freebsd-arm64@0.25.9': optional: true - '@esbuild/freebsd-x64@0.25.8': + '@esbuild/freebsd-x64@0.25.9': optional: true - '@esbuild/linux-arm64@0.25.8': + '@esbuild/linux-arm64@0.25.9': optional: true - '@esbuild/linux-arm@0.25.8': + '@esbuild/linux-arm@0.25.9': optional: true - '@esbuild/linux-ia32@0.25.8': + '@esbuild/linux-ia32@0.25.9': optional: true - '@esbuild/linux-loong64@0.25.8': + '@esbuild/linux-loong64@0.25.9': optional: true - '@esbuild/linux-mips64el@0.25.8': + '@esbuild/linux-mips64el@0.25.9': optional: true - '@esbuild/linux-ppc64@0.25.8': + '@esbuild/linux-ppc64@0.25.9': optional: true - '@esbuild/linux-riscv64@0.25.8': + '@esbuild/linux-riscv64@0.25.9': optional: true - '@esbuild/linux-s390x@0.25.8': + '@esbuild/linux-s390x@0.25.9': optional: true - '@esbuild/linux-x64@0.25.8': + '@esbuild/linux-x64@0.25.9': optional: true - '@esbuild/netbsd-arm64@0.25.8': + '@esbuild/netbsd-arm64@0.25.9': optional: true - '@esbuild/netbsd-x64@0.25.8': + '@esbuild/netbsd-x64@0.25.9': optional: true - '@esbuild/openbsd-arm64@0.25.8': + '@esbuild/openbsd-arm64@0.25.9': optional: true - '@esbuild/openbsd-x64@0.25.8': + '@esbuild/openbsd-x64@0.25.9': optional: true - '@esbuild/openharmony-arm64@0.25.8': + '@esbuild/openharmony-arm64@0.25.9': optional: true - '@esbuild/sunos-x64@0.25.8': + '@esbuild/sunos-x64@0.25.9': optional: true - '@esbuild/win32-arm64@0.25.8': + '@esbuild/win32-arm64@0.25.9': optional: true - '@esbuild/win32-ia32@0.25.8': + '@esbuild/win32-ia32@0.25.9': optional: true - '@esbuild/win32-x64@0.25.8': + '@esbuild/win32-x64@0.25.9': optional: true - '@gerrit0/mini-shiki@3.8.1': + '@gerrit0/mini-shiki@3.12.2': dependencies: - '@shikijs/engine-oniguruma': 3.8.1 - '@shikijs/langs': 3.8.1 - '@shikijs/themes': 3.8.1 - '@shikijs/types': 3.8.1 + '@shikijs/engine-oniguruma': 3.13.0 + '@shikijs/langs': 3.13.0 + '@shikijs/themes': 3.13.0 + '@shikijs/types': 3.13.0 '@shikijs/vscode-textmate': 10.0.2 '@inquirer/checkbox@4.2.0(@types/node@22.16.5)': @@ -2474,80 +2484,83 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@rollup/rollup-android-arm-eabi@4.46.1': + '@rollup/rollup-android-arm-eabi@4.50.1': optional: true - '@rollup/rollup-android-arm64@4.46.1': + '@rollup/rollup-android-arm64@4.50.1': optional: true - '@rollup/rollup-darwin-arm64@4.46.1': + '@rollup/rollup-darwin-arm64@4.50.1': optional: true - '@rollup/rollup-darwin-x64@4.46.1': + '@rollup/rollup-darwin-x64@4.50.1': optional: true - '@rollup/rollup-freebsd-arm64@4.46.1': + '@rollup/rollup-freebsd-arm64@4.50.1': optional: true - '@rollup/rollup-freebsd-x64@4.46.1': + '@rollup/rollup-freebsd-x64@4.50.1': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.46.1': + '@rollup/rollup-linux-arm-gnueabihf@4.50.1': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.46.1': + '@rollup/rollup-linux-arm-musleabihf@4.50.1': optional: true - '@rollup/rollup-linux-arm64-gnu@4.46.1': + '@rollup/rollup-linux-arm64-gnu@4.50.1': optional: true - '@rollup/rollup-linux-arm64-musl@4.46.1': + '@rollup/rollup-linux-arm64-musl@4.50.1': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.46.1': + '@rollup/rollup-linux-loongarch64-gnu@4.50.1': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.46.1': + '@rollup/rollup-linux-ppc64-gnu@4.50.1': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.46.1': + '@rollup/rollup-linux-riscv64-gnu@4.50.1': optional: true - '@rollup/rollup-linux-riscv64-musl@4.46.1': + '@rollup/rollup-linux-riscv64-musl@4.50.1': optional: true - '@rollup/rollup-linux-s390x-gnu@4.46.1': + '@rollup/rollup-linux-s390x-gnu@4.50.1': optional: true - '@rollup/rollup-linux-x64-gnu@4.46.1': + '@rollup/rollup-linux-x64-gnu@4.50.1': optional: true - '@rollup/rollup-linux-x64-musl@4.46.1': + '@rollup/rollup-linux-x64-musl@4.50.1': optional: true - '@rollup/rollup-win32-arm64-msvc@4.46.1': + '@rollup/rollup-openharmony-arm64@4.50.1': optional: true - '@rollup/rollup-win32-ia32-msvc@4.46.1': + '@rollup/rollup-win32-arm64-msvc@4.50.1': optional: true - '@rollup/rollup-win32-x64-msvc@4.46.1': + '@rollup/rollup-win32-ia32-msvc@4.50.1': optional: true - '@shikijs/engine-oniguruma@3.8.1': + '@rollup/rollup-win32-x64-msvc@4.50.1': + optional: true + + '@shikijs/engine-oniguruma@3.13.0': dependencies: - '@shikijs/types': 3.8.1 + '@shikijs/types': 3.13.0 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@3.8.1': + '@shikijs/langs@3.13.0': dependencies: - '@shikijs/types': 3.8.1 + '@shikijs/types': 3.13.0 - '@shikijs/themes@3.8.1': + '@shikijs/themes@3.13.0': dependencies: - '@shikijs/types': 3.8.1 + '@shikijs/types': 3.13.0 - '@shikijs/types@3.8.1': + '@shikijs/types@3.13.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 @@ -2586,7 +2599,7 @@ snapshots: '@types/unist@3.0.3': {} - '@vitest/coverage-istanbul@3.2.4(vitest@3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(yaml@2.8.0))': + '@vitest/coverage-istanbul@3.2.4(vitest@3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.9.2))(yaml@2.8.1))': dependencies: '@istanbuljs/schema': 0.1.3 debug: 4.4.1 @@ -2598,7 +2611,7 @@ snapshots: magicast: 0.3.5 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(yaml@2.8.0) + vitest: 3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.9.2))(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -2610,14 +2623,14 @@ snapshots: chai: 5.2.1 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(vite@7.0.6(@types/node@22.16.5)(yaml@2.8.0))': + '@vitest/mocker@3.2.4(msw@2.10.4(@types/node@22.16.5)(typescript@5.9.2))(vite@7.0.7(@types/node@22.16.5)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - msw: 2.10.4(@types/node@22.16.5)(typescript@5.8.3) - vite: 7.0.6(@types/node@22.16.5)(yaml@2.8.0) + msw: 2.10.4(@types/node@22.16.5)(typescript@5.9.2) + vite: 7.0.7(@types/node@22.16.5)(yaml@2.8.1) '@vitest/pretty-format@3.2.4': dependencies: @@ -2868,34 +2881,34 @@ snapshots: dependencies: es-errors: 1.3.0 - esbuild@0.25.8: + esbuild@0.25.9: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.8 - '@esbuild/android-arm': 0.25.8 - '@esbuild/android-arm64': 0.25.8 - '@esbuild/android-x64': 0.25.8 - '@esbuild/darwin-arm64': 0.25.8 - '@esbuild/darwin-x64': 0.25.8 - '@esbuild/freebsd-arm64': 0.25.8 - '@esbuild/freebsd-x64': 0.25.8 - '@esbuild/linux-arm': 0.25.8 - '@esbuild/linux-arm64': 0.25.8 - '@esbuild/linux-ia32': 0.25.8 - '@esbuild/linux-loong64': 0.25.8 - '@esbuild/linux-mips64el': 0.25.8 - '@esbuild/linux-ppc64': 0.25.8 - '@esbuild/linux-riscv64': 0.25.8 - '@esbuild/linux-s390x': 0.25.8 - '@esbuild/linux-x64': 0.25.8 - '@esbuild/netbsd-arm64': 0.25.8 - '@esbuild/netbsd-x64': 0.25.8 - '@esbuild/openbsd-arm64': 0.25.8 - '@esbuild/openbsd-x64': 0.25.8 - '@esbuild/openharmony-arm64': 0.25.8 - '@esbuild/sunos-x64': 0.25.8 - '@esbuild/win32-arm64': 0.25.8 - '@esbuild/win32-ia32': 0.25.8 - '@esbuild/win32-x64': 0.25.8 + '@esbuild/aix-ppc64': 0.25.9 + '@esbuild/android-arm': 0.25.9 + '@esbuild/android-arm64': 0.25.9 + '@esbuild/android-x64': 0.25.9 + '@esbuild/darwin-arm64': 0.25.9 + '@esbuild/darwin-x64': 0.25.9 + '@esbuild/freebsd-arm64': 0.25.9 + '@esbuild/freebsd-x64': 0.25.9 + '@esbuild/linux-arm': 0.25.9 + '@esbuild/linux-arm64': 0.25.9 + '@esbuild/linux-ia32': 0.25.9 + '@esbuild/linux-loong64': 0.25.9 + '@esbuild/linux-mips64el': 0.25.9 + '@esbuild/linux-ppc64': 0.25.9 + '@esbuild/linux-riscv64': 0.25.9 + '@esbuild/linux-s390x': 0.25.9 + '@esbuild/linux-x64': 0.25.9 + '@esbuild/netbsd-arm64': 0.25.9 + '@esbuild/netbsd-x64': 0.25.9 + '@esbuild/openbsd-arm64': 0.25.9 + '@esbuild/openbsd-x64': 0.25.9 + '@esbuild/openharmony-arm64': 0.25.9 + '@esbuild/sunos-x64': 0.25.9 + '@esbuild/win32-arm64': 0.25.9 + '@esbuild/win32-ia32': 0.25.9 + '@esbuild/win32-x64': 0.25.9 escalade@3.2.0: {} @@ -2921,7 +2934,7 @@ snapshots: fast-uri@3.0.6: {} - fdir@6.4.6(picomatch@4.0.3): + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -3040,19 +3053,19 @@ snapshots: transitivePeerDependencies: - encoding - i18next-korean-postposition-processor@1.0.0(i18next@24.2.3(typescript@5.8.3)): + i18next-korean-postposition-processor@1.0.0(i18next@24.2.3(typescript@5.9.2)): dependencies: - i18next: 24.2.3(typescript@5.8.3) + i18next: 24.2.3(typescript@5.9.2) i18next@22.5.1: dependencies: '@babel/runtime': 7.28.2 - i18next@24.2.3(typescript@5.8.3): + i18next@24.2.3(typescript@5.9.2): dependencies: '@babel/runtime': 7.28.2 optionalDependencies: - typescript: 5.8.3 + typescript: 5.9.2 iconv-lite@0.4.24: dependencies: @@ -3319,7 +3332,7 @@ snapshots: ms@2.1.3: {} - msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3): + msw@2.10.4(@types/node@22.16.5)(typescript@5.9.2): dependencies: '@bundled-es-modules/cookie': 2.0.1 '@bundled-es-modules/statuses': 1.0.1 @@ -3340,7 +3353,7 @@ snapshots: type-fest: 4.41.0 yargs: 17.7.2 optionalDependencies: - typescript: 5.8.3 + typescript: 5.9.2 transitivePeerDependencies: - '@types/node' @@ -3467,30 +3480,31 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - rollup@4.46.1: + rollup@4.50.1: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.46.1 - '@rollup/rollup-android-arm64': 4.46.1 - '@rollup/rollup-darwin-arm64': 4.46.1 - '@rollup/rollup-darwin-x64': 4.46.1 - '@rollup/rollup-freebsd-arm64': 4.46.1 - '@rollup/rollup-freebsd-x64': 4.46.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.46.1 - '@rollup/rollup-linux-arm-musleabihf': 4.46.1 - '@rollup/rollup-linux-arm64-gnu': 4.46.1 - '@rollup/rollup-linux-arm64-musl': 4.46.1 - '@rollup/rollup-linux-loongarch64-gnu': 4.46.1 - '@rollup/rollup-linux-ppc64-gnu': 4.46.1 - '@rollup/rollup-linux-riscv64-gnu': 4.46.1 - '@rollup/rollup-linux-riscv64-musl': 4.46.1 - '@rollup/rollup-linux-s390x-gnu': 4.46.1 - '@rollup/rollup-linux-x64-gnu': 4.46.1 - '@rollup/rollup-linux-x64-musl': 4.46.1 - '@rollup/rollup-win32-arm64-msvc': 4.46.1 - '@rollup/rollup-win32-ia32-msvc': 4.46.1 - '@rollup/rollup-win32-x64-msvc': 4.46.1 + '@rollup/rollup-android-arm-eabi': 4.50.1 + '@rollup/rollup-android-arm64': 4.50.1 + '@rollup/rollup-darwin-arm64': 4.50.1 + '@rollup/rollup-darwin-x64': 4.50.1 + '@rollup/rollup-freebsd-arm64': 4.50.1 + '@rollup/rollup-freebsd-x64': 4.50.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.50.1 + '@rollup/rollup-linux-arm-musleabihf': 4.50.1 + '@rollup/rollup-linux-arm64-gnu': 4.50.1 + '@rollup/rollup-linux-arm64-musl': 4.50.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.50.1 + '@rollup/rollup-linux-ppc64-gnu': 4.50.1 + '@rollup/rollup-linux-riscv64-gnu': 4.50.1 + '@rollup/rollup-linux-riscv64-musl': 4.50.1 + '@rollup/rollup-linux-s390x-gnu': 4.50.1 + '@rollup/rollup-linux-x64-gnu': 4.50.1 + '@rollup/rollup-linux-x64-musl': 4.50.1 + '@rollup/rollup-openharmony-arm64': 4.50.1 + '@rollup/rollup-win32-arm64-msvc': 4.50.1 + '@rollup/rollup-win32-ia32-msvc': 4.50.1 + '@rollup/rollup-win32-x64-msvc': 4.50.1 fsevents: 2.3.3 rrweb-cssom@0.8.0: {} @@ -3604,7 +3618,12 @@ snapshots: tinyglobby@0.2.14: dependencies: - fdir: 6.4.6(picomatch@4.0.3) + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 tinypool@1.1.1: {} @@ -3640,9 +3659,9 @@ snapshots: dependencies: punycode: 2.3.1 - tsconfck@3.1.6(typescript@5.8.3): + tsconfck@3.1.6(typescript@5.9.2): optionalDependencies: - typescript: 5.8.3 + typescript: 5.9.2 tsconfig-paths-webpack-plugin@4.2.0: dependencies: @@ -3663,28 +3682,28 @@ snapshots: type-fest@4.41.0: {} - typedoc-github-theme@0.3.1(typedoc@0.28.8(typescript@5.8.3)): + typedoc-github-theme@0.3.1(typedoc@0.28.13(typescript@5.9.2)): dependencies: - typedoc: 0.28.8(typescript@5.8.3) + typedoc: 0.28.13(typescript@5.9.2) - typedoc-plugin-coverage@4.0.1(typedoc@0.28.8(typescript@5.8.3)): + typedoc-plugin-coverage@4.0.1(typedoc@0.28.13(typescript@5.9.2)): dependencies: - typedoc: 0.28.8(typescript@5.8.3) + typedoc: 0.28.13(typescript@5.9.2) - typedoc-plugin-mdn-links@5.0.9(typedoc@0.28.8(typescript@5.8.3)): + typedoc-plugin-mdn-links@5.0.9(typedoc@0.28.13(typescript@5.9.2)): dependencies: - typedoc: 0.28.8(typescript@5.8.3) + typedoc: 0.28.13(typescript@5.9.2) - typedoc@0.28.8(typescript@5.8.3): + typedoc@0.28.13(typescript@5.9.2): dependencies: - '@gerrit0/mini-shiki': 3.8.1 + '@gerrit0/mini-shiki': 3.12.2 lunr: 2.3.9 markdown-it: 14.1.0 minimatch: 9.0.5 - typescript: 5.8.3 - yaml: 2.8.0 + typescript: 5.9.2 + yaml: 2.8.1 - typescript@5.8.3: {} + typescript@5.9.2: {} uc.micro@2.1.0: {} @@ -3705,13 +3724,13 @@ snapshots: util-deprecate@1.0.2: {} - vite-node@3.2.4(@types/node@22.16.5)(yaml@2.8.0): + vite-node@3.2.4(@types/node@22.16.5)(yaml@2.8.1): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.0.6(@types/node@22.16.5)(yaml@2.8.0) + vite: 7.0.7(@types/node@22.16.5)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -3726,40 +3745,40 @@ snapshots: - tsx - yaml - vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@7.0.6(@types/node@22.16.5)(yaml@2.8.0)): + vite-tsconfig-paths@5.1.4(typescript@5.9.2)(vite@7.0.7(@types/node@22.16.5)(yaml@2.8.1)): dependencies: debug: 4.4.1 globrex: 0.1.2 - tsconfck: 3.1.6(typescript@5.8.3) + tsconfck: 3.1.6(typescript@5.9.2) optionalDependencies: - vite: 7.0.6(@types/node@22.16.5)(yaml@2.8.0) + vite: 7.0.7(@types/node@22.16.5)(yaml@2.8.1) transitivePeerDependencies: - supports-color - typescript - vite@7.0.6(@types/node@22.16.5)(yaml@2.8.0): + vite@7.0.7(@types/node@22.16.5)(yaml@2.8.1): dependencies: - esbuild: 0.25.8 - fdir: 6.4.6(picomatch@4.0.3) + esbuild: 0.25.9 + fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.46.1 - tinyglobby: 0.2.14 + rollup: 4.50.1 + tinyglobby: 0.2.15 optionalDependencies: '@types/node': 22.16.5 fsevents: 2.3.3 - yaml: 2.8.0 + yaml: 2.8.1 - vitest-canvas-mock@0.3.3(vitest@3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(yaml@2.8.0)): + vitest-canvas-mock@0.3.3(vitest@3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.9.2))(yaml@2.8.1)): dependencies: jest-canvas-mock: 2.5.2 - vitest: 3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(yaml@2.8.0) + vitest: 3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.9.2))(yaml@2.8.1) - vitest@3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(yaml@2.8.0): + vitest@3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.9.2))(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(vite@7.0.6(@types/node@22.16.5)(yaml@2.8.0)) + '@vitest/mocker': 3.2.4(msw@2.10.4(@types/node@22.16.5)(typescript@5.9.2))(vite@7.0.7(@types/node@22.16.5)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -3777,8 +3796,8 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.0.6(@types/node@22.16.5)(yaml@2.8.0) - vite-node: 3.2.4(@types/node@22.16.5)(yaml@2.8.0) + vite: 7.0.7(@types/node@22.16.5)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@22.16.5)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.16.5 @@ -3862,7 +3881,7 @@ snapshots: yallist@3.1.1: {} - yaml@2.8.0: {} + yaml@2.8.1: {} yargs-parser@21.1.1: {} diff --git a/public/images/ui/language_icon.png b/public/images/ui/language_icon.png new file mode 100644 index 00000000000..ebe0671ca51 Binary files /dev/null and b/public/images/ui/language_icon.png differ diff --git a/public/images/ui/legacy/language_icon.png b/public/images/ui/legacy/language_icon.png new file mode 100644 index 00000000000..ebe0671ca51 Binary files /dev/null and b/public/images/ui/legacy/language_icon.png differ diff --git a/public/images/ui/legacy/text_images/ca/summary/summary_dexnb_label_ca.png b/public/images/ui/legacy/text_images/ca/summary/summary_dexnb_label_ca.png index a457468d8d0..5477e3385a8 100644 Binary files a/public/images/ui/legacy/text_images/ca/summary/summary_dexnb_label_ca.png and b/public/images/ui/legacy/text_images/ca/summary/summary_dexnb_label_ca.png differ diff --git a/public/images/ui/legacy/text_images/ca/summary/summary_dexnb_label_overlay_shiny_ca.png b/public/images/ui/legacy/text_images/ca/summary/summary_dexnb_label_overlay_shiny_ca.png index 3277a28a59b..1640e46caa0 100644 Binary files a/public/images/ui/legacy/text_images/ca/summary/summary_dexnb_label_overlay_shiny_ca.png and b/public/images/ui/legacy/text_images/ca/summary/summary_dexnb_label_overlay_shiny_ca.png differ diff --git a/public/images/ui/legacy/text_images/ca/summary/summary_stats_expbar_title_ca.png b/public/images/ui/legacy/text_images/ca/summary/summary_stats_expbar_title_ca.png index e9dfb10e5d6..da999975932 100644 Binary files a/public/images/ui/legacy/text_images/ca/summary/summary_stats_expbar_title_ca.png and b/public/images/ui/legacy/text_images/ca/summary/summary_stats_expbar_title_ca.png differ diff --git a/public/images/ui/legacy/text_images/en/battle_ui/overlay_exp_label.png b/public/images/ui/legacy/text_images/en/battle_ui/overlay_exp_label.png index 40b5e8925a1..acb04a84a31 100644 Binary files a/public/images/ui/legacy/text_images/en/battle_ui/overlay_exp_label.png and b/public/images/ui/legacy/text_images/en/battle_ui/overlay_exp_label.png differ diff --git a/public/images/ui/legacy/text_images/en/summary/summary_dexnb_label.png b/public/images/ui/legacy/text_images/en/summary/summary_dexnb_label.png index eab90a91c7f..bf568c486aa 100644 Binary files a/public/images/ui/legacy/text_images/en/summary/summary_dexnb_label.png and b/public/images/ui/legacy/text_images/en/summary/summary_dexnb_label.png differ diff --git a/public/images/ui/legacy/text_images/en/summary/summary_moves_descriptions_title.png b/public/images/ui/legacy/text_images/en/summary/summary_moves_descriptions_title.png index 3d2b4d08376..e83e8cafbfc 100644 Binary files a/public/images/ui/legacy/text_images/en/summary/summary_moves_descriptions_title.png and b/public/images/ui/legacy/text_images/en/summary/summary_moves_descriptions_title.png differ diff --git a/public/images/ui/legacy/text_images/en/summary/summary_moves_effect_title.png b/public/images/ui/legacy/text_images/en/summary/summary_moves_effect_title.png index 55fb0efd832..55c4b545d98 100644 Binary files a/public/images/ui/legacy/text_images/en/summary/summary_moves_effect_title.png and b/public/images/ui/legacy/text_images/en/summary/summary_moves_effect_title.png differ diff --git a/public/images/ui/legacy/text_images/en/summary/summary_moves_moves_title.png b/public/images/ui/legacy/text_images/en/summary/summary_moves_moves_title.png index d869ab4e311..6bbb29c9c5f 100644 Binary files a/public/images/ui/legacy/text_images/en/summary/summary_moves_moves_title.png and b/public/images/ui/legacy/text_images/en/summary/summary_moves_moves_title.png differ diff --git a/public/images/ui/legacy/text_images/en/summary/summary_profile_ability.png b/public/images/ui/legacy/text_images/en/summary/summary_profile_ability.png index 6600db26802..a05c22b7d47 100644 Binary files a/public/images/ui/legacy/text_images/en/summary/summary_profile_ability.png and b/public/images/ui/legacy/text_images/en/summary/summary_profile_ability.png differ diff --git a/public/images/ui/legacy/text_images/en/summary/summary_profile_memo_title.png b/public/images/ui/legacy/text_images/en/summary/summary_profile_memo_title.png index 14cdf099044..3d69c20e57f 100644 Binary files a/public/images/ui/legacy/text_images/en/summary/summary_profile_memo_title.png and b/public/images/ui/legacy/text_images/en/summary/summary_profile_memo_title.png differ diff --git a/public/images/ui/legacy/text_images/en/summary/summary_profile_passive.png b/public/images/ui/legacy/text_images/en/summary/summary_profile_passive.png index 66f56ff435e..c026e87a215 100644 Binary files a/public/images/ui/legacy/text_images/en/summary/summary_profile_passive.png and b/public/images/ui/legacy/text_images/en/summary/summary_profile_passive.png differ diff --git a/public/images/ui/legacy/text_images/en/summary/summary_profile_profile_title.png b/public/images/ui/legacy/text_images/en/summary/summary_profile_profile_title.png index 8d4f82df3b3..4170dccf682 100644 Binary files a/public/images/ui/legacy/text_images/en/summary/summary_profile_profile_title.png and b/public/images/ui/legacy/text_images/en/summary/summary_profile_profile_title.png differ diff --git a/public/images/ui/legacy/text_images/en/summary/summary_stats_expbar_title.png b/public/images/ui/legacy/text_images/en/summary/summary_stats_expbar_title.png index e9dfb10e5d6..da999975932 100644 Binary files a/public/images/ui/legacy/text_images/en/summary/summary_stats_expbar_title.png and b/public/images/ui/legacy/text_images/en/summary/summary_stats_expbar_title.png differ diff --git a/public/images/ui/legacy/text_images/en/summary/summary_stats_item_title.png b/public/images/ui/legacy/text_images/en/summary/summary_stats_item_title.png index 5752b28288c..42e08b3e52a 100644 Binary files a/public/images/ui/legacy/text_images/en/summary/summary_stats_item_title.png and b/public/images/ui/legacy/text_images/en/summary/summary_stats_item_title.png differ diff --git a/public/images/ui/legacy/text_images/en/summary/summary_stats_stats_title.png b/public/images/ui/legacy/text_images/en/summary/summary_stats_stats_title.png index 5531819ef66..f602a43c39d 100644 Binary files a/public/images/ui/legacy/text_images/en/summary/summary_stats_stats_title.png and b/public/images/ui/legacy/text_images/en/summary/summary_stats_stats_title.png differ diff --git a/public/images/ui/legacy/text_images/es-ES/summary/summary_dexnb_label_es-ES.png b/public/images/ui/legacy/text_images/es-ES/summary/summary_dexnb_label_es-ES.png index a457468d8d0..5477e3385a8 100644 Binary files a/public/images/ui/legacy/text_images/es-ES/summary/summary_dexnb_label_es-ES.png and b/public/images/ui/legacy/text_images/es-ES/summary/summary_dexnb_label_es-ES.png differ diff --git a/public/images/ui/legacy/text_images/es-ES/summary/summary_dexnb_label_overlay_shiny_es-ES.png b/public/images/ui/legacy/text_images/es-ES/summary/summary_dexnb_label_overlay_shiny_es-ES.png index 3277a28a59b..1640e46caa0 100644 Binary files a/public/images/ui/legacy/text_images/es-ES/summary/summary_dexnb_label_overlay_shiny_es-ES.png and b/public/images/ui/legacy/text_images/es-ES/summary/summary_dexnb_label_overlay_shiny_es-ES.png differ diff --git a/public/images/ui/legacy/text_images/es-ES/summary/summary_moves_descriptions_title_es-ES.png b/public/images/ui/legacy/text_images/es-ES/summary/summary_moves_descriptions_title_es-ES.png index ffcae31894d..3a4e3c7c375 100644 Binary files a/public/images/ui/legacy/text_images/es-ES/summary/summary_moves_descriptions_title_es-ES.png and b/public/images/ui/legacy/text_images/es-ES/summary/summary_moves_descriptions_title_es-ES.png differ diff --git a/public/images/ui/legacy/text_images/es-ES/summary/summary_moves_effect_title_es-ES.png b/public/images/ui/legacy/text_images/es-ES/summary/summary_moves_effect_title_es-ES.png index 50ce2f51d6f..cf8d1309848 100644 Binary files a/public/images/ui/legacy/text_images/es-ES/summary/summary_moves_effect_title_es-ES.png and b/public/images/ui/legacy/text_images/es-ES/summary/summary_moves_effect_title_es-ES.png differ diff --git a/public/images/ui/legacy/text_images/es-ES/summary/summary_moves_moves_title_es-ES.png b/public/images/ui/legacy/text_images/es-ES/summary/summary_moves_moves_title_es-ES.png index ffca8bdfa10..a601ae79e4f 100644 Binary files a/public/images/ui/legacy/text_images/es-ES/summary/summary_moves_moves_title_es-ES.png and b/public/images/ui/legacy/text_images/es-ES/summary/summary_moves_moves_title_es-ES.png differ diff --git a/public/images/ui/legacy/text_images/es-ES/summary/summary_profile_ability_es-ES.png b/public/images/ui/legacy/text_images/es-ES/summary/summary_profile_ability_es-ES.png index b1b1a84ddcf..71bffe95cfe 100644 Binary files a/public/images/ui/legacy/text_images/es-ES/summary/summary_profile_ability_es-ES.png and b/public/images/ui/legacy/text_images/es-ES/summary/summary_profile_ability_es-ES.png differ diff --git a/public/images/ui/legacy/text_images/es-ES/summary/summary_profile_memo_title_es-ES.png b/public/images/ui/legacy/text_images/es-ES/summary/summary_profile_memo_title_es-ES.png index e837a58e4f9..b7ef7c91fc5 100644 Binary files a/public/images/ui/legacy/text_images/es-ES/summary/summary_profile_memo_title_es-ES.png and b/public/images/ui/legacy/text_images/es-ES/summary/summary_profile_memo_title_es-ES.png differ diff --git a/public/images/ui/legacy/text_images/es-ES/summary/summary_profile_passive_es-ES.png b/public/images/ui/legacy/text_images/es-ES/summary/summary_profile_passive_es-ES.png index 885453e3e98..a50e3cacf58 100644 Binary files a/public/images/ui/legacy/text_images/es-ES/summary/summary_profile_passive_es-ES.png and b/public/images/ui/legacy/text_images/es-ES/summary/summary_profile_passive_es-ES.png differ diff --git a/public/images/ui/legacy/text_images/es-ES/summary/summary_profile_profile_title_es-ES.png b/public/images/ui/legacy/text_images/es-ES/summary/summary_profile_profile_title_es-ES.png index 51ba9300dab..a5ed0e3e169 100644 Binary files a/public/images/ui/legacy/text_images/es-ES/summary/summary_profile_profile_title_es-ES.png and b/public/images/ui/legacy/text_images/es-ES/summary/summary_profile_profile_title_es-ES.png differ diff --git a/public/images/ui/legacy/text_images/es-ES/summary/summary_stats_expbar_title_es-ES.png b/public/images/ui/legacy/text_images/es-ES/summary/summary_stats_expbar_title_es-ES.png index e9dfb10e5d6..da999975932 100644 Binary files a/public/images/ui/legacy/text_images/es-ES/summary/summary_stats_expbar_title_es-ES.png and b/public/images/ui/legacy/text_images/es-ES/summary/summary_stats_expbar_title_es-ES.png differ diff --git a/public/images/ui/legacy/text_images/es-ES/summary/summary_stats_item_title_es-ES.png b/public/images/ui/legacy/text_images/es-ES/summary/summary_stats_item_title_es-ES.png index 15fdb806125..9236aaa1ff8 100644 Binary files a/public/images/ui/legacy/text_images/es-ES/summary/summary_stats_item_title_es-ES.png and b/public/images/ui/legacy/text_images/es-ES/summary/summary_stats_item_title_es-ES.png differ diff --git a/public/images/ui/legacy/text_images/es-ES/summary/summary_stats_stats_title_es-ES.png b/public/images/ui/legacy/text_images/es-ES/summary/summary_stats_stats_title_es-ES.png index 2233461522c..5b59c12984f 100644 Binary files a/public/images/ui/legacy/text_images/es-ES/summary/summary_stats_stats_title_es-ES.png and b/public/images/ui/legacy/text_images/es-ES/summary/summary_stats_stats_title_es-ES.png differ diff --git a/public/images/ui/legacy/text_images/es-MX/summary/summary_dexnb_label_es-MX.png b/public/images/ui/legacy/text_images/es-MX/summary/summary_dexnb_label_es-MX.png index a457468d8d0..5477e3385a8 100644 Binary files a/public/images/ui/legacy/text_images/es-MX/summary/summary_dexnb_label_es-MX.png and b/public/images/ui/legacy/text_images/es-MX/summary/summary_dexnb_label_es-MX.png differ diff --git a/public/images/ui/legacy/text_images/es-MX/summary/summary_dexnb_label_overlay_shiny_es-MX.png b/public/images/ui/legacy/text_images/es-MX/summary/summary_dexnb_label_overlay_shiny_es-MX.png index 3277a28a59b..1640e46caa0 100644 Binary files a/public/images/ui/legacy/text_images/es-MX/summary/summary_dexnb_label_overlay_shiny_es-MX.png and b/public/images/ui/legacy/text_images/es-MX/summary/summary_dexnb_label_overlay_shiny_es-MX.png differ diff --git a/public/images/ui/legacy/text_images/es-MX/summary/summary_moves_descriptions_title_es-MX.png b/public/images/ui/legacy/text_images/es-MX/summary/summary_moves_descriptions_title_es-MX.png index ffcae31894d..3a4e3c7c375 100644 Binary files a/public/images/ui/legacy/text_images/es-MX/summary/summary_moves_descriptions_title_es-MX.png and b/public/images/ui/legacy/text_images/es-MX/summary/summary_moves_descriptions_title_es-MX.png differ diff --git a/public/images/ui/legacy/text_images/es-MX/summary/summary_moves_effect_title_es-MX.png b/public/images/ui/legacy/text_images/es-MX/summary/summary_moves_effect_title_es-MX.png index 50ce2f51d6f..cf8d1309848 100644 Binary files a/public/images/ui/legacy/text_images/es-MX/summary/summary_moves_effect_title_es-MX.png and b/public/images/ui/legacy/text_images/es-MX/summary/summary_moves_effect_title_es-MX.png differ diff --git a/public/images/ui/legacy/text_images/es-MX/summary/summary_moves_moves_title_es-MX.png b/public/images/ui/legacy/text_images/es-MX/summary/summary_moves_moves_title_es-MX.png index ffca8bdfa10..a601ae79e4f 100644 Binary files a/public/images/ui/legacy/text_images/es-MX/summary/summary_moves_moves_title_es-MX.png and b/public/images/ui/legacy/text_images/es-MX/summary/summary_moves_moves_title_es-MX.png differ diff --git a/public/images/ui/legacy/text_images/es-MX/summary/summary_profile_ability_es-MX.png b/public/images/ui/legacy/text_images/es-MX/summary/summary_profile_ability_es-MX.png index b1b1a84ddcf..71bffe95cfe 100644 Binary files a/public/images/ui/legacy/text_images/es-MX/summary/summary_profile_ability_es-MX.png and b/public/images/ui/legacy/text_images/es-MX/summary/summary_profile_ability_es-MX.png differ diff --git a/public/images/ui/legacy/text_images/es-MX/summary/summary_profile_memo_title_es-MX.png b/public/images/ui/legacy/text_images/es-MX/summary/summary_profile_memo_title_es-MX.png index e837a58e4f9..b7ef7c91fc5 100644 Binary files a/public/images/ui/legacy/text_images/es-MX/summary/summary_profile_memo_title_es-MX.png and b/public/images/ui/legacy/text_images/es-MX/summary/summary_profile_memo_title_es-MX.png differ diff --git a/public/images/ui/legacy/text_images/es-MX/summary/summary_profile_passive_es-MX.png b/public/images/ui/legacy/text_images/es-MX/summary/summary_profile_passive_es-MX.png index 885453e3e98..a50e3cacf58 100644 Binary files a/public/images/ui/legacy/text_images/es-MX/summary/summary_profile_passive_es-MX.png and b/public/images/ui/legacy/text_images/es-MX/summary/summary_profile_passive_es-MX.png differ diff --git a/public/images/ui/legacy/text_images/es-MX/summary/summary_profile_profile_title_es-MX.png b/public/images/ui/legacy/text_images/es-MX/summary/summary_profile_profile_title_es-MX.png index 51ba9300dab..a5ed0e3e169 100644 Binary files a/public/images/ui/legacy/text_images/es-MX/summary/summary_profile_profile_title_es-MX.png and b/public/images/ui/legacy/text_images/es-MX/summary/summary_profile_profile_title_es-MX.png differ diff --git a/public/images/ui/legacy/text_images/es-MX/summary/summary_stats_expbar_title_es-MX.png b/public/images/ui/legacy/text_images/es-MX/summary/summary_stats_expbar_title_es-MX.png index e9dfb10e5d6..da999975932 100644 Binary files a/public/images/ui/legacy/text_images/es-MX/summary/summary_stats_expbar_title_es-MX.png and b/public/images/ui/legacy/text_images/es-MX/summary/summary_stats_expbar_title_es-MX.png differ diff --git a/public/images/ui/legacy/text_images/es-MX/summary/summary_stats_item_title_es-MX.png b/public/images/ui/legacy/text_images/es-MX/summary/summary_stats_item_title_es-MX.png index 15fdb806125..9236aaa1ff8 100644 Binary files a/public/images/ui/legacy/text_images/es-MX/summary/summary_stats_item_title_es-MX.png and b/public/images/ui/legacy/text_images/es-MX/summary/summary_stats_item_title_es-MX.png differ diff --git a/public/images/ui/legacy/text_images/es-MX/summary/summary_stats_stats_title_es-MX.png b/public/images/ui/legacy/text_images/es-MX/summary/summary_stats_stats_title_es-MX.png index 2233461522c..5b59c12984f 100644 Binary files a/public/images/ui/legacy/text_images/es-MX/summary/summary_stats_stats_title_es-MX.png and b/public/images/ui/legacy/text_images/es-MX/summary/summary_stats_stats_title_es-MX.png differ diff --git a/public/images/ui/legacy/text_images/fr/battle_ui/overlay_exp_label_fr.png b/public/images/ui/legacy/text_images/fr/battle_ui/overlay_exp_label_fr.png index 40b5e8925a1..acb04a84a31 100644 Binary files a/public/images/ui/legacy/text_images/fr/battle_ui/overlay_exp_label_fr.png and b/public/images/ui/legacy/text_images/fr/battle_ui/overlay_exp_label_fr.png differ diff --git a/public/images/ui/legacy/text_images/ja/battle_ui/overlay_exp_label_ja.png b/public/images/ui/legacy/text_images/ja/battle_ui/overlay_exp_label_ja.png index 40b5e8925a1..acb04a84a31 100644 Binary files a/public/images/ui/legacy/text_images/ja/battle_ui/overlay_exp_label_ja.png and b/public/images/ui/legacy/text_images/ja/battle_ui/overlay_exp_label_ja.png differ diff --git a/public/images/ui/legacy/text_images/ja/summary/summary_stats_expbar_title_ja.png b/public/images/ui/legacy/text_images/ja/summary/summary_stats_expbar_title_ja.png index e9dfb10e5d6..da999975932 100644 Binary files a/public/images/ui/legacy/text_images/ja/summary/summary_stats_expbar_title_ja.png and b/public/images/ui/legacy/text_images/ja/summary/summary_stats_expbar_title_ja.png differ diff --git a/public/images/ui/legacy/text_images/ko/battle_ui/overlay_exp_label_ko.png b/public/images/ui/legacy/text_images/ko/battle_ui/overlay_exp_label_ko.png index 40b5e8925a1..acb04a84a31 100644 Binary files a/public/images/ui/legacy/text_images/ko/battle_ui/overlay_exp_label_ko.png and b/public/images/ui/legacy/text_images/ko/battle_ui/overlay_exp_label_ko.png differ diff --git a/public/images/ui/legacy/text_images/ko/summary/summary_stats_expbar_title_ko.png b/public/images/ui/legacy/text_images/ko/summary/summary_stats_expbar_title_ko.png index e9dfb10e5d6..da999975932 100644 Binary files a/public/images/ui/legacy/text_images/ko/summary/summary_stats_expbar_title_ko.png and b/public/images/ui/legacy/text_images/ko/summary/summary_stats_expbar_title_ko.png differ diff --git a/public/images/ui/legacy/text_images/pt-BR/summary/summary_dexnb_label_overlay_shiny_pt-BR.png b/public/images/ui/legacy/text_images/pt-BR/summary/summary_dexnb_label_overlay_shiny_pt-BR.png index 3277a28a59b..1640e46caa0 100644 Binary files a/public/images/ui/legacy/text_images/pt-BR/summary/summary_dexnb_label_overlay_shiny_pt-BR.png and b/public/images/ui/legacy/text_images/pt-BR/summary/summary_dexnb_label_overlay_shiny_pt-BR.png differ diff --git a/public/images/ui/legacy/text_images/pt-BR/summary/summary_stats_expbar_title_pt-BR.png b/public/images/ui/legacy/text_images/pt-BR/summary/summary_stats_expbar_title_pt-BR.png index e9dfb10e5d6..da999975932 100644 Binary files a/public/images/ui/legacy/text_images/pt-BR/summary/summary_stats_expbar_title_pt-BR.png and b/public/images/ui/legacy/text_images/pt-BR/summary/summary_stats_expbar_title_pt-BR.png differ diff --git a/public/images/ui/legacy/text_images/ro/battle_ui/overlay_exp_label_ro.png b/public/images/ui/legacy/text_images/ro/battle_ui/overlay_exp_label_ro.png index 40b5e8925a1..acb04a84a31 100644 Binary files a/public/images/ui/legacy/text_images/ro/battle_ui/overlay_exp_label_ro.png and b/public/images/ui/legacy/text_images/ro/battle_ui/overlay_exp_label_ro.png differ diff --git a/public/images/ui/legacy/text_images/ro/summary/summary_stats_expbar_title_ro.png b/public/images/ui/legacy/text_images/ro/summary/summary_stats_expbar_title_ro.png index e9dfb10e5d6..da999975932 100644 Binary files a/public/images/ui/legacy/text_images/ro/summary/summary_stats_expbar_title_ro.png and b/public/images/ui/legacy/text_images/ro/summary/summary_stats_expbar_title_ro.png differ diff --git a/public/images/ui/legacy/text_images/tl/battle_ui/overlay_exp_label_tl.png b/public/images/ui/legacy/text_images/tl/battle_ui/overlay_exp_label_tl.png index 40b5e8925a1..acb04a84a31 100644 Binary files a/public/images/ui/legacy/text_images/tl/battle_ui/overlay_exp_label_tl.png and b/public/images/ui/legacy/text_images/tl/battle_ui/overlay_exp_label_tl.png differ diff --git a/public/images/ui/legacy/text_images/tl/summary/summary_stats_expbar_title.png b/public/images/ui/legacy/text_images/tl/summary/summary_stats_expbar_title.png index e9dfb10e5d6..da999975932 100644 Binary files a/public/images/ui/legacy/text_images/tl/summary/summary_stats_expbar_title.png and b/public/images/ui/legacy/text_images/tl/summary/summary_stats_expbar_title.png differ diff --git a/public/images/ui/legacy/text_images/tr/summary/summary_stats_expbar_title_tr.png b/public/images/ui/legacy/text_images/tr/summary/summary_stats_expbar_title_tr.png index e9dfb10e5d6..da999975932 100644 Binary files a/public/images/ui/legacy/text_images/tr/summary/summary_stats_expbar_title_tr.png and b/public/images/ui/legacy/text_images/tr/summary/summary_stats_expbar_title_tr.png differ diff --git a/public/images/ui/legacy/text_images/zh-CN/battle_ui/overlay_exp_label_zh-CN.png b/public/images/ui/legacy/text_images/zh-CN/battle_ui/overlay_exp_label_zh-CN.png index 40b5e8925a1..acb04a84a31 100644 Binary files a/public/images/ui/legacy/text_images/zh-CN/battle_ui/overlay_exp_label_zh-CN.png and b/public/images/ui/legacy/text_images/zh-CN/battle_ui/overlay_exp_label_zh-CN.png differ diff --git a/public/images/ui/legacy/text_images/zh-CN/summary/summary_dexnb_label_zh-CN.png b/public/images/ui/legacy/text_images/zh-CN/summary/summary_dexnb_label_zh-CN.png index eab90a91c7f..bf568c486aa 100644 Binary files a/public/images/ui/legacy/text_images/zh-CN/summary/summary_dexnb_label_zh-CN.png and b/public/images/ui/legacy/text_images/zh-CN/summary/summary_dexnb_label_zh-CN.png differ diff --git a/public/images/ui/legacy/text_images/zh-CN/summary/summary_moves_descriptions_title_zh-CN.png b/public/images/ui/legacy/text_images/zh-CN/summary/summary_moves_descriptions_title_zh-CN.png index 3d2b4d08376..e83e8cafbfc 100644 Binary files a/public/images/ui/legacy/text_images/zh-CN/summary/summary_moves_descriptions_title_zh-CN.png and b/public/images/ui/legacy/text_images/zh-CN/summary/summary_moves_descriptions_title_zh-CN.png differ diff --git a/public/images/ui/legacy/text_images/zh-CN/summary/summary_moves_effect_title_zh-CN.png b/public/images/ui/legacy/text_images/zh-CN/summary/summary_moves_effect_title_zh-CN.png index 55fb0efd832..fbbaac0b260 100644 Binary files a/public/images/ui/legacy/text_images/zh-CN/summary/summary_moves_effect_title_zh-CN.png and b/public/images/ui/legacy/text_images/zh-CN/summary/summary_moves_effect_title_zh-CN.png differ diff --git a/public/images/ui/legacy/text_images/zh-CN/summary/summary_moves_moves_title_zh-CN.png b/public/images/ui/legacy/text_images/zh-CN/summary/summary_moves_moves_title_zh-CN.png index d869ab4e311..6bbb29c9c5f 100644 Binary files a/public/images/ui/legacy/text_images/zh-CN/summary/summary_moves_moves_title_zh-CN.png and b/public/images/ui/legacy/text_images/zh-CN/summary/summary_moves_moves_title_zh-CN.png differ diff --git a/public/images/ui/legacy/text_images/zh-CN/summary/summary_profile_ability_zh-CN.png b/public/images/ui/legacy/text_images/zh-CN/summary/summary_profile_ability_zh-CN.png index 6600db26802..a05c22b7d47 100644 Binary files a/public/images/ui/legacy/text_images/zh-CN/summary/summary_profile_ability_zh-CN.png and b/public/images/ui/legacy/text_images/zh-CN/summary/summary_profile_ability_zh-CN.png differ diff --git a/public/images/ui/legacy/text_images/zh-CN/summary/summary_profile_memo_title_zh-CN.png b/public/images/ui/legacy/text_images/zh-CN/summary/summary_profile_memo_title_zh-CN.png index 14cdf099044..3d69c20e57f 100644 Binary files a/public/images/ui/legacy/text_images/zh-CN/summary/summary_profile_memo_title_zh-CN.png and b/public/images/ui/legacy/text_images/zh-CN/summary/summary_profile_memo_title_zh-CN.png differ diff --git a/public/images/ui/legacy/text_images/zh-CN/summary/summary_profile_passive_zh-CN.png b/public/images/ui/legacy/text_images/zh-CN/summary/summary_profile_passive_zh-CN.png index 66f56ff435e..c026e87a215 100644 Binary files a/public/images/ui/legacy/text_images/zh-CN/summary/summary_profile_passive_zh-CN.png and b/public/images/ui/legacy/text_images/zh-CN/summary/summary_profile_passive_zh-CN.png differ diff --git a/public/images/ui/legacy/text_images/zh-CN/summary/summary_profile_profile_title_zh-CN.png b/public/images/ui/legacy/text_images/zh-CN/summary/summary_profile_profile_title_zh-CN.png index 8d4f82df3b3..4170dccf682 100644 Binary files a/public/images/ui/legacy/text_images/zh-CN/summary/summary_profile_profile_title_zh-CN.png and b/public/images/ui/legacy/text_images/zh-CN/summary/summary_profile_profile_title_zh-CN.png differ diff --git a/public/images/ui/legacy/text_images/zh-CN/summary/summary_stats_expbar_title_zh-CN.png b/public/images/ui/legacy/text_images/zh-CN/summary/summary_stats_expbar_title_zh-CN.png index e9dfb10e5d6..da999975932 100644 Binary files a/public/images/ui/legacy/text_images/zh-CN/summary/summary_stats_expbar_title_zh-CN.png and b/public/images/ui/legacy/text_images/zh-CN/summary/summary_stats_expbar_title_zh-CN.png differ diff --git a/public/images/ui/legacy/text_images/zh-CN/summary/summary_stats_item_title_zh-CN.png b/public/images/ui/legacy/text_images/zh-CN/summary/summary_stats_item_title_zh-CN.png index 5752b28288c..42e08b3e52a 100644 Binary files a/public/images/ui/legacy/text_images/zh-CN/summary/summary_stats_item_title_zh-CN.png and b/public/images/ui/legacy/text_images/zh-CN/summary/summary_stats_item_title_zh-CN.png differ diff --git a/public/images/ui/legacy/text_images/zh-CN/summary/summary_stats_stats_title_zh-CN.png b/public/images/ui/legacy/text_images/zh-CN/summary/summary_stats_stats_title_zh-CN.png index 5531819ef66..f602a43c39d 100644 Binary files a/public/images/ui/legacy/text_images/zh-CN/summary/summary_stats_stats_title_zh-CN.png and b/public/images/ui/legacy/text_images/zh-CN/summary/summary_stats_stats_title_zh-CN.png differ diff --git a/public/images/ui/legacy/text_images/zh-TW/battle_ui/overlay_exp_label_zh-TW.png b/public/images/ui/legacy/text_images/zh-TW/battle_ui/overlay_exp_label_zh-TW.png index 40b5e8925a1..acb04a84a31 100644 Binary files a/public/images/ui/legacy/text_images/zh-TW/battle_ui/overlay_exp_label_zh-TW.png and b/public/images/ui/legacy/text_images/zh-TW/battle_ui/overlay_exp_label_zh-TW.png differ diff --git a/public/images/ui/legacy/text_images/zh-TW/summary/summary_dexnb_label_zh-TW.png b/public/images/ui/legacy/text_images/zh-TW/summary/summary_dexnb_label_zh-TW.png index eab90a91c7f..bf568c486aa 100644 Binary files a/public/images/ui/legacy/text_images/zh-TW/summary/summary_dexnb_label_zh-TW.png and b/public/images/ui/legacy/text_images/zh-TW/summary/summary_dexnb_label_zh-TW.png differ diff --git a/public/images/ui/legacy/text_images/zh-TW/summary/summary_moves_descriptions_title_zh-TW.png b/public/images/ui/legacy/text_images/zh-TW/summary/summary_moves_descriptions_title_zh-TW.png index 3d2b4d08376..e83e8cafbfc 100644 Binary files a/public/images/ui/legacy/text_images/zh-TW/summary/summary_moves_descriptions_title_zh-TW.png and b/public/images/ui/legacy/text_images/zh-TW/summary/summary_moves_descriptions_title_zh-TW.png differ diff --git a/public/images/ui/legacy/text_images/zh-TW/summary/summary_moves_effect_title_zh-TW.png b/public/images/ui/legacy/text_images/zh-TW/summary/summary_moves_effect_title_zh-TW.png index 55fb0efd832..fbbaac0b260 100644 Binary files a/public/images/ui/legacy/text_images/zh-TW/summary/summary_moves_effect_title_zh-TW.png and b/public/images/ui/legacy/text_images/zh-TW/summary/summary_moves_effect_title_zh-TW.png differ diff --git a/public/images/ui/legacy/text_images/zh-TW/summary/summary_moves_moves_title_zh-TW.png b/public/images/ui/legacy/text_images/zh-TW/summary/summary_moves_moves_title_zh-TW.png index d869ab4e311..6bbb29c9c5f 100644 Binary files a/public/images/ui/legacy/text_images/zh-TW/summary/summary_moves_moves_title_zh-TW.png and b/public/images/ui/legacy/text_images/zh-TW/summary/summary_moves_moves_title_zh-TW.png differ diff --git a/public/images/ui/legacy/text_images/zh-TW/summary/summary_profile_ability_zh-TW.png b/public/images/ui/legacy/text_images/zh-TW/summary/summary_profile_ability_zh-TW.png index 6600db26802..a05c22b7d47 100644 Binary files a/public/images/ui/legacy/text_images/zh-TW/summary/summary_profile_ability_zh-TW.png and b/public/images/ui/legacy/text_images/zh-TW/summary/summary_profile_ability_zh-TW.png differ diff --git a/public/images/ui/legacy/text_images/zh-TW/summary/summary_profile_memo_title_zh-TW.png b/public/images/ui/legacy/text_images/zh-TW/summary/summary_profile_memo_title_zh-TW.png index 14cdf099044..3d69c20e57f 100644 Binary files a/public/images/ui/legacy/text_images/zh-TW/summary/summary_profile_memo_title_zh-TW.png and b/public/images/ui/legacy/text_images/zh-TW/summary/summary_profile_memo_title_zh-TW.png differ diff --git a/public/images/ui/legacy/text_images/zh-TW/summary/summary_profile_passive_zh-TW.png b/public/images/ui/legacy/text_images/zh-TW/summary/summary_profile_passive_zh-TW.png index 66f56ff435e..c026e87a215 100644 Binary files a/public/images/ui/legacy/text_images/zh-TW/summary/summary_profile_passive_zh-TW.png and b/public/images/ui/legacy/text_images/zh-TW/summary/summary_profile_passive_zh-TW.png differ diff --git a/public/images/ui/legacy/text_images/zh-TW/summary/summary_profile_profile_title_zh-TW.png b/public/images/ui/legacy/text_images/zh-TW/summary/summary_profile_profile_title_zh-TW.png index 8d4f82df3b3..4170dccf682 100644 Binary files a/public/images/ui/legacy/text_images/zh-TW/summary/summary_profile_profile_title_zh-TW.png and b/public/images/ui/legacy/text_images/zh-TW/summary/summary_profile_profile_title_zh-TW.png differ diff --git a/public/images/ui/legacy/text_images/zh-TW/summary/summary_stats_expbar_title_zh-TW.png b/public/images/ui/legacy/text_images/zh-TW/summary/summary_stats_expbar_title_zh-TW.png index e9dfb10e5d6..da999975932 100644 Binary files a/public/images/ui/legacy/text_images/zh-TW/summary/summary_stats_expbar_title_zh-TW.png and b/public/images/ui/legacy/text_images/zh-TW/summary/summary_stats_expbar_title_zh-TW.png differ diff --git a/public/images/ui/legacy/text_images/zh-TW/summary/summary_stats_item_title_zh-TW.png b/public/images/ui/legacy/text_images/zh-TW/summary/summary_stats_item_title_zh-TW.png index 5752b28288c..42e08b3e52a 100644 Binary files a/public/images/ui/legacy/text_images/zh-TW/summary/summary_stats_item_title_zh-TW.png and b/public/images/ui/legacy/text_images/zh-TW/summary/summary_stats_item_title_zh-TW.png differ diff --git a/public/images/ui/legacy/text_images/zh-TW/summary/summary_stats_stats_title_zh-TW.png b/public/images/ui/legacy/text_images/zh-TW/summary/summary_stats_stats_title_zh-TW.png index 5531819ef66..f602a43c39d 100644 Binary files a/public/images/ui/legacy/text_images/zh-TW/summary/summary_stats_stats_title_zh-TW.png and b/public/images/ui/legacy/text_images/zh-TW/summary/summary_stats_stats_title_zh-TW.png differ diff --git a/public/locales b/public/locales index 090bfefaf7e..74de730a642 160000 --- a/public/locales +++ b/public/locales @@ -1 +1 @@ -Subproject commit 090bfefaf7e9d4efcbca61fa78a9cdf5d701830b +Subproject commit 74de730a64272c8e9ca0a4cdcf3426cbf1b0aeda diff --git a/scripts/create-test/boilerplates/default.ts b/scripts/create-test/boilerplates/default.boilerplate.ts similarity index 66% rename from scripts/create-test/boilerplates/default.ts rename to scripts/create-test/boilerplates/default.boilerplate.ts index fa914b150c2..7b633cf8276 100644 --- a/scripts/create-test/boilerplates/default.ts +++ b/scripts/create-test/boilerplates/default.boilerplate.ts @@ -1,7 +1,10 @@ import { AbilityId } from "#enums/ability-id"; +import { BattlerIndex } from "#enums/battler-index"; import { MoveId } from "#enums/move-id"; +import { MoveResult } from "#enums/move-result"; import { SpeciesId } from "#enums/species-id"; import { GameManager } from "#test/test-utils/game-manager"; +import i18next from "i18next"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -32,12 +35,18 @@ describe("{{description}}", () => { .enemyLevel(100); }); + // Find more awesome utility functions inside `#test/test-utils`! it("should do XYZ", async () => { await game.classicMode.startBattle([SpeciesId.FEEBAS]); + const feebas = game.field.getPlayerPokemon(); + game.move.use(MoveId.SPLASH); + await game.move.forceEnemyMove(MoveId.CELEBRATE); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.toEndOfTurn(); - expect(true).toBe(true); + expect(feebas).toHaveUsedMove({ move: MoveId.SPLASH, result: MoveResult.SUCCESS }); + expect(game).toHaveShownMessage(i18next.t("moveTriggers:splash")); }); }); diff --git a/scripts/create-test/create-test.js b/scripts/create-test/create-test.js index 5e395783da7..df065657346 100644 --- a/scripts/create-test/create-test.js +++ b/scripts/create-test/create-test.js @@ -102,9 +102,9 @@ async function promptFileName(selectedType) { function getBoilerplatePath(choiceType) { switch (choiceType) { // case "Reward": - // return path.join(__dirname, "boilerplates/reward.ts"); + // return path.join(__dirname, "boilerplates/reward.boilerplate.ts"); default: - return path.join(__dirname, "boilerplates/default.ts"); + return path.join(__dirname, "boilerplates/default.boilerplate.ts"); } } diff --git a/src/@types/api/pokerogue-daily-api.ts b/src/@types/api/pokerogue-daily-api.ts index 862ff2f51a3..838af2a2a34 100644 --- a/src/@types/api/pokerogue-daily-api.ts +++ b/src/@types/api/pokerogue-daily-api.ts @@ -1,4 +1,4 @@ -import type { ScoreboardCategory } from "#ui/containers/daily-run-scoreboard"; +import type { ScoreboardCategory } from "#ui/daily-run-scoreboard"; export interface GetDailyRankingsRequest { category: ScoreboardCategory; diff --git a/src/@types/api/pokerogue-save-data-api.ts b/src/@types/api/pokerogue-save-data-api.ts index ebc80ac4ce0..c33d775f114 100644 --- a/src/@types/api/pokerogue-save-data-api.ts +++ b/src/@types/api/pokerogue-save-data-api.ts @@ -1,4 +1,4 @@ -import type { SessionSaveData, SystemSaveData } from "#system/game-data"; +import type { SessionSaveData, SystemSaveData } from "#types/save-data"; export interface UpdateAllSavedataRequest { system: SystemSaveData; diff --git a/src/@types/api/pokerogue-system-save-data-api.ts b/src/@types/api/pokerogue-system-save-data-api.ts index 133f9cda506..2e79b4fb92c 100644 --- a/src/@types/api/pokerogue-system-save-data-api.ts +++ b/src/@types/api/pokerogue-system-save-data-api.ts @@ -1,4 +1,4 @@ -import type { SystemSaveData } from "#system/game-data"; +import type { SystemSaveData } from "#types/save-data"; export interface GetSystemSavedataRequest { clientSessionId: string; diff --git a/src/@types/arena-tags.ts b/src/@types/arena-tags.ts index 390d95a7daa..73f26af052a 100644 --- a/src/@types/arena-tags.ts +++ b/src/@types/arena-tags.ts @@ -1,7 +1,7 @@ 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 "#system/game-data"; +import type { SessionSaveData } from "#types/save-data"; import type { ObjectValues } from "#types/type-helpers"; /** Subset of {@linkcode ArenaTagType}s that apply some negative effect to pokemon that switch in ({@link https://bulbapedia.bulbagarden.net/wiki/List_of_moves_that_cause_entry_hazards#List_of_traps | entry hazards} and Imprison. */ @@ -38,6 +38,7 @@ type SerializableArenaTagTypeMap = Pick; + +/** + * Type for the parameters of {@linkcode Pokemon#=.getAttackDamage | getAttackDamage} + * @interface + */ +export type getAttackDamageParams = Omit; + +/** + * Type for the parameters of {@linkcode Pokemon.getAttackTypeEffectiveness | getAttackTypeEffectiveness} + * and associated helper functions. + */ +export interface getAttackTypeEffectivenessParams { + /** + * The {@linkcode Pokemon} using the move, used to check the user's Scrappy and Mind's Eye abilities + * and the effects of Foresight/Odor Sleuth. + */ + source?: Pokemon; + /** + * If `true`, ignores the effect of strong winds (used by anticipation, forewarn, stealth rocks) + * @defaultValue `false` + */ + ignoreStrongWinds?: boolean; + /** + * If `true`, will prevent changes to game state during calculations. + * @defaultValue `false` + */ + simulated?: boolean; + /** + * The {@linkcode Move} whose type effectiveness is being checked. + * Used for applying {@linkcode VariableMoveTypeChartAttr} + */ + move?: Move; + /** + * Whether to consider this Pokemon's {@linkcode IllusionData | illusion} when determining types. + * @defaultValue `false` + */ + useIllusion?: boolean; +}; diff --git a/src/@types/phase-types.ts b/src/@types/phase-types.ts index 91673053747..2324c927e3a 100644 --- a/src/@types/phase-types.ts +++ b/src/@types/phase-types.ts @@ -1,26 +1,27 @@ +import type { Pokemon } from "#app/field/pokemon"; +import type { Phase } from "#app/phase"; import type { PhaseConstructorMap } from "#app/phase-manager"; import type { ObjectValues } from "#types/type-helpers"; -// Intentionally export the types of everything in phase-manager, as this file is meant to be +// Intentionally [re-]export the types of everything in phase-manager, as this file is meant to be // the centralized place for type definitions for the phase system. export type * from "#app/phase-manager"; -// This file includes helpful types for the phase system. -// It intentionally imports the phase constructor map from the phase manager (and re-exports it) - -/** - * Map of phase names to constructors for said phase - */ +/** Map of phase names to constructors for said phase */ export type PhaseMap = { [K in keyof PhaseConstructorMap]: InstanceType; }; -/** - * Union type of all phase constructors. - */ +/** Union type of all phase constructors. */ export type PhaseClass = ObjectValues; -/** - * Union type of all phase names as strings. - */ +/** Union type of all phase names as strings. */ export type PhaseString = keyof PhaseMap; + +/** Type for predicate functions operating on a specific type of {@linkcode Phase}. */ +export type PhaseConditionFunc = (phase: PhaseMap[T]) => boolean; + +/** Interface type representing the assumption that all phases with pokemon associated are dynamic */ +export interface DynamicPhase extends Phase { + getPokemon(): Pokemon; +} diff --git a/src/@types/save-data.ts b/src/@types/save-data.ts new file mode 100644 index 00000000000..ae359c20949 --- /dev/null +++ b/src/@types/save-data.ts @@ -0,0 +1,161 @@ +import type { PokeballCounts } from "#app/battle-scene"; +import type { Tutorial } from "#app/tutorial"; +import type { BattleType } from "#enums/battle-type"; +import type { GameModes } from "#enums/game-modes"; +import type { MoveId } from "#enums/move-id"; +import type { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import type { Nature } from "#enums/nature"; +import type { PlayerGender } from "#enums/player-gender"; +import type { PokemonType } from "#enums/pokemon-type"; +import type { SpeciesId } from "#enums/species-id"; +import type { MysteryEncounterSaveData } from "#mystery-encounters/mystery-encounter-save-data"; +import type { Variant } from "#sprites/variant"; +import type { ArenaData } from "#system/arena-data"; +import type { ChallengeData } from "#system/challenge-data"; +import type { EggData } from "#system/egg-data"; +import type { GameStats } from "#system/game-stats"; +import type { ModifierData } from "#system/modifier-data"; +import type { PokemonData } from "#system/pokemon-data"; +import type { TrainerData } from "#system/trainer-data"; +import type { DexData } from "./dex-data"; + +export interface SystemSaveData { + trainerId: number; + secretId: number; + gender: PlayerGender; + dexData: DexData; + starterData: StarterData; + gameStats: GameStats; + unlocks: Unlocks; + achvUnlocks: AchvUnlocks; + voucherUnlocks: VoucherUnlocks; + voucherCounts: VoucherCounts; + eggs: EggData[]; + gameVersion: string; + timestamp: number; + eggPity: number[]; + unlockPity: number[]; +} + +export interface SessionSaveData { + seed: string; + playTime: number; + gameMode: GameModes; + party: PokemonData[]; + enemyParty: PokemonData[]; + modifiers: ModifierData[]; + enemyModifiers: ModifierData[]; + arena: ArenaData; + pokeballCounts: PokeballCounts; + money: number; + score: number; + waveIndex: number; + battleType: BattleType; + trainer: TrainerData; + gameVersion: string; + /** The player-chosen name of the run */ + name: string; + timestamp: number; + challenges: ChallengeData[]; + mysteryEncounterType: MysteryEncounterType | -1; // Only defined when current wave is ME, + mysteryEncounterSaveData: MysteryEncounterSaveData; + /** + * Counts the amount of pokemon fainted in your party during the current arena encounter. + */ + playerFaints: number; +} + +export interface Unlocks { + [key: number]: boolean; +} + +export interface AchvUnlocks { + [key: string]: number; +} + +export interface VoucherUnlocks { + [key: string]: number; +} + +export interface VoucherCounts { + [type: string]: number; +} + +export type StarterMoveset = [MoveId] | [MoveId, MoveId] | [MoveId, MoveId, MoveId] | [MoveId, MoveId, MoveId, MoveId]; + +export interface StarterFormMoveData { + [key: number]: StarterMoveset; +} + +export interface StarterMoveData { + [key: number]: StarterMoveset | StarterFormMoveData; +} + +export interface StarterAttributes { + nature?: number; + ability?: number; + variant?: number; + form?: number; + female?: boolean; + shiny?: boolean; + favorite?: boolean; + nickname?: string; + tera?: PokemonType; +} + +export interface DexAttrProps { + shiny: boolean; + female: boolean; + variant: Variant; + formIndex: number; +} + +export interface Starter { + speciesId: SpeciesId; + shiny: boolean; + variant: Variant; + formIndex: number; + female?: boolean; + abilityIndex: number; + passive: boolean; + nature: Nature; + moveset?: StarterMoveset; + pokerus: boolean; + nickname?: string; + teraType?: PokemonType; + ivs: number[]; +} + +export type RunHistoryData = Record; + +export interface RunEntry { + entry: SessionSaveData; + isVictory: boolean; + /*Automatically set to false at the moment - implementation TBD*/ + isFavorite: boolean; +} + +export interface StarterDataEntry { + moveset: StarterMoveset | StarterFormMoveData | null; + eggMoves: number; + candyCount: number; + friendship: number; + abilityAttr: number; + passiveAttr: number; + valueReduction: number; + classicWinCount: number; +} + +export interface StarterData { + [key: number]: StarterDataEntry; +} + +// TODO: Rework into a bitmask +export type TutorialFlags = { + [key in Tutorial]: boolean; +}; + +// TODO: Rework into a bitmask +export interface SeenDialogues { + [key: string]: boolean; +} diff --git a/src/@types/session-save-migrator.ts b/src/@types/session-save-migrator.ts index 56518eaa8b7..c8f53236c63 100644 --- a/src/@types/session-save-migrator.ts +++ b/src/@types/session-save-migrator.ts @@ -1,4 +1,4 @@ -import type { SessionSaveData } from "#system/game-data"; +import type { SessionSaveData } from "./save-data"; export interface SessionSaveMigrator { version: string; diff --git a/src/@types/system-save-migrator.ts b/src/@types/system-save-migrator.ts index 80fff9c7848..4cbcc4c3e15 100644 --- a/src/@types/system-save-migrator.ts +++ b/src/@types/system-save-migrator.ts @@ -1,4 +1,4 @@ -import type { SystemSaveData } from "#system/game-data"; +import type { SystemSaveData } from "./save-data"; export interface SystemSaveMigrator { version: string; diff --git a/src/ai/ai-moveset-gen.ts b/src/ai/ai-moveset-gen.ts new file mode 100644 index 00000000000..f392ca46d3f --- /dev/null +++ b/src/ai/ai-moveset-gen.ts @@ -0,0 +1,770 @@ +import { globalScene } from "#app/global-scene"; +import { speciesEggMoves } from "#balance/egg-moves"; +import { + BASE_LEVEL_WEIGHT_OFFSET, + BASE_WEIGHT_MULTIPLIER, + BOSS_EXTRA_WEIGHT_MULTIPLIER, + COMMON_TIER_TM_LEVEL_REQUIREMENT, + COMMON_TM_MOVESET_WEIGHT, + EGG_MOVE_LEVEL_REQUIREMENT, + EGG_MOVE_TO_LEVEL_WEIGHT, + EGG_MOVE_WEIGHT_MAX, + EVOLUTION_MOVE_WEIGHT, + GREAT_TIER_TM_LEVEL_REQUIREMENT, + GREAT_TM_MOVESET_WEIGHT, + getMaxEggMoveCount, + getMaxTmCount, + RARE_EGG_MOVE_LEVEL_REQUIREMENT, + STAB_BLACKLIST, + ULTRA_TIER_TM_LEVEL_REQUIREMENT, + ULTRA_TM_MOVESET_WEIGHT, +} from "#balance/moveset-generation"; +import { EVOLVE_MOVE, RELEARN_MOVE } from "#balance/pokemon-level-moves"; +import { speciesTmMoves, tmPoolTiers } from "#balance/tms"; +import { allMoves } from "#data/data-lists"; +import { ModifierTier } from "#enums/modifier-tier"; +import { MoveCategory } from "#enums/move-category"; +import type { MoveId } from "#enums/move-id"; +import { PokemonType } from "#enums/pokemon-type"; +import type { SpeciesId } from "#enums/species-id"; +import { Stat } from "#enums/stat"; +import type { EnemyPokemon, Pokemon } from "#field/pokemon"; +import { PokemonMove } from "#moves/pokemon-move"; +import { NumberHolder, randSeedInt } from "#utils/common"; +import { isBeta } from "#utils/utility-vars"; + +/** + * Compute and assign a weight to the level-up moves currently available to the Pokémon + * + * @param pokemon - The Pokémon to generate a level-based move pool for + * @returns A map of move IDs to their computed weights + * + * @remarks + * A move's weight is determined by its level, as follows: + * 1. If the level is an {@linkcode EVOLVE_MOVE} move, weight is 60 + * 2. If it is level 1 with 80+ BP, it is considered a "move reminder" move and + * weight is 60 + * 3. If the Pokémon has a trainer and the move is a {@linkcode RELEARN_MOVE}, + * weight is 60 + * 4. Otherwise, weight is the earliest level the move can be learned + 20 + */ +function getAndWeightLevelMoves(pokemon: Pokemon): Map { + const movePool = new Map(); + let allLevelMoves: [number, MoveId][]; + // TODO: Investigate why there needs to be error handling here + try { + allLevelMoves = pokemon.getLevelMoves(1, true, true, pokemon.hasTrainer()); + } catch (e) { + console.warn("Error encountered trying to generate moveset for %s: %s", pokemon.species.name, e); + return movePool; + } + + const level = pokemon.level; + const hasTrainer = pokemon.hasTrainer(); + + for (const levelMove of allLevelMoves) { + const [learnLevel, id] = levelMove; + if (level < learnLevel) { + break; + } + const move = allMoves[id]; + // Skip unimplemented moves or moves that are already in the pool + if (move.name.endsWith(" (N)") || movePool.has(id)) { + continue; + } + + let weight = learnLevel + BASE_LEVEL_WEIGHT_OFFSET; + switch (learnLevel) { + case EVOLVE_MOVE: + weight = EVOLUTION_MOVE_WEIGHT; + break; + // Assume level 1 moves with 80+ BP are "move reminder" moves and bump their weight. Trainers use actual relearn moves. + case 1: + if (move.power >= 80) { + weight = 60; + } + break; + case RELEARN_MOVE: + if (hasTrainer) { + weight = 60; + } + } + + movePool.set(id, weight); + } + + return movePool; +} + +/** + * Determine which TM tiers a Pokémon can learn based on its level + * @param level - The level of the Pokémon + * @returns A tuple indicating whether the Pokémon can learn common, great, and ultra tier TMs + */ +function getAllowedTmTiers(level: number): [common: boolean, great: boolean, ultra: boolean] { + return [ + level >= COMMON_TIER_TM_LEVEL_REQUIREMENT, + level >= GREAT_TIER_TM_LEVEL_REQUIREMENT, + level >= ULTRA_TIER_TM_LEVEL_REQUIREMENT, + ]; +} + +/** + * Get the TMs that a species can learn based on its ID and formKey + * @param speciesId - The species ID of the Pokémon + * @param level - The level of the Pokémon + * @param formKey - The form key of the Pokémon + * @param levelPool - The current level-based move pool, to avoid duplicates + * @param tmPool - The TM move pool to add to, which will be modified in place + * @param allowedTiers - The tiers of TMs the Pokémon is allowed to learn + * + * @privateRemarks + * Split out from `getAndWeightTmMoves` to allow fusion species to add their TMs + * without duplicating code. + */ +function getTmPoolForSpecies( + speciesId: number, + level: number, + formKey: string, + levelPool: ReadonlyMap, + eggPool: ReadonlyMap, + tmPool: Map, + allowedTiers = getAllowedTmTiers(level), +): void { + const [allowCommon, allowGreat, allowUltra] = allowedTiers; + const tms = speciesTmMoves[speciesId]; + // Species with no learnable TMs (e.g. Ditto) don't have entries in the `speciesTmMoves` object, + // so this is needed to avoid iterating over `undefined` + if (tms == null) { + return; + } + + let moveId: MoveId; + for (const tm of tms) { + if (Array.isArray(tm)) { + if (tm[0] !== formKey) { + continue; + } + moveId = tm[1]; + } else { + moveId = tm; + } + + if (levelPool.has(moveId) || eggPool.has(moveId) || tmPool.has(moveId)) { + continue; + } + switch (tmPoolTiers[moveId]) { + case ModifierTier.COMMON: + allowCommon && tmPool.set(moveId, COMMON_TM_MOVESET_WEIGHT); + break; + case ModifierTier.GREAT: + allowGreat && tmPool.set(moveId, GREAT_TM_MOVESET_WEIGHT); + break; + case ModifierTier.ULTRA: + allowUltra && tmPool.set(moveId, ULTRA_TM_MOVESET_WEIGHT); + break; + } + } +} + +/** + * Compute and assign a weight to the TM moves currently available to the Pokémon + * @param pokemon - The Pokémon to generate a TM-based move pool for + * @param currentSet - The current movepool, to avoid duplicates + * @param tmPool - The TM move pool to add to, which will be modified in place + * @returns A map of move IDs to their computed weights + * + * @remarks + * Only trainer pokemon can learn TM moves, and there are restrictions + * as to how many and which TMs are available based on the level of the Pokémon. + * 1. Before level 25, no TM moves are available + * 2. Between levels 25 and 40, only COMMON tier TMs are available, + */ +function getAndWeightTmMoves( + pokemon: Pokemon, + currentPool: ReadonlyMap, + eggPool: ReadonlyMap, + tmPool: Map, +): void { + const level = pokemon.level; + const allowedTiers = getAllowedTmTiers(level); + if (!allowedTiers.includes(true)) { + return; + } + + const form = pokemon.species.forms[pokemon.formIndex]?.formKey ?? ""; + getTmPoolForSpecies(pokemon.species.speciesId, level, form, currentPool, eggPool, tmPool, allowedTiers); + const fusionFormKey = pokemon.getFusionFormKey(); + const fusionSpecies = pokemon.fusionSpecies?.speciesId; + if (fusionSpecies != null && fusionFormKey != null && fusionFormKey !== "") { + getTmPoolForSpecies(fusionSpecies, level, fusionFormKey, currentPool, eggPool, tmPool, allowedTiers); + } +} + +/** + * Get the weight multiplier for an egg move + * @param levelPool - Map of level up moves to their weights + * @param level - The level of the Pokémon + * @param forRare - Whether this is for a rare egg move + * @param isBoss - Whether the Pokémon having the egg move generated is a boss Pokémon + */ +export function getEggMoveWeight( + // biome-ignore-start lint/correctness/noUnusedFunctionParameters: Saved to allow this algorithm to be tweaked easily without adjusting signatures + levelPool: ReadonlyMap, + level: number, + forRare: boolean, + isBoss: boolean, + // biome-ignore-end lint/correctness/noUnusedFunctionParameters: Endrange +): number { + const levelUpWeightedEggMoveWeight = Math.round(Math.max(...levelPool.values()) * EGG_MOVE_TO_LEVEL_WEIGHT); + // Rare egg moves are always weighted at 5/6 the weight of normal egg moves + return Math.min(levelUpWeightedEggMoveWeight, EGG_MOVE_WEIGHT_MAX) * (forRare ? 5 / 6 : 1); +} + +/** + * Submethod of {@linkcode getAndWeightEggMoves} that adds egg moves for a specific species to the egg move pool + * + * @param rootSpeciesId - The ID of the root species for which to generate the egg move pool. + * @param levelPool - A readonly map of move IDs to their levels, representing moves already learned by leveling up. + * @param eggPool - A map to be populated with egg move IDs and their corresponding weights. + * @param eggMoveWeight - The default weight to assign to regular egg moves. + * @param excludeRare - If true, excludes rare egg moves + * @param rareEggMoveWeight - The weight to assign to rare egg moves; default 0 + * + * @privateRemarks + * Split from `getAndWeightEggMoves` to allow fusion species to add their egg moves without duplicating code. + * + * @remarks + * - Moves present in `levelPool` are excluded from the egg pool. + * - If `excludeRare` is true, rare egg moves (at index 3) are skipped. + * - Rare egg moves are assigned `rareEggMoveWeight`, while others receive `eggMoveWeight`. + */ +function getEggPoolForSpecies( + rootSpeciesId: SpeciesId, + levelPool: ReadonlyMap, + eggPool: Map, + eggMoveWeight: number, + excludeRare: boolean, + rareEggMoveWeight = 0, +): void { + const eggMoves = speciesEggMoves[rootSpeciesId]; + if (eggMoves == null) { + return; + } + for (const [idx, moveId] of eggMoves.entries()) { + if (levelPool.has(moveId) || (idx === 3 && excludeRare)) { + continue; + } + eggPool.set(Math.max(moveId, eggPool.get(moveId) ?? 0), idx === 3 ? rareEggMoveWeight : eggMoveWeight); + } +} + +/** + * Compute and assign a weight to the egg moves currently available to the Pokémon + * @param pokemon - The Pokémon to generate egg moves for + * @param levelPool - The map of level-based moves to their weights + * @param eggPool - A map of move IDs to their weights for egg moves that will be modified in place + * + * @remarks + * This function checks if the Pokémon meets the requirements to learn egg moves, + * and if allowed, calculates the weights for regular and rare egg moves using the provided pools. + */ +function getAndWeightEggMoves( + pokemon: Pokemon, + levelPool: ReadonlyMap, + eggPool: Map, +): void { + const level = pokemon.level; + if (level < EGG_MOVE_LEVEL_REQUIREMENT || !globalScene.currentBattle?.trainer?.config.allowEggMoves) { + return; + } + const isBoss = pokemon.isBoss(); + const excludeRare = isBoss || level < RARE_EGG_MOVE_LEVEL_REQUIREMENT; + const eggMoveWeight = getEggMoveWeight(levelPool, level, false, isBoss); + let rareEggMoveWeight: number | undefined; + if (!excludeRare) { + rareEggMoveWeight = getEggMoveWeight(levelPool, level, true, isBoss); + } + getEggPoolForSpecies( + pokemon.species.getRootSpeciesId(), + levelPool, + eggPool, + eggMoveWeight, + excludeRare, + rareEggMoveWeight, + ); + + const fusionSpecies = pokemon.fusionSpecies?.getRootSpeciesId(); + if (fusionSpecies != null) { + getEggPoolForSpecies(fusionSpecies, levelPool, eggPool, eggMoveWeight, excludeRare, rareEggMoveWeight); + } +} + +/** + * Filter a move pool, removing moves that are not allowed based on conditions + * @param pool - The move pool to filter + * @param isBoss - Whether the Pokémon is a boss + * @param hasTrainer - Whether the Pokémon has a trainer + */ +function filterMovePool(pool: Map, isBoss: boolean, hasTrainer: boolean): void { + for (const [moveId, weight] of pool) { + if (weight <= 0) { + pool.delete(moveId); + continue; + } + const move = allMoves[moveId]; + // Forbid unimplemented moves + if (move.name.endsWith(" (N)")) { + pool.delete(moveId); + continue; + } + // Bosses never get self ko moves or Pain Split + if (isBoss && (move.hasAttr("SacrificialAttr") || move.hasAttr("HpSplitAttr"))) { + pool.delete(moveId); + } + + // No one gets Memento or Final Gambit + if (move.hasAttr("SacrificialAttrOnHit")) { + pool.delete(moveId); + continue; + } + + // Trainers never get OHKO moves + if (hasTrainer && move.hasAttr("OneHitKOAttr")) { + pool.delete(moveId); + } + } +} + +/** + * Perform Trainer-specific adjustments to move weights in a move pool + * @param pool - The move pool to adjust + */ +function adjustWeightsForTrainer(pool: Map): void { + for (const [moveId, weight] of pool.entries()) { + const move = allMoves[moveId]; + let adjustedWeight = weight; + // Half the weight of self KO moves on trainers + adjustedWeight *= move.hasAttr("SacrificialAttr") ? 0.5 : 1; + + // Trainers get a weight bump to stat buffing moves + adjustedWeight *= move.getAttrs("StatStageChangeAttr").some(a => a.stages > 1 && a.selfTarget) ? 1.25 : 1; + + // Trainers get a weight decrease to multiturn moves + adjustedWeight *= !!move.isChargingMove() || !!move.hasAttr("RechargeAttr") ? 0.7 : 1; + if (adjustedWeight !== weight) { + pool.set(moveId, adjustedWeight); + } + } +} + +/** + * Adjust weights of damaging moves in a move pool based on their power and category + * + * @param pool - The move pool to adjust + * @param pokemon - The Pokémon for which the moveset is being generated + * @param willTera - Whether the Pokémon is expected to Tera (i.e., has instant Tera on a Trainer Pokémon); default `false` + * @remarks + * Caps max power at 90 to avoid something like hyper beam ruining the stats. + * pokemon is a pretty soft weighting factor, although it is scaled with the weight multiplier. + */ +function adjustDamageMoveWeights(pool: Map, pokemon: Pokemon, willTera = false): void { + // begin max power at 40 to avoid inflating weights too much when there are only low power moves + let maxPower = 40; + for (const moveId of pool.keys()) { + const move = allMoves[moveId]; + maxPower = Math.max(maxPower, move.calculateEffectivePower()); + if (maxPower >= 90) { + maxPower = 90; + break; + } + } + + const atk = pokemon.getStat(Stat.ATK); + const spAtk = pokemon.getStat(Stat.SPATK); + const lowerStat = Math.min(atk, spAtk); + const higherStat = Math.max(atk, spAtk); + const worseCategory = atk > spAtk ? MoveCategory.SPECIAL : MoveCategory.PHYSICAL; + const statRatio = lowerStat / higherStat; + const adjustmentRatio = Math.min(Math.pow(statRatio, 3) * 1.3, 1); + + for (const [moveId, weight] of pool) { + const move = allMoves[moveId]; + let adjustedWeight = weight; + if (move.category === MoveCategory.STATUS) { + continue; + } + // Scale weight based on their ratio to the highest power move, capping at 50% reduction + adjustedWeight *= Math.max(Math.min(move.calculateEffectivePower() / maxPower, 1), 0.5); + + // Scale weight based the stat it uses to deal damage, based on the ratio between said stat + // and the higher stat + if (move.hasAttr("DefAtkAttr")) { + const def = pokemon.getStat(Stat.DEF); + const defRatio = def / higherStat; + const defAdjustRatio = Math.min(Math.pow(defRatio, 3) * 1.3, 1.1); + adjustedWeight *= defAdjustRatio; + } else if ( + move.category === worseCategory + && !move.hasAttr("PhotonGeyserCategoryAttr") + && !move.hasAttr("ShellSideArmCategoryAttr") + && !(move.hasAttr("TeraMoveCategoryAttr") && willTera) + ) { + // Raw multiply each move's category by the stat it uses to deal damage + // moves that always use the higher offensive stat are left unadjusted + adjustedWeight *= adjustmentRatio; + } + + if (adjustedWeight !== weight) { + pool.set(moveId, adjustedWeight); + } + } +} + +/** + * Calculate the total weight of all moves in a move pool + * @param pool - The move pool to calculate the total weight for + * @returns The total weight of all moves in the pool + */ +function calculateTotalPoolWeight(pool: Map): number { + let totalWeight = 0; + for (const weight of pool.values()) { + totalWeight += weight; + } + return totalWeight; +} + +/** + * Filter a pool and return a new array of moves that pass the predicate + * @param pool - The move pool to filter + * @param predicate - The predicate function to determine if a move should be included + * @param totalWeight - An output parameter to hold the total weight of the filtered pool. Its value is reset to 0 if provided. + * @returns An array of move ID and weight tuples that pass the predicate + */ +function filterPool( + pool: ReadonlyMap, + predicate: (moveId: MoveId) => boolean, + totalWeight?: NumberHolder, +): [id: MoveId, weight: number][] { + let hasTotalWeight = false; + if (totalWeight != null) { + totalWeight.value = 0; + hasTotalWeight = true; + } + const newPool: [id: MoveId, weight: number][] = []; + for (const [moveId, weight] of pool) { + if (predicate(moveId)) { + newPool.push([moveId, weight]); + if (hasTotalWeight) { + // Bang is safe here because we set `hasTotalWeight` in the if check above + totalWeight!.value += weight; + } + } + } + + return newPool; +} + +/** + * Forcibly add a STAB move to the Pokémon's moveset from the provided pools + * + * @remarks + * If no STAB move is available, add any damaging move. + * If no damaging move is available, no move is added + * @param pool - The master move pool + * @param tmPool - The TM move pool + * @param eggPool - The egg move pool + * @param pokemon - The Pokémon for which the moveset is being generated + * @param tmCount - A holder for the count of TM moves selected + * @param eggMoveCount - A holder for the count of egg moves selected + * @param willTera - Whether the Pokémon is expected to Tera (i.e., has instant Tera on a Trainer Pokémon); default `false` + * @param forceAnyDamageIfNoStab - If true, will force any damaging move if no STAB move is available + */ +// biome-ignore lint/nursery/useMaxParams: This is a complex function that needs all these parameters +function forceStabMove( + pool: Map, + tmPool: Map, + eggPool: Map, + pokemon: Pokemon, + tmCount: NumberHolder, + eggMoveCount: NumberHolder, + willTera = false, + forceAnyDamageIfNoStab = false, +): void { + // All Pokemon force a STAB move first + const totalWeight = new NumberHolder(0); + const stabMovePool = filterPool( + pool, + moveId => { + const move = allMoves[moveId]; + return ( + move.category !== MoveCategory.STATUS + && (pokemon.isOfType(move.type) + || (willTera && move.hasAttr("TeraBlastTypeAttr") && pokemon.getTeraType() !== PokemonType.STELLAR)) + && !STAB_BLACKLIST.has(moveId) + ); + }, + totalWeight, + ); + + const chosenPool = + stabMovePool.length > 0 || !forceAnyDamageIfNoStab + ? stabMovePool + : filterPool( + pool, + m => allMoves[m[0]].category !== MoveCategory.STATUS && !STAB_BLACKLIST.has(m[0]), + totalWeight, + ); + + if (chosenPool.length > 0) { + let rand = randSeedInt(totalWeight.value); + let index = 0; + while (rand > chosenPool[index][1]) { + rand -= chosenPool[index++][1]; + } + const selectedId = chosenPool[index][0]; + pool.delete(selectedId); + if (tmPool.has(selectedId)) { + tmPool.delete(selectedId); + tmCount.value++; + } else if (eggPool.has(selectedId)) { + eggPool.delete(selectedId); + eggMoveCount.value++; + } + pokemon.moveset.push(new PokemonMove(selectedId)); + } +} + +/** + * Adjust weights in the remaining move pool based on existing moves in the Pokémon's moveset + * + * @remarks + * Submethod for step 5 of moveset generation + * @param pool - The move pool to filter + * @param pokemon - The Pokémon for which the moveset is being generated + */ +function filterRemainingTrainerMovePool(pool: [id: MoveId, weight: number][], pokemon: Pokemon) { + // Sqrt the weight of any damaging moves with overlapping types. pokemon is about a 0.05 - 0.1 multiplier. + // Other damaging moves 2x weight if 0-1 damaging moves, 0.5x if 2, 0.125x if 3. These weights get 20x if STAB. + // Status moves remain unchanged on weight, pokemon encourages 1-2 + for (const [idx, [moveId, weight]] of pool.entries()) { + let ret: number; + if ( + pokemon.moveset.some( + mo => mo.getMove().category !== MoveCategory.STATUS && mo.getMove().type === allMoves[moveId].type, + ) + ) { + ret = Math.ceil(Math.sqrt(weight)); + } else if (allMoves[moveId].category !== MoveCategory.STATUS) { + ret = Math.ceil( + (weight / Math.max(Math.pow(4, pokemon.moveset.filter(mo => (mo.getMove().power ?? 0) > 1).length) / 8, 0.5)) + * (pokemon.isOfType(allMoves[moveId].type) && !STAB_BLACKLIST.has(moveId) ? 20 : 1), + ); + } else { + ret = weight; + } + pool[idx] = [moveId, ret]; + } +} + +/** + * Fill in the remaining slots in the Pokémon's moveset from the provided pools + * @param pokemon - The Pokémon for which the moveset is being generated + * @param tmPool - The TM move pool + * @param eggMovePool - The egg move pool + * @param tmCount - A holder for the count of moves that have been added to the moveset from TMs + * @param eggMoveCount - A holder for the count of moves that have been added to the moveset from egg moves + * @param baseWeights - The base weights of all moves in the master pool + * @param remainingPool - The remaining move pool to select from + */ +function fillInRemainingMovesetSlots( + pokemon: Pokemon, + tmPool: Map, + eggMovePool: Map, + tmCount: NumberHolder, + eggMoveCount: NumberHolder, + baseWeights: Map, + remainingPool: [id: MoveId, weight: number][], +): void { + const tmCap = getMaxTmCount(pokemon.level); + const eggCap = getMaxEggMoveCount(pokemon.level); + const remainingPoolWeight = new NumberHolder(0); + while (remainingPool.length > pokemon.moveset.length && pokemon.moveset.length < 4) { + const nonLevelMoveCount = tmCount.value + eggMoveCount.value; + remainingPool = filterPool( + baseWeights, + (m: MoveId) => + !pokemon.moveset.some( + mo => + m === mo.moveId || (allMoves[m]?.hasAttr("SacrificialAttr") && mo.getMove()?.hasAttr("SacrificialAttr")), // Only one self-KO move allowed + ) + && (nonLevelMoveCount < tmCap || !tmPool.has(m)) + && (nonLevelMoveCount < eggCap || !eggMovePool.has(m)), + remainingPoolWeight, + ); + if (pokemon.hasTrainer()) { + filterRemainingTrainerMovePool(remainingPool, pokemon); + } + const totalWeight = remainingPool.reduce((v, m) => v + m[1], 0); + let rand = randSeedInt(totalWeight); + let index = 0; + while (rand > remainingPool[index][1]) { + rand -= remainingPool[index++][1]; + } + const selectedMoveId = remainingPool[index][0]; + baseWeights.delete(selectedMoveId); + if (tmPool.has(selectedMoveId)) { + tmCount.value++; + tmPool.delete(selectedMoveId); + } else if (eggMovePool.has(selectedMoveId)) { + eggMoveCount.value++; + eggMovePool.delete(selectedMoveId); + } + pokemon.moveset.push(new PokemonMove(selectedMoveId)); + } +} + +/** + * Debugging function to log computed move weights for a Pokémon + * @param pokemon - The Pokémon for which the move weights were computed + * @param pool - The move pool containing move IDs and their weights + * @param note - Short note to include in the log for context + */ +function debugMoveWeights(pokemon: Pokemon, pool: Map, note: string): void { + if ((isBeta || import.meta.env.DEV) && import.meta.env.NODE_ENV !== "test") { + const moveNameToWeightMap = new Map(); + const sortedByValue = Array.from(pool.entries()).sort((a, b) => b[1] - a[1]); + for (const [moveId, weight] of sortedByValue) { + moveNameToWeightMap.set(allMoves[moveId].name, weight); + } + console.log("%cComputed move weights [%s] for %s", "color: blue", note, pokemon.name, moveNameToWeightMap); + } +} + +/** + * Generate a moveset for a given Pokémon based on its level, types, stats, and whether it is wild or a trainer's Pokémon. + * @param pokemon - The Pokémon to generate a moveset for + * @returns A reference to the Pokémon's moveset array + */ +export function generateMoveset(pokemon: Pokemon): void { + pokemon.moveset = []; + // Step 1: Generate the pools from various sources: level up, egg moves, and TMs + const learnPool = getAndWeightLevelMoves(pokemon); + debugMoveWeights(pokemon, learnPool, "Initial Level Moves"); + const hasTrainer = pokemon.hasTrainer(); + const tmPool = new Map(); + const eggMovePool = new Map(); + + if (hasTrainer) { + getAndWeightEggMoves(pokemon, learnPool, eggMovePool); + eggMovePool.size > 0 && debugMoveWeights(pokemon, eggMovePool, "Initial Egg Moves"); + getAndWeightTmMoves(pokemon, learnPool, eggMovePool, tmPool); + tmPool.size > 0 && debugMoveWeights(pokemon, tmPool, "Initial Tm Moves"); + } + + // Now, combine pools into one master pool. + // The pools are kept around so we know where the move was sourced from + const movePool = new Map([...tmPool.entries(), ...eggMovePool.entries(), ...learnPool.entries()]); + + // Step 2: Filter out forbidden moves + const isBoss = pokemon.isBoss(); + filterMovePool(movePool, isBoss, hasTrainer); + + // Step 3: Adjust weights for trainers + if (hasTrainer) { + adjustWeightsForTrainer(movePool); + } + + /** Determine whether this pokemon will instantly tera */ + const willTera = + hasTrainer + && globalScene.currentBattle?.trainer?.config.trainerAI.instantTeras.includes( + // The cast to EnemyPokemon is safe; includes will just return false if the property doesn't exist + (pokemon as EnemyPokemon).initialTeamIndex, + ); + + adjustDamageMoveWeights(movePool, pokemon, willTera); + + /** The higher this is, the greater the impact of weight. At `0` all moves are equal weight. */ + let weightMultiplier = BASE_WEIGHT_MULTIPLIER; + if (isBoss) { + weightMultiplier += BOSS_EXTRA_WEIGHT_MULTIPLIER; + } + + const baseWeights = new Map(movePool); + for (const [moveId, weight] of baseWeights) { + if (weight <= 0) { + baseWeights.delete(moveId); + continue; + } + baseWeights.set(moveId, Math.ceil(Math.pow(weight, weightMultiplier) * 100)); + } + + const tmCount = new NumberHolder(0); + const eggMoveCount = new NumberHolder(0); + + debugMoveWeights(pokemon, baseWeights, "Pre STAB Move"); + + // Step 4: Force a STAB move if possible + forceStabMove(movePool, tmPool, eggMovePool, pokemon, tmCount, eggMoveCount, willTera); + // Note: To force a secondary stab, call this a second time, and pass `false` for the last parameter + // Would also tweak the function to not consider moves already in the moveset + // e.g. forceStabMove(..., false); + + // Step 5: Fill in remaining slots + fillInRemainingMovesetSlots( + pokemon, + tmPool, + eggMovePool, + tmCount, + eggMoveCount, + baseWeights, + filterPool(baseWeights, (m: MoveId) => !pokemon.moveset.some(mo => m[0] === mo.moveId)), + ); +} + +/** + * Exports for internal testing purposes. + * ⚠️ These *must not* be used outside of tests, as they will not be defined. + * @internal + */ +export const __INTERNAL_TEST_EXPORTS: { + getAndWeightLevelMoves: typeof getAndWeightLevelMoves; + getAllowedTmTiers: typeof getAllowedTmTiers; + getTmPoolForSpecies: typeof getTmPoolForSpecies; + getAndWeightTmMoves: typeof getAndWeightTmMoves; + getEggMoveWeight: typeof getEggMoveWeight; + getEggPoolForSpecies: typeof getEggPoolForSpecies; + getAndWeightEggMoves: typeof getAndWeightEggMoves; + filterMovePool: typeof filterMovePool; + adjustWeightsForTrainer: typeof adjustWeightsForTrainer; + adjustDamageMoveWeights: typeof adjustDamageMoveWeights; + calculateTotalPoolWeight: typeof calculateTotalPoolWeight; + filterPool: typeof filterPool; + forceStabMove: typeof forceStabMove; + filterRemainingTrainerMovePool: typeof filterRemainingTrainerMovePool; + fillInRemainingMovesetSlots: typeof fillInRemainingMovesetSlots; +} = {} as any; + +// We can't use `import.meta.vitest` here, because this would not be set +// until the tests themselves begin to run, which is after imports +// So we rely on NODE_ENV being test instead +if (import.meta.env.NODE_ENV === "test") { + Object.assign(__INTERNAL_TEST_EXPORTS, { + getAndWeightLevelMoves, + getAllowedTmTiers, + getTmPoolForSpecies, + getAndWeightTmMoves, + getEggMoveWeight, + getEggPoolForSpecies, + getAndWeightEggMoves, + filterMovePool, + adjustWeightsForTrainer, + adjustDamageMoveWeights, + calculateTotalPoolWeight, + filterPool, + forceStabMove, + filterRemainingTrainerMovePool, + fillInRemainingMovesetSlots, + }); +} diff --git a/src/battle-scene.ts b/src/battle-scene.ts index d7681f5ff85..289c9a8f051 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -104,7 +104,6 @@ import { import { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterSaveData } from "#mystery-encounters/mystery-encounter-save-data"; import { allMysteryEncounters, mysteryEncountersByBiome } from "#mystery-encounters/mystery-encounters"; -import type { MovePhase } from "#phases/move-phase"; import { expSpriteKeys } from "#sprites/sprite-keys"; import { hasExpSprite } from "#sprites/sprite-utils"; import type { Variant } from "#sprites/variant"; @@ -121,13 +120,13 @@ import { vouchers } from "#system/voucher"; import { trainerConfigs } from "#trainers/trainer-config"; import type { HeldModifierConfig } from "#types/held-modifier-config"; import type { Localizable } from "#types/locales"; -import { AbilityBar } from "#ui/containers/ability-bar"; -import { ArenaFlyout } from "#ui/containers/arena-flyout"; -import { CandyBar } from "#ui/containers/candy-bar"; -import { CharSprite } from "#ui/containers/char-sprite"; -import { PartyExpBar } from "#ui/containers/party-exp-bar"; -import { PokeballTray } from "#ui/containers/pokeball-tray"; -import { PokemonInfoContainer } from "#ui/containers/pokemon-info-container"; +import { AbilityBar } from "#ui/ability-bar"; +import { ArenaFlyout } from "#ui/arena-flyout"; +import { CandyBar } from "#ui/candy-bar"; +import { CharSprite } from "#ui/char-sprite"; +import { PartyExpBar } from "#ui/party-exp-bar"; +import { PokeballTray } from "#ui/pokeball-tray"; +import { PokemonInfoContainer } from "#ui/pokemon-info-container"; import { addTextObject, getTextColor } from "#ui/text"; import { UI } from "#ui/ui"; import { addUiThemeOverrides } from "#ui/ui-theme"; @@ -138,7 +137,6 @@ import { formatMoney, getIvsFromId, isBetween, - isNullOrUndefined, NumberHolder, randomString, randSeedInt, @@ -788,12 +786,14 @@ export class BattleScene extends SceneBase { /** * Returns an array of EnemyPokemon of length 1 or 2 depending on if in a double battle or not. - * Does not actually check if the pokemon are on the field or not. + * @param active - (Default `false`) Whether to consider only {@linkcode Pokemon.isActive | active} on-field pokemon * @returns array of {@linkcode EnemyPokemon} */ - public getEnemyField(): EnemyPokemon[] { + public getEnemyField(active = false): EnemyPokemon[] { const party = this.getEnemyParty(); - return party.slice(0, Math.min(party.length, this.currentBattle?.double ? 2 : 1)); + return party + .slice(0, Math.min(party.length, this.currentBattle?.double ? 2 : 1)) + .filter(p => !active || p.isActive()); } /** @@ -818,25 +818,7 @@ export class BattleScene extends SceneBase { * @param allyPokemon - The {@linkcode Pokemon} allied with the removed Pokemon; will have moves redirected to it */ redirectPokemonMoves(removedPokemon: Pokemon, allyPokemon: Pokemon): void { - // failsafe: if not a double battle just return - if (this.currentBattle.double === false) { - return; - } - if (allyPokemon?.isActive(true)) { - let targetingMovePhase: MovePhase; - do { - targetingMovePhase = this.phaseManager.findPhase( - mp => - mp.is("MovePhase") - && mp.targets.length === 1 - && mp.targets[0] === removedPokemon.getBattlerIndex() - && mp.pokemon.isPlayer() !== allyPokemon.isPlayer(), - ) as MovePhase; - if (targetingMovePhase && targetingMovePhase.targets[0] !== allyPokemon.getBattlerIndex()) { - targetingMovePhase.targets[0] = allyPokemon.getBattlerIndex(); - } - } while (targetingMovePhase); - } + this.phaseManager.redirectMoves(removedPokemon, allyPokemon); } /** @@ -859,20 +841,21 @@ export class BattleScene extends SceneBase { } /** - * Return the {@linkcode Pokemon} associated with a given ID. - * @param pokemonId - The ID whose Pokemon will be retrieved. - * @returns The {@linkcode Pokemon} associated with the given id. - * Returns `null` if the ID is `undefined` or not present in either party. - * @todo Change the `null` to `undefined` and update callers' signatures - - * this is weird and causes a lot of random jank + * Return the {@linkcode Pokemon} associated with the given ID. + * @param pokemonId - The PID whose Pokemon will be retrieved + * @returns The `Pokemon` associated with the given ID, + * or `undefined` if none is found in either team's party. + * @see {@linkcode Pokemon.id} + * @todo `pokemonId` should not allow `undefined` */ - getPokemonById(pokemonId: number | undefined): Pokemon | null { - if (isNullOrUndefined(pokemonId)) { - return null; + public getPokemonById(pokemonId: number | undefined): Pokemon | undefined { + if (pokemonId == null) { + // biome-ignore lint/nursery/noUselessUndefined: More explicit + return undefined; } const party = (this.getPlayerParty() as Pokemon[]).concat(this.getEnemyParty()); - return party.find(p => p.id === pokemonId) ?? null; + return party.find(p => p.id === pokemonId); } addPlayerPokemon( @@ -1319,7 +1302,7 @@ export class BattleScene extends SceneBase { if ( !this.gameMode.hasTrainers || Overrides.BATTLE_TYPE_OVERRIDE === BattleType.WILD - || (Overrides.DISABLE_STANDARD_TRAINERS_OVERRIDE && isNullOrUndefined(trainerData)) + || (Overrides.DISABLE_STANDARD_TRAINERS_OVERRIDE && trainerData == null) ) { newBattleType = BattleType.WILD; } else { @@ -1332,13 +1315,12 @@ export class BattleScene extends SceneBase { if (newBattleType === BattleType.TRAINER) { const trainerType = Overrides.RANDOM_TRAINER_OVERRIDE?.trainerType ?? this.arena.randomTrainerType(newWaveIndex); + const hasDouble = trainerConfigs[trainerType].hasDouble; let doubleTrainer = false; if (trainerConfigs[trainerType].doubleOnly) { doubleTrainer = true; - } else if (trainerConfigs[trainerType].hasDouble) { - doubleTrainer = - Overrides.RANDOM_TRAINER_OVERRIDE?.alwaysDouble - || !randSeedInt(this.getDoubleBattleChance(newWaveIndex, playerField)); + } else if (hasDouble) { + doubleTrainer = !randSeedInt(this.getDoubleBattleChance(newWaveIndex, playerField)); // Add a check that special trainers can't be double except for tate and liza - they should use the normal double chance if ( trainerConfigs[trainerType].trainerTypeDouble @@ -1347,11 +1329,19 @@ export class BattleScene extends SceneBase { doubleTrainer = false; } } - const variant = doubleTrainer - ? TrainerVariant.DOUBLE - : randSeedInt(2) - ? TrainerVariant.FEMALE - : TrainerVariant.DEFAULT; + + // Forcing a double battle on wave 1 causes a bug where only one enemy is sent out, + // making it impossible to complete the fight without a reload + const overrideVariant = + Overrides.RANDOM_TRAINER_OVERRIDE?.trainerVariant === TrainerVariant.DOUBLE + && (!hasDouble || newWaveIndex <= 1) + ? TrainerVariant.DEFAULT + : Overrides.RANDOM_TRAINER_OVERRIDE?.trainerVariant; + + const variant = + overrideVariant + ?? (doubleTrainer ? TrainerVariant.DOUBLE : randSeedInt(2) ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT); + newTrainer = trainerData !== undefined ? trainerData.toTrainer() : new Trainer(trainerType, variant); this.field.add(newTrainer); } @@ -1383,7 +1373,7 @@ export class BattleScene extends SceneBase { newDouble = false; } - if (!isNullOrUndefined(Overrides.BATTLE_STYLE_OVERRIDE)) { + if (Overrides.BATTLE_STYLE_OVERRIDE != null) { let doubleOverrideForWave: "single" | "double" | null = null; switch (Overrides.BATTLE_STYLE_OVERRIDE) { @@ -1426,7 +1416,7 @@ export class BattleScene extends SceneBase { } if (lastBattle?.double && !newDouble) { - this.phaseManager.tryRemovePhase((p: Phase) => p.is("SwitchPhase")); + this.phaseManager.tryRemovePhase("SwitchPhase"); for (const p of this.getPlayerField()) { p.lapseTag(BattlerTagType.COMMANDED); } @@ -1572,7 +1562,7 @@ export class BattleScene extends SceneBase { // Give trainers with specialty types an appropriately-typed form for Wormadam, Rotom, Arceus, Oricorio, Silvally, or Paldean Tauros. !isEggPhase && this.currentBattle?.battleType === BattleType.TRAINER - && !isNullOrUndefined(this.currentBattle.trainer) + && this.currentBattle.trainer != null && this.currentBattle.trainer.config.hasSpecialtyType() ) { if (species.speciesId === SpeciesId.WORMADAM) { @@ -2692,7 +2682,7 @@ export class BattleScene extends SceneBase { } } else if (modifier instanceof FusePokemonModifier) { args.push(this.getPokemonById(modifier.fusePokemonId) as PlayerPokemon); - } else if (modifier instanceof RememberMoveModifier && !isNullOrUndefined(cost)) { + } else if (modifier instanceof RememberMoveModifier && cost != null) { args.push(cost); } @@ -3007,7 +2997,7 @@ export class BattleScene extends SceneBase { } if ( modifier instanceof PokemonHeldItemModifier - && !isNullOrUndefined(modifier.getSpecies()) + && modifier.getSpecies() != null && !this.getPokemonById(modifier.pokemonId)?.hasSpecies(modifier.getSpecies()!) ) { modifiers.splice(m--, 1); @@ -3108,7 +3098,7 @@ export class BattleScene extends SceneBase { * Apply all modifiers that match `modifierType` in a random order * @param modifierType The type of modifier to apply; must extend {@linkcode PersistentModifier} * @param player Whether to search the player (`true`) or the enemy (`false`); Defaults to `true` - * @param ...args The list of arguments needed to invoke `modifierType.apply` + * @param args The list of arguments needed to invoke `modifierType.apply` * @returns the list of all modifiers that matched `modifierType` and were applied. */ applyShuffledModifiers( @@ -3140,7 +3130,7 @@ export class BattleScene extends SceneBase { * Apply all modifiers that match `modifierType` * @param modifierType The type of modifier to apply; must extend {@linkcode PersistentModifier} * @param player Whether to search the player (`true`) or the enemy (`false`); Defaults to `true` - * @param ...args The list of arguments needed to invoke `modifierType.apply` + * @param args The list of arguments needed to invoke `modifierType.apply` * @returns the list of all modifiers that matched `modifierType` and were applied. */ applyModifiers( @@ -3175,7 +3165,7 @@ export class BattleScene extends SceneBase { * Apply the first modifier that matches `modifierType` * @param modifierType The type of modifier to apply; must extend {@linkcode PersistentModifier} * @param player Whether to search the player (`true`) or the enemy (`false`); Defaults to `true` - * @param ...args The list of arguments needed to invoke `modifierType.apply` + * @param args The list of arguments needed to invoke `modifierType.apply` * @returns the first modifier that matches `modifierType` and was applied; return `null` if none matched */ applyModifier( @@ -3323,7 +3313,7 @@ export class BattleScene extends SceneBase { /** * This function retrieves the sprite and audio keys for active Pokemon. * Active Pokemon include both enemy and player Pokemon of the current wave. - * Note: Questions on garbage collection go to @frutescens + * Note: Questions on garbage collection go to `@frutescens` * @returns a string array of active sprite and audio keys that should not be deleted */ getActiveKeys(): string[] { @@ -3573,7 +3563,7 @@ export class BattleScene extends SceneBase { // Loading override or session encounter let encounter: MysteryEncounter | null; if ( - !isNullOrUndefined(Overrides.MYSTERY_ENCOUNTER_OVERRIDE) + Overrides.MYSTERY_ENCOUNTER_OVERRIDE != null && allMysteryEncounters.hasOwnProperty(Overrides.MYSTERY_ENCOUNTER_OVERRIDE) ) { encounter = allMysteryEncounters[Overrides.MYSTERY_ENCOUNTER_OVERRIDE]; @@ -3584,7 +3574,7 @@ export class BattleScene extends SceneBase { encounter = allMysteryEncounters[encounterType ?? -1]; return encounter; } else { - encounter = !isNullOrUndefined(encounterType) ? allMysteryEncounters[encounterType] : null; + encounter = encounterType != null ? allMysteryEncounters[encounterType] : null; } // Check for queued encounters first @@ -3643,7 +3633,7 @@ export class BattleScene extends SceneBase { ? MysteryEncounterTier.ULTRA : MysteryEncounterTier.ROGUE; - if (!isNullOrUndefined(Overrides.MYSTERY_ENCOUNTER_TIER_OVERRIDE)) { + if (Overrides.MYSTERY_ENCOUNTER_TIER_OVERRIDE != null) { tier = Overrides.MYSTERY_ENCOUNTER_TIER_OVERRIDE; } diff --git a/src/constants/colors.ts b/src/constants/colors.ts index 717c5fa5f0d..a2400ef5f90 100644 --- a/src/constants/colors.ts +++ b/src/constants/colors.ts @@ -1,7 +1,8 @@ /** - * @module + * * A big file storing colors used in logging. * Minified by Terser during production builds, so has no overhead. + * @module */ // Colors used in prod diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 6afd4a88bf1..6525ba6bd92 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -33,6 +33,7 @@ import { CommonAnim } from "#enums/move-anims-common"; import { MoveCategory } from "#enums/move-category"; import { MoveFlags } from "#enums/move-flags"; import { MoveId } from "#enums/move-id"; +import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; import { MoveResult } from "#enums/move-result"; import { MoveTarget } from "#enums/move-target"; import { MoveUseMode } from "#enums/move-use-mode"; @@ -67,7 +68,6 @@ import type { Constructor } from "#utils/common"; import { BooleanHolder, coerceArray, - isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, @@ -507,7 +507,7 @@ export class ClearWeatherAbAttr extends AbAttr { private weather: WeatherType[]; /** - * @param weather {@linkcode WeatherType[]} - the weather to be removed + * @param weather - The weather to be removed */ constructor(weather: WeatherType[]) { super(true); @@ -537,7 +537,7 @@ export class ClearTerrainAbAttr extends AbAttr { private terrain: TerrainType[]; /** - * @param terrain {@linkcode TerrainType[]} - the terrain to be removed + * @param terrain - the terrain to be removed */ constructor(terrain: TerrainType[]) { super(true); @@ -655,7 +655,6 @@ export class ReceivedMoveDamageMultiplierAbAttr extends PreDefendAbAttr { /** * Reduces the damage dealt to an allied Pokemon. Used by Friend Guard. - * @see {@linkcode applyPreDefend} */ export class AlliedFieldDamageReductionAbAttr extends PreDefendAbAttr { private damageMultiplier: number; @@ -691,8 +690,6 @@ export interface TypeMultiplierAbAttrParams extends AugmentMoveInteractionAbAttr /** * Determines whether a Pokemon is immune to a move because of an ability. - * @see {@linkcode applyPreDefend} - * @see {@linkcode getCondition} */ export class TypeImmunityAbAttr extends PreDefendAbAttr { private immuneType: PokemonType | null; @@ -842,7 +839,6 @@ export class NonSuperEffectiveImmunityAbAttr extends TypeImmunityAbAttr { /** * Attribute implementing the effects of {@link https://bulbapedia.bulbagarden.net/wiki/Tera_Shell_(Ability) | Tera Shell} * When the source is at full HP, incoming attacks will have a maximum 0.5x type effectiveness multiplier. - * @extends PreDefendAbAttr */ export class FullHpResistTypeAbAttr extends PreDefendAbAttr { /** @@ -1044,7 +1040,7 @@ export class PostDefendStatStageChangeAbAttr extends PostDefendAbAttr { if (this.allOthers) { const ally = pokemon.getAlly(); - const otherPokemon = !isNullOrUndefined(ally) ? pokemon.getOpponents().concat([ally]) : pokemon.getOpponents(); + const otherPokemon = ally != null ? pokemon.getOpponents().concat([ally]) : pokemon.getOpponents(); for (const other of otherPokemon) { globalScene.phaseManager.unshiftNew( "StatStageChangePhase", @@ -1351,11 +1347,9 @@ export class PostDefendContactDamageAbAttr extends PostDefendAbAttr { } } /** - * @description: This ability applies the Perish Song tag to the attacking pokemon + * This ability applies the Perish Song tag to the attacking pokemon * and the defending pokemon if the move makes physical contact and neither pokemon * already has the Perish Song tag. - * @class PostDefendPerishSongAbAttr - * @extends {PostDefendAbAttr} */ export class PostDefendPerishSongAbAttr extends PostDefendAbAttr { private turns: number; @@ -1479,7 +1473,7 @@ export class PostDefendMoveDisableAbAttr extends PostDefendAbAttr { override canApply({ move, opponent: attacker, pokemon }: PostMoveInteractionAbAttrParams): boolean { return ( - isNullOrUndefined(attacker.getTag(BattlerTagType.DISABLED)) + attacker.getTag(BattlerTagType.DISABLED) == null && move.doesFlagEffectApply({ flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon }) && (this.chance === -1 || pokemon.randBattleSeedInt(100) < this.chance) ); @@ -1961,19 +1955,16 @@ export class PreAttackFieldMoveTypePowerBoostAbAttr extends FieldMovePowerBoostA /** * Boosts the power of a specific type of move for all Pokemon in the field. - * @extends PreAttackFieldMoveTypePowerBoostAbAttr */ export class FieldMoveTypePowerBoostAbAttr extends PreAttackFieldMoveTypePowerBoostAbAttr {} /** * Boosts the power of a specific type of move for the user and its allies. - * @extends PreAttackFieldMoveTypePowerBoostAbAttr */ export class UserFieldMoveTypePowerBoostAbAttr extends PreAttackFieldMoveTypePowerBoostAbAttr {} /** * Boosts the power of moves in specified categories. - * @extends FieldMovePowerBoostAbAttr */ export class AllyMoveCategoryPowerBoostAbAttr extends FieldMovePowerBoostAbAttr { /** @@ -2565,7 +2556,7 @@ export class PostIntimidateStatStageChangeAbAttr extends AbAttr { override apply({ pokemon, simulated, cancelled }: AbAttrParamsWithCancel): void { if (!simulated) { - globalScene.phaseManager.pushNew( + globalScene.phaseManager.unshiftNew( "StatStageChangePhase", pokemon.getBattlerIndex(), false, @@ -2579,7 +2570,6 @@ export class PostIntimidateStatStageChangeAbAttr extends AbAttr { /** * Base class for defining all {@linkcode Ability} Attributes post summon - * @see {@linkcode applyPostSummon()} */ export abstract class PostSummonAbAttr extends AbAttr { /** Should the ability activate when gained in battle? This will almost always be true */ @@ -2619,7 +2609,7 @@ export class PostSummonRemoveArenaTagAbAttr extends PostSummonAbAttr { private arenaTags: ArenaTagType[]; /** - * @param arenaTags {@linkcode ArenaTagType[]} - the arena tags to be removed + * @param arenaTags - The arena tags to be removed */ constructor(arenaTags: ArenaTagType[]) { super(true); @@ -2820,7 +2810,7 @@ export class PostSummonAllyHealAbAttr extends PostSummonAbAttr { override apply({ pokemon, simulated }: AbAttrBaseParams): void { const target = pokemon.getAlly(); - if (!simulated && !isNullOrUndefined(target)) { + if (!simulated && target != null) { globalScene.phaseManager.unshiftNew( "PokemonHealPhase", target.getBattlerIndex(), @@ -2851,7 +2841,7 @@ export class PostSummonClearAllyStatStagesAbAttr extends PostSummonAbAttr { override apply({ pokemon, simulated }: AbAttrBaseParams): void { const target = pokemon.getAlly(); - if (!simulated && !isNullOrUndefined(target)) { + if (!simulated && target != null) { for (const s of BATTLE_STATS) { target.setStatStage(s, 0); } @@ -2970,13 +2960,13 @@ export class PostSummonHealStatusAbAttr extends PostSummonRemoveEffectAbAttr { public override canApply({ pokemon }: AbAttrBaseParams): boolean { const status = pokemon.status?.effect; - return !isNullOrUndefined(status) && (this.immuneEffects.length === 0 || this.immuneEffects.includes(status)); + return status != null && (this.immuneEffects.length === 0 || this.immuneEffects.includes(status)); } public override apply({ pokemon }: AbAttrBaseParams): void { // TODO: should probably check against simulated... const status = pokemon.status?.effect; - if (!isNullOrUndefined(status)) { + if (status != null) { this.statusHealed = status; pokemon.resetStatus(false); pokemon.updateInfo(); @@ -3112,7 +3102,7 @@ export class PostSummonCopyAllyStatsAbAttr extends PostSummonAbAttr { } const ally = pokemon.getAlly(); - return !(isNullOrUndefined(ally) || ally.getStatStages().every(s => s === 0)); + return !(ally == null || ally.getStatStages().every(s => s === 0)); } override apply({ pokemon, simulated }: AbAttrBaseParams): void { @@ -3120,7 +3110,7 @@ export class PostSummonCopyAllyStatsAbAttr extends PostSummonAbAttr { return; } const ally = pokemon.getAlly(); - if (!isNullOrUndefined(ally)) { + if (ally != null) { for (const s of BATTLE_STATS) { pokemon.setStatStage(s, ally.getStatStage(s)); } @@ -3180,7 +3170,6 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr { /** * Reverts weather-based forms to their normal forms when the user is summoned. * Used by Cloud Nine and Air Lock. - * @extends PostSummonAbAttr */ export class PostSummonWeatherSuppressedFormChangeAbAttr extends PostSummonAbAttr { override canApply(_params: AbAttrBaseParams): boolean { @@ -3200,7 +3189,6 @@ export class PostSummonWeatherSuppressedFormChangeAbAttr extends PostSummonAbAtt /** * Triggers weather-based form change when summoned into an active weather. * Used by Forecast and Flower Gift. - * @extends PostSummonAbAttr */ export class PostSummonFormChangeByWeatherAbAttr extends PostSummonAbAttr { private ability: AbilityId; @@ -3252,7 +3240,8 @@ export class CommanderAbAttr extends AbAttr { const ally = pokemon.getAlly(); return ( globalScene.currentBattle?.double - && !isNullOrUndefined(ally) + && ally != null + && ally.isActive(true) && ally.species.speciesId === SpeciesId.DONDOZO && !(ally.isFainted() || ally.getTag(BattlerTagType.COMMANDED)) ); @@ -3267,7 +3256,7 @@ export class CommanderAbAttr extends AbAttr { // Apply boosts from this effect to the ally Dondozo pokemon.getAlly()?.addTag(BattlerTagType.COMMANDED, 0, MoveId.NONE, pokemon.id); // Cancel the source Pokemon's next move (if a move is queued) - globalScene.phaseManager.tryRemovePhase(phase => phase.is("MovePhase") && phase.pokemon === pokemon); + globalScene.phaseManager.tryRemovePhase("MovePhase", phase => phase.pokemon === pokemon); } } } @@ -3296,7 +3285,7 @@ export class PreSwitchOutResetStatusAbAttr extends PreSwitchOutAbAttr { } override canApply({ pokemon }: AbAttrBaseParams): boolean { - return !isNullOrUndefined(pokemon.status); + return pokemon.status != null; } override apply({ pokemon, simulated }: AbAttrBaseParams): void { @@ -3382,7 +3371,6 @@ export class PreSwitchOutHealAbAttr extends PreSwitchOutAbAttr { /** * Attribute for form changes that occur on switching out - * @see {@linkcode applyPreSwitchOut} */ export class PreSwitchOutFormChangeAbAttr extends PreSwitchOutAbAttr { private formFunc: (p: Pokemon) => number; @@ -3577,7 +3565,7 @@ export class ProtectStatAbAttr extends PreStatStageChangeAbAttr { } override canApply({ stat, cancelled }: PreStatStageChangeAbAttrParams): boolean { - return !cancelled.value && (isNullOrUndefined(this.protectedStat) || stat === this.protectedStat); + return !cancelled.value && (this.protectedStat == null || stat === this.protectedStat); } /** @@ -3813,11 +3801,7 @@ export class ConditionalUserFieldProtectStatAbAttr extends PreStatStageChangeAbA if (!target) { return false; } - return ( - !cancelled.value - && (isNullOrUndefined(this.protectedStat) || stat === this.protectedStat) - && this.condition(target) - ); + return !cancelled.value && (this.protectedStat == null || stat === this.protectedStat) && this.condition(target); } /** @@ -4221,8 +4205,8 @@ const anticipationCondition: AbAttrCondition = (pokemon: Pokemon) => * Creates an ability condition that causes the ability to fail if that ability * has already been used by that pokemon that battle. It requires an ability to * be specified due to current limitations in how conditions on abilities work. - * @param {AbilityId} ability The ability to check if it's already been applied - * @returns {AbAttrCondition} The condition + * @param ability The ability to check if it's already been applied + * @returns The condition */ function getOncePerBattleCondition(ability: AbilityId): AbAttrCondition { return (pokemon: Pokemon) => { @@ -4546,7 +4530,7 @@ export class PostTurnStatusHealAbAttr extends PostTurnAbAttr { } override canApply({ pokemon }: AbAttrBaseParams): boolean { - return !isNullOrUndefined(pokemon.status) && this.effects.includes(pokemon.status.effect) && !pokemon.isFullHp(); + return pokemon.status != null && this.effects.includes(pokemon.status.effect) && !pokemon.isFullHp(); } override apply({ simulated, passive, pokemon }: AbAttrBaseParams): void { @@ -4613,7 +4597,7 @@ export class PostTurnRestoreBerryAbAttr extends PostTurnAbAttr { /** * @param procChance - function providing chance to restore an item - * @see {@linkcode createEatenBerry()} + * @see {@linkcode createEatenBerry} */ constructor(private procChance: (pokemon: Pokemon) => number) { super(); @@ -4878,11 +4862,10 @@ export class PostTurnHurtIfSleepingAbAttr extends PostTurnAbAttr { /** * Grabs the last failed Pokeball used * @sealed - * @see {@linkcode applyPostTurn} */ export class FetchBallAbAttr extends PostTurnAbAttr { override canApply({ simulated, pokemon }: AbAttrBaseParams): boolean { - return !simulated && !isNullOrUndefined(globalScene.currentBattle.lastUsedPokeball) && !!pokemon.isPlayer; + return !simulated && globalScene.currentBattle.lastUsedPokeball != null && !!pokemon.isPlayer; } /** @@ -4968,7 +4951,6 @@ export class PostMoveUsedAbAttr extends AbAttr { /** * Triggers after a dance move is used either by the opponent or the player - * @extends PostMoveUsedAbAttr */ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr { override canApply({ source, pokemon }: PostMoveUsedAbAttrParams): boolean { @@ -4996,7 +4978,14 @@ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr { // 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); - globalScene.phaseManager.unshiftNew("MovePhase", pokemon, target, move, MoveUseMode.INDIRECT); + globalScene.phaseManager.unshiftNew( + "MovePhase", + pokemon, + target, + move, + MoveUseMode.INDIRECT, + MovePhaseTimingModifier.FIRST, + ); } else if (move.getMove().is("SelfStatusMove")) { // If the move is a SelfStatusMove (ie. Swords Dance) the Dancer should replicate it on itself globalScene.phaseManager.unshiftNew( @@ -5005,6 +4994,7 @@ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr { [pokemon.getBattlerIndex()], move, MoveUseMode.INDIRECT, + MovePhaseTimingModifier.FIRST, ); } } @@ -5027,7 +5017,6 @@ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr { /** * Triggers after the Pokemon loses or consumes an item - * @extends AbAttr */ export class PostItemLostAbAttr extends AbAttr { canApply(_params: Closed): boolean { @@ -5198,7 +5187,6 @@ type ArenaTrapCondition = (user: Pokemon, target: Pokemon) => boolean; /** * Base class for checking if a Pokemon is trapped by arena trap - * @extends AbAttr * @field {@linkcode arenaTrapCondition} Conditional for trapping abilities. * For example, Magnet Pull will only activate if opponent is Steel type. * @see {@linkcode applyCheckTrapped} @@ -5534,7 +5522,6 @@ export interface ReduceStatusEffectDurationAbAttrParams extends AbAttrBaseParams /** * Used by Early Bird, makes the pokemon wake up faster * @param statusEffect - The {@linkcode StatusEffect} to check for - * @see {@linkcode apply} * @sealed */ export class ReduceStatusEffectDurationAbAttr extends AbAttr { @@ -5821,8 +5808,6 @@ export class IgnoreTypeStatusEffectImmunityAbAttr extends AbAttr { /** * Gives money to the user after the battle. - * - * @extends PostBattleAbAttr */ export class MoneyAbAttr extends PostBattleAbAttr { override canApply({ simulated, victory }: PostBattleAbAttrParams): boolean { @@ -5926,7 +5911,6 @@ export class FormBlockDamageAbAttr extends ReceivedMoveDamageMultiplierAbAttr { /** * Base class for defining {@linkcode Ability} attributes before summon * (should use {@linkcode PostSummonAbAttr} for most ability) - * @see {@linkcode applyPreSummon()} */ export class PreSummonAbAttr extends AbAttr { private declare readonly _: never; @@ -6026,11 +6010,6 @@ export class IllusionPostBattleAbAttr extends PostBattleAbAttr { } } -export interface BypassSpeedChanceAbAttrParams extends AbAttrBaseParams { - /** Holds whether the speed check is bypassed after ability application */ - bypass: BooleanHolder; -} - /** * If a Pokémon with this Ability selects a damaging move, it has a 30% chance of going first in its priority bracket. If the Ability activates, this is announced at the start of the turn (after move selection). * @sealed @@ -6046,26 +6025,28 @@ export class BypassSpeedChanceAbAttr extends AbAttr { this.chance = chance; } - override canApply({ bypass, simulated, pokemon }: BypassSpeedChanceAbAttrParams): boolean { + override canApply({ simulated, pokemon }: AbAttrBaseParams): boolean { // TODO: Consider whether we can move the simulated check to the `apply` method // May be difficult as we likely do not want to modify the randBattleSeed const turnCommand = globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()]; - const isCommandFight = turnCommand?.command === Command.FIGHT; const move = turnCommand?.move?.move ? allMoves[turnCommand.move.move] : null; const isDamageMove = move?.category === MoveCategory.PHYSICAL || move?.category === MoveCategory.SPECIAL; return ( - !simulated && !bypass.value && pokemon.randBattleSeedInt(100) < this.chance && isCommandFight && isDamageMove + !simulated + && pokemon.randBattleSeedInt(100) < this.chance + && isDamageMove + && pokemon.canAddTag(BattlerTagType.BYPASS_SPEED) ); } /** * bypass move order in their priority bracket when pokemon choose damaging move */ - override apply({ bypass }: BypassSpeedChanceAbAttrParams): void { - bypass.value = true; + override apply({ pokemon }: AbAttrBaseParams): void { + pokemon.addTag(BattlerTagType.BYPASS_SPEED); } - override getTriggerMessage({ pokemon }: BypassSpeedChanceAbAttrParams, _abilityName: string): string { + override getTriggerMessage({ pokemon }: AbAttrBaseParams, _abilityName: string): string { return i18next.t("abilityTriggers:quickDraw", { pokemonName: getPokemonNameWithAffix(pokemon) }); } } @@ -6073,8 +6054,6 @@ export class BypassSpeedChanceAbAttr extends AbAttr { export interface PreventBypassSpeedChanceAbAttrParams extends AbAttrBaseParams { /** Holds whether the speed check is bypassed after ability application */ bypass: BooleanHolder; - /** Holds whether the Pokemon can check held items for Quick Claw's effects */ - canCheckHeldItems: BooleanHolder; } /** @@ -6101,9 +6080,8 @@ export class PreventBypassSpeedChanceAbAttr extends AbAttr { return isCommandFight && this.condition(pokemon, move!); } - override apply({ bypass, canCheckHeldItems }: PreventBypassSpeedChanceAbAttrParams): void { + override apply({ bypass }: PreventBypassSpeedChanceAbAttrParams): void { bypass.value = false; - canCheckHeldItems.value = false; } } @@ -6201,8 +6179,7 @@ class ForceSwitchOutHelper { if (switchOutTarget.hp > 0) { switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH); - globalScene.phaseManager.prependNewToPhase( - "MoveEndPhase", + globalScene.phaseManager.queueDeferred( "SwitchPhase", this.switchType, switchOutTarget.getFieldIndex(), @@ -6224,8 +6201,7 @@ class ForceSwitchOutHelper { const summonIndex = globalScene.currentBattle.trainer ? globalScene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0; - globalScene.phaseManager.prependNewToPhase( - "MoveEndPhase", + globalScene.phaseManager.queueDeferred( "SwitchSummonPhase", this.switchType, switchOutTarget.getFieldIndex(), @@ -6254,7 +6230,7 @@ class ForceSwitchOutHelper { true, 500, ); - if (globalScene.currentBattle.double && !isNullOrUndefined(allyPokemon)) { + if (globalScene.currentBattle.double && allyPokemon != null) { globalScene.redirectPokemonMoves(switchOutTarget, allyPokemon); } } @@ -6378,8 +6354,6 @@ export class PostDamageAbAttr extends AbAttr { * and its opponents, and determines whether a forced switch-out should occur. * * Used by Wimp Out and Emergency Exit - * - * @see {@linkcode applyPostDamage} * @sealed */ export class PostDamageForceSwitchAbAttr extends PostDamageAbAttr { @@ -6949,7 +6923,7 @@ export function initAbilities() { .attr(TypeImmunityStatStageChangeAbAttr, PokemonType.ELECTRIC, Stat.SPD, 1) .ignorable(), new Ability(AbilityId.RIVALRY, 4) - .attr(MovePowerBoostAbAttr, (user, target, _move) => user?.gender !== Gender.GENDERLESS && target?.gender !== Gender.GENDERLESS && user?.gender === target?.gender, 1.25, true) + .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), new Ability(AbilityId.STEADFAST, 4) .attr(FlinchStatStageChangeAbAttr, [ Stat.SPD ], 1), @@ -6998,6 +6972,7 @@ export function initAbilities() { .attr(StatMultiplierAbAttr, Stat.SPATK, 1.5) .condition(getWeatherCondition(WeatherType.SUNNY, WeatherType.HARSH_SUN)), new Ability(AbilityId.QUICK_FEET, 4) + // TODO: This should ignore the speed drop, not manually undo it .conditionalAttr(pokemon => pokemon.status ? pokemon.status.effect === StatusEffect.PARALYSIS : false, StatMultiplierAbAttr, Stat.SPD, 2) .conditionalAttr(pokemon => !!pokemon.status || pokemon.hasAbility(AbilityId.COMATOSE), StatMultiplierAbAttr, Stat.SPD, 1.5), new Ability(AbilityId.NORMALIZE, 4) @@ -7107,7 +7082,7 @@ export function initAbilities() { .attr(PostDefendMoveDisableAbAttr, 30) .bypassFaint(), new Ability(AbilityId.HEALER, 5) - .conditionalAttr(pokemon => !isNullOrUndefined(pokemon.getAlly()) && randSeedInt(10) < 3, PostTurnResetStatusAbAttr, true), + .conditionalAttr(pokemon => pokemon.getAlly() != null && randSeedInt(10) < 3, PostTurnResetStatusAbAttr, true), new Ability(AbilityId.FRIEND_GUARD, 5) .attr(AlliedFieldDamageReductionAbAttr, 0.75) .ignorable(), @@ -7160,7 +7135,7 @@ export function initAbilities() { new Ability(AbilityId.ANALYTIC, 5) .attr(MovePowerBoostAbAttr, (user) => // Boost power if all other Pokemon have already moved (no other moves are slated to execute) - !globalScene.phaseManager.findPhase((phase) => phase.is("MovePhase") && phase.pokemon.id !== user?.id), + !globalScene.phaseManager.hasPhaseOfType("MovePhase", phase => phase.pokemon.id !== user?.id), 1.3), new Ability(AbilityId.ILLUSION, 5) // The Pokemon generate an illusion if it's available @@ -7741,8 +7716,8 @@ export function initAbilities() { new Ability(AbilityId.SHARPNESS, 9) .attr(MovePowerBoostAbAttr, (_user, _target, move) => move.hasFlag(MoveFlags.SLICING_MOVE), 1.5), new Ability(AbilityId.SUPREME_OVERLORD, 9) - .attr(VariableMovePowerBoostAbAttr, (user, _target, _move) => 1 + 0.1 * Math.min(user.isPlayer() ? globalScene.arena.playerFaints : globalScene.currentBattle.enemyFaints, 5)) - .partial(), // Should only boost once, on summon + .conditionalAttr((p) => (p.isPlayer() ? globalScene.arena.playerFaints : globalScene.currentBattle.enemyFaints) > 0, PostSummonAddBattlerTagAbAttr, BattlerTagType.SUPREME_OVERLORD, 0, true) + .edgeCase(), // Tag is not tied to ability, so suppression/removal etc will not function until a structure to allow this is implemented new Ability(AbilityId.COSTAR, 9, -2) .attr(PostSummonCopyAllyStatsAbAttr), new Ability(AbilityId.TOXIC_DEBRIS, 9) diff --git a/src/data/abilities/apply-ab-attrs.ts b/src/data/abilities/apply-ab-attrs.ts index 58f63c5924a..23b16a4cac7 100644 --- a/src/data/abilities/apply-ab-attrs.ts +++ b/src/data/abilities/apply-ab-attrs.ts @@ -74,7 +74,6 @@ function applyAbAttrsInternal( for (const passive of [false, true]) { params.passive = passive; applySingleAbAttrs(attrType, params, gainedMidTurn, messages); - globalScene.phaseManager.clearPhaseQueueSplice(); } // We need to restore passive to its original state in the case that it was undefined on entry // this is necessary in case this method is called with an object that is reused. diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index c13ebf95ea0..22c0b98b9a4 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -1,39 +1,4 @@ -/** 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 { globalScene } from "#app/global-scene"; -import { getPokemonNameWithAffix } from "#app/messages"; -import { CommonBattleAnim } from "#data/battle-anims"; -import { allMoves } from "#data/data-lists"; -import { AbilityId } from "#enums/ability-id"; -import { ArenaTagSide } from "#enums/arena-tag-side"; -import { ArenaTagType } from "#enums/arena-tag-type"; -import { BattlerTagType } from "#enums/battler-tag-type"; -import { HitResult } from "#enums/hit-result"; -import { CommonAnim } from "#enums/move-anims-common"; -import { MoveCategory } from "#enums/move-category"; -import { MoveId } from "#enums/move-id"; -import { MoveTarget } from "#enums/move-target"; -import { PokemonType } from "#enums/pokemon-type"; -import { Stat } from "#enums/stat"; -import { StatusEffect } from "#enums/status-effect"; -import type { Arena } from "#field/arena"; -import type { Pokemon } from "#field/pokemon"; -import type { - ArenaScreenTagType, - ArenaTagData, - EntryHazardTagType, - RoomArenaTagType, - SerializableArenaTagType, -} from "#types/arena-tags"; -import type { Mutable } from "#types/type-helpers"; -import { BooleanHolder, type NumberHolder, toDmgValue } from "#utils/common"; -import i18next from "i18next"; - /** - * @module * ArenaTags are are meant for effects that are tied to the arena (as opposed to a specific pokemon). * Examples include (but are not limited to) * - Cross-turn effects that persist even if the user/target switches out, such as Happy Hour @@ -76,14 +41,54 @@ import i18next from "i18next"; * ``` * Notes * - If the class has any subclasses, then the second form of `loadTag` *must* be used. + * @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 { globalScene } from "#app/global-scene"; +import { getPokemonNameWithAffix } from "#app/messages"; +import { CommonBattleAnim } from "#data/battle-anims"; +import { allMoves } from "#data/data-lists"; +import { AbilityId } from "#enums/ability-id"; +import { ArenaTagSide } from "#enums/arena-tag-side"; +import { ArenaTagType } from "#enums/arena-tag-type"; +import type { BattlerIndex } from "#enums/battler-index"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { HitResult } from "#enums/hit-result"; +import { CommonAnim } from "#enums/move-anims-common"; +import { MoveCategory } from "#enums/move-category"; +import { MoveId } from "#enums/move-id"; +import { MoveTarget } from "#enums/move-target"; +import { PokemonType } from "#enums/pokemon-type"; +import { Stat } from "#enums/stat"; +import { StatusEffect } from "#enums/status-effect"; +import type { Arena } from "#field/arena"; +import type { Pokemon } from "#field/pokemon"; +import type { + ArenaScreenTagType, + ArenaTagData, + EntryHazardTagType, + RoomArenaTagType, + SerializableArenaTagType, +} from "#types/arena-tags"; +import type { Mutable } from "#types/type-helpers"; +import { BooleanHolder, type NumberHolder, toDmgValue } from "#utils/common"; +import i18next from "i18next"; + /** Interface containing the serializable fields of ArenaTagData. */ interface BaseArenaTag { /** * The tag's remaining duration. Setting to any number `<=0` will make the tag's duration effectively infinite. */ turnCount: number; + /** + * The tag's max duration. + */ + maxDuration: number; /** * The {@linkcode MoveId} that created this tag, or `undefined` if not set by a move. */ @@ -110,12 +115,14 @@ export abstract class ArenaTag implements BaseArenaTag { /** The type of the arena tag */ public abstract readonly tagType: ArenaTagType; public turnCount: number; + public maxDuration: number; public sourceMove?: MoveId; public sourceId: number | undefined; public side: ArenaTagSide; constructor(turnCount: number, sourceMove?: MoveId, sourceId?: number, side: ArenaTagSide = ArenaTagSide.BOTH) { this.turnCount = turnCount; + this.maxDuration = turnCount; this.sourceMove = sourceMove; this.sourceId = sourceId; this.side = side; @@ -138,7 +145,7 @@ export abstract class ArenaTag implements BaseArenaTag { } } - onOverlap(_arena: Arena, _source: Pokemon | null): void {} + onOverlap(_arena: Arena, _source: Pokemon | undefined): void {} /** * Trigger this {@linkcode ArenaTag}'s effect, reducing its duration as applicable. @@ -164,6 +171,7 @@ export abstract class ArenaTag implements BaseArenaTag { */ loadTag(source: BaseArenaTag & Pick): void { this.turnCount = source.turnCount; + this.maxDuration = source.maxDuration; this.sourceMove = source.sourceMove; this.sourceId = source.sourceId; this.side = source.side; @@ -172,9 +180,8 @@ export abstract class ArenaTag implements BaseArenaTag { /** * Helper function that retrieves the source Pokemon * @returns - The source {@linkcode Pokemon} for this tag. - * Returns `null` if `this.sourceId` is `undefined` */ - public getSourcePokemon(): Pokemon | null { + public getSourcePokemon(): Pokemon | undefined { return globalScene.getPokemonById(this.sourceId); } @@ -617,7 +624,7 @@ export class NoCritTag extends SerializableArenaTag { globalScene.phaseManager.queueMessage( i18next.t("arenaTag:noCritOnRemove", { - pokemonNameWithAffix: getPokemonNameWithAffix(source ?? undefined), + pokemonNameWithAffix: getPokemonNameWithAffix(source), moveName: this.getMoveName(), }), ); @@ -1132,7 +1139,7 @@ class ImprisonTag extends EntryHazardTag { /** * This applies the effects of Imprison to any opposing Pokemon that switch into the field while the source Pokemon is still active - * @param {Pokemon} pokemon the Pokemon Imprison is applied to + * @param pokemon the Pokemon Imprison is applied to * @returns `true` */ override activateTrap(pokemon: Pokemon): boolean { @@ -1537,7 +1544,7 @@ export class SuppressAbilitiesTag extends SerializableArenaTag { } } - public override onOverlap(_arena: Arena, source: Pokemon | null): void { + public override onOverlap(_arena: Arena, source: Pokemon | undefined): void { (this as Mutable).sourceCount++; this.playActivationMessage(source); } @@ -1580,7 +1587,7 @@ export class SuppressAbilitiesTag extends SerializableArenaTag { return this.sourceCount > 1; } - private playActivationMessage(pokemon: Pokemon | null) { + private playActivationMessage(pokemon: Pokemon | undefined) { if (pokemon) { globalScene.phaseManager.queueMessage( i18next.t("arenaTag:neutralizingGasOnAdd", { @@ -1591,6 +1598,145 @@ export class SuppressAbilitiesTag extends SerializableArenaTag { } } +/** + * Interface containing data related to a queued healing effect from + * {@link https://bulbapedia.bulbagarden.net/wiki/Healing_Wish_(move) | Healing Wish} + * or {@link https://bulbapedia.bulbagarden.net/wiki/Lunar_Dance_(move) | Lunar Dance}. + */ +interface PendingHealEffect { + /** The {@linkcode Pokemon.id | PID} of the {@linkcode Pokemon} that created the effect. */ + readonly sourceId: number; + /** The {@linkcode MoveId} of the move that created the effect. */ + readonly moveId: MoveId; + /** If `true`, also restores the target's PP when the effect activates. */ + readonly restorePP: boolean; + /** The message to display when the effect activates */ + readonly healMessage: string; +} + +/** + * Arena tag to contain stored healing effects, namely from + * {@link https://bulbapedia.bulbagarden.net/wiki/Healing_Wish_(move) | Healing Wish} + * and {@link https://bulbapedia.bulbagarden.net/wiki/Lunar_Dance_(move) | Lunar Dance}. + * When a damaged Pokemon first enters the effect's {@linkcode BattlerIndex | field position}, + * their HP is fully restored, and they are cured of any non-volatile status condition. + * If the effect is from Lunar Dance, their PP is also restored. + */ +export class PendingHealTag extends SerializableArenaTag { + public readonly tagType = ArenaTagType.PENDING_HEAL; + /** All pending healing effects, organized by {@linkcode BattlerIndex} */ + public readonly pendingHeals: Partial> = {}; + + constructor() { + super(0); + } + + /** + * Adds a pending healing effect to the field. Effects under the same move *and* + * target index as an existing effect are ignored. + * @param targetIndex - The {@linkcode BattlerIndex} under which the effect applies + * @param healEffect - The {@linkcode PendingHealEffect | data} for the pending heal effect + */ + public queueHeal(targetIndex: BattlerIndex, healEffect: PendingHealEffect): void { + const existingHealEffects = this.pendingHeals[targetIndex]; + if (existingHealEffects) { + if (!existingHealEffects.some(he => he.moveId === healEffect.moveId)) { + existingHealEffects.push(healEffect); + } + } else { + this.pendingHeals[targetIndex] = [healEffect]; + } + } + + /** Removes default on-remove message */ + override onRemove(_arena: Arena): void {} + + /** This arena tag is removed at the end of the turn if no pending healing effects are on the field */ + override lapse(_arena: Arena): boolean { + for (const key in this.pendingHeals) { + if (this.pendingHeals[key].length > 0) { + return true; + } + } + return false; + } + + /** + * Applies a pending healing effect on the given target index. If an effect is found for + * the index, the Pokemon at that index is healed to full HP, is cured of any non-volatile status, + * and has its PP fully restored (if the effect is from Lunar Dance). + * @param arena - The {@linkcode Arena} containing this tag + * @param simulated - If `true`, suppresses changes to game state + * @param pokemon - The {@linkcode Pokemon} receiving the healing effect + * @returns `true` if the target Pokemon was healed by this effect + * @todo This should also be called when a Pokemon moves into a new position via Ally Switch + */ + override apply(arena: Arena, simulated: boolean, pokemon: Pokemon): boolean { + const targetIndex = pokemon.getBattlerIndex(); + const targetEffects = this.pendingHeals[targetIndex]; + + if (targetEffects == null || targetEffects.length === 0) { + return false; + } + + const healEffect = targetEffects.find(effect => this.canApply(effect, pokemon)); + + if (healEffect == null) { + return false; + } + + if (simulated) { + return true; + } + + const { sourceId, moveId, restorePP, healMessage } = healEffect; + const sourcePokemon = globalScene.getPokemonById(sourceId); + if (!sourcePokemon) { + console.warn(`Source of pending ${allMoves[moveId].name} effect is undefined!`); + targetEffects.splice(targetEffects.indexOf(healEffect), 1); + // Re-evaluate after the invalid heal effect is removed + return this.apply(arena, simulated, pokemon); + } + + globalScene.phaseManager.unshiftNew( + "PokemonHealPhase", + targetIndex, + pokemon.getMaxHp(), + healMessage, + true, + false, + false, + true, + false, + restorePP, + ); + + targetEffects.splice(targetEffects.indexOf(healEffect), 1); + + return healEffect != null; + } + + /** + * Determines if the given {@linkcode PendingHealEffect} can immediately heal + * the given target {@linkcode Pokemon}. + * @param healEffect - The {@linkcode PendingHealEffect} to evaluate + * @param pokemon - The {@linkcode Pokemon} to evaluate against + * @returns `true` if the Pokemon can be healed by the effect + */ + private canApply(healEffect: PendingHealEffect, pokemon: Pokemon): boolean { + return ( + !pokemon.isFullHp() + || pokemon.status != null + || (healEffect.restorePP && pokemon.getMoveset().some(mv => mv.ppUsed > 0)) + ); + } + + override loadTag(source: BaseArenaTag & Pick): void { + super.loadTag(source); + (this as Mutable).pendingHeals = source.pendingHeals; + } +} + // TODO: swap `sourceMove` and `sourceId` and make `sourceMove` an optional parameter export function getArenaTag( tagType: ArenaTagType, @@ -1654,6 +1800,8 @@ export function getArenaTag( return new FairyLockTag(turnCount, sourceId); case ArenaTagType.NEUTRALIZING_GAS: return new SuppressAbilitiesTag(sourceId); + case ArenaTagType.PENDING_HEAL: + return new PendingHealTag(); default: return null; } @@ -1702,5 +1850,6 @@ export type ArenaTagTypeMap = { [ArenaTagType.GRASS_WATER_PLEDGE]: GrassWaterPledgeTag; [ArenaTagType.FAIRY_LOCK]: FairyLockTag; [ArenaTagType.NEUTRALIZING_GAS]: SuppressAbilitiesTag; + [ArenaTagType.PENDING_HEAL]: PendingHealTag; [ArenaTagType.NONE]: NoneTag; }; diff --git a/src/data/balance/biomes.ts b/src/data/balance/biomes.ts index b253b0ded6e..9af2dbe221c 100644 --- a/src/data/balance/biomes.ts +++ b/src/data/balance/biomes.ts @@ -1119,7 +1119,7 @@ export const biomePokemonPools: BiomePokemonPools = { }, [BiomePoolTier.RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.HITMONLEE, SpeciesId.HITMONCHAN, SpeciesId.LUCARIO, SpeciesId.THROH, SpeciesId.SAWK, { 1: [ SpeciesId.PANCHAM ], 52: [ SpeciesId.PANGORO ] } ] }, [BiomePoolTier.SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.HITMONTOP, SpeciesId.GALLADE, SpeciesId.GALAR_FARFETCHD ] }, - [BiomePoolTier.ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.TERRAKION, { 1: [ SpeciesId.KUBFU ], 60: [ SpeciesId.URSHIFU] }, SpeciesId.GALAR_ZAPDOS ] }, + [BiomePoolTier.ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.TERRAKION, { 1: [ SpeciesId.KUBFU ], 60: [ SpeciesId.URSHIFU ] }, SpeciesId.GALAR_ZAPDOS ] }, [BiomePoolTier.BOSS]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], @@ -1128,7 +1128,7 @@ export const biomePokemonPools: BiomePokemonPools = { [TimeOfDay.ALL]: [ SpeciesId.HITMONLEE, SpeciesId.HITMONCHAN, SpeciesId.HARIYAMA, SpeciesId.MEDICHAM, SpeciesId.LUCARIO, SpeciesId.TOXICROAK, SpeciesId.THROH, SpeciesId.SAWK, SpeciesId.SCRAFTY, SpeciesId.MIENSHAO, SpeciesId.BEWEAR, SpeciesId.GRAPPLOCT, SpeciesId.ANNIHILAPE ] }, [BiomePoolTier.BOSS_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.HITMONTOP, SpeciesId.GALLADE, SpeciesId.PANGORO, SpeciesId.SIRFETCHD, SpeciesId.HISUI_DECIDUEYE ] }, - [BiomePoolTier.BOSS_SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.TERRAKION, { 1: [ SpeciesId.KUBFU ], 60: [ SpeciesId.URSHIFU] } ] }, + [BiomePoolTier.BOSS_SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.TERRAKION, { 1: [ SpeciesId.KUBFU ], 60: [ SpeciesId.URSHIFU ] } ] }, [BiomePoolTier.BOSS_ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.ZAMAZENTA, SpeciesId.GALAR_ZAPDOS ] } }, [BiomeId.FACTORY]: { @@ -1597,10 +1597,10 @@ export const biomePokemonPools: BiomePokemonPools = { [BiomePoolTier.UNCOMMON]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ { 1: [ SpeciesId.SOLOSIS ], 32: [ SpeciesId.DUOSION ], 41: [ SpeciesId.REUNICLUS ] } ] }, [BiomePoolTier.RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.DITTO, { 1: [ SpeciesId.PORYGON ], 30: [ SpeciesId.PORYGON2 ] } ] }, [BiomePoolTier.SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.ROTOM ] }, - [BiomePoolTier.ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ { 1: [SpeciesId.TYPE_NULL], 60: [ SpeciesId.SILVALLY ] } ] }, + [BiomePoolTier.ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ { 1: [ SpeciesId.TYPE_NULL ], 60: [ SpeciesId.SILVALLY ] } ] }, [BiomePoolTier.BOSS]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.MUK, SpeciesId.ELECTRODE, SpeciesId.BRONZONG, SpeciesId.MAGNEZONE, SpeciesId.PORYGON_Z, SpeciesId.REUNICLUS, SpeciesId.KLINKLANG ] }, [BiomePoolTier.BOSS_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [] }, - [BiomePoolTier.BOSS_SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.ROTOM, SpeciesId.ZYGARDE, { 1: [SpeciesId.TYPE_NULL], 60: [ SpeciesId.SILVALLY ] } ] }, + [BiomePoolTier.BOSS_SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.ROTOM, SpeciesId.ZYGARDE, { 1: [ SpeciesId.TYPE_NULL ], 60: [ SpeciesId.SILVALLY ] } ] }, [BiomePoolTier.BOSS_ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.MEWTWO, SpeciesId.MIRAIDON ] } }, [BiomeId.END]: { @@ -5627,10 +5627,12 @@ export function initBiomes() { ] ], [ SpeciesId.TYPE_NULL, PokemonType.NORMAL, -1, [ - [ BiomeId.LABORATORY, BiomePoolTier.ULTRA_RARE ] + [ BiomeId.LABORATORY, BiomePoolTier.ULTRA_RARE ], + [ BiomeId.LABORATORY, BiomePoolTier.BOSS_SUPER_RARE ] ] ], [ SpeciesId.SILVALLY, PokemonType.NORMAL, -1, [ + [ BiomeId.LABORATORY, BiomePoolTier.ULTRA_RARE ], [ BiomeId.LABORATORY, BiomePoolTier.BOSS_SUPER_RARE ] ] ], @@ -5773,10 +5775,12 @@ export function initBiomes() { ] ], [ SpeciesId.POIPOLE, PokemonType.POISON, -1, [ - [ BiomeId.SWAMP, BiomePoolTier.ULTRA_RARE ] + [ BiomeId.SWAMP, BiomePoolTier.ULTRA_RARE ], + [ BiomeId.SWAMP, BiomePoolTier.BOSS_SUPER_RARE ] ] ], [ SpeciesId.NAGANADEL, PokemonType.POISON, PokemonType.DRAGON, [ + [ BiomeId.SWAMP, BiomePoolTier.ULTRA_RARE ], [ BiomeId.SWAMP, BiomePoolTier.BOSS_SUPER_RARE ] ] ], @@ -6165,10 +6169,12 @@ export function initBiomes() { ] ], [ SpeciesId.KUBFU, PokemonType.FIGHTING, -1, [ - [ BiomeId.DOJO, BiomePoolTier.ULTRA_RARE ] + [ BiomeId.DOJO, BiomePoolTier.ULTRA_RARE ], + [ BiomeId.DOJO, BiomePoolTier.BOSS_SUPER_RARE ] ] ], [ SpeciesId.URSHIFU, PokemonType.FIGHTING, PokemonType.DARK, [ + [ BiomeId.DOJO, BiomePoolTier.ULTRA_RARE ], [ BiomeId.DOJO, BiomePoolTier.BOSS_SUPER_RARE ] ] ], @@ -7209,7 +7215,8 @@ export function initBiomes() { ], [ TrainerType.SCIENTIST, [ [ BiomeId.DESERT, BiomePoolTier.COMMON ], - [ BiomeId.RUINS, BiomePoolTier.COMMON ] + [ BiomeId.RUINS, BiomePoolTier.COMMON ], + [ BiomeId.LABORATORY, BiomePoolTier.COMMON ] ] ], [ TrainerType.SMASHER, []], @@ -7224,7 +7231,8 @@ export function initBiomes() { ] ], [ TrainerType.SWIMMER, [ - [ BiomeId.SEA, BiomePoolTier.COMMON ] + [ BiomeId.SEA, BiomePoolTier.COMMON ], + [ BiomeId.SEABED, BiomePoolTier.COMMON ] ] ], [ TrainerType.TWINS, [ @@ -7590,11 +7598,13 @@ export function initBiomes() { [ TrainerType.ALDER, []], [ TrainerType.IRIS, []], [ TrainerType.DIANTHA, []], + [ TrainerType.KUKUI, []], [ TrainerType.HAU, []], + [ TrainerType.LEON, []], + [ TrainerType.MUSTARD, []], [ TrainerType.GEETA, []], [ TrainerType.NEMONA, []], [ TrainerType.KIERAN, []], - [ TrainerType.LEON, []], [ TrainerType.RIVAL, []] ]; diff --git a/src/data/balance/moveset-generation.ts b/src/data/balance/moveset-generation.ts new file mode 100644 index 00000000000..90a602ca97e --- /dev/null +++ b/src/data/balance/moveset-generation.ts @@ -0,0 +1,235 @@ +/* + * SPDX-Copyright-Text: 2025 Pagefault Games + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +/** + * # Balance: Moveset Generation Configuration + * + * This module contains configuration constants and functions that control + * the limitations and rules around moveset generation for generated Pokémon. + * + * + * ### Move Weights + * + * The various move weight constants in this module control how likely + * certain categories of moves are to appear in a generated Pokémon's + * moveset. Higher weights make a move more likely to be chosen. + * The constants here specify the *base* weight for a move when first computed. + * These weights are post-processed (and then scaled up such that weights have a larger impact, + * for instance, on boss Pokémon) before being used in the actual moveset generation. + * + * Post Processing of weights includes, but is not limited to: + * - Adjusting weights of status moves + * - Adjusting weights based on the move's power relative to the highest power available + * - Adjusting weights based on the stat the move uses to calculate damage relative to the higher stat + * + * + * All weights go through additional post-processing based on + * their expected power (accuracy * damage * expected number of hits) + * + * @module + */ + +import { MoveId } from "#enums/move-id"; + + +//#region Constants +/** + * The minimum level for a Pokémon to generate with a move it can only learn + * from a common tier TM + */ +export const COMMON_TIER_TM_LEVEL_REQUIREMENT = 25; +/** + * The minimum level for a Pokémon to generate with a move it can only learn + * from a great tier TM + */ +export const GREAT_TIER_TM_LEVEL_REQUIREMENT = 40; +/** + * The minimum level for a Pokémon to generate with a move it can only learn + * from an ultra tier TM + */ +export const ULTRA_TIER_TM_LEVEL_REQUIREMENT = 55; + +/** Below this level, Pokémon will be unable to generate with any egg moves */ +export const EGG_MOVE_LEVEL_REQUIREMENT = 60; +/** Below this level, Pokémon will be unable to generate with rare egg moves */ +export const RARE_EGG_MOVE_LEVEL_REQUIREMENT = 170; + +// Note: Not exported, only for use with `getMaxTmCount +/** Below this level, Pokémon will be unable to generate with any TMs */ +const ONE_TM_THRESHOLD = 25; +/** Below this level, Pokémon will generate with at most 1 TM */ +const TWO_TM_THRESHOLD = 41; +/** Below this level, Pokémon will generate with at most two TMs */ +const THREE_TM_THRESHOLD = 71; +/** Below this level, Pokémon will generate with at most three TMs */ +const FOUR_TM_THRESHOLD = 101; + +/** Below this level, Pokémon will be unable to generate any egg moves */ +const ONE_EGG_MOVE_THRESHOLD = 80; +/** Below this level, Pokémon will generate with at most 1 egg moves */ +const TWO_EGG_MOVE_THRESHOLD = 121; +/** Below this level, Pokémon will generate with at most 2 egg moves */ +const THREE_EGG_MOVE_THRESHOLD = 161; +/** Above this level, Pokémon will generate with at most 3 egg moves */ +const FOUR_EGG_MOVE_THRESHOLD = 201; + + +/** The weight given to TMs in the common tier during moveset generation */ +export const COMMON_TM_MOVESET_WEIGHT = 12; +/** The weight given to TMs in the great tier during moveset generation */ +export const GREAT_TM_MOVESET_WEIGHT = 14; +/** The weight given to TMs in the ultra tier during moveset generation */ +export const ULTRA_TM_MOVESET_WEIGHT = 18; + +/** + * The base weight offset for level moves + * + * @remarks + * The relative likelihood of moves learned at different levels is determined by + * the ratio of their weights, + * or, the formula: + * `(levelB + BASE_LEVEL_WEIGHT_OFFSET) / (levelA + BASE_LEVEL_WEIGHT_OFFSET)` + * + * For example, consider move A and B that are learned at levels 1 and 60, respectively, + * but have no other differences (same power, accuracy, category, etc). + * The following table demonstrates the likelihood of move B being chosen over move A. + * + * | Offset | Likelihood | + * |--------|------------| + * | 0 | 60x | + * | 1 | 30x | + * | 5 | 10.8x | + * | 20 | 3.8x | + * | 60 | 2x | + * + * Note that increasing this without adjusting the other weights will decrease the likelihood of non-level moves + * + * For a complete picture, see {@link https://www.desmos.com/calculator/wgln4dxigl} + */ +export const BASE_LEVEL_WEIGHT_OFFSET = 20; + +/** + * The maximum weight an egg move can ever have + * @remarks + * Egg moves have their weights adjusted based on the maximum weight of the Pokémon's + * level-up moves. Rare Egg moves are always 5/6th of the computed egg move weight. + * Boss pokemon are not allowed to spawn with rare egg moves. + * @see {@linkcode EGG_MOVE_TO_LEVEL_WEIGHT} + */ +export const EGG_MOVE_WEIGHT_MAX = 60; +/** + * The percentage of the Pokémon's highest weighted level move to the weight an + * egg move can generate with + */ +export const EGG_MOVE_TO_LEVEL_WEIGHT = 0.85; +/** The weight given to evolution moves */ +export const EVOLUTION_MOVE_WEIGHT = 70; +/** The weight given to relearn moves */ +export const RELEARN_MOVE_WEIGHT = 60; + +/** The base weight multiplier to use + * + * The higher the number, the more impact weights have on the final move selection. + * i.e. if set to 0, all moves have equal chance of being selected regardless of their weight. + */ +export const BASE_WEIGHT_MULTIPLIER = 1.6; + +/** The additional weight added onto {@linkcode BASE_WEIGHT_MULTIPLIER} for boss Pokémon */ +export const BOSS_EXTRA_WEIGHT_MULTIPLIER = 0.4; + + + +/** + * Set of moves that should be blacklisted from the forced STAB during moveset generation + * + * @remarks + * During moveset generation, trainer pokemon attempt to force their pokemon to generate with STAB + * moves in their movesets. Moves in this list not be considered to be "STAB" moves for this purpose. + * This does *not* prevent them from appearing in the moveset, but they will never + * be selected as a forced STAB move. + */ +export const STAB_BLACKLIST: ReadonlySet = new Set([ + MoveId.BEAT_UP, + MoveId.BELCH, + MoveId.BIDE, + MoveId.COMEUPPANCE, + MoveId.COUNTER, + MoveId.DOOM_DESIRE, + MoveId.DRAGON_RAGE, + MoveId.DREAM_EATER, + MoveId.ENDEAVOR, + MoveId.EXPLOSION, + MoveId.FAKE_OUT, + MoveId.FIRST_IMPRESSION, + MoveId.FISSURE, + MoveId.FLING, + MoveId.FOCUS_PUNCH, + MoveId.FUTURE_SIGHT, + MoveId.GUILLOTINE, + MoveId.HOLD_BACK, + MoveId.HORN_DRILL, + MoveId.LAST_RESORT, + MoveId.METAL_BURST, + MoveId.MIRROR_COAT, + MoveId.MISTY_EXPLOSION, + MoveId.NATURAL_GIFT, + MoveId.NATURES_MADNESS, + MoveId.NIGHT_SHADE, + MoveId.PSYWAVE, + MoveId.RUINATION, + MoveId.SELF_DESTRUCT, + MoveId.SHEER_COLD, + MoveId.SHELL_TRAP, + MoveId.SKY_DROP, + MoveId.SNORE, + MoveId.SONIC_BOOM, + MoveId.SPIT_UP, + MoveId.STEEL_BEAM, + MoveId.STEEL_ROLLER, + MoveId.SUPER_FANG, + MoveId.SYNCHRONOISE, + MoveId.UPPER_HAND, +]); + +//#endregion Constants + +/** + * Get the maximum number of TMs a Pokémon is allowed to learn based on + * its level + * @param level - The level of the Pokémon + * @returns The number of TMs the Pokémon can learn at this level + */ +export function getMaxTmCount(level: number) { + if (level < ONE_TM_THRESHOLD) { + return 0; + } + if (level < TWO_TM_THRESHOLD) { + return 1; + } + if (level < THREE_TM_THRESHOLD) { + return 2; + } + if (level < FOUR_TM_THRESHOLD) { + return 3; + } + return 4; +} + + +export function getMaxEggMoveCount(level: number): number { + if (level < ONE_EGG_MOVE_THRESHOLD) { + return 0; + } + if (level < TWO_EGG_MOVE_THRESHOLD) { + return 1; + } + if (level < THREE_EGG_MOVE_THRESHOLD) { + return 2; + } + if (level < FOUR_EGG_MOVE_THRESHOLD) { + return 3; + } + return 4; +} diff --git a/src/data/balance/pokemon-evolutions.ts b/src/data/balance/pokemon-evolutions.ts index d364dc036b1..0c2fa4e78fa 100644 --- a/src/data/balance/pokemon-evolutions.ts +++ b/src/data/balance/pokemon-evolutions.ts @@ -14,7 +14,7 @@ import { TimeOfDay } from "#enums/time-of-day"; import { WeatherType } from "#enums/weather-type"; import type { Pokemon } from "#field/pokemon"; import type { SpeciesStatBoosterItem, SpeciesStatBoosterModifierType } from "#modifiers/modifier-type"; -import { coerceArray, isNullOrUndefined, randSeedInt } from "#utils/common"; +import { coerceArray, randSeedInt } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import { toCamelCase } from "#utils/strings"; import i18next from "i18next"; @@ -53,6 +53,7 @@ export enum EvolutionItem { PRISM_SCALE, RAZOR_CLAW, RAZOR_FANG, + OVAL_STONE, REAPER_CLOTH, ELECTIRIZER, MAGMARIZER, @@ -128,7 +129,7 @@ export class SpeciesEvolutionCondition { } public get description(): string[] { - if (!isNullOrUndefined(this.desc)) { + if (this.desc != null) { return this.desc; } this.desc = this.data.map(cond => { @@ -161,11 +162,11 @@ export class SpeciesEvolutionCondition { case EvoCondKey.HELD_ITEM: return i18next.t(`pokemonEvolutions:heldItem.${toCamelCase(cond.itemKey)}`); } - }).filter(s => !isNullOrUndefined(s)); // Filter out stringless conditions + }).filter(s => s != null); // Filter out stringless conditions return this.desc; } - public conditionsFulfilled(pokemon: Pokemon): boolean { + public conditionsFulfilled(pokemon: Pokemon, forFusion = false): boolean { console.log(this.data); return this.data.every(cond => { switch (cond.key) { @@ -185,7 +186,7 @@ export class SpeciesEvolutionCondition { m.getStackCount() + pokemon.getPersistentTreasureCount() >= cond.value ); case EvoCondKey.GENDER: - return pokemon.gender === cond.gender; + return cond.gender === (forFusion ? pokemon.fusionGender : pokemon.gender); case EvoCondKey.SHEDINJA: // Shedinja cannot be evolved into directly return false; case EvoCondKey.BIOME: @@ -233,7 +234,7 @@ export class SpeciesFormEvolution { this.evoFormKey = evoFormKey; this.level = level; this.item = item || EvolutionItem.NONE; - if (!isNullOrUndefined(condition)) { + if (condition != null) { this.condition = new SpeciesEvolutionCondition(...coerceArray(condition)); } this.wildDelay = wildDelay ?? SpeciesWildEvolutionDelay.NONE; @@ -291,8 +292,8 @@ export class SpeciesFormEvolution { return ( pokemon.level >= this.level && // Check form key, using the fusion's form key if we're checking the fusion - (isNullOrUndefined(this.preFormKey) || (forFusion ? pokemon.getFusionFormKey() : pokemon.getFormKey()) === this.preFormKey) && - (isNullOrUndefined(this.condition) || this.condition.conditionsFulfilled(pokemon)) && + (this.preFormKey == null || (forFusion ? pokemon.getFusionFormKey() : pokemon.getFormKey()) === this.preFormKey) && + (this.condition == null || this.condition.conditionsFulfilled(pokemon, forFusion)) && ((item ?? EvolutionItem.NONE) === (this.item ?? EvolutionItem.NONE)) ); } @@ -305,11 +306,11 @@ export class SpeciesFormEvolution { */ public isValidItemEvolution(pokemon: Pokemon, forFusion = false): boolean { return ( - !isNullOrUndefined(this.item) && + this.item != null && pokemon.level >= this.level && // Check form key, using the fusion's form key if we're checking the fusion - (isNullOrUndefined(this.preFormKey) || (forFusion ? pokemon.getFusionFormKey() : pokemon.getFormKey()) === this.preFormKey) && - (isNullOrUndefined(this.condition) || this.condition.conditionsFulfilled(pokemon)) + (this.preFormKey == null || (forFusion ? pokemon.getFusionFormKey() : pokemon.getFormKey()) === this.preFormKey) && + (this.condition == null || this.condition.conditionsFulfilled(pokemon)) ); } @@ -1496,10 +1497,13 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesFormEvolution(SpeciesId.DUDUNSPARCE, "", "two-segment", 32, null, {key: EvoCondKey.MOVE, move: MoveId.HYPER_DRILL}, SpeciesWildEvolutionDelay.LONG) ], [SpeciesId.GLIGAR]: [ - new SpeciesEvolution(SpeciesId.GLISCOR, 1, EvolutionItem.RAZOR_FANG, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]} /* Razor fang at night*/, SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(SpeciesId.GLISCOR, 1, EvolutionItem.RAZOR_FANG, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]}, SpeciesWildEvolutionDelay.VERY_LONG) ], [SpeciesId.SNEASEL]: [ - new SpeciesEvolution(SpeciesId.WEAVILE, 1, EvolutionItem.RAZOR_CLAW, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]} /* Razor claw at night*/, SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(SpeciesId.WEAVILE, 1, EvolutionItem.RAZOR_CLAW, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]}, SpeciesWildEvolutionDelay.VERY_LONG) + ], + [SpeciesId.HAPPINY]: [ + new SpeciesEvolution(SpeciesId.CHANSEY, 1, EvolutionItem.OVAL_STONE, {key: EvoCondKey.TIME, time: [TimeOfDay.DAWN, TimeOfDay.DAY]}, SpeciesWildEvolutionDelay.SHORT) ], [SpeciesId.URSARING]: [ new SpeciesEvolution(SpeciesId.URSALUNA, 1, EvolutionItem.PEAT_BLOCK, null, SpeciesWildEvolutionDelay.VERY_LONG) //Ursaring does not evolve into Bloodmoon Ursaluna @@ -1760,7 +1764,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.CROBAT, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 120}, SpeciesWildEvolutionDelay.VERY_LONG) ], [SpeciesId.CHANSEY]: [ - new SpeciesEvolution(SpeciesId.BLISSEY, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 200}, SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(SpeciesId.BLISSEY, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 180}, SpeciesWildEvolutionDelay.LONG) ], [SpeciesId.PICHU]: [ new SpeciesFormEvolution(SpeciesId.PIKACHU, "spiky", "partner", 1, null, {key: EvoCondKey.FRIENDSHIP, value: 90}, SpeciesWildEvolutionDelay.SHORT), @@ -1787,9 +1791,6 @@ export const pokemonEvolutions: PokemonEvolutions = { [SpeciesId.CHINGLING]: [ new SpeciesEvolution(SpeciesId.CHIMECHO, 1, null, [{key: EvoCondKey.FRIENDSHIP, value: 90}, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]}], SpeciesWildEvolutionDelay.MEDIUM) ], - [SpeciesId.HAPPINY]: [ - new SpeciesEvolution(SpeciesId.CHANSEY, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 160}, SpeciesWildEvolutionDelay.SHORT) - ], [SpeciesId.MUNCHLAX]: [ new SpeciesEvolution(SpeciesId.SNORLAX, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 120}, SpeciesWildEvolutionDelay.LONG) ], diff --git a/src/data/battle-anims.ts b/src/data/battle-anims.ts index 1dcb7d7eebf..573a1730796 100644 --- a/src/data/battle-anims.ts +++ b/src/data/battle-anims.ts @@ -7,7 +7,7 @@ import { AnimBlendType, AnimFocus, AnimFrameTarget, ChargeAnim, CommonAnim } fro import { MoveFlags } from "#enums/move-flags"; import { MoveId } from "#enums/move-id"; import type { Pokemon } from "#field/pokemon"; -import { coerceArray, getFrameMs, isNullOrUndefined, type nil } from "#utils/common"; +import { coerceArray, getFrameMs, type nil } from "#utils/common"; import { getEnumKeys, getEnumValues } from "#utils/enums"; import { toKebabCase } from "#utils/strings"; import Phaser from "phaser"; @@ -388,7 +388,7 @@ class AnimTimedAddBgEvent extends AnimTimedBgEvent { moveAnim.bgSprite.setAlpha(this.opacity / 255); globalScene.field.add(moveAnim.bgSprite); const fieldPokemon = globalScene.getEnemyPokemon(false) ?? globalScene.getPlayerPokemon(false); - if (!isNullOrUndefined(priority)) { + if (priority != null) { globalScene.field.moveTo(moveAnim.bgSprite as Phaser.GameObjects.GameObject, priority); } else if (fieldPokemon?.isOnField()) { globalScene.field.moveBelow(moveAnim.bgSprite as Phaser.GameObjects.GameObject, fieldPokemon); @@ -524,7 +524,7 @@ export async function initEncounterAnims(encounterAnim: EncounterAnim | Encounte const encounterAnimNames = getEnumKeys(EncounterAnim); const encounterAnimFetches: Promise>[] = []; for (const anim of anims) { - if (encounterAnims.has(anim) && !isNullOrUndefined(encounterAnims.get(anim))) { + if (encounterAnims.has(anim) && encounterAnims.get(anim) != null) { continue; } encounterAnimFetches.push( @@ -1240,7 +1240,7 @@ export abstract class BattleAnim { const graphicIndex = graphicFrameCount++; const moveSprite = sprites[graphicIndex]; - if (!isNullOrUndefined(frame.priority)) { + if (frame.priority != null) { const setSpritePriority = (priority: number) => { if (existingFieldSprites.length > priority) { // Move to specified priority index diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index cc2a403cd53..8abd98f4683 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1,3 +1,42 @@ +/** + * BattlerTags are used to represent semi-persistent effects that can be attached to a Pokemon. + * Note that before serialization, a new tag object is created, and then `loadTag` is called on the + * tag with the object that was serialized. + * + * This means it is straightforward to avoid serializing fields. + * Fields that are not set in the constructor and not set in `loadTag` will thus not be serialized. + * + * Any battler tag that can persist across sessions must extend SerializableBattlerTag in its class definition signature. + * Only tags that persist across waves (meaning their effect can last >1 turn) should be considered + * serializable. + * + * Serializable battler tags have strict requirements for their fields. + * Properties that are not necessary to reconstruct the tag must not be serialized. This can be avoided + * by using a private property. If access to the property is needed outside of the class, then + * a getter (and potentially, a setter) should be used instead. + * + * If a property that is intended to be private must be serialized, then it should instead + * be declared as a public readonly propety. Then, in the `loadTag` method (or any method inside the class that needs to adjust the property) + * use `(this as Mutable).propertyName = value;` + * These rules ensure that Typescript is aware of the shape of the serialized version of the class. + * + * If any new serializable fields *are* added, then the class *must* override the + * `loadTag` method to set the new fields. Its signature *must* match the example below: + * ``` + * class ExampleTag extends SerializableBattlerTag { + * // Example, if we add 2 new fields that should be serialized: + * public a: string; + * public b: number; + * // Then we must also define a loadTag method with one of the following signatures + * public override loadTag(source: BaseBattlerTag & Pick(source: BaseBattlerTag & Pick): void; + * } + * ``` + * Notes + * - If the class has any subclasses, then the second form of `loadTag` *must* be used. + * @module + */ + import { applyAbAttrs } from "#abilities/apply-ab-attrs"; import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; @@ -49,48 +88,9 @@ import type { TypeBoostTagType, } from "#types/battler-tags"; import type { Mutable } from "#types/type-helpers"; -import { BooleanHolder, coerceArray, getFrameMs, isNullOrUndefined, NumberHolder, toDmgValue } from "#utils/common"; +import { BooleanHolder, coerceArray, getFrameMs, NumberHolder, toDmgValue } from "#utils/common"; import { toCamelCase } from "#utils/strings"; -/** - * @module - * BattlerTags are used to represent semi-persistent effects that can be attached to a Pokemon. - * Note that before serialization, a new tag object is created, and then `loadTag` is called on the - * tag with the object that was serialized. - * - * This means it is straightforward to avoid serializing fields. - * Fields that are not set in the constructor and not set in `loadTag` will thus not be serialized. - * - * Any battler tag that can persist across sessions must extend SerializableBattlerTag in its class definition signature. - * Only tags that persist across waves (meaning their effect can last >1 turn) should be considered - * serializable. - * - * Serializable battler tags have strict requirements for their fields. - * Properties that are not necessary to reconstruct the tag must not be serialized. This can be avoided - * by using a private property. If access to the property is needed outside of the class, then - * a getter (and potentially, a setter) should be used instead. - * - * If a property that is intended to be private must be serialized, then it should instead - * be declared as a public readonly propety. Then, in the `loadTag` method (or any method inside the class that needs to adjust the property) - * use `(this as Mutable).propertyName = value;` - * These rules ensure that Typescript is aware of the shape of the serialized version of the class. - * - * If any new serializable fields *are* added, then the class *must* override the - * `loadTag` method to set the new fields. Its signature *must* match the example below: - * ``` - * class ExampleTag extends SerializableBattlerTag { - * // Example, if we add 2 new fields that should be serialized: - * public a: string; - * public b: number; - * // Then we must also define a loadTag method with one of the following signatures - * public override loadTag(source: BaseBattlerTag & Pick(source: BaseBattlerTag & Pick): void; - * } - * ``` - * Notes - * - If the class has any subclasses, then the second form of `loadTag` *must* be used. - */ - /** Interface containing the serializable fields of BattlerTag */ interface BaseBattlerTag { /** The tag's remaining duration */ @@ -198,7 +198,7 @@ export class BattlerTag implements BaseBattlerTag { * Helper function that retrieves the source Pokemon object * @returns The source {@linkcode Pokemon}, or `null` if none is found */ - public getSourcePokemon(): Pokemon | null { + public getSourcePokemon(): Pokemon | undefined { return globalScene.getPokemonById(this.sourceId); } } @@ -378,7 +378,7 @@ export class DisabledTag extends MoveRestrictionBattlerTag { // Disable fails against struggle or an empty move history // TODO: Confirm if this is redundant given Disable/Cursed Body's disable conditions const move = pokemon.getLastNonVirtualMove(); - if (isNullOrUndefined(move) || move.move === MoveId.STRUGGLE) { + if (move == null || move.move === MoveId.STRUGGLE) { return; } @@ -451,7 +451,7 @@ export class GorillaTacticsTag extends MoveRestrictionBattlerTag { override canAdd(pokemon: Pokemon): boolean { // Choice items ignore struggle, so Gorilla Tactics should too const lastSelectedMove = pokemon.getLastNonVirtualMove(); - return !isNullOrUndefined(lastSelectedMove) && lastSelectedMove.move !== MoveId.STRUGGLE; + return lastSelectedMove != null && lastSelectedMove.move !== MoveId.STRUGGLE; } /** @@ -606,17 +606,7 @@ export class ShellTrapTag extends BattlerTag { // Trap should only be triggered by opponent's Physical moves if (phaseData?.move.category === MoveCategory.PHYSICAL && pokemon.isOpponent(phaseData.attacker)) { - const shellTrapPhaseIndex = globalScene.phaseManager.phaseQueue.findIndex( - phase => phase.is("MovePhase") && phase.pokemon === pokemon, - ); - const firstMovePhaseIndex = globalScene.phaseManager.phaseQueue.findIndex(phase => phase.is("MovePhase")); - - // Only shift MovePhase timing if it's not already next up - if (shellTrapPhaseIndex !== -1 && shellTrapPhaseIndex !== firstMovePhaseIndex) { - const shellTrapMovePhase = globalScene.phaseManager.phaseQueue.splice(shellTrapPhaseIndex, 1)[0]; - globalScene.phaseManager.prependToPhase(shellTrapMovePhase, "MovePhase"); - } - + globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === pokemon); this.activated = true; } @@ -968,7 +958,7 @@ export class InfatuatedTag extends SerializableBattlerTag { phaseManager.queueMessage( i18next.t("battlerTags:infatuatedLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - sourcePokemonName: getPokemonNameWithAffix(globalScene.getPokemonById(this.sourceId!) ?? undefined), // TODO: is that bang correct? + sourcePokemonName: getPokemonNameWithAffix(this.getSourcePokemon()), }), ); phaseManager.unshiftNew("CommonAnimPhase", pokemon.getBattlerIndex(), undefined, CommonAnim.ATTRACT); @@ -1279,22 +1269,9 @@ export class EncoreTag extends MoveRestrictionBattlerTag { }), ); - const movePhase = globalScene.phaseManager.findPhase(m => m.is("MovePhase") && m.pokemon === pokemon); - if (movePhase) { - const movesetMove = pokemon.getMoveset().find(m => m.moveId === this.moveId); - if (movesetMove) { - const lastMove = pokemon.getLastXMoves(1)[0]; - globalScene.phaseManager.tryReplacePhase( - m => m.is("MovePhase") && m.pokemon === pokemon, - globalScene.phaseManager.create( - "MovePhase", - pokemon, - lastMove.targets ?? [], - movesetMove, - MoveUseMode.NORMAL, - ), - ); - } + const movesetMove = pokemon.getMoveset().find(m => m.moveId === this.moveId); + if (movesetMove) { + globalScene.phaseManager.changePhaseMove((phase: MovePhase) => phase.pokemon === pokemon, movesetMove); } } @@ -1305,7 +1282,7 @@ export class EncoreTag extends MoveRestrictionBattlerTag { override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { if (lapseType === BattlerTagLapseType.CUSTOM) { const encoredMove = pokemon.getMoveset().find(m => m.moveId === this.moveId); - return !isNullOrUndefined(encoredMove) && encoredMove.getPpRatio() > 0; + return encoredMove != null && encoredMove.getPpRatio() > 0; } return super.lapse(pokemon, lapseType); } @@ -2159,7 +2136,7 @@ export class HighestStatBoostTag extends AbilityBattlerTag { null, false, null, - true, + false, ); } @@ -2474,10 +2451,7 @@ export class RemovedTypeTag extends SerializableBattlerTag { } } -/** - * Battler tag for effects that ground the source, allowing Ground-type moves to hit them. - * @description `IGNORE_FLYING`: Persistent grounding effects (i.e. from Smack Down and Thousand Waves) - */ +/** Battler tag for effects that ground the source, allowing Ground-type moves to hit them. */ export class GroundedTag extends SerializableBattlerTag { public override readonly tagType = BattlerTagType.IGNORE_FLYING; constructor(tagType: BattlerTagType.IGNORE_FLYING, lapseType: BattlerTagLapseType, sourceMove: MoveId) { @@ -2485,11 +2459,7 @@ export class GroundedTag extends SerializableBattlerTag { } } -/** - * @description `ROOSTED`: Tag for temporary grounding if only source of ungrounding is flying and pokemon uses Roost. - * Roost removes flying type from a pokemon for a single turn. - */ - +/** Removes flying type from a pokemon for a single turn */ export class RoostedTag extends BattlerTag { private isBaseFlying: boolean; private isBasePureFlying: boolean; @@ -2568,7 +2538,7 @@ export class FormBlockDamageTag extends SerializableBattlerTag { /** * Applies the tag to the Pokémon. * Triggers a form change if the Pokémon is not in its defense form. - * @param {Pokemon} pokemon The Pokémon to which the tag is added. + * @param pokemon The Pokémon to which the tag is added. */ onAdd(pokemon: Pokemon): void { super.onAdd(pokemon); @@ -3585,6 +3555,25 @@ export class GrudgeTag extends SerializableBattlerTag { } } +/** + * Tag to allow the affected Pokemon's move to go first in its priority bracket. + * Used for {@link https://bulbapedia.bulbagarden.net/wiki/Quick_Draw_(Ability) | Quick Draw} + * and {@link https://bulbapedia.bulbagarden.net/wiki/Quick_Claw | Quick Claw}. + */ +export class BypassSpeedTag extends BattlerTag { + public override readonly tagType = BattlerTagType.BYPASS_SPEED; + + constructor() { + super(BattlerTagType.BYPASS_SPEED, BattlerTagLapseType.TURN_END, 1); + } + + override canAdd(pokemon: Pokemon): boolean { + const bypass = new BooleanHolder(true); + applyAbAttrs("PreventBypassSpeedChanceAbAttr", { pokemon, bypass }); + return bypass.value; + } +} + /** * Tag used to heal the user of Psycho Shift of its status effect if Psycho Shift succeeds in transferring its status effect to the target Pokemon */ @@ -3633,6 +3622,41 @@ export class MagicCoatTag extends BattlerTag { } } +/** + * Tag associated with {@linkcode AbilityId.SUPREME_OVERLORD} + */ +export class SupremeOverlordTag extends AbilityBattlerTag { + public override readonly tagType = BattlerTagType.SUPREME_OVERLORD; + /** The number of faints at the time the user was sent out */ + public readonly faintCount: number; + constructor() { + super(BattlerTagType.SUPREME_OVERLORD, AbilityId.SUPREME_OVERLORD, BattlerTagLapseType.FAINT, 0); + } + + public override onAdd(pokemon: Pokemon): boolean { + (this as Mutable).faintCount = Math.min( + pokemon.isPlayer() ? globalScene.arena.playerFaints : globalScene.currentBattle.enemyFaints, + 5, + ); + globalScene.phaseManager.queueMessage( + i18next.t("battlerTags:supremeOverlordOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), + ); + return true; + } + + /** + * @returns The damage multiplier for Supreme Overlord + */ + public getBoost(): number { + return 1 + 0.1 * this.faintCount; + } + + public override loadTag(source: BaseBattlerTag & Pick): void { + super.loadTag(source); + (this as Mutable).faintCount = source.faintCount; + } +} + /** * Retrieves a {@linkcode BattlerTag} based on the provided tag type, turn count, source move, and source ID. * @param sourceId - The ID of the pokemon adding the tag @@ -3833,6 +3857,10 @@ export function getBattlerTag( return new PsychoShiftTag(); case BattlerTagType.MAGIC_COAT: return new MagicCoatTag(); + case BattlerTagType.SUPREME_OVERLORD: + return new SupremeOverlordTag(); + case BattlerTagType.BYPASS_SPEED: + return new BypassSpeedTag(); } } @@ -3967,4 +3995,6 @@ export type BattlerTagTypeMap = { [BattlerTagType.GRUDGE]: GrudgeTag; [BattlerTagType.PSYCHO_SHIFT]: PsychoShiftTag; [BattlerTagType.MAGIC_COAT]: MagicCoatTag; + [BattlerTagType.SUPREME_OVERLORD]: SupremeOverlordTag; + [BattlerTagType.BYPASS_SPEED]: BypassSpeedTag; }; diff --git a/src/data/challenge.ts b/src/data/challenge.ts index a8ec92df57d..65faa900af7 100644 --- a/src/data/challenge.ts +++ b/src/data/challenge.ts @@ -1,5 +1,6 @@ import type { FixedBattleConfig } from "#app/battle"; import { getRandomTrainerFunc } from "#app/battle"; +import { globalScene } from "#app/global-scene"; import { defaultStarterSpeciesAndEvolutions } from "#balance/pokemon-evolutions"; import { speciesStarterCosts } from "#balance/starters"; import type { PokemonSpecies } from "#data/pokemon-species"; @@ -12,6 +13,7 @@ import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves"; import { ModifierTier } from "#enums/modifier-tier"; import { MoveId } from "#enums/move-id"; import type { MoveSourceType } from "#enums/move-source-type"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Nature } from "#enums/nature"; import { PokemonType } from "#enums/pokemon-type"; import { SpeciesId } from "#enums/species-id"; @@ -21,9 +23,10 @@ import type { EnemyPokemon, PlayerPokemon, Pokemon } from "#field/pokemon"; import { Trainer } from "#field/trainer"; import type { ModifierTypeOption } from "#modifiers/modifier-type"; import { PokemonMove } from "#moves/pokemon-move"; -import type { DexAttrProps, GameData, StarterDataEntry } from "#system/game-data"; +import type { GameData } from "#system/game-data"; import { RibbonData, type RibbonFlag } from "#system/ribbons/ribbon-data"; import type { DexEntry } from "#types/dex-data"; +import type { DexAttrProps, StarterDataEntry } from "#types/save-data"; import { type BooleanHolder, isBetween, type NumberHolder, randSeedItem } from "#utils/common"; import { deepCopy } from "#utils/data"; import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils"; @@ -56,7 +59,7 @@ export abstract class Challenge { } /** - * @param id {@link Challenges} The enum value for the challenge + * @param id - The enum value for the challenge */ constructor(id: Challenges, maxValue: number = Number.MAX_SAFE_INTEGER) { this.id = id; @@ -85,9 +88,9 @@ export abstract class Challenge { } /** - * Used for unlockable challenges to check if they're unlocked. - * @param data {@link GameData} The save data. - * @returns {@link boolean} Whether this challenge is unlocked. + * Check if an unlockable challenge is unlocked + * @param data - The save data + * @returns Whether this challenge is unlocked */ isUnlocked(data: GameData): boolean { return this.conditions.every(f => f(data)); @@ -95,8 +98,8 @@ export abstract class Challenge { /** * Adds an unlock condition to this challenge. - * @param condition {@link ChallengeCondition} The condition to add. - * @returns {@link Challenge} This challenge + * @param condition - The condition to add + * @returns This challenge */ condition(condition: ChallengeCondition): Challenge { this.conditions.push(condition); @@ -105,7 +108,7 @@ export abstract class Challenge { } /** - * @returns {@link string} The localised name of this challenge. + * @returns The localised name of this challenge. */ getName(): string { return i18next.t(`challenges:${this.geti18nKey()}.name`); @@ -132,7 +135,7 @@ export abstract class Challenge { /** * Increase the value of the challenge - * @returns {@link boolean} Returns true if the value changed + * @returns Returns true if the value changed */ increaseValue(): boolean { if (this.value < this.maxValue) { @@ -144,7 +147,7 @@ export abstract class Challenge { /** * Decrease the value of the challenge - * @returns {@link boolean} Returns true if the value changed + * @returns Returns true if the value changed */ decreaseValue(): boolean { if (this.value > 0) { @@ -163,7 +166,7 @@ export abstract class Challenge { /** * Decrease the severity of the challenge - * @returns {@link boolean} Returns true if the value changed + * @returns Returns true if the value changed */ decreaseSeverity(): boolean { if (this.severity > 0) { @@ -175,7 +178,7 @@ export abstract class Challenge { /** * Increase the severity of the challenge - * @returns {@link boolean} Returns true if the value changed + * @returns Returns true if the value changed */ increaseSeverity(): boolean { if (this.severity < this.maxSeverity) { @@ -187,7 +190,7 @@ export abstract class Challenge { /** * Gets the "difficulty" value of this challenge. - * @returns {@link integer} The difficulty value. + * @returns The difficulty value. */ getDifficulty(): number { return this.value; @@ -195,7 +198,7 @@ export abstract class Challenge { /** * Gets the minimum difficulty added by this challenge. - * @returns {@link integer} The difficulty value. + * @returns The difficulty value. */ getMinDifficulty(): number { return 0; @@ -203,7 +206,7 @@ export abstract class Challenge { /** * Clones a challenge, either from another challenge or json. Chainable. - * @param source The source challenge or json. + * @param _source - The source challenge or json. * @returns This challenge. */ static loadChallenge(_source: Challenge | any): Challenge { @@ -212,10 +215,10 @@ export abstract class Challenge { /** * An apply function for STARTER_CHOICE challenges. Derived classes should alter this. - * @param _pokemon {@link PokemonSpecies} The pokemon to check the validity of. - * @param _valid {@link BooleanHolder} A BooleanHolder, the value gets set to false if the pokemon isn't allowed. - * @param _dexAttr {@link DexAttrProps} The dex attributes of the pokemon. - * @returns {@link boolean} Whether this function did anything. + * @param _pokemon - The Pokémon to check the validity of + * @param _valid - Holder for whether the Pokémon is valid or not + * @param _dexAttr - The dex attributes of the Pokémon + * @returns Whether this function did anything. */ applyStarterChoice(_pokemon: PokemonSpecies, _valid: BooleanHolder, _dexAttr: DexAttrProps): boolean { return false; @@ -223,8 +226,8 @@ export abstract class Challenge { /** * An apply function for STARTER_POINTS challenges. Derived classes should alter this. - * @param _points {@link NumberHolder} The amount of points you have available. - * @returns {@link boolean} Whether this function did anything. + * @param _points - Holder for amount of starter points the user has to spend + * @returns Whether this function did anything */ applyStarterPoints(_points: NumberHolder): boolean { return false; @@ -232,9 +235,9 @@ export abstract class Challenge { /** * An apply function for STARTER_COST challenges. Derived classes should alter this. - * @param _species {@link SpeciesId} The pokemon to change the cost of. - * @param _cost {@link NumberHolder} The cost of the starter. - * @returns {@link boolean} Whether this function did anything. + * @param _species - The pokémon to change the cost of + * @param _cost - Holder for the cost of the starter Pokémon + * @returns Whether this function did anything. */ applyStarterCost(_species: SpeciesId, _cost: NumberHolder): boolean { return false; @@ -242,8 +245,8 @@ export abstract class Challenge { /** * An apply function for STARTER_SELECT_MODIFY challenges. Derived classes should alter this. - * @param _pokemon {@link Pokemon} The starter pokemon to modify. - * @returns {@link boolean} Whether this function did anything. + * @param _pokemon - The starter Pokémon to modify. + * @returns Whether this function did anything. */ applyStarterSelectModify(_speciesId: SpeciesId, _dexEntry: DexEntry, _starterDataEntry: StarterDataEntry): boolean { return false; @@ -251,8 +254,8 @@ export abstract class Challenge { /** * An apply function for STARTER_MODIFY challenges. Derived classes should alter this. - * @param _pokemon {@link Pokemon} The starter pokemon to modify. - * @returns {@link boolean} Whether this function did anything. + * @param _pokemon - The starter Pokémon to modify. + * @returns Whether this function did anything. */ applyStarterModify(_pokemon: Pokemon): boolean { return false; @@ -260,9 +263,9 @@ export abstract class Challenge { /** * An apply function for POKEMON_IN_BATTLE challenges. Derived classes should alter this. - * @param _pokemon {@link Pokemon} The pokemon to check the validity of. - * @param _valid {@link BooleanHolder} A BooleanHolder, the value gets set to false if the pokemon isn't allowed. - * @returns {@link boolean} Whether this function did anything. + * @param _pokemon - The Pokémon to check the validity of + * @param _valid - Holds a boolean that will be set to false if the Pokémon isn't allowed + * @returns Whether this function did anything */ applyPokemonInBattle(_pokemon: Pokemon, _valid: BooleanHolder): boolean { return false; @@ -270,9 +273,9 @@ export abstract class Challenge { /** * An apply function for FIXED_BATTLE challenges. Derived classes should alter this. - * @param _waveIndex {@link Number} The current wave index. - * @param _battleConfig {@link FixedBattleConfig} The battle config to modify. - * @returns {@link boolean} Whether this function did anything. + * @param _waveIndex The current wave index + * @param _battleConfig - The battle config to modify + * @returns Whether this function did anything */ applyFixedBattle(_waveIndex: number, _battleConfig: FixedBattleConfig): boolean { return false; @@ -280,8 +283,8 @@ export abstract class Challenge { /** * An apply function for TYPE_EFFECTIVENESS challenges. Derived classes should alter this. - * @param _effectiveness {@linkcode NumberHolder} The current effectiveness of the move. - * @returns Whether this function did anything. + * @param _effectiveness - The current effectiveness of the move + * @returns Whether this function did anything */ applyTypeEffectiveness(_effectiveness: NumberHolder): boolean { return false; @@ -289,11 +292,11 @@ export abstract class Challenge { /** * An apply function for AI_LEVEL challenges. Derived classes should alter this. - * @param _level {@link NumberHolder} The generated level. - * @param _levelCap {@link Number} The current level cap. - * @param _isTrainer {@link Boolean} Whether this is a trainer pokemon. - * @param _isBoss {@link Boolean} Whether this is a non-trainer boss pokemon. - * @returns {@link boolean} Whether this function did anything. + * @param _level - The generated level. + * @param _levelCap - The current level cap. + * @param _isTrainer - Whether this is a trainer Pokémon + * @param _isBoss - Whether this is a non-trainer boss Pokémon + * @returns - Whether this function did anything */ applyLevelChange(_level: NumberHolder, _levelCap: number, _isTrainer: boolean, _isBoss: boolean): boolean { return false; @@ -301,9 +304,9 @@ export abstract class Challenge { /** * An apply function for AI_MOVE_SLOTS challenges. Derived classes should alter this. - * @param pokemon {@link Pokemon} The pokemon that is being considered. - * @param moveSlots {@link NumberHolder} The amount of move slots. - * @returns {@link boolean} Whether this function did anything. + * @param _pokemon - The Pokémon that is being considered + * @param _moveSlots - The amount of move slots + * @returns Whether this function did anything */ applyMoveSlot(_pokemon: Pokemon, _moveSlots: NumberHolder): boolean { return false; @@ -311,9 +314,9 @@ export abstract class Challenge { /** * An apply function for PASSIVE_ACCESS challenges. Derived classes should alter this. - * @param pokemon {@link Pokemon} The pokemon to change. - * @param hasPassive {@link BooleanHolder} Whether it should have its passive. - * @returns {@link boolean} Whether this function did anything. + * @param _pokemon - The Pokémon to change + * @param _hasPassive - Whether it should have its passive + * @returns Whether this function did anything */ applyPassiveAccess(_pokemon: Pokemon, _hasPassive: BooleanHolder): boolean { return false; @@ -321,7 +324,7 @@ export abstract class Challenge { /** * An apply function for GAME_MODE_MODIFY challenges. Derived classes should alter this. - * @returns {@link boolean} Whether this function did anything. + * @returns Whether this function did anything */ applyGameModeModify(): boolean { return false; @@ -329,11 +332,11 @@ export abstract class Challenge { /** * An apply function for MOVE_ACCESS. Derived classes should alter this. - * @param _pokemon {@link Pokemon} What pokemon would learn the move. - * @param _moveSource {@link MoveSourceType} What source the pokemon would get the move from. - * @param _move {@link MoveId} The move in question. - * @param _level {@link NumberHolder} The level threshold for access. - * @returns {@link boolean} Whether this function did anything. + * @param _pokemon - What Pokémon would learn the move + * @param _moveSource - What source the Pokémon would get the move from + * @param _move - The move in question + * @param _level - The level threshold for access + * @returns Whether this function did anything */ applyMoveAccessLevel(_pokemon: Pokemon, _moveSource: MoveSourceType, _move: MoveId, _level: NumberHolder): boolean { return false; @@ -341,21 +344,21 @@ export abstract class Challenge { /** * An apply function for MOVE_WEIGHT. Derived classes should alter this. - * @param _pokemon {@link Pokemon} What pokemon would learn the move. - * @param _moveSource {@link MoveSourceType} What source the pokemon would get the move from. - * @param _move {@link MoveId} The move in question. - * @param _weight {@link NumberHolder} The base weight of the move - * @returns {@link boolean} Whether this function did anything. + * @param _pokemon - What Pokémon would learn the move + * @param _moveSource - What source the Pokémon would get the move from + * @param _move - The move in question. + * @param _weight - The base weight of the move + * @returns Whether this function did anything */ - applyMoveWeight(_pokemon: Pokemon, _moveSource: MoveSourceType, _move: MoveId, _level: NumberHolder): boolean { + applyMoveWeight(_pokemon: Pokemon, _moveSource: MoveSourceType, _move: MoveId, _weight: NumberHolder): boolean { return false; } /** * An apply function for FlipStats. Derived classes should alter this. - * @param _pokemon {@link Pokemon} What pokemon would learn the move. - * @param _baseStats What are the stats to flip. - * @returns {@link boolean} Whether this function did anything. + * @param _pokemon - What Pokémon would learn the move + * @param _baseStats What are the stats to flip + * @returns Whether this function did anything */ applyFlipStat(_pokemon: Pokemon, _baseStats: number[]) { return false; @@ -381,9 +384,9 @@ export abstract class Challenge { /** * An apply function for POKEMON_ADD_TO_PARTY. Derived classes should alter this. - * @param _pokemon - The pokemon being caught - * @param _status - Whether the pokemon can be added to the party or not - * @return Whether this function did anything + * @param _pokemon - The Pokémon being caught + * @param _status - Whether the Pokémon can be added to the party or not + * @returns Whether this function did anything */ applyPokemonAddToParty(_pokemon: EnemyPokemon, _status: BooleanHolder): boolean { return false; @@ -391,8 +394,8 @@ export abstract class Challenge { /** * An apply function for POKEMON_FUSION. Derived classes should alter this. - * @param _pokemon - The pokemon being checked - * @param _status - Whether the selected pokemon is allowed to fuse or not + * @param _pokemon - The Pokémon being checked + * @param _status - Whether the selected Pokémon is allowed to fuse or not * @returns Whether this function did anything */ applyPokemonFusion(_pokemon: PlayerPokemon, _status: BooleanHolder): boolean { @@ -670,10 +673,7 @@ export class SingleGenerationChallenge extends Challenge { return false; } - /** - * @overrides - */ - getDifficulty(): number { + override getDifficulty(): number { return this.value > 0 ? 1 : 0; } @@ -756,10 +756,7 @@ export class SingleTypeChallenge extends Challenge { return false; } - /** - * @overrides - */ - getDifficulty(): number { + override getDifficulty(): number { return this.value > 0 ? 1 : 0; } @@ -1081,7 +1078,12 @@ export class LimitedCatchChallenge extends Challenge { override applyPokemonAddToParty(pokemon: EnemyPokemon, status: BooleanHolder): boolean { if (status.value) { - status.value = pokemon.metWave % 10 === 1; + const isTeleporter = + globalScene.currentBattle.mysteryEncounter?.encounterType === MysteryEncounterType.TELEPORTING_HIJINKS + && globalScene.currentBattle.mysteryEncounter.selectedOption + !== globalScene.currentBattle.mysteryEncounter.options[2]; // don't allow catch when not choosing biome change option + const isFirstWave = pokemon.metWave % 10 === 1; + status.value = isTeleporter || isFirstWave; return true; } return false; diff --git a/src/data/daily-run.ts b/src/data/daily-run.ts index a0d3358ecb0..776dff1bf46 100644 --- a/src/data/daily-run.ts +++ b/src/data/daily-run.ts @@ -6,8 +6,8 @@ import { PokemonSpecies } from "#data/pokemon-species"; import { BiomeId } from "#enums/biome-id"; import { PartyMemberStrength } from "#enums/party-member-strength"; import { SpeciesId } from "#enums/species-id"; -import type { Starter } from "#ui/handlers/starter-select-ui-handler"; -import { isNullOrUndefined, randSeedGauss, randSeedInt, randSeedItem } from "#utils/common"; +import type { Starter } from "#types/save-data"; +import { randSeedGauss, randSeedInt, randSeedItem } from "#utils/common"; import { getEnumValues } from "#utils/enums"; import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils"; @@ -32,7 +32,7 @@ export function getDailyRunStarters(seed: string): Starter[] { const startingLevel = globalScene.gameMode.getStartingLevel(); const eventStarters = getDailyEventSeedStarters(seed); - if (!isNullOrUndefined(eventStarters)) { + if (eventStarters != null) { starters.push(...eventStarters); return; } @@ -66,8 +66,11 @@ function getDailyRunStarter(starterSpeciesForm: PokemonSpeciesForm, startingLeve const formIndex = starterSpeciesForm instanceof PokemonSpecies ? undefined : starterSpeciesForm.formIndex; const pokemon = globalScene.addPlayerPokemon(starterSpecies, startingLevel, undefined, formIndex); const starter: Starter = { - species: starterSpecies, - dexAttr: pokemon.getDexAttr(), + speciesId: starterSpecies.speciesId, + shiny: pokemon.shiny, + variant: pokemon.variant, + formIndex: pokemon.formIndex, + ivs: pokemon.ivs, abilityIndex: pokemon.abilityIndex, passive: false, nature: pokemon.getNature(), @@ -127,7 +130,7 @@ const dailyBiomeWeights: BiomeWeights = { export function getDailyStartingBiome(): BiomeId { const eventBiome = getDailyEventSeedBiome(globalScene.seed); - if (!isNullOrUndefined(eventBiome)) { + if (eventBiome != null) { return eventBiome; } diff --git a/src/data/egg-hatch-data.ts b/src/data/egg-hatch-data.ts index e78dc4d7984..efe06d5a504 100644 --- a/src/data/egg-hatch-data.ts +++ b/src/data/egg-hatch-data.ts @@ -1,7 +1,7 @@ import { globalScene } from "#app/global-scene"; import type { PlayerPokemon } from "#field/pokemon"; -import type { StarterDataEntry } from "#system/game-data"; import type { DexEntry } from "#types/dex-data"; +import type { StarterDataEntry } from "#types/save-data"; /** * Stores data associated with a specific egg and the hatched pokemon diff --git a/src/data/moves/move-utils.ts b/src/data/moves/move-utils.ts index eedeea53087..1fe0880317b 100644 --- a/src/data/moves/move-utils.ts +++ b/src/data/moves/move-utils.ts @@ -7,7 +7,7 @@ import { PokemonType } from "#enums/pokemon-type"; import type { Pokemon } from "#field/pokemon"; import { applyMoveAttrs } from "#moves/apply-attrs"; import type { Move, MoveTargetSet, UserMoveConditionFunc } from "#moves/move"; -import { isNullOrUndefined, NumberHolder } from "#utils/common"; +import { NumberHolder } from "#utils/common"; /** * Return whether the move targets the field @@ -78,7 +78,7 @@ export function getMoveTargets(user: Pokemon, move: MoveId, replaceTarget?: Move case MoveTarget.OTHER: case MoveTarget.ALL_NEAR_OTHERS: case MoveTarget.ALL_OTHERS: - set = !isNullOrUndefined(ally) ? opponents.concat([ally]) : opponents; + set = ally != null ? opponents.concat([ally]) : opponents; multiple = moveTarget === MoveTarget.ALL_NEAR_OTHERS || moveTarget === MoveTarget.ALL_OTHERS; break; case MoveTarget.NEAR_ENEMY: @@ -95,22 +95,22 @@ export function getMoveTargets(user: Pokemon, move: MoveId, replaceTarget?: Move return { targets: [-1 as BattlerIndex], multiple: false }; case MoveTarget.NEAR_ALLY: case MoveTarget.ALLY: - set = !isNullOrUndefined(ally) ? [ally] : []; + set = ally != null ? [ally] : []; break; case MoveTarget.USER_OR_NEAR_ALLY: case MoveTarget.USER_AND_ALLIES: case MoveTarget.USER_SIDE: - set = !isNullOrUndefined(ally) ? [user, ally] : [user]; + set = ally != null ? [user, ally] : [user]; multiple = moveTarget !== MoveTarget.USER_OR_NEAR_ALLY; break; case MoveTarget.ALL: case MoveTarget.BOTH_SIDES: - set = (!isNullOrUndefined(ally) ? [user, ally] : [user]).concat(opponents); + set = (ally != null ? [user, ally] : [user]).concat(opponents); multiple = true; break; case MoveTarget.CURSE: { - const extraTargets = !isNullOrUndefined(ally) ? [ally] : []; + const extraTargets = ally != null ? [ally] : []; set = user.getTypes(true).includes(PokemonType.GHOST) ? opponents.concat(extraTargets) : [user]; } break; diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index ab63149786c..5819c259666 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -6,7 +6,7 @@ import { loggedInUser } from "#app/account"; import type { GameMode } from "#app/game-mode"; import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; -import type { EntryHazardTag } from "#data/arena-tag"; +import type { EntryHazardTag, PendingHealTag } from "#data/arena-tag"; import { WeakenMoveTypeTag } from "#data/arena-tag"; import { MoveChargeAnim } from "#data/battle-anims"; import { @@ -18,6 +18,7 @@ import { ShellTrapTag, StockpilingTag, SubstituteTag, + SupremeOverlordTag, TrappedTag, TypeBoostTag, } from "#data/battler-tags"; @@ -80,19 +81,18 @@ import { applyMoveAttrs } from "#moves/apply-attrs"; import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSketchMoves, invalidSleepTalkMoves } from "#moves/invalid-moves"; import { frenzyMissFunc, getMoveTargets } from "#moves/move-utils"; import { PokemonMove } from "#moves/pokemon-move"; -import { MoveEndPhase } from "#phases/move-end-phase"; import { MovePhase } from "#phases/move-phase"; import { PokemonHealPhase } from "#phases/pokemon-heal-phase"; -import { SwitchSummonPhase } from "#phases/switch-summon-phase"; import type { AttackMoveResult } from "#types/attack-move-result"; import type { Localizable } from "#types/locales"; import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString, MoveMessageFunc } from "#types/move-types"; import type { TurnMove } from "#types/turn-move"; -import { BooleanHolder, coerceArray, type Constructor, isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common"; +import { BooleanHolder, coerceArray, type Constructor, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common"; import { getEnumValues } from "#utils/enums"; import { toCamelCase, toTitleCase } from "#utils/strings"; import i18next from "i18next"; import { applyChallenges } from "#utils/challenge-utils"; +import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; import type { AbstractConstructor } from "#types/type-helpers"; /** @@ -643,9 +643,9 @@ export abstract class Move implements Localizable { * will not consider {@linkcode AbilityId.WIND_RIDER | Wind Rider }. * * To simply check whether the move has a flag, use {@linkcode hasFlag}. - * @param flag {@linkcode MoveFlags} MoveFlag to check on user and/or target - * @param user {@linkcode Pokemon} the Pokemon using the move - * @param target {@linkcode Pokemon} the Pokemon receiving the move + * @param flag - MoveFlag to check on user and/or target + * @param user - the Pokemon using the move + * @param target - the Pokemon receiving the move * @param isFollowUp (defaults to `false`) `true` if the move was used as a follow up * @returns boolean * @see {@linkcode hasFlag} @@ -835,7 +835,7 @@ export abstract class Move implements Localizable { applyAbAttrs("VariableMovePowerAbAttr", abAttrParams); const ally = source.getAlly(); - if (!isNullOrUndefined(ally)) { + if (ally != null) { applyAbAttrs("AllyMoveCategoryPowerBoostAbAttr", {...abAttrParams, pokemon: ally}); } @@ -879,6 +879,8 @@ export abstract class Move implements Localizable { power.value *= 1.5; } + power.value *= (source.getTag(BattlerTagType.SUPREME_OVERLORD) as SupremeOverlordTag | undefined)?.getBoost() ?? 1; + return power.value; } @@ -888,6 +890,10 @@ export abstract class Move implements Localizable { applyMoveAttrs("IncrementMovePriorityAttr", user, null, this, priority); applyAbAttrs("ChangeMovePriorityAbAttr", {pokemon: user, simulated, move: this, priority}); + if (user.getTag(BattlerTagType.BYPASS_SPEED)) { + priority.value += 0.2; + } + return priority.value; } @@ -965,7 +971,7 @@ export abstract class Move implements Localizable { // ...and cannot enhance Pollen Puff when targeting an ally. const ally = user.getAlly(); - const exceptPollenPuffAlly: boolean = this.id === MoveId.POLLEN_PUFF && !isNullOrUndefined(ally) && targets.includes(ally.getBattlerIndex()) + const exceptPollenPuffAlly: boolean = this.id === MoveId.POLLEN_PUFF && ally != null && targets.includes(ally.getBattlerIndex()) return (!restrictSpread || !isMultiTarget) && !this.isChargingMove() @@ -1143,11 +1149,7 @@ function ChargeMove(Base: TBase, nameAppend: string) { export class ChargingAttackMove extends ChargeMove(AttackMove, "ChargingAttackMove") {} export class ChargingSelfStatusMove extends ChargeMove(SelfStatusMove, "ChargingSelfStatusMove") {} -/** - * Base class defining all {@linkcode Move} Attributes - * @abstract - * @see {@linkcode apply} - */ +/** Base class defining all {@linkcode Move} Attributes */ export abstract class MoveAttr { /** Should this {@linkcode Move} target the user? */ public selfTarget: boolean; @@ -1243,8 +1245,6 @@ interface MoveEffectAttrOptions { /** * Base class defining all Move Effect Attributes - * @extends MoveAttr - * @see {@linkcode apply} */ export class MoveEffectAttr extends MoveAttr { /** @@ -1460,7 +1460,6 @@ export class PreMoveMessageAttr extends MoveAttr { * Attribute for moves that can be conditionally interrupted to be considered to * have failed before their "useMove" message is displayed. Currently used by * Focus Punch. - * @extends MoveAttr */ export class PreUseInterruptAttr extends MoveAttr { protected message: string | MoveMessageFunc; @@ -1504,7 +1503,6 @@ export class PreUseInterruptAttr extends MoveAttr { /** * Attribute for Status moves that take attack type effectiveness * into consideration (i.e. {@linkcode https://bulbapedia.bulbagarden.net/wiki/Thunder_Wave_(move) | Thunder Wave}) - * @extends MoveAttr */ export class RespectAttackTypeImmunityAttr extends MoveAttr { } @@ -1788,9 +1786,7 @@ export class RecoilAttr extends MoveEffectAttr { /** * Attribute used for moves which self KO the user regardless if the move hits a target - * @extends MoveEffectAttr - * @see {@linkcode apply} - **/ + */ export class SacrificialAttr extends MoveEffectAttr { constructor() { super(true, { trigger: MoveEffectTrigger.POST_TARGET }); @@ -1821,9 +1817,7 @@ export class SacrificialAttr extends MoveEffectAttr { /** * Attribute used for moves which self KO the user but only if the move hits a target - * @extends MoveEffectAttr - * @see {@linkcode apply} - **/ + */ export class SacrificialAttrOnHit extends MoveEffectAttr { constructor() { super(true); @@ -1860,8 +1854,6 @@ export class SacrificialAttrOnHit extends MoveEffectAttr { /** * Attribute used for moves which cut the user's Max HP in half. * Triggers using {@linkcode MoveEffectTrigger.POST_TARGET}. - * @extends MoveEffectAttr - * @see {@linkcode apply} */ export class HalfSacrificialAttr extends MoveEffectAttr { constructor() { @@ -1961,8 +1953,6 @@ export class AddSubstituteAttr extends MoveEffectAttr { /** * Heals the user or target by {@linkcode healRatio} depending on the value of {@linkcode selfTarget} - * @extends MoveEffectAttr - * @see {@linkcode apply} */ export class HealAttr extends MoveEffectAttr { constructor( @@ -2051,8 +2041,6 @@ export class RestAttr extends HealAttr { /** * Cures the user's party of non-volatile status conditions, ie. Heal Bell, Aromatherapy - * @extends MoveEffectAttr - * @see {@linkcode apply} */ export class PartyStatusCureAttr extends MoveEffectAttr { /** Message to display after using move */ @@ -2111,7 +2099,6 @@ export class PartyStatusCureAttr extends MoveEffectAttr { /** * Applies damage to the target's ally equal to 1/16 of that ally's max HP. - * @extends MoveEffectAttr */ export class FlameBurstAttr extends MoveEffectAttr { constructor() { @@ -2133,7 +2120,7 @@ export class FlameBurstAttr extends MoveEffectAttr { const targetAlly = target.getAlly(); const cancelled = new BooleanHolder(false); - if (!isNullOrUndefined(targetAlly)) { + if (targetAlly != null) { applyAbAttrs("BlockNonDirectDamageAbAttr", {pokemon: targetAlly, cancelled}); } @@ -2146,7 +2133,7 @@ export class FlameBurstAttr extends MoveEffectAttr { } getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { - return !isNullOrUndefined(target.getAlly()) ? -5 : 0; + return target.getAlly() != null ? -5 : 0; } } @@ -2166,24 +2153,15 @@ export class SacrificialFullRestoreAttr extends SacrificialAttr { return false; } - // We don't know which party member will be chosen, so pick the highest max HP in the party - const party = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty(); - const maxPartyMemberHp = party.map(p => p.getMaxHp()).reduce((maxHp: number, hp: number) => Math.max(hp, maxHp), 0); - - const pm = globalScene.phaseManager; - - pm.pushPhase( - pm.create("PokemonHealPhase", - user.getBattlerIndex(), - maxPartyMemberHp, - i18next.t(this.moveMessage, { pokemonName: getPokemonNameWithAffix(user) }), - true, - false, - false, - true, - false, - this.restorePP), - true); + // Add a tag to the field if it doesn't already exist, then queue a delayed healing effect in the user's current slot. + globalScene.arena.addTag(ArenaTagType.PENDING_HEAL, 0, move.id, user.id); // Arguments after first go completely unused + const tag = globalScene.arena.getTag(ArenaTagType.PENDING_HEAL) as PendingHealTag; + tag.queueHeal(user.getBattlerIndex(), { + sourceId: user.id, + moveId: move.id, + restorePP: this.restorePP, + healMessage: i18next.t(this.moveMessage, { pokemonName: getPokemonNameWithAffix(user) }), + }); return true; } @@ -2200,8 +2178,6 @@ export class SacrificialFullRestoreAttr extends SacrificialAttr { /** * Attribute used for moves which ignore type-based debuffs from weather, namely Hydro Steam. * Called during damage calculation after getting said debuff from getAttackTypeMultiplier in the Pokemon class. - * @extends MoveAttr - * @see {@linkcode apply} */ export class IgnoreWeatherTypeDebuffAttr extends MoveAttr { /** The {@linkcode WeatherType} this move ignores */ @@ -2280,8 +2256,6 @@ export class SandHealAttr extends WeatherHealAttr { /** * Heals the target or the user by either {@linkcode normalHealRatio} or {@linkcode boostedHealRatio} * depending on the evaluation of {@linkcode condition} - * @extends HealAttr - * @see {@linkcode apply} */ export class BoostHealAttr extends HealAttr { /** Healing received when {@linkcode condition} is false */ @@ -2314,8 +2288,6 @@ export class BoostHealAttr extends HealAttr { /** * Heals the target only if it is the ally - * @extends HealAttr - * @see {@linkcode apply} */ export class HealOnAllyAttr extends HealAttr { override canApply(user: Pokemon, target: Pokemon, _move: Move, _args?: any[]): boolean { @@ -2334,9 +2306,6 @@ export class HealOnAllyAttr extends HealAttr { /** * Heals user as a side effect of a move that hits a target. * Healing is based on {@linkcode healRatio} * the amount of damage dealt or a stat of the target. - * @extends MoveEffectAttr - * @see {@linkcode apply} - * @see {@linkcode getUserBenefitScore} */ // TODO: Make Strength Sap its own attribute that extends off of this one export class HitHealAttr extends MoveEffectAttr { @@ -2406,8 +2375,6 @@ export class HitHealAttr extends MoveEffectAttr { * Attribute used for moves that change priority in a turn given a condition, * e.g. Grassy Glide * Called when move order is calculated in {@linkcode TurnStartPhase}. - * @extends MoveAttr - * @see {@linkcode apply} */ export class IncrementMovePriorityAttr extends MoveAttr { /** The condition for a move's priority being incremented */ @@ -2443,10 +2410,8 @@ export class IncrementMovePriorityAttr extends MoveAttr { /** * Attribute used for attack moves that hit multiple times per use, e.g. Bullet Seed. * + * @remarks * Applied at the beginning of {@linkcode MoveEffectPhase}. - * - * @extends MoveAttr - * @see {@linkcode apply} */ export class MultiHitAttr extends MoveAttr { /** This move's intrinsic multi-hit type. It should never be modified. */ @@ -2956,8 +2921,6 @@ export class StealEatBerryAttr extends EatBerryAttr { /** * Move attribute that signals that the move should cure a status effect - * @extends MoveEffectAttr - * @see {@linkcode apply()} */ export class HealStatusEffectAttr extends MoveEffectAttr { /** List of Status Effects to cure */ @@ -3040,8 +3003,6 @@ export class BypassSleepAttr extends MoveAttr { /** * Attribute used for moves that bypass the burn damage reduction of physical moves, currently only facade * Called during damage calculation - * @extends MoveAttr - * @see {@linkcode apply} */ export class BypassBurnDamageReductionAttr extends MoveAttr { /** Prevents the move's damage from being reduced by burn @@ -3150,7 +3111,6 @@ export class OneHitKOAttr extends MoveAttr { /** * Attribute that allows charge moves to resolve in 1 turn under a given condition. * Should only be used for {@linkcode ChargingMove | ChargingMoves} as a `chargeAttr`. - * @extends MoveAttr */ export class InstantChargeAttr extends MoveAttr { /** The condition in which the move with this attribute instantly charges */ @@ -3187,14 +3147,13 @@ export class InstantChargeAttr extends MoveAttr { /** * Attribute that allows charge moves to resolve in 1 turn while specific {@linkcode WeatherType | Weather} * is active. Should only be used for {@linkcode ChargingMove | ChargingMoves} as a `chargeAttr`. - * @extends InstantChargeAttr */ export class WeatherInstantChargeAttr extends InstantChargeAttr { constructor(weatherTypes: WeatherType[]) { super((user, move) => { const currentWeather = globalScene.arena.weather; - if (isNullOrUndefined(currentWeather?.weatherType)) { + if (currentWeather?.weatherType == null) { return false; } else { return !currentWeather?.isEffectSuppressed() @@ -3317,7 +3276,6 @@ export class WishAttr extends MoveEffectAttr { /** * Attribute that cancels the associated move's effects when set to be combined with the user's ally's * subsequent move this turn. Used for Grass Pledge, Water Pledge, and Fire Pledge. - * @extends OverrideMoveEffectAttr */ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr { constructor() { @@ -3343,7 +3301,7 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr { const overridden = args[0] as BooleanHolder; - const allyMovePhase = globalScene.phaseManager.findPhase((phase) => phase.is("MovePhase") && phase.pokemon.isPlayer() === user.isPlayer()); + const allyMovePhase = globalScene.phaseManager.getMovePhase((phase) => phase.pokemon.isPlayer() === user.isPlayer()); if (allyMovePhase) { const allyMove = allyMovePhase.move.getMove(); if (allyMove !== move && allyMove.hasAttr("AwaitCombinedPledgeAttr")) { @@ -3356,11 +3314,7 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr { })); // Move the ally's MovePhase (if needed) so that the ally moves next - const allyMovePhaseIndex = globalScene.phaseManager.phaseQueue.indexOf(allyMovePhase); - const firstMovePhaseIndex = globalScene.phaseManager.phaseQueue.findIndex((phase) => phase.is("MovePhase")); - if (allyMovePhaseIndex !== firstMovePhaseIndex) { - globalScene.phaseManager.prependToPhase(globalScene.phaseManager.phaseQueue.splice(allyMovePhaseIndex, 1)[0], "MovePhase"); - } + globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === user.getAlly()); overridden.value = true; return true; @@ -3372,7 +3326,6 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr { /** * Set of optional parameters that may be applied to stat stage changing effects - * @extends MoveEffectAttrOptions * @see {@linkcode StatStageChangeAttr} */ interface StatStageChangeAttrOptions extends MoveEffectAttrOptions { @@ -3389,9 +3342,6 @@ interface StatStageChangeAttrOptions extends MoveEffectAttrOptions { * @param stages How many stages to change the stat(s) by, [-6, 6] * @param selfTarget `true` if the move is self-targetting * @param options {@linkcode StatStageChangeAttrOptions} Container for any optional parameters for this attribute. - * - * @extends MoveEffectAttr - * @see {@linkcode apply} */ export class StatStageChangeAttr extends MoveEffectAttr { public stats: BattleStat[]; @@ -3822,8 +3772,6 @@ export class ResetStatsAttr extends MoveEffectAttr { /** * Attribute used for status moves, specifically Heart, Guard, and Power Swap, * that swaps the user's and target's corresponding stat stages. - * @extends MoveEffectAttr - * @see {@linkcode apply} */ export class SwapStatStagesAttr extends MoveEffectAttr { /** The stat stages to be swapped between the user and the target */ @@ -4111,8 +4059,6 @@ export class WeightPowerAttr extends VariablePowerAttr { /** * Attribute used for Electro Ball move. - * @extends VariablePowerAttr - * @see {@linkcode apply} **/ export class ElectroBallPowerAttr extends VariablePowerAttr { /** @@ -4146,8 +4092,6 @@ export class ElectroBallPowerAttr extends VariablePowerAttr { /** * Attribute used for Gyro Ball move. - * @extends VariablePowerAttr - * @see {@linkcode apply} **/ export class GyroBallPowerAttr extends VariablePowerAttr { /** @@ -4403,11 +4347,11 @@ const countPositiveStatStages = (pokemon: Pokemon): number => { export class PositiveStatStagePowerAttr extends VariablePowerAttr { /** - * @param {Pokemon} user The pokemon that is being used to calculate the amount of positive stats - * @param {Pokemon} target N/A - * @param {Move} move N/A - * @param {any[]} args The argument for VariablePowerAttr, accumulates and sets the amount of power multiplied by stats - * @returns {boolean} Returns true if attribute is applied + * @param user The pokemon that is being used to calculate the amount of positive stats + * @param target N/A + * @param move N/A + * @param args The argument for VariablePowerAttr, accumulates and sets the amount of power multiplied by stats + * @returns Returns true if attribute is applied */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const positiveStatStages: number = countPositiveStatStages(user); @@ -4427,10 +4371,10 @@ export class PunishmentPowerAttr extends VariablePowerAttr { private PUNISHMENT_MAX_BASE_POWER = 200; /** - * @param {Pokemon} user N/A - * @param {Pokemon} target The pokemon that the move is being used against, as well as calculating the stats for the min/max base power - * @param {Move} move N/A - * @param {any[]} args The value that is being changed due to VariablePowerAttr + * @param user N/A + * @param target The pokemon that the move is being used against, as well as calculating the stats for the min/max base power + * @param move N/A + * @param args The value that is being changed due to VariablePowerAttr * @returns Returns true if attribute is applied */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { @@ -4546,8 +4490,6 @@ const hasStockpileStacksCondition: MoveConditionFunc = (user) => { /** * Attribute used for multi-hit moves that increase power in increments of the * move's base power for each hit, namely Triple Kick and Triple Axel. - * @extends VariablePowerAttr - * @see {@linkcode apply} */ export class MultiHitPowerIncrementAttr extends VariablePowerAttr { /** The max number of base power increments allowed for this move */ @@ -4584,8 +4526,6 @@ export class MultiHitPowerIncrementAttr extends VariablePowerAttr { * Attribute used for moves that double in power if the given move immediately * preceded the move applying the attribute, namely Fusion Flare and * Fusion Bolt. - * @extends VariablePowerAttr - * @see {@linkcode apply} */ export class LastMoveDoublePowerAttr extends VariablePowerAttr { /** The move that must precede the current move */ @@ -4609,28 +4549,7 @@ export class LastMoveDoublePowerAttr extends VariablePowerAttr { */ apply(user: Pokemon, _target: Pokemon, _move: Move, args: any[]): boolean { const power = args[0] as NumberHolder; - const enemy = user.getOpponent(0); - const pokemonActed: Pokemon[] = []; - - if (enemy?.turnData.acted) { - pokemonActed.push(enemy); - } - - if (globalScene.currentBattle.double) { - const userAlly = user.getAlly(); - const enemyAlly = enemy?.getAlly(); - - if (userAlly?.turnData.acted) { - pokemonActed.push(userAlly); - } - if (enemyAlly?.turnData.acted) { - pokemonActed.push(enemyAlly); - } - } - - pokemonActed.sort((a, b) => b.turnData.order - a.turnData.order); - - for (const p of pokemonActed) { + for (const p of globalScene.phaseManager.dynamicQueueManager.getLastTurnOrder().slice(0, -1).reverse()) { const [ lastMove ] = p.getLastXMoves(1); if (lastMove.result !== MoveResult.FAIL) { if ((lastMove.result === MoveResult.SUCCESS) && (lastMove.move === this.move)) { @@ -4688,7 +4607,6 @@ export class CombinedPledgeStabBoostAttr extends MoveAttr { /** * Variable Power attribute for {@link https://bulbapedia.bulbagarden.net/wiki/Round_(move) | Round}. * Doubles power if another Pokemon has previously selected Round this turn. - * @extends VariablePowerAttr */ export class RoundPowerAttr extends VariablePowerAttr { override apply(user: Pokemon, target: Pokemon, move: Move, args: [NumberHolder]): boolean { @@ -4706,8 +4624,6 @@ export class RoundPowerAttr extends VariablePowerAttr { * Attribute for the "combo" effect of {@link https://bulbapedia.bulbagarden.net/wiki/Round_(move) | Round}. * Preempts the next move in the turn order with the first instance of any Pokemon * using Round. Also marks the Pokemon using the cued Round to double the move's power. - * @extends MoveEffectAttr - * @see {@linkcode RoundPowerAttr} */ export class CueNextRoundAttr extends MoveEffectAttr { constructor() { @@ -4715,20 +4631,13 @@ export class CueNextRoundAttr extends MoveEffectAttr { } override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean { - const nextRoundPhase = globalScene.phaseManager.findPhase(phase => - phase.is("MovePhase") && phase.move.moveId === MoveId.ROUND - ); + const nextRoundPhase = globalScene.phaseManager.getMovePhase(phase => phase.move.moveId === MoveId.ROUND); if (!nextRoundPhase) { return false; } - // Update the phase queue so that the next Pokemon using Round moves next - const nextRoundIndex = globalScene.phaseManager.phaseQueue.indexOf(nextRoundPhase); - const nextMoveIndex = globalScene.phaseManager.phaseQueue.findIndex(phase => phase.is("MovePhase")); - if (nextRoundIndex !== nextMoveIndex) { - globalScene.phaseManager.prependToPhase(globalScene.phaseManager.phaseQueue.splice(nextRoundIndex, 1)[0], "MovePhase"); - } + globalScene.phaseManager.forceMoveNext(phase => phase.move.moveId === MoveId.ROUND); // Mark the corresponding Pokemon as having "joined the Round" (for doubling power later) nextRoundPhase.pokemon.turnData.joinedRound = true; @@ -4909,8 +4818,6 @@ export class StormAccuracyAttr extends VariableAccuracyAttr { /** * Attribute used for moves which never miss * against Pokemon with the {@linkcode BattlerTagType.MINIMIZED} - * @extends VariableAccuracyAttr - * @see {@linkcode apply} */ export class AlwaysHitMinimizeAttr extends VariableAccuracyAttr { /** @@ -4983,9 +4890,8 @@ export class PhotonGeyserCategoryAttr extends VariableMoveCategoryAttr { * Attribute used for tera moves that change category based on the user's Atk and SpAtk stats * Note: Currently, `getEffectiveStat` does not ignore all abilities that affect stats except those * with the attribute of `StatMultiplierAbAttr` - * TODO: Remove the `.partial()` tag from Tera Blast and Tera Starstorm when the above issue is resolved - * @extends VariableMoveCategoryAttr */ +// TODO: Remove the `.partial()` tag from Tera Blast and Tera Starstorm when the above issue is resolved export class TeraMoveCategoryAttr extends VariableMoveCategoryAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const category = (args[0] as NumberHolder); @@ -5002,7 +4908,6 @@ export class TeraMoveCategoryAttr extends VariableMoveCategoryAttr { /** * Increases the power of Tera Blast if the user is Terastallized into Stellar type - * @extends VariablePowerAttr */ export class TeraBlastPowerAttr extends VariablePowerAttr { /** @@ -5029,8 +4934,6 @@ export class TeraBlastPowerAttr extends VariablePowerAttr { /** * Change the move category to status when used on the ally - * @extends VariableMoveCategoryAttr - * @see {@linkcode apply} */ export class StatusCategoryOnAllyAttr extends VariableMoveCategoryAttr { /** @@ -5262,8 +5165,6 @@ export class WeatherBallTypeAttr extends VariableMoveTypeAttr { /** * Changes the move's type to match the current terrain. * Has no effect if the user is not grounded. - * @extends VariableMoveTypeAttr - * @see {@linkcode apply} */ export class TerrainPulseTypeAttr extends VariableMoveTypeAttr { /** @@ -5311,7 +5212,6 @@ export class TerrainPulseTypeAttr extends VariableMoveTypeAttr { /** * Changes type based on the user's IVs - * @extends VariableMoveTypeAttr */ export class HiddenPowerTypeAttr extends VariableMoveTypeAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { @@ -5339,7 +5239,6 @@ export class HiddenPowerTypeAttr extends VariableMoveTypeAttr { /** * Changes the type of Tera Blast to match the user's tera type - * @extends VariableMoveTypeAttr */ export class TeraBlastTypeAttr extends VariableMoveTypeAttr { /** @@ -5366,7 +5265,6 @@ export class TeraBlastTypeAttr extends VariableMoveTypeAttr { /** * Attribute used for Tera Starstorm that changes the move type to Stellar - * @extends VariableMoveTypeAttr */ export class TeraStarstormTypeAttr extends VariableMoveTypeAttr { /** @@ -5412,7 +5310,6 @@ export class MatchUserTypeAttr extends VariableMoveTypeAttr { /** * Changes the type of a Pledge move based on the Pledge move combined with it. - * @extends VariableMoveTypeAttr */ export class CombinedPledgeTypeAttr extends VariableMoveTypeAttr { override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { @@ -5562,10 +5459,10 @@ export class SheerColdAccuracyAttr extends OneHitKOAccuracyAttr { /** * Changes the normal One Hit KO Accuracy Attr to implement the Gen VII changes, * where if the user is Ice-Type, it has more accuracy. - * @param {Pokemon} user Pokemon that is using the move; checks the Pokemon's level. - * @param {Pokemon} target Pokemon that is receiving the move; checks the Pokemon's level. - * @param {Move} move N/A - * @param {any[]} args Uses the accuracy argument, allowing to change it from either 0 if it doesn't pass + * @param user Pokemon that is using the move; checks the Pokemon's level. + * @param target Pokemon that is receiving the move; checks the Pokemon's level. + * @param move N/A + * @param args Uses the accuracy argument, allowing to change it from either 0 if it doesn't pass * the first if/else, or 30/20 depending on the type of the user Pokemon. * @returns Returns true if move is successful, false if misses. */ @@ -5680,7 +5577,6 @@ export class FrenzyAttr extends MoveEffectAttr { /** * Attribute that grants {@link https://bulbapedia.bulbagarden.net/wiki/Semi-invulnerable_turn | semi-invulnerability} to the user during * the associated move's charging phase. Should only be used for {@linkcode ChargingMove | ChargingMoves} as a `chargeAttr`. - * @extends MoveEffectAttr */ export class SemiInvulnerableAttr extends MoveEffectAttr { /** The type of {@linkcode SemiInvulnerableTag} to grant to the user */ @@ -5806,7 +5702,6 @@ export class AddBattlerTagAttr extends MoveEffectAttr { /** * Adds a {@link https://bulbapedia.bulbagarden.net/wiki/Seeding | Seeding} effect to the target * as seen with Leech Seed and Sappy Seed. - * @extends AddBattlerTagAttr */ export class LeechSeedAttr extends AddBattlerTagAttr { constructor() { @@ -5816,7 +5711,6 @@ export class LeechSeedAttr extends AddBattlerTagAttr { /** * Adds the appropriate battler tag for Smack Down and Thousand arrows - * @extends AddBattlerTagAttr */ export class FallDownAttr extends AddBattlerTagAttr { constructor() { @@ -5841,7 +5735,6 @@ export class FallDownAttr extends AddBattlerTagAttr { /** * Adds the appropriate battler tag for Gulp Missile when Surf or Dive is used. - * @extends MoveEffectAttr */ export class GulpMissileTagAttr extends MoveEffectAttr { constructor() { @@ -5881,7 +5774,6 @@ export class GulpMissileTagAttr extends MoveEffectAttr { /** * Attribute to implement Jaw Lock's linked trapping effect between the user and target - * @extends AddBattlerTagAttr */ export class JawLockAttr extends AddBattlerTagAttr { constructor() { @@ -6047,7 +5939,6 @@ export class ProtectAttr extends AddBattlerTagAttr { /** * Attribute to remove all Substitutes from the field. - * @extends MoveEffectAttr * @see {@link https://bulbapedia.bulbagarden.net/wiki/Tidy_Up_(move) | Tidy Up} * @see {@linkcode SubstituteTag} */ @@ -6079,7 +5970,6 @@ export class RemoveAllSubstitutesAttr extends MoveEffectAttr { * Attribute used when a move can deal damage to {@linkcode BattlerTagType} * Moves that always hit but do not deal double damage: Thunder, Fissure, Sky Uppercut, * Smack Down, Hurricane, Thousand Arrows - * @extends MoveAttr */ export class HitsTagAttr extends MoveAttr { /** The {@linkcode BattlerTagType} this move hits */ @@ -6193,8 +6083,6 @@ export class AddArenaTrapTagAttr extends AddArenaTagAttr { /** * Attribute used for Stone Axe and Ceaseless Edge. * Applies the given ArenaTrapTag when move is used. - * @extends AddArenaTagAttr - * @see {@linkcode apply} */ export class AddArenaTrapTagHitAttr extends AddArenaTagAttr { /** @@ -6287,10 +6175,7 @@ export class RemoveScreensAttr extends MoveEffectAttr { } } -/*Swaps arena effects between the player and enemy side - * @extends MoveEffectAttr - * @see {@linkcode apply} -*/ +/** Swaps arena effects between the player and enemy side */ export class SwapArenaTagsAttr extends MoveEffectAttr { public SwapTags: ArenaTagType[]; @@ -6356,8 +6241,6 @@ export class AddPledgeEffectAttr extends AddArenaTagAttr { /** * Attribute used for Revival Blessing. - * @extends MoveEffectAttr - * @see {@linkcode apply} */ export class RevivalBlessingAttr extends MoveEffectAttr { constructor() { @@ -6387,15 +6270,15 @@ export class RevivalBlessingAttr extends MoveEffectAttr { pokemon.heal(Math.min(toDmgValue(0.5 * pokemon.getMaxHp()), pokemon.getMaxHp())); globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:revivalBlessing", { pokemonName: getPokemonNameWithAffix(pokemon) }), 0, true); const allyPokemon = user.getAlly(); - if (globalScene.currentBattle.double && globalScene.getEnemyParty().length > 1 && !isNullOrUndefined(allyPokemon)) { + if (globalScene.currentBattle.double && globalScene.getEnemyParty().length > 1 && allyPokemon != null) { // Handle cases where revived pokemon needs to get switched in on same turn if (allyPokemon.isFainted() || allyPokemon === pokemon) { // Enemy switch phase should be removed and replaced with the revived pkmn switching in - globalScene.phaseManager.tryRemovePhase((phase: SwitchSummonPhase) => phase.is("SwitchSummonPhase") && phase.getPokemon() === pokemon); + globalScene.phaseManager.tryRemovePhase("SwitchSummonPhase", phase => phase.getFieldIndex() === slotIndex); // If the pokemon being revived was alive earlier in the turn, cancel its move // (revived pokemon can't move in the turn they're brought back) // TODO: might make sense to move this to `FaintPhase` after checking for Rev Seed (rather than handling it in the move) - globalScene.phaseManager.findPhase((phase: MovePhase) => phase.pokemon === pokemon)?.cancel(); + globalScene.phaseManager.getMovePhase((phase: MovePhase) => phase.pokemon === pokemon)?.cancel(); if (user.fieldPosition === FieldPosition.CENTER) { user.setFieldPosition(FieldPosition.LEFT); } @@ -6422,7 +6305,6 @@ export class RevivalBlessingAttr extends MoveEffectAttr { } } - export class ForceSwitchOutAttr extends MoveEffectAttr { constructor( private selfSwitch: boolean = false, @@ -6477,8 +6359,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { if (this.switchType === SwitchType.FORCE_SWITCH) { switchOutTarget.leaveField(true); const slotIndex = eligibleNewIndices[user.randBattleSeedInt(eligibleNewIndices.length)]; - globalScene.phaseManager.prependNewToPhase( - "MoveEndPhase", + globalScene.phaseManager.queueDeferred( "SwitchSummonPhase", this.switchType, switchOutTarget.getFieldIndex(), @@ -6488,7 +6369,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { ); } else { switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH); - globalScene.phaseManager.prependNewToPhase("MoveEndPhase", + globalScene.phaseManager.queueDeferred( "SwitchPhase", this.switchType, switchOutTarget.getFieldIndex(), @@ -6517,7 +6398,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { if (this.switchType === SwitchType.FORCE_SWITCH) { switchOutTarget.leaveField(true); const slotIndex = eligibleNewIndices[user.randBattleSeedInt(eligibleNewIndices.length)]; - globalScene.phaseManager.prependNewToPhase("MoveEndPhase", + globalScene.phaseManager.queueDeferred( "SwitchSummonPhase", this.switchType, switchOutTarget.getFieldIndex(), @@ -6527,7 +6408,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { ); } else { switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH); - globalScene.phaseManager.prependNewToPhase("MoveEndPhase", + globalScene.phaseManager.queueDeferred( "SwitchSummonPhase", this.switchType, switchOutTarget.getFieldIndex(), @@ -6557,7 +6438,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:fled", { pokemonName: getPokemonNameWithAffix(switchOutTarget) }), null, true, 500); // in double battles redirect potential moves off fled pokemon - if (globalScene.currentBattle.double && !isNullOrUndefined(allyPokemon)) { + if (globalScene.currentBattle.double && allyPokemon != null) { globalScene.redirectPokemonMoves(switchOutTarget, allyPokemon); } } @@ -6781,7 +6662,7 @@ export class CopyBiomeTypeAttr extends MoveEffectAttr { /** * Retrieves a type from the current terrain * @param terrainType {@linkcode TerrainType} - * @returns {@linkcode Type} + * @returns */ private getTypeForTerrain(terrainType: TerrainType): PokemonType { switch (terrainType) { @@ -6802,7 +6683,7 @@ export class CopyBiomeTypeAttr extends MoveEffectAttr { /** * Retrieves a type from the current biome * @param biomeType {@linkcode BiomeId} - * @returns {@linkcode Type} + * @returns */ private getTypeForBiome(biomeType: BiomeId): PokemonType { switch (biomeType) { @@ -6865,7 +6746,7 @@ export class CopyBiomeTypeAttr extends MoveEffectAttr { } } -/** +/** * Attribute to override the target's current types to the given type. * Used by {@linkcode MoveId.SOAK} and {@linkcode MoveId.MAGIC_POWDER}. */ @@ -6935,8 +6816,6 @@ export class FirstMoveTypeAttr extends MoveEffectAttr { /** * Attribute used to call a move. * Used by other move attributes: {@linkcode RandomMoveAttr}, {@linkcode RandomMovesetMoveAttr}, {@linkcode CopyMoveAttr} - * @see {@linkcode apply} for move call - * @extends OverrideMoveEffectAttr */ class CallMoveAttr extends OverrideMoveEffectAttr { protected invalidMoves: ReadonlySet; @@ -6960,7 +6839,7 @@ class CallMoveAttr extends OverrideMoveEffectAttr { : moveTargets.targets[user.randBattleSeedInt(moveTargets.targets.length)]]; globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", move.id); - globalScene.phaseManager.unshiftNew("MovePhase", user, targets, new PokemonMove(move.id), MoveUseMode.FOLLOW_UP); + globalScene.phaseManager.unshiftNew("MovePhase", user, targets, new PokemonMove(move.id), MoveUseMode.FOLLOW_UP, MovePhaseTimingModifier.FIRST); return true; } } @@ -6968,8 +6847,6 @@ class CallMoveAttr extends OverrideMoveEffectAttr { /** * Attribute used to call a random move. * Used for {@linkcode MoveId.METRONOME} - * @see {@linkcode apply} for move selection and move call - * @extends CallMoveAttr to call a selected move */ export class RandomMoveAttr extends CallMoveAttr { constructor(invalidMoves: ReadonlySet) { @@ -7017,8 +6894,6 @@ export class RandomMoveAttr extends CallMoveAttr { * Fails if the user has no callable moves. * * Invalid moves are indicated by what is passed in to invalidMoves: {@linkcode invalidAssistMoves} or {@linkcode invalidSleepTalkMoves} - * @extends RandomMoveAttr to use the callMove function on a moveId - * @see {@linkcode getCondition} for move selection */ export class RandomMovesetMoveAttr extends CallMoveAttr { private includeParty: boolean; @@ -7196,7 +7071,7 @@ export class NaturePowerAttr extends OverrideMoveEffectAttr { // Load the move's animation if we didn't already and unshift a new usage phase globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", moveId); - globalScene.phaseManager.unshiftNew("MovePhase", user, [ target.getBattlerIndex() ], new PokemonMove(moveId), MoveUseMode.FOLLOW_UP); + globalScene.phaseManager.unshiftNew("MovePhase", user, [ target.getBattlerIndex() ], new PokemonMove(moveId), MoveUseMode.FOLLOW_UP, MovePhaseTimingModifier.FIRST); return true; } } @@ -7204,8 +7079,6 @@ export class NaturePowerAttr extends OverrideMoveEffectAttr { /** * Attribute used to copy a previously-used move. * Used for {@linkcode MoveId.COPYCAT} and {@linkcode MoveId.MIRROR_MOVE} - * @see {@linkcode apply} for move selection and move call - * @extends CallMoveAttr to call a selected move */ export class CopyMoveAttr extends CallMoveAttr { private mirrorMove: boolean; @@ -7225,7 +7098,7 @@ export class CopyMoveAttr extends CallMoveAttr { getCondition(): MoveConditionFunc { return (_user, target, _move) => { const lastMove = this.mirrorMove ? target.getLastNonVirtualMove(false, false)?.move : globalScene.currentBattle.lastMove; - return !isNullOrUndefined(lastMove) && !this.invalidMoves.has(lastMove); + return lastMove != null && !this.invalidMoves.has(lastMove); }; } } @@ -7272,7 +7145,7 @@ export class RepeatMoveAttr extends MoveEffectAttr { && firstTarget !== target.getAlly() ) { const ally = firstTarget.getAlly(); - if (!isNullOrUndefined(ally) && ally.isActive()) { + if (ally != null && ally.isActive()) { moveTargets = [ ally.getBattlerIndex() ]; } } @@ -7282,7 +7155,7 @@ export class RepeatMoveAttr extends MoveEffectAttr { targetPokemonName: getPokemonNameWithAffix(target) })); target.turnData.extraTurns++; - globalScene.phaseManager.appendNewToPhase("MoveEndPhase", "MovePhase", target, moveTargets, movesetMove, MoveUseMode.NORMAL); + globalScene.phaseManager.unshiftNew("MovePhase", target, moveTargets, movesetMove, MoveUseMode.NORMAL, MovePhaseTimingModifier.FIRST); return true; } @@ -7541,11 +7414,11 @@ export class SketchAttr extends MoveEffectAttr { } /** * User copies the opponent's last used move, if possible - * @param {Pokemon} user Pokemon that used the move and will replace Sketch with the copied move - * @param {Pokemon} target Pokemon that the user wants to copy a move from - * @param {Move} move Move being used - * @param {any[]} args Unused - * @returns {boolean} true if the function succeeds, otherwise false + * @param user Pokemon that used the move and will replace Sketch with the copied move + * @param target Pokemon that the user wants to copy a move from + * @param move Move being used + * @param args Unused + * @returns true if the function succeeds, otherwise false */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { @@ -7579,7 +7452,7 @@ export class SketchAttr extends MoveEffectAttr { } const targetMove = target.getLastNonVirtualMove(); - return !isNullOrUndefined(targetMove) + return targetMove != null && !invalidSketchMoves.has(targetMove.move) && user.getMoveset().every(m => m.moveId !== targetMove.move) }; @@ -7636,7 +7509,7 @@ export class AbilityCopyAttr extends MoveEffectAttr { user.setTempAbility(target.getAbility()); const ally = user.getAlly(); - if (this.copyToPartner && globalScene.currentBattle?.double && !isNullOrUndefined(ally) && ally.hp) { // TODO is this the best way to check that the ally is active? + if (this.copyToPartner && globalScene.currentBattle?.double && ally != null && ally.hp) { // TODO is this the best way to check that the ally is active? globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:copiedTargetAbility", { pokemonName: getPokemonNameWithAffix(ally), targetName: getPokemonNameWithAffix(target), abilityName: allAbilities[target.getAbility().id].name })); ally.setTempAbility(target.getAbility()); } @@ -7708,10 +7581,6 @@ export class SwitchAbilitiesAttr extends MoveEffectAttr { /** * Attribute used for moves that suppress abilities like {@linkcode MoveId.GASTRO_ACID}. * A suppressed ability cannot be activated. - * - * @extends MoveEffectAttr - * @see {@linkcode apply} - * @see {@linkcode getCondition} */ export class SuppressAbilitiesAttr extends MoveEffectAttr { /** Sets ability suppression for the target pokemon and displays a message. */ @@ -7737,8 +7606,7 @@ export class SuppressAbilitiesAttr extends MoveEffectAttr { /** * Applies the effects of {@linkcode SuppressAbilitiesAttr} if the target has already moved this turn. - * @extends MoveEffectAttr - * @see {@linkcode MoveId.CORE_ENFORCER} (the move which uses this effect) + * @see {@linkcode MoveId.CORE_ENFORCER} */ export class SuppressAbilitiesIfActedAttr extends MoveEffectAttr { /** @@ -7787,8 +7655,6 @@ export class TransformAttr extends MoveEffectAttr { /** * Attribute used for status moves, namely Speed Swap, * that swaps the user's and target's corresponding stats. - * @extends MoveEffectAttr - * @see {@linkcode apply} */ export class SwapStatAttr extends MoveEffectAttr { /** The stat to be swapped between the user and the target */ @@ -7829,7 +7695,6 @@ export class SwapStatAttr extends MoveEffectAttr { /** * Attribute used to switch the user's own stats. * Used by Power Shift. - * @extends MoveEffectAttr */ export class ShiftStatAttr extends MoveEffectAttr { private statToSwitch: EffectiveStat; @@ -7844,7 +7709,7 @@ export class ShiftStatAttr extends MoveEffectAttr { /** * Switches the user's stats based on the {@linkcode statToSwitch} and {@linkcode statToSwitchWith} attributes. - * @param {Pokemon} user the {@linkcode Pokemon} that used the move + * @param user the {@linkcode Pokemon} that used the move * @param target n/a * @param move n/a * @param args n/a @@ -7872,7 +7737,7 @@ export class ShiftStatAttr extends MoveEffectAttr { /** * Encourages the user to use the move if the stat to switch with is greater than the stat to switch. - * @param {Pokemon} user the {@linkcode Pokemon} that used the move + * @param user the {@linkcode Pokemon} that used the move * @param target n/a * @param move n/a * @returns number of points to add to the user's benefit score @@ -7886,8 +7751,6 @@ export class ShiftStatAttr extends MoveEffectAttr { * Attribute used for status moves, namely Power Split and Guard Split, * that take the average of a user's and target's corresponding * stats and assign that average back to each corresponding stat. - * @extends MoveEffectAttr - * @see {@linkcode apply} */ export class AverageStatsAttr extends MoveEffectAttr { /** The stats to be averaged individually between the user and the target */ @@ -7940,11 +7803,7 @@ export class MoneyAttr extends MoveEffectAttr { } } -/** - * Applies {@linkcode BattlerTagType.DESTINY_BOND} to the user. - * - * @extends MoveEffectAttr - */ +/** Applies {@linkcode BattlerTagType.DESTINY_BOND} to the user */ export class DestinyBondAttr extends MoveEffectAttr { constructor() { super(true, { trigger: MoveEffectTrigger.PRE_APPLY }); @@ -7955,7 +7814,7 @@ export class DestinyBondAttr extends MoveEffectAttr { * @param user {@linkcode Pokemon} that is having the tag applied to. * @param target {@linkcode Pokemon} N/A * @param move {@linkcode Move} {@linkcode Move.DESTINY_BOND} - * @param {any[]} args N/A + * @param args N/A * @returns true */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { @@ -7965,10 +7824,7 @@ export class DestinyBondAttr extends MoveEffectAttr { } } -/** - * Attribute to apply a battler tag to the target if they have had their stats boosted this turn. - * @extends AddBattlerTagAttr - */ +/** Attribute to apply a battler tag to the target if they have had their stats boosted this turn */ export class AddBattlerTagIfBoostedAttr extends AddBattlerTagAttr { constructor(tag: BattlerTagType) { super(tag, false, false, 2, 5); @@ -7978,7 +7834,7 @@ export class AddBattlerTagIfBoostedAttr extends AddBattlerTagAttr { * @param user {@linkcode Pokemon} using this move * @param target {@linkcode Pokemon} target of this move * @param move {@linkcode Move} being used - * @param {any[]} args N/A + * @param args N/A * @returns true */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { @@ -7991,7 +7847,6 @@ export class AddBattlerTagIfBoostedAttr extends AddBattlerTagAttr { /** * Attribute to apply a status effect to the target if they have had their stats boosted this turn. - * @extends MoveEffectAttr */ export class StatusIfBoostedAttr extends MoveEffectAttr { public effect: StatusEffect; @@ -8005,7 +7860,7 @@ export class StatusIfBoostedAttr extends MoveEffectAttr { * @param user {@linkcode Pokemon} using this move * @param target {@linkcode Pokemon} target of this move * @param move {@linkcode Move} N/A - * @param {any[]} args N/A + * @param args N/A * @returns true */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { @@ -8073,12 +7928,7 @@ export class AfterYouAttr extends MoveEffectAttr { */ override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean { globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:afterYou", { targetName: getPokemonNameWithAffix(target) })); - - // Will find next acting phase of the targeted pokémon, delete it and queue it right after us. - const targetNextPhase = globalScene.phaseManager.findPhase(phase => phase.pokemon === target); - if (targetNextPhase && globalScene.phaseManager.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) { - globalScene.phaseManager.prependToPhase(targetNextPhase, "MovePhase"); - } + globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === target); return true; } @@ -8087,7 +7937,6 @@ export class AfterYouAttr extends MoveEffectAttr { /** * Move effect to force the target to move last, ignoring priority. * If applied to multiple targets, they move in speed order after all other moves. - * @extends MoveEffectAttr */ export class ForceLastAttr extends MoveEffectAttr { /** @@ -8102,45 +7951,11 @@ export class ForceLastAttr extends MoveEffectAttr { override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean { globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:forceLast", { targetPokemonName: getPokemonNameWithAffix(target) })); - // TODO: Refactor this to be more readable and less janky - const targetMovePhase = globalScene.phaseManager.findPhase((phase) => phase.pokemon === target); - if (targetMovePhase && !targetMovePhase.isForcedLast() && globalScene.phaseManager.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) { - // Finding the phase to insert the move in front of - - // Either the end of the turn or in front of another, slower move which has also been forced last - const prependPhase = globalScene.phaseManager.findPhase((phase) => - [ MovePhase, MoveEndPhase ].every(cls => !(phase instanceof cls)) - || (phase.is("MovePhase")) && phaseForcedSlower(phase, target, !!globalScene.arena.getTag(ArenaTagType.TRICK_ROOM)) - ); - if (prependPhase) { - globalScene.phaseManager.phaseQueue.splice( - globalScene.phaseManager.phaseQueue.indexOf(prependPhase), - 0, - globalScene.phaseManager.create("MovePhase", target, [ ...targetMovePhase.targets ], targetMovePhase.move, targetMovePhase.useMode, true) - ); - } - } + globalScene.phaseManager.forceMoveLast((phase: MovePhase) => phase.pokemon === target); return true; } } -/** - * Returns whether a {@linkcode MovePhase} has been forced last and the corresponding pokemon is slower than {@linkcode target}. - - * TODO: - - Make this a class method - - Make this look at speed order from TurnStartPhase -*/ -const phaseForcedSlower = (phase: MovePhase, target: Pokemon, trickRoom: boolean): boolean => { - let slower: boolean; - // quashed pokemon still have speed ties - if (phase.pokemon.getEffectiveStat(Stat.SPD) === target.getEffectiveStat(Stat.SPD)) { - slower = !!target.randBattleSeedInt(2); - } else { - slower = !trickRoom ? phase.pokemon.getEffectiveStat(Stat.SPD) < target.getEffectiveStat(Stat.SPD) : phase.pokemon.getEffectiveStat(Stat.SPD) > target.getEffectiveStat(Stat.SPD); - } - return phase.isForcedLast() && slower; -}; - const failOnGravityCondition: MoveConditionFunc = (user, target, move) => !globalScene.arena.getTag(ArenaTagType.GRAVITY); const failOnBossCondition: MoveConditionFunc = (user, target, move) => !target.isBossImmune(); @@ -8164,7 +7979,7 @@ const userSleptOrComatoseCondition: MoveConditionFunc = (user) => user.status?.e const targetSleptOrComatoseCondition: MoveConditionFunc = (_user: Pokemon, target: Pokemon, _move: Move) => target.status?.effect === StatusEffect.SLEEP || target.hasAbility(AbilityId.COMATOSE); -const failIfLastCondition: MoveConditionFunc = () => globalScene.phaseManager.findPhase(phase => phase.is("MovePhase")) !== undefined; +const failIfLastCondition: MoveConditionFunc = () => globalScene.phaseManager.hasPhaseOfType("MovePhase"); const failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => { const party: Pokemon[] = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty(); @@ -8176,7 +7991,7 @@ const failIfGhostTypeCondition: MoveConditionFunc = (user: Pokemon, target: Poke const failIfNoTargetHeldItemsCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => target.getHeldItems().filter(i => i.isTransferable)?.length > 0; const attackedByItemMessageFunc = (user: Pokemon, target: Pokemon, move: Move) => { - if (isNullOrUndefined(target)) { // Fix bug when used against targets that have both fainted + if (target == null) { // Fix bug when used against targets that have both fainted return ""; } const heldItems = target.getHeldItems().filter(i => i.isTransferable); @@ -8256,11 +8071,11 @@ export class ResistLastMoveTypeAttr extends MoveEffectAttr { /** * User changes its type to a random type that resists the target's last used move - * @param {Pokemon} user Pokemon that used the move and will change types - * @param {Pokemon} target Opposing pokemon that recently used a move - * @param {Move} move Move being used - * @param {any[]} args Unused - * @returns {boolean} true if the function succeeds + * @param user Pokemon that used the move and will change types + * @param target Opposing pokemon that recently used a move + * @param move Move being used + * @param args Unused + * @returns true if the function succeeds */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { if (!super.apply(user, target, move, args)) { @@ -8325,9 +8140,6 @@ export class ResistLastMoveTypeAttr extends MoveEffectAttr { * Drops the target's immunity to types it is immune to * and makes its evasiveness be ignored during accuracy * checks. Used by: {@linkcode MoveId.ODOR_SLEUTH | Odor Sleuth}, {@linkcode MoveId.MIRACLE_EYE | Miracle Eye} and {@linkcode MoveId.FORESIGHT | Foresight} - * - * @extends AddBattlerTagAttr - * @see {@linkcode apply} */ export class ExposedMoveAttr extends AddBattlerTagAttr { constructor(tagType: BattlerTagType) { @@ -8727,7 +8539,7 @@ export function initMoves() { .attr(AddBattlerTagAttr, BattlerTagType.DISABLED, false, true) .condition((_user, target, _move) => { const lastNonVirtualMove = target.getLastNonVirtualMove(); - return !isNullOrUndefined(lastNonVirtualMove) && lastNonVirtualMove.move !== MoveId.STRUGGLE; + return lastNonVirtualMove != null && lastNonVirtualMove.move !== MoveId.STRUGGLE; }) .ignoresSubstitute() .reflectable(), @@ -10071,7 +9883,7 @@ export function initMoves() { .condition(failOnGravityCondition) .condition((_user, target, _move) => ![ SpeciesId.DIGLETT, SpeciesId.DUGTRIO, SpeciesId.ALOLA_DIGLETT, SpeciesId.ALOLA_DUGTRIO, SpeciesId.SANDYGAST, SpeciesId.PALOSSAND, SpeciesId.WIGLETT, SpeciesId.WUGTRIO ].includes(target.species.speciesId)) .condition((_user, target, _move) => !(target.species.speciesId === SpeciesId.GENGAR && target.getFormKey() === "mega")) - .condition((_user, target, _move) => isNullOrUndefined(target.getTag(BattlerTagType.INGRAIN)) && isNullOrUndefined(target.getTag(BattlerTagType.IGNORE_FLYING))) + .condition((_user, target, _move) => target.getTag(BattlerTagType.INGRAIN) == null && target.getTag(BattlerTagType.IGNORE_FLYING) == null) .attr(AddBattlerTagAttr, BattlerTagType.TELEKINESIS, false, true, 3) .attr(AddBattlerTagAttr, BattlerTagType.FLOATING, false, true, 3) .reflectable(), diff --git a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts index 5462c0eb336..00e98048ada 100644 --- a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts +++ b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts @@ -46,9 +46,9 @@ import { } from "#mystery-encounters/mystery-encounter-requirements"; import { getRandomPartyMemberFunc, trainerConfigs } from "#trainers/trainer-config"; import { TrainerPartyCompoundTemplate, TrainerPartyTemplate } from "#trainers/trainer-party-template"; -import { MoveInfoOverlay } from "#ui/containers/move-info-overlay"; -import type { OptionSelectItem } from "#ui/handlers/abstract-option-select-ui-handler"; -import { isNullOrUndefined, randSeedInt, randSeedShuffle } from "#utils/common"; +import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; +import { MoveInfoOverlay } from "#ui/move-info-overlay"; +import { randSeedInt, randSeedShuffle } from "#utils/common"; import i18next from "i18next"; /** the i18n namespace for the encounter */ @@ -571,7 +571,7 @@ function getTrainerConfigForWave(waveIndex: number) { .setPartyMemberFunc( 4, getRandomPartyMemberFunc([pool3Mon.species], TrainerSlot.TRAINER, true, p => { - if (!isNullOrUndefined(pool3Mon.formIndex)) { + if (pool3Mon.formIndex != null) { p.formIndex = pool3Mon.formIndex; p.generateAndPopulateMoveset(); p.generateName(); @@ -603,7 +603,7 @@ function getTrainerConfigForWave(waveIndex: number) { .setPartyMemberFunc( 3, getRandomPartyMemberFunc([pool3Mon.species], TrainerSlot.TRAINER, true, p => { - if (!isNullOrUndefined(pool3Mon.formIndex)) { + if (pool3Mon.formIndex != null) { p.formIndex = pool3Mon.formIndex; p.generateAndPopulateMoveset(); p.generateName(); @@ -613,7 +613,7 @@ function getTrainerConfigForWave(waveIndex: number) { .setPartyMemberFunc( 4, getRandomPartyMemberFunc([pool3Mon2.species], TrainerSlot.TRAINER, true, p => { - if (!isNullOrUndefined(pool3Mon2.formIndex)) { + if (pool3Mon2.formIndex != null) { p.formIndex = pool3Mon2.formIndex; p.generateAndPopulateMoveset(); p.generateName(); @@ -648,7 +648,7 @@ function getTrainerConfigForWave(waveIndex: number) { .setPartyMemberFunc( 3, getRandomPartyMemberFunc([pool3Mon.species], TrainerSlot.TRAINER, true, p => { - if (!isNullOrUndefined(pool3Mon.formIndex)) { + if (pool3Mon.formIndex != null) { p.formIndex = pool3Mon.formIndex; p.generateAndPopulateMoveset(); p.generateName(); @@ -687,7 +687,7 @@ function getTrainerConfigForWave(waveIndex: number) { .setPartyMemberFunc( 2, getRandomPartyMemberFunc([pool3Mon.species], TrainerSlot.TRAINER, true, p => { - if (!isNullOrUndefined(pool3Mon.formIndex)) { + if (pool3Mon.formIndex != null) { p.formIndex = pool3Mon.formIndex; p.generateAndPopulateMoveset(); p.generateName(); @@ -697,7 +697,7 @@ function getTrainerConfigForWave(waveIndex: number) { .setPartyMemberFunc( 3, getRandomPartyMemberFunc([pool3Mon2.species], TrainerSlot.TRAINER, true, p => { - if (!isNullOrUndefined(pool3Mon2.formIndex)) { + if (pool3Mon2.formIndex != null) { p.formIndex = pool3Mon2.formIndex; p.generateAndPopulateMoveset(); p.generateName(); diff --git a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts index 42907455e22..24ea7167864 100644 --- a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts +++ b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts @@ -45,7 +45,7 @@ import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option"; import { trainerConfigs } from "#trainers/trainer-config"; import { TrainerPartyCompoundTemplate, TrainerPartyTemplate } from "#trainers/trainer-party-template"; -import type { OptionSelectConfig } from "#ui/handlers/abstract-option-select-ui-handler"; +import type { OptionSelectConfig } from "#ui/abstract-option-select-ui-handler"; import { randSeedInt, randSeedShuffle } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import i18next from "i18next"; diff --git a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts index 598f9d496a2..33512ff0760 100644 --- a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts +++ b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts @@ -37,7 +37,7 @@ import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encou import { MoveRequirement } from "#mystery-encounters/mystery-encounter-requirements"; import { DANCING_MOVES } from "#mystery-encounters/requirement-groups"; import { PokemonData } from "#system/pokemon-data"; -import type { OptionSelectItem } from "#ui/handlers/abstract-option-select-ui-handler"; +import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import i18next from "i18next"; diff --git a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts index 65d22bfc6de..426eafb5e67 100644 --- a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts +++ b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts @@ -15,7 +15,7 @@ import { getRandomPlayerPokemon, getRandomSpeciesByStarterCost } from "#mystery- import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option"; -import { isNullOrUndefined, randSeedInt } from "#utils/common"; +import { randSeedInt } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; /** i18n namespace for encounter */ @@ -192,7 +192,7 @@ export const DarkDealEncounter: MysteryEncounter = MysteryEncounterBuilder.withE }; }), }; - if (!isNullOrUndefined(bossSpecies.forms) && bossSpecies.forms.length > 0) { + if (bossSpecies.forms != null && bossSpecies.forms.length > 0) { pokemonConfig.formIndex = 0; } const config: EnemyPartyConfig = { diff --git a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts index 79cccd91b26..8cd4c8bee66 100644 --- a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts +++ b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts @@ -33,7 +33,7 @@ import { MoneyRequirement, } from "#mystery-encounters/mystery-encounter-requirements"; import i18next from "#plugins/i18n"; -import type { OptionSelectItem } from "#ui/handlers/abstract-option-select-ui-handler"; +import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; import { randSeedItem } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; diff --git a/src/data/mystery-encounters/encounters/field-trip-encounter.ts b/src/data/mystery-encounters/encounters/field-trip-encounter.ts index 30c4026fcad..81d9bce3a76 100644 --- a/src/data/mystery-encounters/encounters/field-trip-encounter.ts +++ b/src/data/mystery-encounters/encounters/field-trip-encounter.ts @@ -18,7 +18,7 @@ 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 type { OptionSelectItem } from "#ui/handlers/abstract-option-select-ui-handler"; +import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; import i18next from "i18next"; /** i18n namespace for the encounter */ diff --git a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts index 1cc31eaa21f..0f37a1fae94 100644 --- a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts +++ b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts @@ -45,7 +45,7 @@ import { TypeRequirement, } from "#mystery-encounters/mystery-encounter-requirements"; import { FIRE_RESISTANT_ABILITIES } from "#mystery-encounters/requirement-groups"; -import { isNullOrUndefined, randSeedInt } from "#utils/common"; +import { randSeedInt } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; /** the i18n namespace for the encounter */ @@ -238,7 +238,7 @@ export const FieryFalloutEncounter: MysteryEncounter = MysteryEncounterBuilder.w // Burn random member const burnable = nonFireTypes.filter( - p => isNullOrUndefined(p.status) || isNullOrUndefined(p.status.effect) || p.status.effect === StatusEffect.NONE, + p => p.status == null || p.status.effect == null || p.status.effect === StatusEffect.NONE, ); if (burnable?.length > 0) { const roll = randSeedInt(burnable.length); diff --git a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts index d883fdbb567..f2363ade500 100644 --- a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts +++ b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts @@ -414,7 +414,7 @@ function summonPlayerPokemonAnimation(pokemon: PlayerPokemon): Promise { pokemon.resetTurnData(); globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true); - globalScene.phaseManager.pushNew("PostSummonPhase", pokemon.getBattlerIndex()); + globalScene.phaseManager.unshiftNew("PostSummonPhase", pokemon.getBattlerIndex()); resolve(); }); }, diff --git a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts index 7dbbe24fb69..e2166e99f6a 100644 --- a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts +++ b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts @@ -42,8 +42,8 @@ import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encou import { PartySizeRequirement } from "#mystery-encounters/mystery-encounter-requirements"; import { PokemonData } from "#system/pokemon-data"; import { MusicPreference } from "#system/settings"; -import type { OptionSelectItem } from "#ui/handlers/abstract-option-select-ui-handler"; -import { isNullOrUndefined, NumberHolder, randInt, randSeedInt, randSeedItem, randSeedShuffle } from "#utils/common"; +import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; +import { NumberHolder, randInt, randSeedInt, randSeedItem, randSeedShuffle } from "#utils/common"; import { getEnumKeys } from "#utils/enums"; import { getRandomLocaleEntry } from "#utils/i18n"; import { getPokemonSpecies } from "#utils/pokemon-utils"; @@ -537,7 +537,7 @@ function generateTradeOption(alreadyUsedSpecies: PokemonSpecies[], originalBst?: bstCap = originalBst + 100; bstMin = originalBst - 100; } - while (isNullOrUndefined(newSpecies)) { + while (newSpecies == null) { // Get all non-legendary species that fall within the Bst range requirements let validSpecies = allSpecies.filter(s => { const isLegendaryOrMythical = s.legendary || s.subLegendary || s.mythical; @@ -550,7 +550,7 @@ function generateTradeOption(alreadyUsedSpecies: PokemonSpecies[], originalBst?: if (validSpecies?.length > 20) { validSpecies = randSeedShuffle(validSpecies); newSpecies = validSpecies.pop(); - while (isNullOrUndefined(newSpecies) || alreadyUsedSpecies.includes(newSpecies)) { + while (newSpecies == null || alreadyUsedSpecies.includes(newSpecies)) { newSpecies = validSpecies.pop(); } } else { diff --git a/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts b/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts index b5084743613..67e778d8c4b 100644 --- a/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts @@ -669,7 +669,6 @@ function onGameOver() { // Clear any leftover battle phases globalScene.phaseManager.clearPhaseQueue(); - globalScene.phaseManager.clearPhaseQueueSplice(); // Return enemy Pokemon const pokemon = globalScene.getEnemyPokemon(); diff --git a/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts b/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts index 292c866c0ee..51efa0c7586 100644 --- a/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts @@ -28,7 +28,7 @@ import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option"; import { MoneyRequirement } from "#mystery-encounters/mystery-encounter-requirements"; import { PokemonData } from "#system/pokemon-data"; -import { isNullOrUndefined, randSeedInt, randSeedItem } from "#utils/common"; +import { randSeedInt, randSeedItem } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; /** the i18n namespace for this encounter */ @@ -81,7 +81,7 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter = MysteryEncounterBui let tries = 0; // Reroll any species that don't have HAs - while ((isNullOrUndefined(species.abilityHidden) || species.abilityHidden === AbilityId.NONE) && tries < 5) { + while ((species.abilityHidden == null || species.abilityHidden === AbilityId.NONE) && tries < 5) { species = getSalesmanSpeciesOffer(); tries++; } @@ -110,7 +110,7 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter = MysteryEncounterBui */ if ( r === 0 - || ((isNullOrUndefined(species.abilityHidden) || species.abilityHidden === AbilityId.NONE) + || ((species.abilityHidden == null || species.abilityHidden === AbilityId.NONE) && validEventEncounters.length === 0) ) { // If you roll 1%, give shiny Magikarp with random variant @@ -118,7 +118,7 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter = MysteryEncounterBui pokemon = new PlayerPokemon(species, 5, 2, undefined, undefined, true); } else if ( validEventEncounters.length > 0 - && (r <= EVENT_THRESHOLD || isNullOrUndefined(species.abilityHidden) || species.abilityHidden === AbilityId.NONE) + && (r <= EVENT_THRESHOLD || species.abilityHidden == null || species.abilityHidden === AbilityId.NONE) ) { tries = 0; do { diff --git a/src/data/mystery-encounters/encounters/training-session-encounter.ts b/src/data/mystery-encounters/encounters/training-session-encounter.ts index 796840c431f..1f3778a5d2c 100644 --- a/src/data/mystery-encounters/encounters/training-session-encounter.ts +++ b/src/data/mystery-encounters/encounters/training-session-encounter.ts @@ -27,8 +27,8 @@ import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option"; import { PokemonData } from "#system/pokemon-data"; import type { HeldModifierConfig } from "#types/held-modifier-config"; -import type { OptionSelectItem } from "#ui/handlers/abstract-option-select-ui-handler"; -import { isNullOrUndefined, randSeedShuffle } from "#utils/common"; +import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; +import { randSeedShuffle } from "#utils/common"; import { getEnumValues } from "#utils/enums"; import i18next from "i18next"; @@ -324,7 +324,7 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde // Only update the fusion's dex data if the Pokemon is already caught in dex (ignore rentals) const rootFusionSpecies = playerPokemon.fusionSpecies?.getRootSpeciesId(); if ( - !isNullOrUndefined(rootFusionSpecies) + rootFusionSpecies != null && speciesStarterCosts.hasOwnProperty(rootFusionSpecies) && !!globalScene.gameData.dexData[rootFusionSpecies].caughtAttr ) { diff --git a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts index 7bbc4a57757..cd61a6852f7 100644 --- a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts +++ b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts @@ -32,7 +32,7 @@ import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encou import { MoveRequirement, PersistentModifierRequirement } from "#mystery-encounters/mystery-encounter-requirements"; import { CHARMING_MOVES } from "#mystery-encounters/requirement-groups"; import { PokemonData } from "#system/pokemon-data"; -import { isNullOrUndefined, randSeedInt } from "#utils/common"; +import { randSeedInt } from "#utils/common"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/uncommonBreed"; @@ -167,7 +167,7 @@ export const UncommonBreedEncounter: MysteryEncounter = MysteryEncounterBuilder. const encounter = globalScene.currentBattle.mysteryEncounter!; const eggMove = encounter.misc.eggMove; - if (!isNullOrUndefined(eggMove)) { + if (eggMove != null) { // Check what type of move the egg move is to determine target const pokemonMove = new PokemonMove(eggMove); const move = pokemonMove.getMove(); diff --git a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts index 1fcbd2961d1..abd81fb92ea 100644 --- a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts +++ b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts @@ -41,7 +41,7 @@ import { PokemonData } from "#system/pokemon-data"; import { trainerConfigs } from "#trainers/trainer-config"; import { TrainerPartyTemplate } from "#trainers/trainer-party-template"; import type { HeldModifierConfig } from "#types/held-modifier-config"; -import { isNullOrUndefined, NumberHolder, randSeedInt, randSeedShuffle } from "#utils/common"; +import { NumberHolder, randSeedInt, randSeedShuffle } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; /** i18n namespace for encounter */ @@ -634,7 +634,7 @@ function getTransformedSpecies( alreadyUsedSpecies: PokemonSpecies[], ): PokemonSpecies { let newSpecies: PokemonSpecies | undefined; - while (isNullOrUndefined(newSpecies)) { + while (newSpecies == null) { const bstCap = originalBst + bstSearchRange[1]; const bstMin = Math.max(originalBst + bstSearchRange[0], 0); @@ -655,7 +655,7 @@ function getTransformedSpecies( if (validSpecies?.length > 20) { validSpecies = randSeedShuffle(validSpecies); newSpecies = validSpecies.pop(); - while (isNullOrUndefined(newSpecies) || alreadyUsedSpecies.includes(newSpecies)) { + while (newSpecies == null || alreadyUsedSpecies.includes(newSpecies)) { newSpecies = validSpecies.pop(); } } else { @@ -771,12 +771,12 @@ async function addEggMoveToNewPokemonMoveset( if (eggMoves) { const eggMoveIndices = randSeedShuffle([0, 1, 2, 3]); let randomEggMoveIndex = eggMoveIndices.pop(); - let randomEggMove = !isNullOrUndefined(randomEggMoveIndex) ? eggMoves[randomEggMoveIndex] : null; + let randomEggMove = randomEggMoveIndex != null ? eggMoves[randomEggMoveIndex] : null; let retries = 0; while (retries < 3 && (!randomEggMove || newPokemon.moveset.some(m => m.moveId === randomEggMove))) { // If Pokemon already knows this move, roll for another egg move randomEggMoveIndex = eggMoveIndices.pop(); - randomEggMove = !isNullOrUndefined(randomEggMoveIndex) ? eggMoves[randomEggMoveIndex] : null; + randomEggMove = randomEggMoveIndex != null ? eggMoves[randomEggMoveIndex] : null; retries++; } @@ -791,11 +791,7 @@ async function addEggMoveToNewPokemonMoveset( } // For pokemon that the player owns (including ones just caught), unlock the egg move - if ( - !forBattle - && !isNullOrUndefined(randomEggMoveIndex) - && !!globalScene.gameData.dexData[speciesRootForm].caughtAttr - ) { + if (!forBattle && randomEggMoveIndex != null && !!globalScene.gameData.dexData[speciesRootForm].caughtAttr) { await globalScene.gameData.setEggMoveUnlocked(getPokemonSpecies(speciesRootForm), randomEggMoveIndex, true); } } diff --git a/src/data/mystery-encounters/mystery-encounter-option.ts b/src/data/mystery-encounters/mystery-encounter-option.ts index fc7bb15d343..1b3b260414d 100644 --- a/src/data/mystery-encounters/mystery-encounter-option.ts +++ b/src/data/mystery-encounters/mystery-encounter-option.ts @@ -12,7 +12,7 @@ import { MoneyRequirement, TypeRequirement, } from "#mystery-encounters/mystery-encounter-requirements"; -import { isNullOrUndefined, randSeedInt } from "#utils/common"; +import { randSeedInt } from "#utils/common"; // biome-ignore lint/suspicious/noConfusingVoidType: void unions in callbacks are OK export type OptionPhaseCallback = () => Promise; @@ -62,7 +62,7 @@ export class MysteryEncounterOption implements IMysteryEncounterOption { onPostOptionPhase?: OptionPhaseCallback; constructor(option: IMysteryEncounterOption | null) { - if (!isNullOrUndefined(option)) { + if (option != null) { Object.assign(this, option); } this.hasDexProgress = this.hasDexProgress ?? false; diff --git a/src/data/mystery-encounters/mystery-encounter-requirements.ts b/src/data/mystery-encounters/mystery-encounter-requirements.ts index f20d513419e..85906044b77 100644 --- a/src/data/mystery-encounters/mystery-encounter-requirements.ts +++ b/src/data/mystery-encounters/mystery-encounter-requirements.ts @@ -15,7 +15,7 @@ import { WeatherType } from "#enums/weather-type"; import type { PlayerPokemon } from "#field/pokemon"; import { AttackTypeBoosterModifier } from "#modifiers/modifier"; import type { AttackTypeBoosterModifierType } from "#modifiers/modifier-type"; -import { coerceArray, isNullOrUndefined } from "#utils/common"; +import { coerceArray } from "#utils/common"; export interface EncounterRequirement { meetsRequirement(): boolean; // Boolean to see if a requirement is met @@ -219,7 +219,7 @@ export class WaveRangeRequirement extends EncounterSceneRequirement { } override meetsRequirement(): boolean { - if (!isNullOrUndefined(this.waveRange) && this.waveRange[0] <= this.waveRange[1]) { + if (this.waveRange != null && this.waveRange[0] <= this.waveRange[1]) { const waveIndex = globalScene.currentBattle.waveIndex; if ( (waveIndex >= 0 && this.waveRange[0] >= 0 && this.waveRange[0] > waveIndex) @@ -275,11 +275,7 @@ export class TimeOfDayRequirement extends EncounterSceneRequirement { override meetsRequirement(): boolean { const timeOfDay = globalScene.arena?.getTimeOfDay(); - return !( - !isNullOrUndefined(timeOfDay) - && this.requiredTimeOfDay?.length > 0 - && !this.requiredTimeOfDay.includes(timeOfDay) - ); + return !(timeOfDay != null && this.requiredTimeOfDay?.length > 0 && !this.requiredTimeOfDay.includes(timeOfDay)); } override getDialogueToken(_pokemon?: PlayerPokemon): [string, string] { @@ -298,7 +294,7 @@ export class WeatherRequirement extends EncounterSceneRequirement { override meetsRequirement(): boolean { const currentWeather = globalScene.arena.weather?.weatherType; return !( - !isNullOrUndefined(currentWeather) + currentWeather != null && this.requiredWeather?.length > 0 && !this.requiredWeather.includes(currentWeather!) ); @@ -307,7 +303,7 @@ export class WeatherRequirement extends EncounterSceneRequirement { override getDialogueToken(_pokemon?: PlayerPokemon): [string, string] { const currentWeather = globalScene.arena.weather?.weatherType; let token = ""; - if (!isNullOrUndefined(currentWeather)) { + if (currentWeather != null) { token = WeatherType[currentWeather].replace("_", " ").toLocaleLowerCase(); } return ["weather", token]; @@ -331,7 +327,7 @@ export class PartySizeRequirement extends EncounterSceneRequirement { } override meetsRequirement(): boolean { - if (!isNullOrUndefined(this.partySizeRange) && this.partySizeRange[0] <= this.partySizeRange[1]) { + if (this.partySizeRange != null && this.partySizeRange[0] <= this.partySizeRange[1]) { const partySize = this.excludeDisallowedPokemon ? globalScene.getPokemonAllowedInBattle().length : globalScene.getPlayerParty().length; @@ -363,7 +359,7 @@ export class PersistentModifierRequirement extends EncounterSceneRequirement { override meetsRequirement(): boolean { const partyPokemon = globalScene.getPlayerParty(); - if (isNullOrUndefined(partyPokemon) || this.requiredHeldItemModifiers?.length < 0) { + if (partyPokemon == null || this.requiredHeldItemModifiers?.length < 0) { return false; } let modifierCount = 0; @@ -396,7 +392,7 @@ export class MoneyRequirement extends EncounterSceneRequirement { override meetsRequirement(): boolean { const money = globalScene.money; - if (isNullOrUndefined(money)) { + if (money == null) { return false; } @@ -429,7 +425,7 @@ export class SpeciesRequirement extends EncounterPokemonRequirement { override meetsRequirement(): boolean { const partyPokemon = globalScene.getPlayerParty(); - if (isNullOrUndefined(partyPokemon) || this.requiredSpecies?.length < 0) { + if (partyPokemon == null || this.requiredSpecies?.length < 0) { return false; } return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; @@ -469,7 +465,7 @@ export class NatureRequirement extends EncounterPokemonRequirement { override meetsRequirement(): boolean { const partyPokemon = globalScene.getPlayerParty(); - if (isNullOrUndefined(partyPokemon) || this.requiredNature?.length < 0) { + if (partyPokemon == null || this.requiredNature?.length < 0) { return false; } return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; @@ -484,7 +480,7 @@ export class NatureRequirement extends EncounterPokemonRequirement { } override getDialogueToken(pokemon?: PlayerPokemon): [string, string] { - if (!isNullOrUndefined(pokemon?.nature) && this.requiredNature.includes(pokemon.nature)) { + if (pokemon?.nature != null && this.requiredNature.includes(pokemon.nature)) { return ["nature", Nature[pokemon.nature]]; } return ["nature", ""]; @@ -508,7 +504,7 @@ export class TypeRequirement extends EncounterPokemonRequirement { override meetsRequirement(): boolean { let partyPokemon = globalScene.getPlayerParty(); - if (isNullOrUndefined(partyPokemon)) { + if (partyPokemon == null) { return false; } @@ -561,7 +557,7 @@ export class MoveRequirement extends EncounterPokemonRequirement { override meetsRequirement(): boolean { const partyPokemon = globalScene.getPlayerParty(); - if (isNullOrUndefined(partyPokemon) || this.requiredMoves?.length < 0) { + if (partyPokemon == null || this.requiredMoves?.length < 0) { return false; } return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; @@ -612,7 +608,7 @@ export class CompatibleMoveRequirement extends EncounterPokemonRequirement { override meetsRequirement(): boolean { const partyPokemon = globalScene.getPlayerParty(); - if (isNullOrUndefined(partyPokemon) || this.requiredMoves?.length < 0) { + if (partyPokemon == null || this.requiredMoves?.length < 0) { return false; } return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; @@ -668,7 +664,7 @@ export class AbilityRequirement extends EncounterPokemonRequirement { override meetsRequirement(): boolean { const partyPokemon = globalScene.getPlayerParty(); - if (isNullOrUndefined(partyPokemon) || this.requiredAbilities?.length < 0) { + if (partyPokemon == null || this.requiredAbilities?.length < 0) { return false; } return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; @@ -692,7 +688,7 @@ export class AbilityRequirement extends EncounterPokemonRequirement { override getDialogueToken(pokemon?: PlayerPokemon): [string, string] { const matchingAbility = this.requiredAbilities.find(a => pokemon?.hasAbility(a, false)); - if (!isNullOrUndefined(matchingAbility)) { + if (matchingAbility != null) { return ["ability", allAbilities[matchingAbility].name]; } return ["ability", ""]; @@ -713,7 +709,7 @@ export class StatusEffectRequirement extends EncounterPokemonRequirement { override meetsRequirement(): boolean { const partyPokemon = globalScene.getPlayerParty(); - if (isNullOrUndefined(partyPokemon) || this.requiredStatusEffect?.length < 0) { + if (partyPokemon == null || this.requiredStatusEffect?.length < 0) { return false; } const x = this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; @@ -727,11 +723,7 @@ export class StatusEffectRequirement extends EncounterPokemonRequirement { return this.requiredStatusEffect.some(statusEffect => { if (statusEffect === StatusEffect.NONE) { // StatusEffect.NONE also checks for null or undefined status - return ( - isNullOrUndefined(pokemon.status) - || isNullOrUndefined(pokemon.status.effect) - || pokemon.status.effect === statusEffect - ); + return pokemon.status == null || pokemon.status.effect == null || pokemon.status.effect === statusEffect; } return pokemon.status?.effect === statusEffect; }); @@ -742,11 +734,7 @@ export class StatusEffectRequirement extends EncounterPokemonRequirement { return !this.requiredStatusEffect.some(statusEffect => { if (statusEffect === StatusEffect.NONE) { // StatusEffect.NONE also checks for null or undefined status - return ( - isNullOrUndefined(pokemon.status) - || isNullOrUndefined(pokemon.status.effect) - || pokemon.status.effect === statusEffect - ); + return pokemon.status == null || pokemon.status.effect == null || pokemon.status.effect === statusEffect; } return pokemon.status?.effect === statusEffect; }); @@ -756,9 +744,7 @@ export class StatusEffectRequirement extends EncounterPokemonRequirement { override getDialogueToken(pokemon?: PlayerPokemon): [string, string] { const reqStatus = this.requiredStatusEffect.filter(a => { if (a === StatusEffect.NONE) { - return ( - isNullOrUndefined(pokemon?.status) || isNullOrUndefined(pokemon.status.effect) || pokemon.status.effect === a - ); + return pokemon?.status == null || pokemon.status.effect == null || pokemon.status.effect === a; } return pokemon!.status?.effect === a; }); @@ -788,7 +774,7 @@ export class CanFormChangeWithItemRequirement extends EncounterPokemonRequiremen override meetsRequirement(): boolean { const partyPokemon = globalScene.getPlayerParty(); - if (isNullOrUndefined(partyPokemon) || this.requiredFormChangeItem?.length < 0) { + if (partyPokemon == null || this.requiredFormChangeItem?.length < 0) { return false; } return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; @@ -847,7 +833,7 @@ export class HeldItemRequirement extends EncounterPokemonRequirement { override meetsRequirement(): boolean { const partyPokemon = globalScene.getPlayerParty(); - if (isNullOrUndefined(partyPokemon)) { + if (partyPokemon == null) { return false; } return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; @@ -911,7 +897,7 @@ export class AttackTypeBoosterHeldItemTypeRequirement extends EncounterPokemonRe override meetsRequirement(): boolean { const partyPokemon = globalScene.getPlayerParty(); - if (isNullOrUndefined(partyPokemon)) { + if (partyPokemon == null) { return false; } return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; @@ -978,7 +964,7 @@ export class LevelRequirement extends EncounterPokemonRequirement { override meetsRequirement(): boolean { // Party Pokemon inside required level range - if (!isNullOrUndefined(this.requiredLevelRange) && this.requiredLevelRange[0] <= this.requiredLevelRange[1]) { + if (this.requiredLevelRange != null && this.requiredLevelRange[0] <= this.requiredLevelRange[1]) { const partyPokemon = globalScene.getPlayerParty(); const pokemonInRange = this.queryParty(partyPokemon); if (pokemonInRange.length < this.minNumberOfPokemon) { @@ -1019,10 +1005,7 @@ export class FriendshipRequirement extends EncounterPokemonRequirement { override meetsRequirement(): boolean { // Party Pokemon inside required friendship range - if ( - !isNullOrUndefined(this.requiredFriendshipRange) - && this.requiredFriendshipRange[0] <= this.requiredFriendshipRange[1] - ) { + if (this.requiredFriendshipRange != null && this.requiredFriendshipRange[0] <= this.requiredFriendshipRange[1]) { const partyPokemon = globalScene.getPlayerParty(); const pokemonInRange = this.queryParty(partyPokemon); if (pokemonInRange.length < this.minNumberOfPokemon) { @@ -1071,7 +1054,7 @@ export class HealthRatioRequirement extends EncounterPokemonRequirement { override meetsRequirement(): boolean { // Party Pokemon's health inside required health range - if (!isNullOrUndefined(this.requiredHealthRange) && this.requiredHealthRange[0] <= this.requiredHealthRange[1]) { + if (this.requiredHealthRange != null && this.requiredHealthRange[0] <= this.requiredHealthRange[1]) { const partyPokemon = globalScene.getPlayerParty(); const pokemonInRange = this.queryParty(partyPokemon); if (pokemonInRange.length < this.minNumberOfPokemon) { @@ -1098,7 +1081,7 @@ export class HealthRatioRequirement extends EncounterPokemonRequirement { override getDialogueToken(pokemon?: PlayerPokemon): [string, string] { const hpRatio = pokemon?.getHpRatio(); - if (!isNullOrUndefined(hpRatio)) { + if (hpRatio != null) { return ["healthRatio", Math.floor(hpRatio * 100).toString() + "%"]; } return ["healthRatio", ""]; @@ -1119,7 +1102,7 @@ export class WeightRequirement extends EncounterPokemonRequirement { override meetsRequirement(): boolean { // Party Pokemon's weight inside required weight range - if (!isNullOrUndefined(this.requiredWeightRange) && this.requiredWeightRange[0] <= this.requiredWeightRange[1]) { + if (this.requiredWeightRange != null && this.requiredWeightRange[0] <= this.requiredWeightRange[1]) { const partyPokemon = globalScene.getPlayerParty(); const pokemonInRange = this.queryParty(partyPokemon); if (pokemonInRange.length < this.minNumberOfPokemon) { diff --git a/src/data/mystery-encounters/mystery-encounter-save-data.ts b/src/data/mystery-encounters/mystery-encounter-save-data.ts index f04abccba5f..71cd2517a95 100644 --- a/src/data/mystery-encounters/mystery-encounter-save-data.ts +++ b/src/data/mystery-encounters/mystery-encounter-save-data.ts @@ -1,7 +1,6 @@ import { BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT } from "#app/constants"; import type { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import type { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import { isNullOrUndefined } from "#utils/common"; export class SeenEncounterData { type: MysteryEncounterType; @@ -28,7 +27,7 @@ export class MysteryEncounterSaveData { queuedEncounters: QueuedEncounter[] = []; constructor(data?: MysteryEncounterSaveData) { - if (!isNullOrUndefined(data)) { + if (data != null) { Object.assign(this, data); } diff --git a/src/data/mystery-encounters/mystery-encounter.ts b/src/data/mystery-encounters/mystery-encounter.ts index ea621238121..f18660b5d71 100644 --- a/src/data/mystery-encounters/mystery-encounter.ts +++ b/src/data/mystery-encounters/mystery-encounter.ts @@ -25,7 +25,7 @@ import { StatusEffectRequirement, WaveRangeRequirement, } from "#mystery-encounters/mystery-encounter-requirements"; -import { coerceArray, isNullOrUndefined, randSeedInt } from "#utils/common"; +import { coerceArray, randSeedInt } from "#utils/common"; import { capitalizeFirstLetter } from "#utils/strings"; export interface EncounterStartOfBattleEffect { @@ -275,7 +275,7 @@ export class MysteryEncounter implements IMysteryEncounter { private seedOffset?: any; constructor(encounter: IMysteryEncounter | null) { - if (!isNullOrUndefined(encounter)) { + if (encounter != null) { Object.assign(this, encounter); } this.encounterTier = this.encounterTier ?? MysteryEncounterTier.COMMON; @@ -576,10 +576,9 @@ export class MysteryEncounterBuilder implements Partial { */ /** - * @static Defines the type of encounter which is used as an identifier, should be tied to a unique MysteryEncounterType - * NOTE: if new functions are added to {@linkcode MysteryEncounter} class + * Defines the type of encounter which is used as an identifier, should be tied to a unique MysteryEncounterType * @param encounterType - * @returns this + * @returns a new instance of MysteryEncounterBuilder with encounterType set */ static withEncounterType( encounterType: MysteryEncounterType, diff --git a/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts b/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts index 26602b8ae31..a5810406ef9 100644 --- a/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts +++ b/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts @@ -3,7 +3,7 @@ import type { MoveId } from "#enums/move-id"; import type { PlayerPokemon } from "#field/pokemon"; import { PokemonMove } from "#moves/pokemon-move"; import { EncounterPokemonRequirement } from "#mystery-encounters/mystery-encounter-requirements"; -import { coerceArray, isNullOrUndefined } from "#utils/common"; +import { coerceArray } from "#utils/common"; /** * {@linkcode CanLearnMoveRequirement} options @@ -44,7 +44,7 @@ export class CanLearnMoveRequirement extends EncounterPokemonRequirement { .getPlayerParty() .filter(pkm => (this.includeFainted ? pkm.isAllowedInChallenge() : pkm.isAllowedInBattle())); - if (isNullOrUndefined(partyPokemon) || this.requiredMoves?.length < 0) { + if (partyPokemon == null || this.requiredMoves?.length < 0) { return false; } diff --git a/src/data/mystery-encounters/utils/encounter-dialogue-utils.ts b/src/data/mystery-encounters/utils/encounter-dialogue-utils.ts index 1a9b008f9e9..be681f731e8 100644 --- a/src/data/mystery-encounters/utils/encounter-dialogue-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-dialogue-utils.ts @@ -1,7 +1,6 @@ import { globalScene } from "#app/global-scene"; import type { TextStyle } from "#enums/text-style"; import { getTextWithColors } from "#ui/text"; -import { isNullOrUndefined } from "#utils/common"; import i18next from "i18next"; /** @@ -11,7 +10,7 @@ import i18next from "i18next"; * @param primaryStyle Can define a text style to be applied to the entire string. Must be defined for BBCodeText styles to be applied correctly */ export function getEncounterText(keyOrString?: string, primaryStyle?: TextStyle): string | null { - if (isNullOrUndefined(keyOrString)) { + if (keyOrString == null) { return null; } diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index fdc92994e66..0ba0dec896a 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -46,10 +46,10 @@ import type { PokemonData } from "#system/pokemon-data"; import type { TrainerConfig } from "#trainers/trainer-config"; import { trainerConfigs } from "#trainers/trainer-config"; import type { HeldModifierConfig } from "#types/held-modifier-config"; -import type { OptionSelectConfig, OptionSelectItem } from "#ui/handlers/abstract-option-select-ui-handler"; -import type { PartyOption, PokemonSelectFilter } from "#ui/handlers/party-ui-handler"; -import { PartyUiMode } from "#ui/handlers/party-ui-handler"; -import { coerceArray, isNullOrUndefined, randomString, randSeedInt, randSeedItem } from "#utils/common"; +import type { OptionSelectConfig, OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; +import type { PartyOption, PokemonSelectFilter } from "#ui/party-ui-handler"; +import { PartyUiMode } from "#ui/party-ui-handler"; +import { coerceArray, randomString, randSeedInt, randSeedItem } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import i18next from "i18next"; @@ -143,7 +143,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig): const trainerType = partyConfig?.trainerType; const partyTrainerConfig = partyConfig?.trainerConfig; let trainerConfig: TrainerConfig; - if (!isNullOrUndefined(trainerType) || partyTrainerConfig) { + if (trainerType != null || partyTrainerConfig) { globalScene.currentBattle.mysteryEncounter!.encounterMode = MysteryEncounterMode.TRAINER_BATTLE; if (globalScene.currentBattle.trainer) { globalScene.currentBattle.trainer.setVisible(false); @@ -154,7 +154,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig): const doubleTrainer = trainerConfig.doubleOnly || (trainerConfig.hasDouble && !!partyConfig.doubleBattle); doubleBattle = doubleTrainer; - const trainerFemale = isNullOrUndefined(partyConfig.female) ? !!randSeedInt(2) : partyConfig.female; + const trainerFemale = partyConfig.female == null ? !!randSeedInt(2) : partyConfig.female; const newTrainer = new Trainer( trainerConfig.trainerType, doubleTrainer ? TrainerVariant.DOUBLE : trainerFemale ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT, @@ -202,7 +202,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig): let dataSource: PokemonData | undefined; let isBoss = false; if (!loaded) { - if ((!isNullOrUndefined(trainerType) || trainerConfig) && battle.trainer) { + if ((trainerType != null || trainerConfig) && battle.trainer) { // Allows overriding a trainer's pokemon to use specific species/data if (partyConfig?.pokemonConfigs && e < partyConfig.pokemonConfigs.length) { const config = partyConfig.pokemonConfigs[e]; @@ -258,7 +258,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig): enemyPokemon.resetSummonData(); } - if ((!loaded && isNullOrUndefined(partyConfig.countAsSeen)) || partyConfig.countAsSeen) { + if ((!loaded && partyConfig.countAsSeen == null) || partyConfig.countAsSeen) { globalScene.gameData.setPokemonSeen(enemyPokemon, true, !!(trainerType || trainerConfig)); } @@ -266,7 +266,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig): const config = partyConfig.pokemonConfigs[e]; // Set form - if (!isNullOrUndefined(config.nickname)) { + if (config.nickname != null) { enemyPokemon.nickname = btoa(unescape(encodeURIComponent(config.nickname))); } @@ -276,22 +276,22 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig): } // Set form - if (!isNullOrUndefined(config.formIndex)) { + if (config.formIndex != null) { enemyPokemon.formIndex = config.formIndex; } // Set shiny - if (!isNullOrUndefined(config.shiny)) { + if (config.shiny != null) { enemyPokemon.shiny = config.shiny; } // Set Variant - if (enemyPokemon.shiny && !isNullOrUndefined(config.variant)) { + if (enemyPokemon.shiny && config.variant != null) { enemyPokemon.variant = config.variant; } // Set custom mystery encounter data fields (such as sprite scale, custom abilities, types, etc.) - if (!isNullOrUndefined(config.customPokemonData)) { + if (config.customPokemonData != null) { enemyPokemon.customPokemonData = config.customPokemonData; } @@ -300,7 +300,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig): let segments = config.bossSegments ?? globalScene.getEncounterBossSegments(globalScene.currentBattle.waveIndex, level, enemySpecies, true); - if (!isNullOrUndefined(config.bossSegmentModifier)) { + if (config.bossSegmentModifier != null) { segments += config.bossSegmentModifier; } enemyPokemon.setBoss(true, segments); @@ -335,18 +335,18 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig): } // Set ability - if (!isNullOrUndefined(config.abilityIndex)) { + if (config.abilityIndex != null) { enemyPokemon.abilityIndex = config.abilityIndex; } // Set gender - if (!isNullOrUndefined(config.gender)) { + if (config.gender != null) { enemyPokemon.gender = config.gender!; enemyPokemon.summonData.gender = config.gender; } // Set AI type - if (!isNullOrUndefined(config.aiType)) { + if (config.aiType != null) { enemyPokemon.aiType = config.aiType; } @@ -738,7 +738,7 @@ export function setEncounterRewards( if (customShopRewards) { globalScene.phaseManager.unshiftNew("SelectModifierPhase", 0, undefined, customShopRewards); } else { - globalScene.phaseManager.tryRemovePhase(p => p.is("MysteryEncounterRewardsPhase")); + globalScene.phaseManager.removeAllPhasesOfType("MysteryEncounterRewardsPhase"); } if (eggRewards) { @@ -812,8 +812,7 @@ export function leaveEncounterWithoutBattle( encounterMode: MysteryEncounterMode = MysteryEncounterMode.NO_BATTLE, ) { globalScene.currentBattle.mysteryEncounter!.encounterMode = encounterMode; - globalScene.phaseManager.clearPhaseQueue(); - globalScene.phaseManager.clearPhaseQueueSplice(); + globalScene.phaseManager.clearPhaseQueue(true); handleMysteryEncounterVictory(addHealPhase); } @@ -826,7 +825,7 @@ export function handleMysteryEncounterVictory(addHealPhase = false, doNotContinu const allowedPkm = globalScene.getPlayerParty().filter(pkm => pkm.isAllowedInBattle()); if (allowedPkm.length === 0) { - globalScene.phaseManager.clearPhaseQueue(); + globalScene.phaseManager.clearPhaseQueue(true); globalScene.phaseManager.unshiftNew("GameOverPhase"); return; } @@ -869,7 +868,7 @@ export function handleMysteryEncounterBattleFailed(addHealPhase = false, doNotCo const allowedPkm = globalScene.getPlayerParty().filter(pkm => pkm.isAllowedInBattle()); if (allowedPkm.length === 0) { - globalScene.phaseManager.clearPhaseQueue(); + globalScene.phaseManager.clearPhaseQueue(true); globalScene.phaseManager.unshiftNew("GameOverPhase"); return; } @@ -969,7 +968,7 @@ export function handleMysteryEncounterBattleStartEffects() { /** * Can queue extra phases or logic during {@linkcode TurnInitPhase} * Should mostly just be used for injecting custom phases into the battle system on turn start - * @return boolean - if true, will skip the remainder of the {@linkcode TurnInitPhase} + * @returns boolean - if true, will skip the remainder of the {@linkcode TurnInitPhase} */ export function handleMysteryEncounterTurnStartEffects(): boolean { const encounter = globalScene.currentBattle.mysteryEncounter; @@ -986,7 +985,7 @@ export function handleMysteryEncounterTurnStartEffects(): boolean { * @param level the level of the mon, which differs between MEs * @param isBoss whether the mon should be a Boss * @param rerollHidden whether the mon should get an extra roll for Hidden Ability - * @returns {@linkcode EnemyPokemon} for the requested encounter + * @returns for the requested encounter */ export function getRandomEncounterSpecies(level: number, isBoss = false, rerollHidden = false): EnemyPokemon { let bossSpecies: PokemonSpecies; diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index 0d07300d00d..01d4659d379 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -31,11 +31,11 @@ import { showEncounterText, } from "#mystery-encounters/encounter-dialogue-utils"; import { achvs } from "#system/achv"; -import type { PartyOption } from "#ui/handlers/party-ui-handler"; -import { PartyUiMode } from "#ui/handlers/party-ui-handler"; -import { SummaryUiMode } from "#ui/handlers/summary-ui-handler"; +import type { PartyOption } from "#ui/party-ui-handler"; +import { PartyUiMode } from "#ui/party-ui-handler"; +import { SummaryUiMode } from "#ui/summary-ui-handler"; import { applyChallenges } from "#utils/challenge-utils"; -import { BooleanHolder, isNullOrUndefined, randSeedInt } from "#utils/common"; +import { BooleanHolder, randSeedInt } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import i18next from "i18next"; @@ -276,7 +276,7 @@ export function getRandomSpeciesByStarterCost( if (types && types.length > 0) { filteredSpecies = filteredSpecies.filter( - s => types.includes(s[0].type1) || (!isNullOrUndefined(s[0].type2) && types.includes(s[0].type2)), + s => types.includes(s[0].type1) || (s[0].type2 != null && types.includes(s[0].type2)), ); } diff --git a/src/data/phase-priority-queue.ts b/src/data/phase-priority-queue.ts deleted file mode 100644 index 2c83348cc7b..00000000000 --- a/src/data/phase-priority-queue.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { globalScene } from "#app/global-scene"; -import type { Phase } from "#app/phase"; -import { TrickRoomTag } from "#data/arena-tag"; -import { DynamicPhaseType } from "#enums/dynamic-phase-type"; -import { Stat } from "#enums/stat"; -import { ActivatePriorityQueuePhase } from "#phases/activate-priority-queue-phase"; -import { PostSummonActivateAbilityPhase } from "#phases/post-summon-activate-ability-phase"; -import type { PostSummonPhase } from "#phases/post-summon-phase"; -import { BooleanHolder } from "#utils/common"; - -/** - * Stores a list of {@linkcode Phase}s - * - * Dynamically updates ordering to always pop the highest "priority", based on implementation of {@linkcode reorder} - */ -export abstract class PhasePriorityQueue { - protected abstract queue: Phase[]; - - /** - * Sorts the elements in the queue - */ - public abstract reorder(): void; - - /** - * Calls {@linkcode reorder} and shifts the queue - * @returns The front element of the queue after sorting - */ - public pop(): Phase | undefined { - this.reorder(); - return this.queue.shift(); - } - - /** - * Adds a phase to the queue - * @param phase The phase to add - */ - public push(phase: Phase): void { - this.queue.push(phase); - } - - /** - * Removes all phases from the queue - */ - public clear(): void { - this.queue.splice(0, this.queue.length); - } - - /** - * Attempt to remove one or more Phases from the current queue. - * @param phaseFilter - The function to select phases for removal - * @param removeCount - The maximum number of phases to remove, or `all` to remove all matching phases; - * default `1` - * @returns The number of successfully removed phases - * @todo Remove this eventually once the patchwork bug this is used for is fixed - */ - public tryRemovePhase(phaseFilter: (phase: Phase) => boolean, removeCount: number | "all" = 1): number { - if (removeCount === "all") { - removeCount = this.queue.length; - } else if (removeCount < 1) { - return 0; - } - let numRemoved = 0; - - do { - const phaseIndex = this.queue.findIndex(phaseFilter); - if (phaseIndex === -1) { - break; - } - this.queue.splice(phaseIndex, 1); - numRemoved++; - } while (numRemoved < removeCount && this.queue.length > 0); - - return numRemoved; - } -} - -/** - * Priority Queue for {@linkcode PostSummonPhase} and {@linkcode PostSummonActivateAbilityPhase} - * - * Orders phases first by ability priority, then by the {@linkcode Pokemon}'s effective speed - */ -export class PostSummonPhasePriorityQueue extends PhasePriorityQueue { - protected override queue: PostSummonPhase[] = []; - - public override reorder(): void { - this.queue.sort((phaseA: PostSummonPhase, phaseB: PostSummonPhase) => { - if (phaseA.getPriority() === phaseB.getPriority()) { - return ( - (phaseB.getPokemon().getEffectiveStat(Stat.SPD) - phaseA.getPokemon().getEffectiveStat(Stat.SPD)) - * (isTrickRoom() ? -1 : 1) - ); - } - - return phaseB.getPriority() - phaseA.getPriority(); - }); - } - - public override push(phase: PostSummonPhase): void { - super.push(phase); - this.queueAbilityPhase(phase); - } - - /** - * Queues all necessary {@linkcode PostSummonActivateAbilityPhase}s for each pushed {@linkcode PostSummonPhase} - * @param phase The {@linkcode PostSummonPhase} that was pushed onto the queue - */ - private queueAbilityPhase(phase: PostSummonPhase): void { - const phasePokemon = phase.getPokemon(); - - phasePokemon.getAbilityPriorities().forEach((priority, idx) => { - this.queue.push(new PostSummonActivateAbilityPhase(phasePokemon.getBattlerIndex(), priority, !!idx)); - globalScene.phaseManager.appendToPhase( - new ActivatePriorityQueuePhase(DynamicPhaseType.POST_SUMMON), - "ActivatePriorityQueuePhase", - (p: ActivatePriorityQueuePhase) => p.getType() === DynamicPhaseType.POST_SUMMON, - ); - }); - } -} - -function isTrickRoom(): boolean { - const speedReversed = new BooleanHolder(false); - globalScene.arena.applyTags(TrickRoomTag, false, speedReversed); - return speedReversed.value; -} diff --git a/src/data/pokemon-forms/form-change-triggers.ts b/src/data/pokemon-forms/form-change-triggers.ts index 9da3538741c..f51b090878f 100644 --- a/src/data/pokemon-forms/form-change-triggers.ts +++ b/src/data/pokemon-forms/form-change-triggers.ts @@ -289,7 +289,7 @@ export class SpeciesFormChangeRevertWeatherFormTrigger extends SpeciesFormChange /** * Checks if the Pokemon has the required ability and the weather is one that will revert * the Pokemon to its original form or the weather or ability is suppressed - * @param {Pokemon} pokemon the pokemon that is trying to do the form change + * @param pokemon the pokemon that is trying to do the form change * @returns `true` if the Pokemon will revert to its original form, `false` otherwise */ canChange(pokemon: Pokemon): boolean { diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index f91d3aaf1f2..7c00bf5dff7 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -26,9 +26,9 @@ import { loadPokemonVariantAssets } from "#sprites/pokemon-sprite"; import { hasExpSprite } from "#sprites/sprite-utils"; import type { Variant, VariantSet } from "#sprites/variant"; import { populateVariantColorCache, variantColorCache, variantData } from "#sprites/variant"; -import type { StarterMoveset } from "#system/game-data"; import type { Localizable } from "#types/locales"; -import { isNullOrUndefined, randSeedFloat, randSeedGauss, randSeedInt } from "#utils/common"; +import type { StarterMoveset } from "#types/save-data"; +import { randSeedFloat, randSeedGauss, randSeedInt } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import { toCamelCase, toPascalCase } from "#utils/strings"; import { argbFromRgba, QuantizerCelebi, rgbaFromArgb } from "@material/material-color-utilities"; @@ -197,7 +197,7 @@ export abstract class PokemonSpeciesForm { * @returns The id of the ability */ getPassiveAbility(formIndex?: number): AbilityId { - if (isNullOrUndefined(formIndex)) { + if (formIndex == null) { formIndex = this.formIndex; } let starterSpeciesId = this.speciesId; @@ -551,7 +551,7 @@ export abstract class PokemonSpeciesForm { const spriteKey = this.getSpriteKey(female, formIndex, shiny, variant, back); globalScene.loadPokemonAtlas(spriteKey, this.getSpriteAtlasPath(female, formIndex, shiny, variant, back)); globalScene.load.audio(this.getCryKey(formIndex), `audio/${this.getCryKey(formIndex)}.m4a`); - if (!isNullOrUndefined(variant)) { + if (variant != null) { await this.loadVariantColors(spriteKey, female, variant, back, formIndex); } return new Promise(resolve => { @@ -579,7 +579,7 @@ export abstract class PokemonSpeciesForm { const spritePath = this.getSpriteAtlasPath(female, formIndex, shiny, variant, back) .replace("variant/", "") .replace(/_[1-3]$/, ""); - if (!isNullOrUndefined(variant)) { + if (variant != null) { loadPokemonVariantAssets(spriteKey, spritePath, variant).then(() => resolve()); } }); @@ -791,7 +791,7 @@ export class PokemonSpecies extends PokemonSpeciesForm implements Localizable { * @returns A randomly rolled gender based on this Species' {@linkcode malePercent}. */ generateGender(): Gender { - if (isNullOrUndefined(this.malePercent)) { + if (this.malePercent == null) { return Gender.GENDERLESS; } @@ -919,7 +919,7 @@ export class PokemonSpecies extends PokemonSpeciesForm implements Localizable { * The calculation with evolution delay is a weighted average of the easeIn and easeOut functions where preferredMinLevel is the denominator. * This also means a lower value of x will lead to a higher evolution chance. * @param strength {@linkcode PartyMemberStrength} The strength of the party member in question - * @returns {@linkcode number} The level difference from expected evolution level tolerated for a mon to be unevolved. Lower value = higher evolution chance. + * @returns The level difference from expected evolution level tolerated for a mon to be unevolved. Lower value = higher evolution chance. */ private getStrengthLevelDiff(strength: PartyMemberStrength): number { switch (Math.min(strength, PartyMemberStrength.STRONGER)) { diff --git a/src/data/pokemon/pokemon-data.ts b/src/data/pokemon/pokemon-data.ts index 87ffbbab4cd..4fbb70bccb2 100644 --- a/src/data/pokemon/pokemon-data.ts +++ b/src/data/pokemon/pokemon-data.ts @@ -16,7 +16,6 @@ import type { AttackMoveResult } from "#types/attack-move-result"; import type { IllusionData } from "#types/illusion-data"; import type { TurnMove } from "#types/turn-move"; import type { CoerceNullPropertiesToUndefined } from "#types/type-helpers"; -import { isNullOrUndefined } from "#utils/common"; import { getPokemonSpeciesForm } from "#utils/pokemon-utils"; /** @@ -64,14 +63,14 @@ function deserializePokemonSpeciesForm(value: SerializedSpeciesForm | PokemonSpe // @ts-expect-error: We may be deserializing a PokemonSpeciesForm, but we catch later on let { id, formIdx } = value; - if (isNullOrUndefined(id) || isNullOrUndefined(formIdx)) { + if (id == null || formIdx == null) { // @ts-expect-error: Typescript doesn't know that in block, `value` must be a PokemonSpeciesForm id = value.speciesId; // @ts-expect-error: Same as above (plus we are accessing a protected property) formIdx = value._formIndex; } // If for some reason either of these fields are null/undefined, we cannot reconstruct the species form - if (isNullOrUndefined(id) || isNullOrUndefined(formIdx)) { + if (id == null || formIdx == null) { return null; } return getPokemonSpeciesForm(id, formIdx); @@ -151,13 +150,13 @@ export class PokemonSummonData { public moveHistory: TurnMove[] = []; constructor(source?: PokemonSummonData | SerializedPokemonSummonData) { - if (isNullOrUndefined(source)) { + if (source == null) { return; } // TODO: Rework this into an actual generic function for use elsewhere for (const [key, value] of Object.entries(source)) { - if (isNullOrUndefined(value) && this.hasOwnProperty(key)) { + if (value == null && this.hasOwnProperty(key)) { continue; } @@ -171,7 +170,7 @@ export class PokemonSummonData { const illusionData = { ...value, }; - if (!isNullOrUndefined(illusionData.fusionSpecies)) { + if (illusionData.fusionSpecies != null) { switch (typeof illusionData.fusionSpecies) { case "object": illusionData.fusionSpecies = allSpecies[illusionData.fusionSpecies.speciesId]; @@ -224,18 +223,18 @@ export class PokemonSummonData { CoerceNullPropertiesToUndefined, "speciesForm" | "fusionSpeciesForm" | "illusion" >), - speciesForm: isNullOrUndefined(speciesForm) - ? undefined - : { id: speciesForm.speciesId, formIdx: speciesForm.formIndex }, - fusionSpeciesForm: isNullOrUndefined(fusionSpeciesForm) - ? undefined - : { id: fusionSpeciesForm.speciesId, formIdx: fusionSpeciesForm.formIndex }, - illusion: isNullOrUndefined(illusion) - ? undefined - : { - ...(this.illusion as Omit), - fusionSpecies: illusionSpeciesForm?.speciesId, - }, + speciesForm: speciesForm == null ? undefined : { id: speciesForm.speciesId, formIdx: speciesForm.formIndex }, + fusionSpeciesForm: + fusionSpeciesForm == null + ? undefined + : { id: fusionSpeciesForm.speciesId, formIdx: fusionSpeciesForm.formIndex }, + illusion: + illusion == null + ? undefined + : { + ...(this.illusion as Omit), + fusionSpecies: illusionSpeciesForm?.speciesId, + }, }; // Replace `null` with `undefined`, as `undefined` never gets serialized for (const [key, value] of Object.entries(t)) { @@ -278,7 +277,7 @@ export class PokemonBattleData { public berriesEaten: BerryType[] = []; constructor(source?: PokemonBattleData | Partial) { - if (!isNullOrUndefined(source)) { + if (source != null) { this.hitCount = source.hitCount ?? 0; this.hasEatenBerry = source.hasEatenBerry ?? false; this.berriesEaten = source.berriesEaten ?? []; diff --git a/src/data/splash-messages.ts b/src/data/splash-messages.ts index 21e7e5d05e6..a7791af78ac 100644 --- a/src/data/splash-messages.ts +++ b/src/data/splash-messages.ts @@ -234,7 +234,7 @@ const seasonalSplashMessages: Season[] = [ "valentines.happyValentines", "valentines.fullOfLove", "valentines.applinForYou", - "valentines.thePowerOfLoveIsThreeThirtyBST", + "valentines.thePowerOfLoveIsThreeThirtyBst", "valentines.haveAHeartScale", "valentines.i<3You", ], @@ -265,7 +265,7 @@ const seasonalSplashMessages: Season[] = [ "aprilFools.whoIsFinn", "aprilFools.watchOutForShadowPokemon", "aprilFools.nowWithDarkTypeLuxray", - "aprilFools.onlyOnPokerogueNetAGAIN", + "aprilFools.onlyOnPokerogueNetAgain", "aprilFools.noFreeVouchers", "aprilFools.altffourAchievementPoints", "aprilFools.rokePogue", diff --git a/src/data/terrain.ts b/src/data/terrain.ts index 139230605bf..315ed919e03 100644 --- a/src/data/terrain.ts +++ b/src/data/terrain.ts @@ -22,10 +22,12 @@ export interface SerializedTerrain { export class Terrain { public terrainType: TerrainType; public turnsLeft: number; + public maxDuration: number; - constructor(terrainType: TerrainType, turnsLeft?: number) { + constructor(terrainType: TerrainType, turnsLeft = 0, maxDuration: number = turnsLeft) { this.terrainType = terrainType; - this.turnsLeft = turnsLeft || 0; + this.turnsLeft = turnsLeft; + this.maxDuration = maxDuration; } lapse(): boolean { diff --git a/src/data/trainers/trainer-config.ts b/src/data/trainers/trainer-config.ts index 2870fd7f808..b5786d1f0a2 100644 --- a/src/data/trainers/trainer-config.ts +++ b/src/data/trainers/trainer-config.ts @@ -3,6 +3,8 @@ import { globalScene } from "#app/global-scene"; import { pokemonEvolutions, pokemonPrevolutions } from "#balance/pokemon-evolutions"; import { signatureSpecies } from "#balance/signature-species"; import { tmSpecies } from "#balance/tms"; +// biome-ignore lint/correctness/noUnusedImports: Used in a tsdoc comment +import type { RARE_EGG_MOVE_LEVEL_REQUIREMENT } from "#data/balance/moveset-generation"; import { modifierTypes } from "#data/data-lists"; import { doubleBattleDialogue } from "#data/double-battle-dialogue"; import { Gender } from "#data/gender"; @@ -41,7 +43,8 @@ import type { TrainerConfigs, TrainerTierPools, } from "#types/trainer-funcs"; -import { coerceArray, isNullOrUndefined, randSeedInt, randSeedIntRange, randSeedItem } from "#utils/common"; +import type { Mutable } from "#types/type-helpers"; +import { coerceArray, randSeedInt, randSeedIntRange, randSeedItem } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import { toCamelCase, toTitleCase } from "#utils/strings"; import i18next from "i18next"; @@ -119,6 +122,15 @@ export class TrainerConfig { public hasVoucher = false; public trainerAI: TrainerAI; + /** + * Whether this trainer's Pokémon are allowed to generate with egg moves + * @defaultValue `false` + * + * @see {@linkcode setEggMovesAllowed} + * @see {@linkcode RARE_EGG_MOVE_LEVEL_THRESHOLD} + */ + public readonly allowEggMoves: boolean = false; + public encounterMessages: string[] = []; public victoryMessages: string[] = []; public defeatMessages: string[] = []; @@ -204,7 +216,7 @@ export class TrainerConfig { /** * Returns the derived trainer type for a given trainer type. * @param trainerTypeToDeriveFrom - The trainer type to derive from. (If null, the this.trainerType property will be used.) - * @returns {TrainerType} - The derived trainer type. + * @returns - The derived trainer type. */ getDerivedType(trainerTypeToDeriveFrom: TrainerType | null = null): TrainerType { let trainerType = trainerTypeToDeriveFrom ? trainerTypeToDeriveFrom : this.trainerType; @@ -274,9 +286,9 @@ export class TrainerConfig { /** * Sets the configuration for trainers with genders, including the female name and encounter background music (BGM). - * @param {string} [nameFemale] The name of the female trainer. If 'Ivy', a localized name will be assigned. - * @param {TrainerType | string} [femaleEncounterBgm] The encounter BGM for the female trainer, which can be a TrainerType or a string. - * @returns {TrainerConfig} The updated TrainerConfig instance. + * @param [nameFemale] The name of the female trainer. If 'Ivy', a localized name will be assigned. + * @param [femaleEncounterBgm] The encounter BGM for the female trainer, which can be a TrainerType or a string. + * @returns The updated TrainerConfig instance. */ setHasGenders(nameFemale?: string, femaleEncounterBgm?: TrainerType | string): TrainerConfig { // If the female name is 'Ivy' (the rival), assign a localized name. @@ -314,7 +326,7 @@ export class TrainerConfig { * Sets the configuration for trainers with double battles, including the name of the double trainer and the encounter BGM. * @param nameDouble The name of the double trainer (e.g., "Ace Duo" for Trainer Class Doubles or "red_blue_double" for NAMED trainer doubles). * @param doubleEncounterBgm The encounter BGM for the double trainer, which can be a TrainerType or a string. - * @returns {TrainerConfig} The updated TrainerConfig instance. + * @returns The updated TrainerConfig instance. */ setHasDouble(nameDouble: string, doubleEncounterBgm?: TrainerType | string): TrainerConfig { this.hasDouble = true; @@ -331,7 +343,7 @@ export class TrainerConfig { /** * Sets the trainer type for double battles. * @param trainerTypeDouble The TrainerType of the partner in a double battle. - * @returns {TrainerConfig} The updated TrainerConfig instance. + * @returns The updated TrainerConfig instance. */ setDoubleTrainerType(trainerTypeDouble: TrainerType): TrainerConfig { this.trainerTypeDouble = trainerTypeDouble; @@ -356,7 +368,7 @@ export class TrainerConfig { /** * Sets the title for double trainers * @param titleDouble The key for the title in the i18n file. (e.g., "champion_double"). - * @returns {TrainerConfig} The updated TrainerConfig instance. + * @returns The updated TrainerConfig instance. */ setDoubleTitle(titleDouble: string): TrainerConfig { // First check if i18n is initialized @@ -387,8 +399,27 @@ export class TrainerConfig { return this; } - setBoss(): TrainerConfig { + /** + * Allow this trainer's Pokémon to have egg moves when generating their movesets. + * + * @remarks + * It is redundant to call this if {@linkcode setBoss} is also called on the configuration. + * @returns `this` for method chaining + * @see {@linkcode allowEggMoves} + */ + public setEggMovesAllowed(): this { + (this as Mutable).allowEggMoves = true; + return this; + } + + /** + * Set this trainer as a boss trainer + * @returns `this` for method chaining + * @see {@linkcode isBoss} + */ + public setBoss(): TrainerConfig { this.isBoss = true; + (this as Mutable).allowEggMoves = true; return this; } @@ -474,7 +505,7 @@ export class TrainerConfig { .fill(null) .map((_, i) => i) .filter(i => shedinjaCanTera || party[i].species.speciesId !== SpeciesId.SHEDINJA); // Shedinja can only Tera on Bug specialty type (or no specialty type) - const setPartySlot = !isNullOrUndefined(slot) ? Phaser.Math.Wrap(slot, 0, party.length) : -1; // If we have a tera slot defined, wrap it to party size. + const setPartySlot = slot != null ? Phaser.Math.Wrap(slot, 0, party.length) : -1; // If we have a tera slot defined, wrap it to party size. for (let t = 0; t < Math.min(count(), party.length); t++) { const randomIndex = partyMemberIndexes.indexOf(setPartySlot) > -1 ? setPartySlot : randSeedItem(partyMemberIndexes); @@ -523,9 +554,9 @@ export class TrainerConfig { * Initializes the trainer configuration for an evil team admin. * @param title The title of the evil team admin. * @param poolName The evil team the admin belongs to. - * @param {SpeciesId | SpeciesId[]} signatureSpecies The signature species for the evil team leader. + * @param signatureSpecies The signature species for the evil team leader. * @param specialtyType The specialty Type of the admin, if they have one - * @returns {TrainerConfig} The updated TrainerConfig instance. + * @returns The updated TrainerConfig instance. */ initForEvilTeamAdmin( title: string, @@ -537,7 +568,7 @@ export class TrainerConfig { initI18n(); } - if (!isNullOrUndefined(specialtyType)) { + if (specialtyType != null) { this.setSpecialtyType(specialtyType); } @@ -566,7 +597,7 @@ export class TrainerConfig { /** * Initializes the trainer configuration for a Stat Trainer, as part of the Trainer's Test Mystery Encounter. * @param _isMale Whether the stat trainer is Male or Female (for localization of the title). - * @returns {TrainerConfig} The updated TrainerConfig instance. + * @returns The updated TrainerConfig instance. */ initForStatTrainer(_isMale = false): TrainerConfig { if (!getIsInitialized()) { @@ -590,10 +621,10 @@ export class TrainerConfig { /** * Initializes the trainer configuration for an evil team leader. Temporarily hardcoding evil leader teams though. - * @param {SpeciesId | SpeciesId[]} signatureSpecies The signature species for the evil team leader. - * @param {PokemonType} specialtyType The specialty type for the evil team Leader. + * @param signatureSpecies The signature species for the evil team leader. + * @param specialtyType The specialty type for the evil team Leader. * @param boolean Whether or not this is the rematch fight - * @returns {TrainerConfig} The updated TrainerConfig instance. + * @returns The updated TrainerConfig instance. */ initForEvilTeamLeader( title: string, @@ -612,7 +643,7 @@ export class TrainerConfig { signatureSpecies.forEach((speciesPool, s) => { this.setPartyMemberFunc(-(s + 1), getRandomPartyMemberFunc(coerceArray(speciesPool))); }); - if (!isNullOrUndefined(specialtyType)) { + if (specialtyType != null) { this.setSpeciesFilter(p => p.isOfType(specialtyType)); this.setSpecialtyType(specialtyType); } @@ -631,12 +662,12 @@ export class TrainerConfig { /** * Initializes the trainer configuration for a Gym Leader. - * @param {SpeciesId | SpeciesId[]} signatureSpecies The signature species for the Gym Leader. Added to party in reverse order. + * @param signatureSpecies The signature species for the Gym Leader. Added to party in reverse order. * @param isMale Whether the Gym Leader is Male or Not (for localization of the title). - * @param {PokemonType} specialtyType The specialty type for the Gym Leader. + * @param specialtyType The specialty type for the Gym Leader. * @param ignoreMinTeraWave Whether the Gym Leader always uses Tera (true), or only Teras after {@linkcode GYM_LEADER_TERA_WAVE} (false). Defaults to false. * @param teraSlot Optional, sets the party member in this slot to Terastallize. Wraps based on party size. - * @returns {TrainerConfig} The updated TrainerConfig instance. + * @returns The updated TrainerConfig instance. */ initForGymLeader( signatureSpecies: (SpeciesId | SpeciesId[])[], @@ -717,7 +748,7 @@ export class TrainerConfig { }); // Set species filter and specialty type if provided, otherwise filter by base total. - if (!isNullOrUndefined(specialtyType)) { + if (specialtyType != null) { this.setSpeciesFilter(p => p.isOfType(specialtyType) && p.baseTotal >= ELITE_FOUR_MINIMUM_BST); this.setSpecialtyType(specialtyType); } else { @@ -748,9 +779,9 @@ export class TrainerConfig { /** * Initializes the trainer configuration for a Champion. - * @param {SpeciesId | SpeciesId[]} signatureSpecies The signature species for the Champion. + * @param signatureSpecies The signature species for the Champion. * @param isMale Whether the Champion is Male or Female (for localization of the title). - * @returns {TrainerConfig} The updated TrainerConfig instance. + * @returns The updated TrainerConfig instance. */ initForChampion(isMale: boolean): TrainerConfig { // Check if the internationalization (i18n) system is initialized. @@ -785,7 +816,7 @@ export class TrainerConfig { /** * Sets a localized name for the trainer. This should only be used for trainers that dont use a "initFor" function and are considered "named" trainers * @param name - The name of the trainer. - * @returns {TrainerConfig} The updated TrainerConfig instance. + * @returns The updated TrainerConfig instance. */ setLocalizedName(name: string): TrainerConfig { // Check if the internationalization (i18n) system is initialized. @@ -798,9 +829,9 @@ export class TrainerConfig { /** * Retrieves the title for the trainer based on the provided trainer slot and variant. - * @param {TrainerSlot} trainerSlot - The slot to determine which title to use. Defaults to TrainerSlot.NONE. - * @param {TrainerVariant} variant - The variant of the trainer to determine the specific title. - * @returns {string} - The title of the trainer. + * @param trainerSlot - The slot to determine which title to use. Defaults to TrainerSlot.NONE. + * @param variant - The variant of the trainer to determine the specific title. + * @returns - The title of the trainer. */ getTitle(trainerSlot: TrainerSlot = TrainerSlot.NONE, variant: TrainerVariant): string { const ret = this.name; @@ -895,7 +926,7 @@ export class TrainerConfig { * @returns `true` if `specialtyType` is defined and not {@link PokemonType.UNKNOWN} */ hasSpecialtyType(): boolean { - return !isNullOrUndefined(this.specialtyType) && this.specialtyType !== PokemonType.UNKNOWN; + return this.specialtyType != null && this.specialtyType !== PokemonType.UNKNOWN; } /** @@ -2942,7 +2973,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.SLOWBRO, SpeciesId.GALAR_SLOWBRO], TrainerSlot.TRAINER, true, p => { // Tera Ice Slowbro/G-Slowbro p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.ICE_BEAM)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.ICE_BEAM)) { // Check if Ice Beam is in the moveset, if not, replace the third move with Ice Beam. p.moveset[2] = new PokemonMove(MoveId.ICE_BEAM); } @@ -2967,7 +2998,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.STEELIX], TrainerSlot.TRAINER, true, p => { // Tera Fighting Steelix p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.BODY_PRESS)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.BODY_PRESS)) { // Check if Body Press is in the moveset, if not, replace the third move with Body Press. p.moveset[2] = new PokemonMove(MoveId.BODY_PRESS); } @@ -2992,7 +3023,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.ARBOK, SpeciesId.WEEZING], TrainerSlot.TRAINER, true, p => { // Tera Ghost Arbok/Weezing p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.TERA_BLAST)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.TERA_BLAST)) { // Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast. p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST); } @@ -3018,7 +3049,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.GYARADOS, SpeciesId.AERODACTYL], TrainerSlot.TRAINER, true, p => { // Tera Dragon Gyarados/Aerodactyl p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.TERA_BLAST)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.TERA_BLAST)) { // Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast. p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST); } @@ -3079,7 +3110,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.GENGAR], TrainerSlot.TRAINER, true, p => { // Tera Dark Gengar p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.DARK_PULSE)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.DARK_PULSE)) { // Check if Dark Pulse is in the moveset, if not, replace the third move with Dark Pulse. p.moveset[2] = new PokemonMove(MoveId.DARK_PULSE); } @@ -3163,7 +3194,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.DHELMISE], TrainerSlot.TRAINER, true, p => { // Tera Dragon Dhelmise p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.TERA_BLAST)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.TERA_BLAST)) { // Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast. p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST); } @@ -3193,7 +3224,7 @@ export const trainerConfigs: TrainerConfigs = { p.setBoss(true, 2); p.abilityIndex = 1; // Sniper p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.X_SCISSOR)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.X_SCISSOR)) { // Check if X-Scissor is in the moveset, if not, replace the third move with X-Scissor. p.moveset[2] = new PokemonMove(MoveId.X_SCISSOR); } @@ -3232,7 +3263,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.STEELIX, SpeciesId.LOPUNNY], TrainerSlot.TRAINER, true, p => { // Tera Fire Steelix/Lopunny p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.TERA_BLAST)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.TERA_BLAST)) { // Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast. p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST); } @@ -3375,7 +3406,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.CERULEDGE], TrainerSlot.TRAINER, true, p => { // Tera Steel Ceruledge p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.IRON_HEAD)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.IRON_HEAD)) { // Check if Iron Head is in the moveset, if not, replace the third move with Iron Head. p.moveset[2] = new PokemonMove(MoveId.IRON_HEAD); } @@ -3413,7 +3444,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.INCINEROAR], TrainerSlot.TRAINER, true, p => { // Tera Fighting Incineroar p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.CROSS_CHOP)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.CROSS_CHOP)) { // Check if Cross Chop is in the moveset, if not, replace the third move with Cross Chop. p.moveset[2] = new PokemonMove(MoveId.CROSS_CHOP); } @@ -3486,7 +3517,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.DECIDUEYE], TrainerSlot.TRAINER, true, p => { // Tera Flying Decidueye p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.BRAVE_BIRD)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.BRAVE_BIRD)) { // Check if Brave Bird is in the moveset, if not, replace the third move with Brave Bird. p.moveset[2] = new PokemonMove(MoveId.BRAVE_BIRD); } @@ -3511,7 +3542,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.TOXICROAK], TrainerSlot.TRAINER, true, p => { // Tera Dark Toxicroak p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.SUCKER_PUNCH)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.SUCKER_PUNCH)) { // Check if Sucker Punch is in the moveset, if not, replace the third move with Sucker Punch. p.moveset[2] = new PokemonMove(MoveId.SUCKER_PUNCH); } @@ -3536,7 +3567,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.EISCUE], TrainerSlot.TRAINER, true, p => { // Tera Water Eiscue p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.LIQUIDATION)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.LIQUIDATION)) { // Check if Liquidation is in the moveset, if not, replace the third move with Liquidation. p.moveset[2] = new PokemonMove(MoveId.LIQUIDATION); } @@ -3598,7 +3629,7 @@ export const trainerConfigs: TrainerConfigs = { // Tera Dragon Torkoal p.abilityIndex = 1; // Drought p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.TERA_BLAST)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.TERA_BLAST)) { // Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast. p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST); } @@ -3695,7 +3726,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.EXEGGUTOR], TrainerSlot.TRAINER, true, p => { // Tera Fire Exeggutor p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.TERA_BLAST)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.TERA_BLAST)) { // Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast. p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST); } @@ -3705,7 +3736,7 @@ export const trainerConfigs: TrainerConfigs = { 3, getRandomPartyMemberFunc([SpeciesId.TALONFLAME], TrainerSlot.TRAINER, true, p => { p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.SUNNY_DAY)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.SUNNY_DAY)) { // Check if Sunny Day is in the moveset, if not, replace the third move with Sunny Day. p.moveset[2] = new PokemonMove(MoveId.SUNNY_DAY); } @@ -3728,7 +3759,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.REUNICLUS], TrainerSlot.TRAINER, true, p => { // Tera Steel Reuniclus p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.FLASH_CANNON)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.FLASH_CANNON)) { // Check if Flash Cannon is in the moveset, if not, replace the third move with Flash Cannon. p.moveset[2] = new PokemonMove(MoveId.FLASH_CANNON); } @@ -3756,7 +3787,7 @@ export const trainerConfigs: TrainerConfigs = { // Tera Fairy Excadrill p.setBoss(true, 2); p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.TERA_BLAST)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.TERA_BLAST)) { // Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast. p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST); } @@ -3771,7 +3802,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.SCEPTILE], TrainerSlot.TRAINER, true, p => { // Tera Dragon Sceptile p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.DUAL_CHOP)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.DUAL_CHOP)) { // Check if Dual Chop is in the moveset, if not, replace the third move with Dual Chop. p.moveset[2] = new PokemonMove(MoveId.DUAL_CHOP); } @@ -3841,7 +3872,7 @@ export const trainerConfigs: TrainerConfigs = { p.formIndex = 1; // Partner Pikachu p.gender = Gender.MALE; p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.VOLT_TACKLE)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.VOLT_TACKLE)) { // Check if Volt Tackle is in the moveset, if not, replace the first move with Volt Tackle. p.moveset[0] = new PokemonMove(MoveId.VOLT_TACKLE); } @@ -4072,7 +4103,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.KELDEO], TrainerSlot.TRAINER, true, p => { p.pokeball = PokeballType.ROGUE_BALL; p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.SECRET_SWORD)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.SECRET_SWORD)) { // Check if Secret Sword is in the moveset, if not, replace the third move with Secret Sword. p.moveset[2] = new PokemonMove(MoveId.SECRET_SWORD); } @@ -4401,7 +4432,7 @@ export const trainerConfigs: TrainerConfigs = { 5, getRandomPartyMemberFunc([SpeciesId.KINGAMBIT], TrainerSlot.TRAINER, true, p => { p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.TERA_BLAST)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.TERA_BLAST)) { // Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast. p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST); } @@ -4480,7 +4511,7 @@ export const trainerConfigs: TrainerConfigs = { 4, getRandomPartyMemberFunc([SpeciesId.TERAPAGOS], TrainerSlot.TRAINER, true, p => { p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.TERA_STARSTORM)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.TERA_STARSTORM)) { // Check if Tera Starstorm is in the moveset, if not, replace the first move with Tera Starstorm. p.moveset[0] = new PokemonMove(MoveId.TERA_STARSTORM); } @@ -4494,7 +4525,7 @@ export const trainerConfigs: TrainerConfigs = { p.setBoss(true, 2); p.teraType = PokemonType.FIGHTING; p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.TERA_BLAST)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.TERA_BLAST)) { // Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast. p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST); } @@ -5054,7 +5085,7 @@ export const trainerConfigs: TrainerConfigs = { 2, getRandomPartyMemberFunc([SpeciesId.HONCHKROW], TrainerSlot.TRAINER, true, p => { p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.SUCKER_PUNCH)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.SUCKER_PUNCH)) { // Check if Sucker Punch is in the moveset, if not, replace the third move with Sucker Punch. p.moveset[2] = new PokemonMove(MoveId.SUCKER_PUNCH); } @@ -5517,7 +5548,7 @@ export const trainerConfigs: TrainerConfigs = { p.formIndex = randSeedInt(18); // Random Silvally Form p.generateAndPopulateMoveset(); p.pokeball = PokeballType.ROGUE_BALL; - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.MULTI_ATTACK)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.MULTI_ATTACK)) { // Check if Multi Attack is in the moveset, if not, replace the first move with Multi Attack. p.moveset[0] = new PokemonMove(MoveId.MULTI_ATTACK); } @@ -5590,7 +5621,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.GOLISOPOD], TrainerSlot.TRAINER, true, p => { p.setBoss(true, 2); p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.FIRST_IMPRESSION)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.FIRST_IMPRESSION)) { // Check if First Impression is in the moveset, if not, replace the third move with First Impression. p.moveset[2] = new PokemonMove(MoveId.FIRST_IMPRESSION); p.gender = Gender.MALE; @@ -5607,7 +5638,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.GOLISOPOD], TrainerSlot.TRAINER, true, p => { p.setBoss(true, 2); p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.FIRST_IMPRESSION)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.FIRST_IMPRESSION)) { // Check if First Impression is in the moveset, if not, replace the third move with First Impression. p.moveset[2] = new PokemonMove(MoveId.FIRST_IMPRESSION); p.abilityIndex = 2; // Anticipation @@ -5643,7 +5674,7 @@ export const trainerConfigs: TrainerConfigs = { p.generateAndPopulateMoveset(); p.pokeball = PokeballType.ROGUE_BALL; p.formIndex = randSeedInt(4, 1); // Shock, Burn, Chill, or Douse Drive - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.TECHNO_BLAST)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.TECHNO_BLAST)) { // Check if Techno Blast is in the moveset, if not, replace the third move with Techno Blast. p.moveset[2] = new PokemonMove(MoveId.TECHNO_BLAST); } @@ -5778,7 +5809,7 @@ export const trainerConfigs: TrainerConfigs = { p.setBoss(true, 2); p.abilityIndex = 2; // Pixilate p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.HYPER_VOICE)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.HYPER_VOICE)) { // Check if Hyper Voice is in the moveset, if not, replace the second move with Hyper Voice. p.moveset[1] = new PokemonMove(MoveId.HYPER_VOICE); p.gender = Gender.FEMALE; @@ -5807,7 +5838,7 @@ export const trainerConfigs: TrainerConfigs = { p.setBoss(true, 2); p.abilityIndex = 2; // Pixilate p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.HYPER_VOICE)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.HYPER_VOICE)) { // Check if Hyper Voice is in the moveset, if not, replace the second move with Hyper Voice. p.moveset[1] = new PokemonMove(MoveId.HYPER_VOICE); p.gender = Gender.FEMALE; diff --git a/src/data/weather.ts b/src/data/weather.ts index 84a5e1ba4f8..49af505dc62 100644 --- a/src/data/weather.ts +++ b/src/data/weather.ts @@ -19,10 +19,12 @@ export interface SerializedWeather { export class Weather { public weatherType: WeatherType; public turnsLeft: number; + public maxDuration: number; - constructor(weatherType: WeatherType, turnsLeft?: number) { + constructor(weatherType: WeatherType, turnsLeft = 0, maxDuration: number = turnsLeft) { this.weatherType = weatherType; - this.turnsLeft = !this.isImmutable() ? turnsLeft || 0 : 0; + this.turnsLeft = this.isImmutable() ? 0 : turnsLeft; + this.maxDuration = this.isImmutable() ? 0 : maxDuration; } lapse(): boolean { diff --git a/src/dynamic-queue-manager.ts b/src/dynamic-queue-manager.ts new file mode 100644 index 00000000000..7c65a79d743 --- /dev/null +++ b/src/dynamic-queue-manager.ts @@ -0,0 +1,187 @@ +import type { DynamicPhase, PhaseConditionFunc, PhaseString } from "#app/@types/phase-types"; +import type { PokemonMove } from "#app/data/moves/pokemon-move"; +import type { Pokemon } from "#app/field/pokemon"; +import type { Phase } from "#app/phase"; +import type { MovePhase } from "#app/phases/move-phase"; +import { MovePhasePriorityQueue } from "#app/queues/move-phase-priority-queue"; +import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-queue"; +import { PostSummonPhasePriorityQueue } from "#app/queues/post-summon-phase-priority-queue"; +import type { PriorityQueue } from "#app/queues/priority-queue"; +import type { BattlerIndex } from "#enums/battler-index"; +import type { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; + +// TODO: might be easier to define which phases should be dynamic instead +/** All phases which have defined a `getPokemon` method but should not be sorted dynamically */ +const nonDynamicPokemonPhases: readonly PhaseString[] = [ + "SummonPhase", + "CommandPhase", + "LearnMovePhase", + "MoveEffectPhase", + "MoveEndPhase", + "FaintPhase", + "DamageAnimPhase", + "VictoryPhase", + "PokemonHealPhase", + "WeatherEffectPhase", + "ShowAbilityPhase", + "HideAbilityPhase", + "ExpPhase", + "ShowPartyExpBarPhase", + "HidePartyExpBarPhase", +] as const; + +/** + * The dynamic queue manager holds priority queues for phases which are queued as dynamic. + * + * Dynamic phases are generally those which hold a pokemon and are unshifted, not pushed. \ + * Queues work by sorting their entries in speed order (and possibly with more complex ordering) before each time a phase is popped. + * + * As the holder, this structure is also used to access and modify queued phases. + * This is mostly used in redirection, cancellation, etc. of {@linkcode MovePhase}s. + */ +export class DynamicQueueManager { + /** Maps phase types to their corresponding queues */ + private readonly dynamicPhaseMap: Map>; + + constructor() { + this.dynamicPhaseMap = new Map(); + // PostSummon and Move phases have specialized queues + this.dynamicPhaseMap.set("PostSummonPhase", new PostSummonPhasePriorityQueue()); + this.dynamicPhaseMap.set("MovePhase", new MovePhasePriorityQueue()); + } + + /** Removes all phases from the manager */ + public clearQueues(): void { + for (const queue of this.dynamicPhaseMap.values()) { + queue.clear(); + } + } + + /** + * Adds a new phase to the manager and creates the priority queue for it if one does not exist. + * @param phase - The {@linkcode Phase} to add + * @returns `true` if the phase was added, or `false` if it is not dynamic + */ + public queueDynamicPhase(phase: T): boolean { + if (!this.isDynamicPhase(phase)) { + return false; + } + + if (!this.dynamicPhaseMap.has(phase.phaseName)) { + // TS can't figure out that T is dynamic at this point, but it does know that `typeof phase` is + this.dynamicPhaseMap.set(phase.phaseName, new PokemonPhasePriorityQueue()); + } + this.dynamicPhaseMap.get(phase.phaseName)?.push(phase); + return true; + } + + /** + * Returns the highest-priority (generally by speed) {@linkcode Phase} of the specified type + * @param type - The {@linkcode PhaseString | type} to pop + * @returns The popped {@linkcode Phase}, or `undefined` if none of the specified type exist + */ + public popNextPhase(type: PhaseString): Phase | undefined { + return this.dynamicPhaseMap.get(type)?.pop(); + } + + /** + * Determines if there is a queued dynamic {@linkcode Phase} meeting the conditions + * @param type - The {@linkcode PhaseString | type} of phase to search for + * @param condition - An optional {@linkcode PhaseConditionFunc} to add conditions to the search + * @returns Whether a matching phase exists + */ + public exists(type: T, condition?: PhaseConditionFunc): boolean { + return !!this.dynamicPhaseMap.get(type)?.has(condition); + } + + /** + * Finds and removes a single queued {@linkcode Phase} + * @param type - The {@linkcode PhaseString | type} of phase to search for + * @param phaseFilter - A {@linkcode PhaseConditionFunc} to specify conditions for the phase + * @returns Whether a removal occurred + */ + public removePhase(type: T, condition?: PhaseConditionFunc): boolean { + return !!this.dynamicPhaseMap.get(type)?.remove(condition); + } + + /** + * Sets the timing modifier of a move (i.e. to force it first or last) + * @param condition - A {@linkcode PhaseConditionFunc} to specify conditions for the move + * @param modifier - The {@linkcode MovePhaseTimingModifier} to switch the move to + */ + public setMoveTimingModifier(condition: PhaseConditionFunc<"MovePhase">, modifier: MovePhaseTimingModifier): void { + this.getMovePhaseQueue().setTimingModifier(condition, modifier); + } + + /** + * Finds the {@linkcode MovePhase} meeting the condition and changes its move + * @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function + * @param move - The {@linkcode PokemonMove | move} to use in replacement + */ + public setMoveForPhase(condition: PhaseConditionFunc<"MovePhase">, move: PokemonMove): void { + this.getMovePhaseQueue().setMoveForPhase(condition, move); + } + + /** + * Redirects moves which were targeted at a {@linkcode Pokemon} that has been removed + * @param removedPokemon - The removed {@linkcode Pokemon} + * @param allyPokemon - The ally of the removed pokemon + */ + public redirectMoves(removedPokemon: Pokemon, allyPokemon: Pokemon): void { + this.getMovePhaseQueue().redirectMoves(removedPokemon, allyPokemon); + } + + /** + * Finds a {@linkcode MovePhase} meeting the condition + * @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function + * @returns The MovePhase, or `undefined` if it does not exist + */ + public getMovePhase(condition: PhaseConditionFunc<"MovePhase">): MovePhase | undefined { + return this.getMovePhaseQueue().find(condition); + } + + /** + * Finds and cancels a {@linkcode MovePhase} meeting the condition + * @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function + */ + public cancelMovePhase(condition: PhaseConditionFunc<"MovePhase">): void { + this.getMovePhaseQueue().cancelMove(condition); + } + + /** + * Sets the move order to a static array rather than a dynamic queue + * @param order - The order of {@linkcode BattlerIndex}s + */ + public setMoveOrder(order: BattlerIndex[]): void { + this.getMovePhaseQueue().setMoveOrder(order); + } + + /** + * @returns An in-order array of {@linkcode Pokemon}, representing the turn order as played out in the most recent turn + */ + public getLastTurnOrder(): Pokemon[] { + return this.getMovePhaseQueue().getTurnOrder(); + } + + /** Clears the stored `Move` turn order */ + public clearLastTurnOrder(): void { + this.getMovePhaseQueue().clearTurnOrder(); + } + + /** Internal helper to get the {@linkcode MovePhasePriorityQueue} */ + private getMovePhaseQueue(): MovePhasePriorityQueue { + return this.dynamicPhaseMap.get("MovePhase") as MovePhasePriorityQueue; + } + + /** + * Internal helper to determine if a phase is dynamic. + * @param phase - The {@linkcode Phase} to check + * @returns Whether `phase` is dynamic + * @privateRemarks + * Currently, this checks that `phase` has a `getPokemon` method + * and is not blacklisted in `nonDynamicPokemonPhases`. + */ + private isDynamicPhase(phase: Phase): phase is DynamicPhase { + return typeof (phase as any).getPokemon === "function" && !nonDynamicPokemonPhases.includes(phase.phaseName); + } +} diff --git a/src/enums/arena-tag-side.ts b/src/enums/arena-tag-side.ts index 5f25a74ab36..50741751fbb 100644 --- a/src/enums/arena-tag-side.ts +++ b/src/enums/arena-tag-side.ts @@ -1,3 +1,4 @@ +// TODO: rename to something else (this isn't used only for arena tags) export enum ArenaTagSide { BOTH, PLAYER, diff --git a/src/enums/arena-tag-type.ts b/src/enums/arena-tag-type.ts index 30f053b98bd..717845cf2d9 100644 --- a/src/enums/arena-tag-type.ts +++ b/src/enums/arena-tag-type.ts @@ -34,4 +34,5 @@ export enum ArenaTagType { GRASS_WATER_PLEDGE = "GRASS_WATER_PLEDGE", FAIRY_LOCK = "FAIRY_LOCK", NEUTRALIZING_GAS = "NEUTRALIZING_GAS", + PENDING_HEAL = "PENDING_HEAL", } diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index 6d9d2dd4a92..4f0ac491e8b 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -94,4 +94,6 @@ export enum BattlerTagType { ENDURE_TOKEN = "ENDURE_TOKEN", POWDER = "POWDER", MAGIC_COAT = "MAGIC_COAT", + SUPREME_OVERLORD = "SUPREME_OVERLORD", + BYPASS_SPEED = "BYPASS_SPEED", } diff --git a/src/enums/dynamic-phase-type.ts b/src/enums/dynamic-phase-type.ts deleted file mode 100644 index 3146b136dac..00000000000 --- a/src/enums/dynamic-phase-type.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Enum representation of the phase types held by implementations of {@linkcode PhasePriorityQueue}. - */ -// TODO: We currently assume these are in order -export enum DynamicPhaseType { - POST_SUMMON, -} diff --git a/src/enums/move-flags.ts b/src/enums/move-flags.ts index e639a1eb190..acd73f897e7 100644 --- a/src/enums/move-flags.ts +++ b/src/enums/move-flags.ts @@ -29,7 +29,7 @@ export enum MoveFlags { SLICING_MOVE = 1 << 8, /** * Indicates a move should be affected by {@linkcode AbilityId.RECKLESS} - * @see {@linkcode Move.recklessMove()} + * @see {@linkcode Move.recklessMove} */ RECKLESS_MOVE = 1 << 9, /** Indicates a move should be affected by {@linkcode AbilityId.BULLETPROOF} */ diff --git a/src/enums/move-phase-timing-modifier.ts b/src/enums/move-phase-timing-modifier.ts new file mode 100644 index 00000000000..a452d37e7ff --- /dev/null +++ b/src/enums/move-phase-timing-modifier.ts @@ -0,0 +1,16 @@ +import type { ObjectValues } from "#types/type-helpers"; + +/** + * Enum representing modifiers for the timing of MovePhases. + * + * @remarks + * This system is entirely independent of and takes precedence over move priority + */ +export const MovePhaseTimingModifier = Object.freeze({ + /** Used when moves go last regardless of speed and priority (i.e. Quash) */ + LAST: 0, + NORMAL: 1, + /** Used to trigger moves immediately (i.e. ones that were called through Instruct). */ + FIRST: 2, +}); +export type MovePhaseTimingModifier = ObjectValues; diff --git a/src/events/arena.ts b/src/events/arena.ts index 5415b8eb026..9f818a36c89 100644 --- a/src/events/arena.ts +++ b/src/events/arena.ts @@ -16,56 +16,47 @@ export enum ArenaEventType { TAG_REMOVED = "onTagRemoved", } -/** - * Base container class for all {@linkcode ArenaEventType} events - * @extends Event - */ +/** Base container class for all {@linkcode ArenaEventType} events */ export class ArenaEvent extends Event { /** The total duration of the {@linkcode ArenaEventType} */ public duration: number; - constructor(eventType: ArenaEventType, duration: number) { + /** The maximum duration of the {@linkcode ArenaEventType} */ + public maxDuration: number; + constructor(eventType: ArenaEventType, duration: number, maxDuration: number = duration) { super(eventType); this.duration = duration; + this.maxDuration = maxDuration; } } -/** - * Container class for {@linkcode ArenaEventType.WEATHER_CHANGED} events - * @extends ArenaEvent - */ +/** Container class for {@linkcode ArenaEventType.WEATHER_CHANGED} events */ export class WeatherChangedEvent extends ArenaEvent { /** The {@linkcode WeatherType} being overridden */ public oldWeatherType: WeatherType; /** The {@linkcode WeatherType} being set */ public newWeatherType: WeatherType; - constructor(oldWeatherType: WeatherType, newWeatherType: WeatherType, duration: number) { - super(ArenaEventType.WEATHER_CHANGED, duration); + constructor(oldWeatherType: WeatherType, newWeatherType: WeatherType, duration: number, maxDuration?: number) { + super(ArenaEventType.WEATHER_CHANGED, duration, maxDuration); this.oldWeatherType = oldWeatherType; this.newWeatherType = newWeatherType; } } -/** - * Container class for {@linkcode ArenaEventType.TERRAIN_CHANGED} events - * @extends ArenaEvent - */ +/** Container class for {@linkcode ArenaEventType.TERRAIN_CHANGED} events */ export class TerrainChangedEvent extends ArenaEvent { /** The {@linkcode TerrainType} being overridden */ public oldTerrainType: TerrainType; /** The {@linkcode TerrainType} being set */ public newTerrainType: TerrainType; - constructor(oldTerrainType: TerrainType, newTerrainType: TerrainType, duration: number) { - super(ArenaEventType.TERRAIN_CHANGED, duration); + constructor(oldTerrainType: TerrainType, newTerrainType: TerrainType, duration: number, maxDuration?: number) { + super(ArenaEventType.TERRAIN_CHANGED, duration, maxDuration); this.oldTerrainType = oldTerrainType; this.newTerrainType = newTerrainType; } } -/** - * Container class for {@linkcode ArenaEventType.TAG_ADDED} events - * @extends ArenaEvent - */ +/** Container class for {@linkcode ArenaEventType.TAG_ADDED} events */ export class TagAddedEvent extends ArenaEvent { /** The {@linkcode ArenaTagType} being added */ public arenaTagType: ArenaTagType; @@ -80,10 +71,11 @@ export class TagAddedEvent extends ArenaEvent { arenaTagType: ArenaTagType, arenaTagSide: ArenaTagSide, duration: number, + maxDuration?: number, arenaTagLayers?: number, arenaTagMaxLayers?: number, ) { - super(ArenaEventType.TAG_ADDED, duration); + super(ArenaEventType.TAG_ADDED, duration, maxDuration); this.arenaTagType = arenaTagType; this.arenaTagSide = arenaTagSide; @@ -91,10 +83,7 @@ export class TagAddedEvent extends ArenaEvent { this.arenaTagMaxLayers = arenaTagMaxLayers!; // TODO: is this bang correct? } } -/** - * Container class for {@linkcode ArenaEventType.TAG_REMOVED} events - * @extends ArenaEvent - */ +/** Container class for {@linkcode ArenaEventType.TAG_REMOVED} events */ export class TagRemovedEvent extends ArenaEvent { /** The {@linkcode ArenaTagType} being removed */ public arenaTagType: ArenaTagType; diff --git a/src/events/battle-scene.ts b/src/events/battle-scene.ts index 29aee1053cd..fdee812f54e 100644 --- a/src/events/battle-scene.ts +++ b/src/events/battle-scene.ts @@ -43,10 +43,7 @@ export enum BattleSceneEventType { NEW_ARENA = "onNewArena", } -/** - * Container class for {@linkcode BattleSceneEventType.CANDY_UPGRADE_NOTIFICATION_CHANGED} events - * @extends Event - */ +/** Container class for {@linkcode BattleSceneEventType.CANDY_UPGRADE_NOTIFICATION_CHANGED} events */ export class CandyUpgradeNotificationChangedEvent extends Event { /** The new value the setting was changed to */ public newValue: number; @@ -57,10 +54,7 @@ export class CandyUpgradeNotificationChangedEvent extends Event { } } -/** - * Container class for {@linkcode BattleSceneEventType.MOVE_USED} events - * @extends Event - */ +/** Container class for {@linkcode BattleSceneEventType.MOVE_USED} events */ export class MoveUsedEvent extends Event { /** The ID of the {@linkcode Pokemon} that used the {@linkcode Move} */ public pokemonId: number; @@ -76,10 +70,7 @@ export class MoveUsedEvent extends Event { this.ppUsed = ppUsed; } } -/** - * Container class for {@linkcode BattleSceneEventType.BERRY_USED} events - * @extends Event - */ +/** Container class for {@linkcode BattleSceneEventType.BERRY_USED} events */ export class BerryUsedEvent extends Event { /** The {@linkcode BerryModifier} being used */ public berryModifier: BerryModifier; @@ -90,28 +81,19 @@ export class BerryUsedEvent extends Event { } } -/** - * Container class for {@linkcode BattleSceneEventType.ENCOUNTER_PHASE} events - * @extends Event - */ +/** Container class for {@linkcode BattleSceneEventType.ENCOUNTER_PHASE} events */ export class EncounterPhaseEvent extends Event { constructor() { super(BattleSceneEventType.ENCOUNTER_PHASE); } } -/** - * Container class for {@linkcode BattleSceneEventType.TURN_INIT} events - * @extends Event - */ +/** Container class for {@linkcode BattleSceneEventType.TURN_INIT} events */ export class TurnInitEvent extends Event { constructor() { super(BattleSceneEventType.TURN_INIT); } } -/** - * Container class for {@linkcode BattleSceneEventType.TURN_END} events - * @extends Event - */ +/** Container class for {@linkcode BattleSceneEventType.TURN_END} events */ export class TurnEndEvent extends Event { /** The amount of turns in the current battle */ public turnCount: number; @@ -121,10 +103,7 @@ export class TurnEndEvent extends Event { this.turnCount = turnCount; } } -/** - * Container class for {@linkcode BattleSceneEventType.NEW_ARENA} events - * @extends Event - */ +/** Container class for {@linkcode BattleSceneEventType.NEW_ARENA} events */ export class NewArenaEvent extends Event { constructor() { super(BattleSceneEventType.NEW_ARENA); diff --git a/src/events/egg.ts b/src/events/egg.ts index a0c26c82883..30a005d192f 100644 --- a/src/events/egg.ts +++ b/src/events/egg.ts @@ -8,7 +8,6 @@ export enum EggEventType { /** * Container class for {@linkcode EggEventType.EGG_COUNT_CHANGED} events - * @extends Event */ export class EggCountChangedEvent extends Event { /** The updated egg count. */ diff --git a/src/field/arena.ts b/src/field/arena.ts index ed23e42b119..3e214ff1ea7 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -36,7 +36,7 @@ import type { Pokemon } from "#field/pokemon"; import { FieldEffectModifier } from "#modifiers/modifier"; import type { Move } from "#moves/move"; import type { AbstractConstructor } from "#types/type-helpers"; -import { type Constructor, isNullOrUndefined, NumberHolder, randSeedInt } from "#utils/common"; +import { type Constructor, NumberHolder, randSeedInt } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; export class Arena { @@ -339,12 +339,12 @@ export class Arena { const weatherDuration = new NumberHolder(0); - if (!isNullOrUndefined(user)) { + if (user != null) { weatherDuration.value = 5; globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, weatherDuration); } - this.weather = weather ? new Weather(weather, weatherDuration.value) : null; + this.weather = weather ? new Weather(weather, weatherDuration.value, weatherDuration.value) : null; this.eventTarget.dispatchEvent( new WeatherChangedEvent(oldWeatherType, this.weather?.weatherType!, this.weather?.turnsLeft!), ); // TODO: is this bang correct? @@ -371,9 +371,15 @@ export class Arena { /** * Function to trigger all weather based form changes + * @param source - The Pokemon causing the changes by removing itself from the field */ - triggerWeatherBasedFormChanges(): void { + triggerWeatherBasedFormChanges(source?: Pokemon): void { globalScene.getField(true).forEach(p => { + // TODO - This is a bandaid. Abilities leaving the field needs a better approach than + // calling this method for every switch out that happens + if (p === source) { + return; + } const isCastformWithForecast = p.hasAbility(AbilityId.FORECAST) && p.species.speciesId === SpeciesId.CASTFORM; const isCherrimWithFlowerGift = p.hasAbility(AbilityId.FLOWER_GIFT) && p.species.speciesId === SpeciesId.CHERRIM; @@ -420,12 +426,12 @@ export class Arena { const terrainDuration = new NumberHolder(0); - if (!isNullOrUndefined(user)) { + if (user != null) { terrainDuration.value = 5; globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, terrainDuration); } - this.terrain = terrain ? new Terrain(terrain, terrainDuration.value) : null; + this.terrain = terrain ? new Terrain(terrain, terrainDuration.value, terrainDuration.value) : null; this.eventTarget.dispatchEvent( new TerrainChangedEvent(oldTerrainType, this.terrain?.terrainType!, this.terrain?.turnsLeft!), @@ -705,8 +711,8 @@ export class Arena { existingTag.onOverlap(this, globalScene.getPokemonById(sourceId)); if (existingTag instanceof EntryHazardTag) { - const { tagType, side, turnCount, layers, maxLayers } = existingTag as EntryHazardTag; - this.eventTarget.dispatchEvent(new TagAddedEvent(tagType, side, turnCount, layers, maxLayers)); + const { tagType, side, turnCount, maxDuration, layers, maxLayers } = existingTag as EntryHazardTag; + this.eventTarget.dispatchEvent(new TagAddedEvent(tagType, side, turnCount, maxDuration, layers, maxLayers)); } return false; @@ -721,7 +727,7 @@ export class Arena { const { layers = 0, maxLayers = 0 } = newTag instanceof EntryHazardTag ? newTag : {}; this.eventTarget.dispatchEvent( - new TagAddedEvent(newTag.tagType, newTag.side, newTag.turnCount, layers, maxLayers), + new TagAddedEvent(newTag.tagType, newTag.side, newTag.turnCount, newTag.maxDuration, layers, maxLayers), ); } @@ -732,14 +738,12 @@ export class Arena { * Attempt to get a tag from the Arena via {@linkcode getTagOnSide} that applies to both sides * @param tagType - The {@linkcode ArenaTagType} to retrieve * @returns The existing {@linkcode ArenaTag}, or `undefined` if not present. - * @overload */ getTag(tagType: ArenaTagType): ArenaTag | undefined; /** * Attempt to get a tag from the Arena via {@linkcode getTagOnSide} that applies to both sides * @param tagType - The constructor of the {@linkcode ArenaTag} to retrieve * @returns The existing {@linkcode ArenaTag}, or `undefined` if not present. - * @overload */ getTag(tagType: Constructor | AbstractConstructor): T | undefined; getTag(tagType: ArenaTagType | Constructor | AbstractConstructor): ArenaTag | undefined { diff --git a/src/field/mystery-encounter-intro.ts b/src/field/mystery-encounter-intro.ts index cf0a0f30529..4b4a234251a 100644 --- a/src/field/mystery-encounter-intro.ts +++ b/src/field/mystery-encounter-intro.ts @@ -5,7 +5,6 @@ import { getSpriteKeysFromSpecies } from "#mystery-encounters/encounter-pokemon- import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; import { loadPokemonVariantAssets } from "#sprites/pokemon-sprite"; import type { Variant } from "#sprites/variant"; -import { isNullOrUndefined } from "#utils/common"; import type { GameObjects } from "phaser"; type PlayAnimationConfig = Phaser.Types.Animations.PlayAnimationConfig; @@ -98,7 +97,7 @@ export class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Container { ...config, }; - if (!isNullOrUndefined(result.species)) { + if (result.species != null) { const keys = getSpriteKeysFromSpecies(result.species, undefined, undefined, result.isShiny, result.variant); result.spriteKey = keys.spriteKey; result.fileRoot = keys.fileRoot; @@ -205,12 +204,12 @@ export class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Container { n++; } - if (!isNullOrUndefined(pokemonShinySparkle)) { + if (pokemonShinySparkle != null) { // Offset the sparkle to match the Pokemon's position pokemonShinySparkle.setPosition(sprite.x, sprite.y); } - if (!isNullOrUndefined(alpha)) { + if (alpha != null) { sprite.setAlpha(alpha); tintSprite.setAlpha(alpha); } @@ -234,7 +233,7 @@ export class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Container { this.spriteConfigs.forEach(config => { if (config.isPokemon) { globalScene.loadPokemonAtlas(config.spriteKey, config.fileRoot); - if (config.isShiny && !isNullOrUndefined(config.variant)) { + if (config.isShiny && config.variant != null) { shinyPromises.push(loadPokemonVariantAssets(config.spriteKey, config.fileRoot, config.variant)); } } else if (config.isItem) { diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 3f6aefcf955..f490a5a15e6 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -1,5 +1,6 @@ import type { Ability, PreAttackModifyDamageAbAttrParams } from "#abilities/ability"; import { applyAbAttrs, applyOnGainAbAttrs, applyOnLoseAbAttrs } from "#abilities/apply-ab-attrs"; +import { generateMoveset } from "#app/ai/ai-moveset-gen"; import type { AnySound, BattleScene } from "#app/battle-scene"; import { PLAYER_PARTY_MAX_SIZE, RARE_CANDY_FRIENDSHIP_CAP } from "#app/constants"; import { timedEventManager } from "#app/global-event-manager"; @@ -18,7 +19,7 @@ import type { LevelMoves } from "#balance/pokemon-level-moves"; import { EVOLVE_MOVE, RELEARN_MOVE } from "#balance/pokemon-level-moves"; import { BASE_HIDDEN_ABILITY_CHANCE, BASE_SHINY_CHANCE, SHINY_EPIC_CHANCE, SHINY_VARIANT_CHANCE } from "#balance/rates"; import { getStarterValueFriendshipCap, speciesStarterCosts } from "#balance/starters"; -import { reverseCompatibleTms, tmPoolTiers, tmSpecies } from "#balance/tms"; +import { reverseCompatibleTms, tmSpecies } from "#balance/tms"; import type { SuppressAbilitiesTag } from "#data/arena-tag"; import { NoCritTag, WeakenMoveScreenTag } from "#data/arena-tag"; import { @@ -81,7 +82,6 @@ import { DexAttr } from "#enums/dex-attr"; import { FieldPosition } from "#enums/field-position"; import { HitResult } from "#enums/hit-result"; import { LearnMoveSituation } from "#enums/learn-move-situation"; -import { ModifierTier } from "#enums/modifier-tier"; import { MoveCategory } from "#enums/move-category"; import { MoveFlags } from "#enums/move-flags"; import { MoveId } from "#enums/move-id"; @@ -130,26 +130,30 @@ import { TempStatStageBoosterModifier, } from "#modifiers/modifier"; import { applyMoveAttrs } from "#moves/apply-attrs"; -// biome-ignore lint/correctness/noUnusedImports: TSDoc -import type { Move, VariableMoveTypeChartAttr } from "#moves/move"; +import type { Move } from "#moves/move"; import { getMoveTargets } from "#moves/move-utils"; import { PokemonMove } from "#moves/pokemon-move"; import { loadMoveAnimations } from "#sprites/pokemon-asset-loader"; import type { Variant } from "#sprites/variant"; import { populateVariantColors, variantColorCache, variantData } from "#sprites/variant"; import { achvs } from "#system/achv"; -import type { StarterDataEntry, StarterMoveset } from "#system/game-data"; import type { PokemonData } from "#system/pokemon-data"; import { RibbonData } from "#system/ribbons/ribbon-data"; import { awardRibbonsToSpeciesLine } from "#system/ribbons/ribbon-methods"; import type { AbAttrMap, AbAttrString, TypeMultiplierAbAttrParams } from "#types/ability-types"; +import type { + getAttackDamageParams, + getAttackTypeEffectivenessParams, + getBaseDamageParams, +} from "#types/damage-params"; import type { DamageCalculationResult, DamageResult } from "#types/damage-result"; import type { IllusionData } from "#types/illusion-data"; +import type { StarterDataEntry, StarterMoveset } from "#types/save-data"; import type { TurnMove } from "#types/turn-move"; import { BattleInfo } from "#ui/battle-info"; import { EnemyBattleInfo } from "#ui/enemy-battle-info"; -import type { PartyOption } from "#ui/handlers/party-ui-handler"; -import { PartyUiHandler, PartyUiMode } from "#ui/handlers/party-ui-handler"; +import type { PartyOption } from "#ui/party-ui-handler"; +import { PartyUiHandler, PartyUiMode } from "#ui/party-ui-handler"; import { PlayerBattleInfo } from "#ui/player-battle-info"; import { applyChallenges } from "#utils/challenge-utils"; import { @@ -160,7 +164,6 @@ import { fixedInt, getIvsFromId, isBetween, - isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, @@ -178,68 +181,6 @@ import i18next from "i18next"; import Phaser from "phaser"; import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; -/** Base typeclass for damage parameter methods, used for DRY */ -type damageParams = { - /** The attacking {@linkcode Pokemon} */ - source: Pokemon; - /** The move used in the attack */ - move: Move; - /** The move's {@linkcode MoveCategory} after variable-category effects are applied */ - moveCategory: MoveCategory; - /** If `true`, ignores this Pokemon's defensive ability effects */ - ignoreAbility?: boolean; - /** If `true`, ignores the attacking Pokemon's ability effects */ - ignoreSourceAbility?: boolean; - /** If `true`, ignores the ally Pokemon's ability effects */ - ignoreAllyAbility?: boolean; - /** If `true`, ignores the ability effects of the attacking pokemon's ally */ - ignoreSourceAllyAbility?: boolean; - /** If `true`, calculates damage for a critical hit */ - isCritical?: boolean; - /** If `true`, suppresses changes to game state during the calculation */ - simulated?: boolean; - /** If defined, used in place of calculated effectiveness values */ - effectiveness?: number; -}; - -/** Type for the parameters of {@linkcode Pokemon#getBaseDamage | getBaseDamage} */ -type getBaseDamageParams = Omit; - -/** Type for the parameters of {@linkcode Pokemon#getAttackDamage | getAttackDamage} */ -type getAttackDamageParams = Omit; - -/** - * Type for the parameters of {@linkcode Pokemon.getAttackTypeEffectiveness | getAttackTypeEffectiveness} - * and associated helper functions. - */ -type getAttackTypeEffectivenessParams = { - /** - * The {@linkcode Pokemon} using the move, used to check the user's Scrappy and Mind's Eye abilities - * and the effects of Foresight/Odor Sleuth. - */ - source?: Pokemon; - /** - * If `true`, ignores the effect of strong winds (used by anticipation, forewarn, stealth rocks) - * @defaultValue `false` - */ - ignoreStrongWinds?: boolean; - /** - * If `true`, will prevent changes to game state during calculations. - * @defaultValue `false` - */ - simulated?: boolean; - /** - * The {@linkcode Move} whose type effectiveness is being checked. - * Used for applying {@linkcode VariableMoveTypeChartAttr} - */ - move?: Move; - /** - * Whether to consider this Pokemon's {@linkcode IllusionData | illusion} when determining types. - * @defaultValue `false` - */ - useIllusion?: boolean; -}; - export abstract class Pokemon extends Phaser.GameObjects.Container { /** * This pokemon's {@link https://bulbapedia.bulbagarden.net/wiki/Personality_value | Personality value/PID}, @@ -276,20 +217,46 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @todo Make private */ public status: Status | null; + /** + * The Pokémon's current friendship value, ranging from 0 to 255. + * @see {@link https://bulbapedia.bulbagarden.net/wiki/Friendship} + */ public friendship: number; + /** + * The level at which this Pokémon was met + * @remarks + * Primarily used for displaying in the summary screen + */ public metLevel: number; + /** + * The ID of the biome this Pokémon was met in + * @remarks + * Primarily used for display in the summary screen. + */ public metBiome: BiomeId | -1; + // TODO: figure out why this is used and document it (seems only to be read for getting the Pokémon's egg moves) public metSpecies: SpeciesId; + /** The wave index at which this Pokémon was met/encountered */ public metWave: number; public luck: number; public pauseEvolutions: boolean; public pokerus: boolean; + /** + * Indicates whether this Pokémon has left or is about to leave the field + * @remarks + * When `true` on a Wild Pokemon, this indicates it is about to flee. + */ public switchOutStatus = false; public evoCounter: number; + /** The type this Pokémon turns into when Terastallized */ public teraType: PokemonType; + /** Whether this Pokémon is currently Terastallized */ public isTerastallized: boolean; + /** The set of Types that have been boosted by this Pokémon's Stellar Terastallization. */ public stellarTypesBoosted: PokemonType[]; + // TODO: Create a fusionData class / interface and move all fusion-related fields there, exposed via getters + /** If this Pokémon is a fusion, the species it is fused with; `null` if not a fusion */ public fusionSpecies: PokemonSpecies | null; public fusionFormIndex: number; public fusionAbilityIndex: number; @@ -321,11 +288,18 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** Used by Mystery Encounters to execute pokemon-specific logic (such as stat boosts) at start of battle */ public mysteryEncounterBattleEffects?: (pokemon: Pokemon) => void; + /** The position of this Pokémon on the field */ public fieldPosition: FieldPosition; public maskEnabled: boolean; public maskSprite: Phaser.GameObjects.Sprite | null; + /** + * The set of all TMs that have been used on this Pokémon + * + * @remarks + * Used to allow re-learning TM moves via, e.g., the Memory Mushroom + */ public usedTMs: MoveId[]; private shinySparkle: Phaser.GameObjects.Sprite; @@ -550,7 +524,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { abstract initBattleInfo(): void; - isOnField(): boolean { + public isOnField(): boolean { if (!globalScene) { return false; } @@ -602,7 +576,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return this.isAllowedInBattle() && (!onField || this.isOnField()); } - getDexAttr(): bigint { + public getDexAttr(): bigint { let ret = 0n; if (this.gender !== Gender.GENDERLESS) { ret |= this.gender !== Gender.FEMALE ? DexAttr.MALE : DexAttr.FEMALE; @@ -616,9 +590,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Sets the Pokemon's name. Only called when loading a Pokemon so this function needs to be called when * initializing hardcoded Pokemon or else it will not display the form index name properly. - * @returns n/a */ - generateName(): void { + public generateName(): void { if (!this.fusionSpecies) { this.name = this.species.getName(this.formIndex); return; @@ -872,11 +845,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * Gracefully handle errors loading a variant sprite. Log if it fails and attempt to fall back on * non-experimental sprites before giving up. * - * @param cacheKey the cache key for the variant color sprite - * @param attemptedSpritePath the sprite path that failed to load - * @param useExpSprite was the attempted sprite experimental - * @param battleSpritePath the filename of the sprite - * @param optionalParams any additional params to log + * @param cacheKey - The cache key for the variant color sprite + * @param attemptedSpritePath - The sprite path that failed to load + * @param useExpSprite - Whether the attempted sprite was experimental + * @param battleSpritePath - The filename of the sprite + * @param optionalParams - Any additional params to log */ async fallbackVariantColor( cacheKey: string, @@ -919,7 +892,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return this.fallbackVariantColor(cacheKey, spritePath, useExpSprite, battleSpritePath, error); }) .then(c => { - if (!isNullOrUndefined(c)) { + if (c != null) { variantColorCache[cacheKey] = c; } }); @@ -942,6 +915,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return this.fusionSpecies.forms[this.fusionFormIndex].formKey; } + //#region Atlas and sprite ID methods // TODO: Add more documentation for all these attributes. // They may be all similar, but what each one actually _does_ is quite unclear at first glance @@ -1070,6 +1044,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { fusionVariant, ); } + //#endregion Atlas and sprite ID methods /** * Return this Pokemon's {@linkcode PokemonSpeciesForm | SpeciesForm}. @@ -1099,7 +1074,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * (such as by the effects of {@linkcode MoveId.TRANSFORM} or {@linkcode AbilityId.IMPOSTER}. * @returns Whether this Pokemon is currently transformed. */ - isTransformed(): boolean { + public isTransformed(): boolean { return this.summonData.speciesForm !== null; } @@ -1108,7 +1083,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @param target - The {@linkcode Pokemon} being transformed into * @returns Whether this Pokemon can transform into `target`. */ - canTransformInto(target: Pokemon): boolean { + public canTransformInto(target: Pokemon): boolean { return !( // Neither pokemon can be already transformed ( @@ -1129,7 +1104,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @param useIllusion - Whether to consider the species of this Pokemon's illusion; default `false` * @returns The {@linkcode PokemonSpeciesForm} of this Pokemon's fusion counterpart. */ - getFusionSpeciesForm(ignoreOverride = false, useIllusion = false): PokemonSpeciesForm { + public getFusionSpeciesForm(ignoreOverride = false, useIllusion = false): PokemonSpeciesForm { const fusionSpecies: PokemonSpecies = useIllusion && this.summonData.illusion ? this.summonData.illusion.fusionSpecies! : this.fusionSpecies!; const fusionFormIndex = @@ -1174,7 +1149,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** Resets the pokemon's field sprite properties, including position, alpha, and scale */ - resetSprite(): void { + public resetSprite(): void { // Resetting properties should not be shown on the field this.setVisible(false); @@ -1225,9 +1200,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Attempts to animate a given {@linkcode Phaser.GameObjects.Sprite} * @see {@linkcode Phaser.GameObjects.Sprite.play} - * @param sprite {@linkcode Phaser.GameObjects.Sprite} to animate - * @param tintSprite {@linkcode Phaser.GameObjects.Sprite} placed on top of the sprite to add a color tint - * @param animConfig {@linkcode String} to pass to {@linkcode Phaser.GameObjects.Sprite.play} + * @param sprite - Sprite to animate + * @param tintSprite - Sprite placed on top of the sprite to add a color tint + * @param animConfig - String to pass to the sprite's {@linkcode Phaser.GameObjects.Sprite.play | play} method * @returns true if the sprite was able to be animated */ tryPlaySprite(sprite: Phaser.GameObjects.Sprite, tintSprite: Phaser.GameObjects.Sprite, key: string): boolean { @@ -1292,7 +1267,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } } - setFieldPosition(fieldPosition: FieldPosition, duration?: number): Promise { + /** + * Set the field position of this Pokémon + * @param fieldPosition - The new field position + * @param duration - How long the transition should take, in milliseconds; if `0` or `undefined`, the position is changed instantly + */ + public setFieldPosition(fieldPosition: FieldPosition, duration?: number): Promise { return new Promise(resolve => { if (fieldPosition === this.fieldPosition) { resolve(); @@ -1508,7 +1488,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } const ally = this.getAlly(); - if (!isNullOrUndefined(ally)) { + if (ally != null) { applyAbAttrs("AllyStatMultiplierAbAttr", { pokemon: ally, stat, @@ -1642,10 +1622,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return baseStats; } + // TODO: Convert this into a getter getNature(): Nature { return this.customPokemonData.nature !== -1 ? this.customPokemonData.nature : this.nature; } + // TODO: Convert this into a setter OR just add a listener for calculateStats... setNature(nature: Nature): void { this.nature = nature; this.calculateStats(); @@ -1656,7 +1638,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { this.calculateStats(); } - generateNature(naturePool?: Nature[]): void { + /** + * Randomly generate and set this Pokémon's nature + * @param naturePool - An optional array of Natures to choose from. If not provided, all natures will be considered. + */ + private generateNature(naturePool?: Nature[]): void { if (naturePool === undefined) { naturePool = getEnumValues(Nature); } @@ -1664,10 +1650,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { this.setNature(nature); } + // TODO: Convert this into a getter isFullHp(): boolean { return this.hp >= this.getMaxHp(); } + // TODO: Convert this into a getter getMaxHp(): number { return this.getStat(Stat.HP); } @@ -1677,6 +1665,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return this.getMaxHp() - this.hp; } + /** + * Return the ratio of this Pokémon's current HP to its maximum HP + * @param precise - Whether to return the exact HP ratio (e.g. `0.54321`), or one rounded to the nearest %; default `false` + * @returns The current HP ratio + */ getHpRatio(precise = false): number { return precise ? this.hp / this.getMaxHp() : Math.round((this.hp / this.getMaxHp()) * 100) / 100; } @@ -1691,7 +1684,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { if (useIllusion && this.summonData.illusion) { return this.summonData.illusion.gender; } - if (!ignoreOverride && !isNullOrUndefined(this.summonData.gender)) { + if (!ignoreOverride && this.summonData.gender != null) { return this.summonData.gender; } return this.gender; @@ -1707,25 +1700,37 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { if (useIllusion && this.summonData.illusion?.fusionGender) { return this.summonData.illusion.fusionGender; } - if (!ignoreOverride && !isNullOrUndefined(this.summonData.fusionGender)) { + if (!ignoreOverride && this.summonData.fusionGender != null) { return this.summonData.fusionGender; } return this.fusionGender; } /** - * Check whether this Pokemon is shiny. - * @param useIllusion - Whether to consider this pokemon's illusion if present; default `false` + * Check whether this Pokémon is shiny, including its fusion species + * + * @param useIllusion - Whether to consider an active illusion * @returns Whether this Pokemon is shiny + * @see {@linkcode isBaseShiny} */ isShiny(useIllusion = false): boolean { return this.isBaseShiny(useIllusion) || this.isFusionShiny(useIllusion); } + /** + * Get whether this Pokémon's _base_ species is shiny + * @param useIllusion - Whether to consider an active illusion; default `false` + * @returns Whether the pokemon is shiny + */ isBaseShiny(useIllusion = false) { return useIllusion ? (this.summonData.illusion?.shiny ?? this.shiny) : this.shiny; } + /** + * Get whether this Pokémon's _fusion_ species is shiny + * @param useIllusion - Whether to consider an active illusion; default `true` + * @returns Whether this Pokémon's fusion species is shiny, or `false` if there is no fusion + */ isFusionShiny(useIllusion = false) { if (!this.isFusion(useIllusion)) { return false; @@ -1735,7 +1740,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Check whether this Pokemon is doubly shiny (both normal and fusion are shiny). - * @param useIllusion - Whether to consider this pokemon's illusion if present; default `false` + * @param useIllusion - Whether to consider an active illusion; default `false` * @returns Whether this pokemon's base and fusion counterparts are both shiny. */ isDoubleShiny(useIllusion = false): boolean { @@ -1743,10 +1748,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Return this Pokemon's {@linkcode Variant | shiny variant}. + * Return this Pokemon's shiny variant. * If a fusion, returns the maximum of the two variants. * Only meaningful if this pokemon is actually shiny. - * @param useIllusion - Whether to consider this pokemon's illusion if present; default `false` + * @param useIllusion - Whether to consider an active illusion; default `false` * @returns The shiny variant of this Pokemon. */ getVariant(useIllusion = false): Variant { @@ -1761,6 +1766,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Return the base pokemon's variant. Equivalent to {@linkcode getVariant} if this pokemon is not a fusion. + * @param useIllusion - Whether to consider an active illusion; default `false` * @returns The shiny variant of this Pokemon's base species. */ getBaseVariant(useIllusion = false): Variant { @@ -1769,10 +1775,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Return the fused pokemon's variant. + * Get the shiny variant of this Pokémon's _fusion_ species * * @remarks * Always returns `0` if the pokemon is not a fusion. + * @param useIllusion - Whether to consider an active illusion * @returns The shiny variant of this pokemon's fusion species. */ getFusionVariant(useIllusion = false): Variant { @@ -1793,7 +1800,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Return whether this {@linkcode Pokemon} is currently fused with anything. - * @param useIllusion - Whether to consider this pokemon's illusion if present; default `false` + * @param useIllusion - Whether to consider an active illusion; default `false` * @returns Whether this Pokemon is currently fused with another species. */ isFusion(useIllusion = false): boolean { @@ -1802,7 +1809,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Return this {@linkcode Pokemon}'s name. - * @param useIllusion - Whether to consider this pokemon's illusion if present; default `false` + * @param useIllusion - Whether to consider an active illusion; default `false` * @returns This Pokemon's name. * @see {@linkcode getNameToRender} - gets this Pokemon's display name. */ @@ -1826,7 +1833,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @returns Whether this Pokemon has this species as either its base or fusion counterpart. */ hasSpecies(species: SpeciesId, formKey?: string): boolean { - if (isNullOrUndefined(formKey)) { + if (formKey == null) { return this.species.speciesId === species || this.fusionSpecies?.speciesId === species; } @@ -1908,8 +1915,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @param includeTeraType - Whether to use this Pokemon's tera type if Terastallized; default `false` * @param forDefend - Whether this Pokemon is currently receiving an attack; default `false` * @param ignoreOverride - Whether to ignore any overrides caused by {@linkcode MoveId.TRANSFORM | Transform}; default `false` - * @param useIllusion - Whether to consider this Pokemon's illusion if present; default `false` - * @returns An array of {@linkcode PokemonType}s corresponding to this Pokemon's typing (real or percieved). + * @param useIllusion - Whether to consider an active illusion; default `false` + * @returns An array of {@linkcode PokemonType}s corresponding to this Pokemon's typing (real or perceived). */ public getTypes( includeTeraType = false, @@ -1974,7 +1981,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { secondType = fusionType1; } - if (secondType === PokemonType.UNKNOWN && isNullOrUndefined(fusionType2)) { + if (secondType === PokemonType.UNKNOWN && fusionType2 == null) { // If second pokemon was monotype and shared its primary type secondType = customTypes @@ -2057,12 +2064,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return allAbilities[Overrides.ENEMY_ABILITY_OVERRIDE]; } if (this.isFusion()) { - if (!isNullOrUndefined(this.fusionCustomPokemonData?.ability) && this.fusionCustomPokemonData.ability !== -1) { + if (this.fusionCustomPokemonData?.ability != null && this.fusionCustomPokemonData.ability !== -1) { return allAbilities[this.fusionCustomPokemonData.ability]; } return allAbilities[this.getFusionSpeciesForm(ignoreOverride).getAbility(this.fusionAbilityIndex)]; } - if (!isNullOrUndefined(this.customPokemonData.ability) && this.customPokemonData.ability !== -1) { + if (this.customPokemonData.ability != null && this.customPokemonData.ability !== -1) { return allAbilities[this.customPokemonData.ability]; } let abilityId = this.getSpeciesForm(ignoreOverride).getAbility(this.abilityIndex); @@ -2086,7 +2093,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { if (Overrides.ENEMY_PASSIVE_ABILITY_OVERRIDE && this.isEnemy()) { return allAbilities[Overrides.ENEMY_PASSIVE_ABILITY_OVERRIDE]; } - if (!isNullOrUndefined(this.customPokemonData.passive) && this.customPokemonData.passive !== -1) { + if (this.customPokemonData.passive != null && this.customPokemonData.passive !== -1) { return allAbilities[this.customPokemonData.passive]; } @@ -2117,10 +2124,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Sets the {@linkcode Pokemon}'s ability and activates it if it normally activates on summon + * Set this Pokémon's temporary ability, activating it if it normally activates on summon * * Also clears primal weather if it is from the ability being changed - * @param ability New Ability + * @param ability - The temporary ability to set + * @param passive - Whether to set the passive ability instead of the non-passive one; default `false` */ public setTempAbility(ability: Ability, passive = false): void { applyOnLoseAbAttrs({ pokemon: this, passive }); @@ -2180,11 +2188,13 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Checks whether an ability of a pokemon can be currently applied. This should rarely be + * Check whether this Pokémon can apply its current ability + * + * @remarks + * This should rarely be * directly called, as {@linkcode hasAbility} and {@linkcode hasAbilityWithAttr} already call this. - * @see {@linkcode hasAbility} {@linkcode hasAbilityWithAttr} Intended ways to check abilities in most cases - * @param passive If true, check if passive can be applied instead of non-passive - * @returns `true` if the ability can be applied + * @param passive - Whether to check the passive (`true`) or non-passive (`false`) ability; default `false` + * @returns Whether the ability can be applied */ public canApplyAbility(passive = false): boolean { if (passive && !this.hasPassive()) { @@ -2270,7 +2280,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { public getWeight(): number { const autotomizedTag = this.getTag(AutotomizedTag); let weightRemoved = 0; - if (!isNullOrUndefined(autotomizedTag)) { + if (autotomizedTag != null) { weightRemoved = 100 * autotomizedTag.autotomizeCount; } const minWeight = 0.1; @@ -2399,14 +2409,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Calculates the effectiveness of a move against the Pokémon. - * This includes modifiers from move and ability attributes. - * @param source {@linkcode Pokemon} The attacking Pokémon. - * @param move {@linkcode Move} The move being used by the attacking Pokémon. - * @param ignoreAbility Whether to ignore abilities that might affect type effectiveness or immunity (defaults to `false`). - * @param simulated Whether to apply abilities via simulated calls (defaults to `true`) - * @param cancelled {@linkcode BooleanHolder} Stores whether the move was cancelled by a non-type-based immunity. - * @param useIllusion - Whether we want the attack move effectiveness on the illusion or not + * Calculate the effectiveness of the move against this Pokémon, including + * modifiers from move and ability attributes + * @param source - The attacking Pokémon. + * @param move - The move being used by the attacking Pokémon. + * @param ignoreAbility - Whether to ignore abilities that might affect type effectiveness or immunity (defaults to `false`). + * @param simulated - Whether to apply abilities via simulated calls (defaults to `true`) + * @param cancelled - Stores whether the move was cancelled by a non-type-based immunity. + * @param useIllusion - Whether to consider an active illusion * @returns The type damage multiplier, indicating the effectiveness of the move */ getMoveEffectiveness( @@ -2417,7 +2427,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { cancelled?: BooleanHolder, useIllusion = false, ): TypeDamageMultiplier { - if (!isNullOrUndefined(this.turnData?.moveEffectiveness)) { + if (this.turnData?.moveEffectiveness != null) { return this.turnData?.moveEffectiveness; } @@ -2459,14 +2469,15 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { if (!cancelledHolder.value) { const defendingSidePlayField = this.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); - defendingSidePlayField.forEach(p => + defendingSidePlayField.forEach((p: (typeof defendingSidePlayField)[0]) => { applyAbAttrs("FieldPriorityMoveImmunityAbAttr", { pokemon: p, opponent: source, move, cancelled: cancelledHolder, - }), - ); + simulated, + }); + }); } } @@ -2487,7 +2498,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { typeMultiplier.value = 0; } - return (!cancelledHolder.value ? typeMultiplier.value : 0) as TypeDamageMultiplier; + return (cancelledHolder.value ? 0 : typeMultiplier.value) as TypeDamageMultiplier; } /** @@ -2603,10 +2614,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Computes the given Pokemon's matchup score against this Pokemon. + * Compute the given Pokémon's matchup score against this Pokémon + * @remarks * In most cases, this score ranges from near-zero to 16, but the maximum possible matchup score is 64. - * @param opponent {@linkcode Pokemon} The Pokemon to compare this Pokemon against - * @returns A score value based on how favorable this Pokemon is when fighting the given Pokemon + * @param opponent - The Pokemon to compare this Pokémon against + * @returns A score value based on how favorable this Pokémon is when fighting the given Pokémon */ getMatchupScore(opponent: Pokemon): number { const enemyTypes = opponent.getTypes(true, false, false, true); @@ -2692,6 +2704,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return (atkScore + defScore) * Math.min(hpDiffRatio, 1); } + /** + * Get the first evolution this Pokémon meets the conditions to evolve into + * @remarks + * Fusion evolutions are also considered. + * @returns The evolution this pokemon can currently evolve into, or `null` if it cannot evolve + */ getEvolution(): SpeciesFormEvolution | null { if (pokemonEvolutions.hasOwnProperty(this.species.speciesId)) { const evolutions = pokemonEvolutions[this.species.speciesId]; @@ -2707,7 +2725,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { e => new FusionSpeciesFormEvolution(this.species.speciesId, e), ); for (const fe of fusionEvolutions) { - if (fe.validate(this)) { + if (fe.validate(this, true)) { return fe; } } @@ -2717,12 +2735,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Gets all level up moves in a given range for a particular pokemon. - * @param {number} startingLevel Don't include moves below this level - * @param {boolean} includeEvolutionMoves Whether to include evolution moves - * @param {boolean} simulateEvolutionChain Whether to include moves from prior evolutions - * @param {boolean} includeRelearnerMoves Whether to include moves that would require a relearner. Note the move relearner inherently allows evolution moves - * @returns {LevelMoves} A list of moves and the levels they can be learned at + * Get all level up moves in a given range for a particular pokemon. + * @param startingLevel - Don't include moves below this level + * @param includeEvolutionMoves - Whether to include evolution moves + * @param simulateEvolutionChain - Whether to include moves from prior evolutions + * @param includeRelearnerMoves - Whether to include moves that would require a relearner. Note the move relearner inherently allows evolution moves + * @returns A list of moves and the levels they can be learned at */ getLevelMoves( startingLevel?: number, @@ -2846,21 +2864,23 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * This causes problems when there are intentional duplicates (i.e. Smeargle with Sketch) */ if (levelMoves) { - this.getUniqueMoves(levelMoves, ret); + Pokemon.getUniqueMoves(levelMoves, ret); } return ret; } /** - * Helper function for getLevelMoves. + * Helper function for getLevelMoves + * + * @remarks * Finds all non-duplicate items from the input, and pushes them into the output. * Two items count as duplicate if they have the same Move, regardless of level. * - * @param levelMoves the input array to search for non-duplicates from - * @param ret the output array to be pushed into. + * @param levelMoves - The input array to search for non-duplicates from + * @param ret - The output array to be pushed into. */ - private getUniqueMoves(levelMoves: LevelMoves, ret: LevelMoves): void { + private static getUniqueMoves(levelMoves: LevelMoves, ret: LevelMoves): void { const uniqueMoves: MoveId[] = []; for (const lm of levelMoves) { if (!uniqueMoves.find(m => m === lm[1])) { @@ -2872,13 +2892,17 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Get a list of all egg moves - * * @returns list of egg moves */ getEggMoves(): MoveId[] | undefined { return speciesEggMoves[this.getSpeciesForm().getRootSpeciesId()]; } + /** + * Create a new {@linkcode PokemonMove} and set it to the specified move index in this Pokémon's moveset. + * @param moveIndex - The index of the move to set + * @param moveId - The ID of the move to set + */ setMove(moveIndex: number, moveId: MoveId): void { if (moveId === MoveId.NONE) { return; @@ -2891,14 +2915,16 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Function that tries to set a Pokemon shiny based on the trainer's trainer ID and secret ID. + * Attempt to set the Pokémon's shininess based on the trainer's trainer ID and secret ID. * Endless Pokemon in the end biome are unable to be set to shiny * + * @remarks + * * The exact mechanic is that it calculates E as the XOR of the player's trainer ID and secret ID. * F is calculated as the XOR of the first 16 bits of the Pokemon's ID with the last 16 bits. * The XOR of E and F are then compared to the {@linkcode shinyThreshold} (or {@linkcode thresholdOverride} if set) to see whether or not to generate a shiny. * The base shiny odds are {@linkcode BASE_SHINY_CHANCE} / 65536 - * @param thresholdOverride number that is divided by 2^16 (65536) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm) + * @param thresholdOverride - number that is divided by 2^16 (65536) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm) * @returns true if the Pokemon has been set as a shiny, false otherwise */ trySetShiny(thresholdOverride?: number): boolean { @@ -2939,14 +2965,16 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Function that tries to set a Pokemon shiny based on seed. + * Tries to set a Pokémon's shininess based on seed + * + * @remarks * For manual use only, usually to roll a Pokemon's shiny chance a second time. * If it rolls shiny, or if it's already shiny, also sets a random variant and give the Pokemon the associated luck. * * The base shiny odds are {@linkcode BASE_SHINY_CHANCE} / `65536` - * @param thresholdOverride number that is divided by `2^16` (`65536`) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm) - * @param applyModifiersToOverride If {@linkcode thresholdOverride} is set and this is true, will apply Shiny Charm and event modifiers to {@linkcode thresholdOverride} - * @returns `true` if the Pokemon has been set as a shiny, `false` otherwise + * @param thresholdOverride - number that is divided by `2^16` (`65536`) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm) + * @param applyModifiersToOverride - If {@linkcode thresholdOverride} is set and this is true, will apply Shiny Charm and event modifiers to {@linkcode thresholdOverride} + * @returns Whether this Pokémon was set to shiny */ public trySetShinySeed(thresholdOverride?: number, applyModifiersToOverride?: boolean): boolean { if (!this.shiny) { @@ -2972,11 +3000,18 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Generates a shiny variant - * @returns `0-2`, with the following probabilities: - * - Has a 10% chance of returning `2` (epic variant) - * - Has a 30% chance of returning `1` (rare variant) - * - Has a 60% chance of returning `0` (basic shiny) + * Randomly generate a shiny variant + * + * @remarks + * Variants are returned with the following probabilities: + * + * | Variant | Description | Probability | + * |---------|----------------|-------------| + * | 0 | Basic shiny | 60% | + * | 1 | Rare variant | 30% | + * | 2 | Epic variant | 10% | + * + * @returns The randomly chosen shiny variant */ protected generateShinyVariant(): Variant { const formIndex: number = this.formIndex; @@ -3012,12 +3047,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Function that tries to set a Pokemon to have its hidden ability based on seed, if it exists. + * Function that tries to set this Pokemon to have its hidden ability based on seed, if it exists. + * + * @remarks * For manual use only, usually to roll a Pokemon's hidden ability chance a second time. * * The base hidden ability odds are {@linkcode BASE_HIDDEN_ABILITY_CHANCE} / `65536` - * @param thresholdOverride number that is divided by `2^16` (`65536`) to get the HA chance, overrides {@linkcode haThreshold} if set (bypassing HA rate modifiers such as Ability Charm) - * @param applyModifiersToOverride If {@linkcode thresholdOverride} is set and this is true, will apply Ability Charm to {@linkcode thresholdOverride} + * @param thresholdOverride - number that is divided by `2^16` (`65536`) to get the HA chance, overrides {@linkcode haThreshold} if set (bypassing HA rate modifiers such as Ability Charm) + * @param applyModifiersToOverride - If {@linkcode thresholdOverride} is set and this is true, will apply Ability Charm to {@linkcode thresholdOverride} * @returns `true` if the Pokemon has been set to have its hidden ability, `false` otherwise */ public tryRerollHiddenAbilitySeed(thresholdOverride?: number, applyModifiersToOverride?: boolean): boolean { @@ -3036,6 +3073,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return this.abilityIndex === 2; } + /** + * Generate a fusion species and add it to this Pokémon + * @param forStarter - Whether this fusion is being generated for a starter Pokémon; default `false` + */ public generateFusionSpecies(forStarter?: boolean): void { const hiddenAbilityChance = new NumberHolder(BASE_HIDDEN_ABILITY_CHANCE); if (!this.hasTrainer()) { @@ -3102,6 +3143,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { this.generateName(); } + /** Remove the fusion species from this Pokémon */ public clearFusionSpecies(): void { this.fusionSpecies = null; this.fusionFormIndex = 0; @@ -3116,248 +3158,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { this.calculateStats(); } - /** Generates a semi-random moveset for a Pokemon */ + /** Generate a semi-random moveset for this Pokémon */ public generateAndPopulateMoveset(): void { - this.moveset = []; - let movePool: [MoveId, number][] = []; - const allLevelMoves = this.getLevelMoves(1, true, true, this.hasTrainer()); - if (!allLevelMoves) { - console.warn("Error encountered trying to generate moveset for:", this.species.name); - return; - } - - for (const levelMove of allLevelMoves) { - if (this.level < levelMove[0]) { - break; - } - let weight = levelMove[0] + 20; - // Evolution Moves - if (levelMove[0] === EVOLVE_MOVE) { - weight = 70; - } - // Assume level 1 moves with 80+ BP are "move reminder" moves and bump their weight. Trainers use actual relearn moves. - if ( - (levelMove[0] === 1 && allMoves[levelMove[1]].power >= 80) - || (levelMove[0] === RELEARN_MOVE && this.hasTrainer()) - ) { - weight = 60; - } - if (!movePool.some(m => m[0] === levelMove[1]) && !allMoves[levelMove[1]].name.endsWith(" (N)")) { - movePool.push([levelMove[1], weight]); - } - } - - if (this.hasTrainer()) { - const tms = Object.keys(tmSpecies); - for (const tm of tms) { - const moveId = Number.parseInt(tm) as MoveId; - let compatible = false; - for (const p of tmSpecies[tm]) { - if (Array.isArray(p)) { - if ( - p[0] === this.species.speciesId - || (this.fusionSpecies - && p[0] === this.fusionSpecies.speciesId - && p.slice(1).indexOf(this.species.forms[this.formIndex]) > -1) - ) { - compatible = true; - break; - } - } else if (p === this.species.speciesId || (this.fusionSpecies && p === this.fusionSpecies.speciesId)) { - compatible = true; - break; - } - } - if (compatible && !movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)")) { - if (tmPoolTiers[moveId] === ModifierTier.COMMON && this.level >= 15) { - movePool.push([moveId, 24]); - } else if (tmPoolTiers[moveId] === ModifierTier.GREAT && this.level >= 30) { - movePool.push([moveId, 28]); - } else if (tmPoolTiers[moveId] === ModifierTier.ULTRA && this.level >= 50) { - movePool.push([moveId, 34]); - } - } - } - - // No egg moves below level 60 - if (this.level >= 60) { - for (let i = 0; i < 3; i++) { - const moveId = speciesEggMoves[this.species.getRootSpeciesId()][i]; - if (!movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)")) { - movePool.push([moveId, 60]); - } - } - const moveId = speciesEggMoves[this.species.getRootSpeciesId()][3]; - // No rare egg moves before e4 - if ( - this.level >= 170 - && !movePool.some(m => m[0] === moveId) - && !allMoves[moveId].name.endsWith(" (N)") - && !this.isBoss() - ) { - movePool.push([moveId, 50]); - } - if (this.fusionSpecies) { - for (let i = 0; i < 3; i++) { - const moveId = speciesEggMoves[this.fusionSpecies.getRootSpeciesId()][i]; - if (!movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)")) { - movePool.push([moveId, 60]); - } - } - const moveId = speciesEggMoves[this.fusionSpecies.getRootSpeciesId()][3]; - // No rare egg moves before e4 - if ( - this.level >= 170 - && !movePool.some(m => m[0] === moveId) - && !allMoves[moveId].name.endsWith(" (N)") - && !this.isBoss() - ) { - movePool.push([moveId, 50]); - } - } - } - } - - // Bosses never get self ko moves or Pain Split - if (this.isBoss()) { - movePool = movePool.filter( - m => !allMoves[m[0]].hasAttr("SacrificialAttr") && !allMoves[m[0]].hasAttr("HpSplitAttr"), - ); - } - // No one gets Memento or Final Gambit - movePool = movePool.filter(m => !allMoves[m[0]].hasAttr("SacrificialAttrOnHit")); - if (this.hasTrainer()) { - // Trainers never get OHKO moves - movePool = movePool.filter(m => !allMoves[m[0]].hasAttr("OneHitKOAttr")); - // Half the weight of self KO moves - movePool = movePool.map(m => [m[0], m[1] * (allMoves[m[0]].hasAttr("SacrificialAttr") ? 0.5 : 1)]); - // Trainers get a weight bump to stat buffing moves - movePool = movePool.map(m => [ - m[0], - m[1] * (allMoves[m[0]].getAttrs("StatStageChangeAttr").some(a => a.stages > 1 && a.selfTarget) ? 1.25 : 1), - ]); - // Trainers get a weight decrease to multiturn moves - movePool = movePool.map(m => [ - m[0], - m[1] * (!!allMoves[m[0]].isChargingMove() || !!allMoves[m[0]].hasAttr("RechargeAttr") ? 0.7 : 1), - ]); - } - - // Weight towards higher power moves, by reducing the power of moves below the highest power. - // Caps max power at 90 to avoid something like hyper beam ruining the stats. - // This is a pretty soft weighting factor, although it is scaled with the weight multiplier. - const maxPower = Math.min( - movePool.reduce((v, m) => Math.max(allMoves[m[0]].calculateEffectivePower(), v), 40), - 90, - ); - movePool = movePool.map(m => [ - m[0], - m[1] - * (allMoves[m[0]].category === MoveCategory.STATUS - ? 1 - : Math.max(Math.min(allMoves[m[0]].calculateEffectivePower() / maxPower, 1), 0.5)), - ]); - - // Weight damaging moves against the lower stat. This uses a non-linear relationship. - // If the higher stat is 1 - 1.09x higher, no change. At higher stat ~1.38x lower stat, off-stat moves have half weight. - // One third weight at ~1.58x higher, one quarter weight at ~1.73x higher, one fifth at ~1.87x, and one tenth at ~2.35x higher. - const atk = this.getStat(Stat.ATK); - const spAtk = this.getStat(Stat.SPATK); - const worseCategory: MoveCategory = atk > spAtk ? MoveCategory.SPECIAL : MoveCategory.PHYSICAL; - const statRatio = worseCategory === MoveCategory.PHYSICAL ? atk / spAtk : spAtk / atk; - movePool = movePool.map(m => [ - m[0], - m[1] * (allMoves[m[0]].category === worseCategory ? Math.min(Math.pow(statRatio, 3) * 1.3, 1) : 1), - ]); - - /** The higher this is the more the game weights towards higher level moves. At `0` all moves are equal weight. */ - let weightMultiplier = 1.6; - if (this.isBoss()) { - weightMultiplier += 0.4; - } - const baseWeights: [MoveId, number][] = movePool.map(m => [ - m[0], - Math.ceil(Math.pow(m[1], weightMultiplier) * 100), - ]); - - // All Pokemon force a STAB move first - const stabMovePool = baseWeights.filter( - m => allMoves[m[0]].category !== MoveCategory.STATUS && this.isOfType(allMoves[m[0]].type), - ); - - if (stabMovePool.length > 0) { - const totalWeight = stabMovePool.reduce((v, m) => v + m[1], 0); - let rand = randSeedInt(totalWeight); - let index = 0; - while (rand > stabMovePool[index][1]) { - rand -= stabMovePool[index++][1]; - } - this.moveset.push(new PokemonMove(stabMovePool[index][0])); - } else { - // If there are no damaging STAB moves, just force a random damaging move - const attackMovePool = baseWeights.filter(m => allMoves[m[0]].category !== MoveCategory.STATUS); - if (attackMovePool.length > 0) { - const totalWeight = attackMovePool.reduce((v, m) => v + m[1], 0); - let rand = randSeedInt(totalWeight); - let index = 0; - while (rand > attackMovePool[index][1]) { - rand -= attackMovePool[index++][1]; - } - this.moveset.push(new PokemonMove(attackMovePool[index][0], 0, 0)); - } - } - - while (baseWeights.length > this.moveset.length && this.moveset.length < 4) { - if (this.hasTrainer()) { - // Sqrt the weight of any damaging moves with overlapping types. This is about a 0.05 - 0.1 multiplier. - // Other damaging moves 2x weight if 0-1 damaging moves, 0.5x if 2, 0.125x if 3. These weights get 20x if STAB. - // Status moves remain unchanged on weight, this encourages 1-2 - movePool = baseWeights - .filter( - m => - !this.moveset.some( - mo => - m[0] === mo.moveId - || (allMoves[m[0]].hasAttr("SacrificialAttr") && mo.getMove().hasAttr("SacrificialAttr")), // Only one self-KO move allowed - ), - ) - .map(m => { - let ret: number; - if ( - this.moveset.some( - mo => mo.getMove().category !== MoveCategory.STATUS && mo.getMove().type === allMoves[m[0]].type, - ) - ) { - ret = Math.ceil(Math.sqrt(m[1])); - } else if (allMoves[m[0]].category !== MoveCategory.STATUS) { - ret = Math.ceil( - (m[1] / Math.max(Math.pow(4, this.moveset.filter(mo => (mo.getMove().power ?? 0) > 1).length) / 8, 0.5)) - * (this.isOfType(allMoves[m[0]].type) ? 20 : 1), - ); - } else { - ret = m[1]; - } - return [m[0], ret]; - }); - } else { - // Non-trainer pokemon just use normal weights - movePool = baseWeights.filter( - m => - !this.moveset.some( - mo => - m[0] === mo.moveId - || (allMoves[m[0]].hasAttr("SacrificialAttr") && mo.getMove().hasAttr("SacrificialAttr")), // Only one self-KO move allowed - ), - ); - } - const totalWeight = movePool.reduce((v, m) => v + m[1], 0); - let rand = randSeedInt(totalWeight); - let index = 0; - while (rand > movePool[index][1]) { - rand -= movePool[index++][1]; - } - this.moveset.push(new PokemonMove(movePool[index][0])); - } + generateMoveset(this); // Trigger FormChange, except for enemy Pokemon during Mystery Encounters, to avoid crashes if ( @@ -3374,6 +3177,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return move?.isUsable(this, ignorePp) ?? false; } + /** Show this Pokémon's info panel */ showInfo(): void { if (!this.battleInfo.visible) { const otherBattleInfo = globalScene.fieldUI @@ -3402,7 +3206,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } } - hideInfo(): Promise { + /** Hide this Pokémon's info panel */ + async hideInfo(): Promise { return new Promise(resolve => { if (this.battleInfo?.visible) { globalScene.tweens.add({ @@ -3426,14 +3231,6 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { }); } - /** - * sets if the pokemon is switching out (if it's a enemy wild implies it's going to flee) - * @param status - boolean - */ - setSwitchOutStatus(status: boolean): void { - this.switchOutStatus = status; - } - updateInfo(instant?: boolean): Promise { return this.battleInfo.updateInfo(this, instant); } @@ -3444,8 +3241,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Adds experience to this PlayerPokemon, subject to wave based level caps. - * @param exp The amount of experience to add - * @param ignoreLevelCap Whether to ignore level caps when adding experience (defaults to false) + * @param exp - The amount of experience to add + * @param ignoreLevelCap - Whether to ignore level caps when adding experience; default `false` */ addExp(exp: number, ignoreLevelCap = false) { const maxExpLevel = globalScene.getMaxExpLevel(ignoreLevelCap); @@ -3462,8 +3259,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Compares if `this` and {@linkcode target} are on the same team. - * @param target the {@linkcode Pokemon} to compare against. + * Check whether the specified Pokémon is an opponent + * @param target - The {@linkcode Pokemon} to compare against * @returns `true` if the two pokemon are allies, `false` otherwise */ public isOpponent(target: Pokemon): boolean { @@ -3508,18 +3305,19 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Calculates the stat stage multiplier of the user against an opponent. + * Calculate the stat stage multiplier of the user against an opponent * - * Note that this does not apply to evasion or accuracy + * @remarks + * This does not apply to evasion or accuracy * @see {@linkcode getAccuracyMultiplier} * @param stat - The {@linkcode EffectiveStat} to calculate * @param opponent - The {@linkcode Pokemon} being targeted * @param move - The {@linkcode Move} being used - * @param ignoreOppAbility determines whether the effects of the opponent's abilities (i.e. Unaware) should be ignored (`false` by default) - * @param isCritical determines whether a critical hit has occurred or not (`false` by default) - * @param simulated determines whether effects are applied without altering game state (`true` by default) - * @param ignoreHeldItems determines whether this Pokemon's held items should be ignored during the stat calculation, default `false` - * @return the stat stage multiplier to be used for effective stat calculation + * @param ignoreOppAbility - determines whether the effects of the opponent's abilities (i.e. Unaware) should be ignored; default `false` + * @param isCritical - determines whether a critical hit has occurred or not; default `false` + * @param simulated - determines whether effects are applied without altering game state; default `true` + * @param ignoreHeldItems - determines whether this Pokemon's held items should be ignored during the stat calculation; default `false` + * @returns the stat stage multiplier to be used for effective stat calculation */ getStatStageMultiplier( stat: EffectiveStat, @@ -3575,8 +3373,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * This method considers various factors such as the user's accuracy level, the target's evasion level, * abilities, and modifiers to compute the final accuracy multiplier. * - * @param target {@linkcode Pokemon} - The target Pokémon against which the move is used. - * @param sourceMove {@linkcode Move} - The move being used by the user. + * @param target - The target Pokémon against which the move is used. + * @param sourceMove - The move being used by the user. * @returns The calculated accuracy multiplier. */ getAccuracyMultiplier(target: Pokemon, sourceMove: Move): number { @@ -3629,7 +3427,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { }); const ally = this.getAlly(); - if (!isNullOrUndefined(ally)) { + if (ally != null) { const ignore = this.hasAbilityWithAttr("MoveAbilityBypassAbAttr") || sourceMove.hasFlag(MoveFlags.IGNORE_ABILITIES); applyAbAttrs("AllyStatMultiplierAbAttr", { @@ -3655,15 +3453,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Calculates the base damage of the given move against this Pokemon when attacked by the given source. * Used during damage calculation and for Shell Side Arm's forecasting effect. - * @param source - The attacking {@linkcode Pokemon}. - * @param move - The {@linkcode Move} used in the attack. - * @param moveCategory - The move's {@linkcode MoveCategory} after variable-category effects are applied. - * @param ignoreAbility - If `true`, ignores this Pokemon's defensive ability effects (defaults to `false`). - * @param ignoreSourceAbility - If `true`, ignore's the attacking Pokemon's ability effects (defaults to `false`). - * @param ignoreAllyAbility - If `true`, ignores the ally Pokemon's ability effects (defaults to `false`). - * @param ignoreSourceAllyAbility - If `true`, ignores the attacking Pokemon's ally's ability effects (defaults to `false`). - * @param isCritical - if `true`, calculates effective stats as if the hit were critical (defaults to `false`). - * @param simulated - if `true`, suppresses changes to game state during calculation (defaults to `true`). + * @param __namedParameters.source - Needed for proper typedoc rendering * @returns The move's base damage against this Pokemon when used by the source Pokemon. */ getBaseDamage({ @@ -3781,15 +3571,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Calculates the damage of an attack made by another Pokemon against this Pokemon - * @param source {@linkcode Pokemon} the attacking Pokemon - * @param move The {@linkcode Move} used in the attack - * @param ignoreAbility If `true`, ignores this Pokemon's defensive ability effects - * @param ignoreSourceAbility If `true`, ignores the attacking Pokemon's ability effects - * @param ignoreAllyAbility If `true`, ignores the ally Pokemon's ability effects - * @param ignoreSourceAllyAbility If `true`, ignores the ability effects of the attacking pokemon's ally - * @param isCritical If `true`, calculates damage for a critical hit. - * @param simulated If `true`, suppresses changes to game state during the calculation. - * @param effectiveness If defined, used in place of calculated effectiveness values + * @param __namedParameters.source - Needed for proper typedoc rendering * @returns The {@linkcode DamageCalculationResult} */ getAttackDamage({ @@ -4045,7 +3827,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { const ally = this.getAlly(); /** Additionally apply friend guard damage reduction if ally has it. */ - if (globalScene.currentBattle.double && !isNullOrUndefined(ally) && ally.isActive(true)) { + if (globalScene.currentBattle.double && ally != null && ally.isActive(true)) { applyAbAttrs("AlliedFieldDamageReductionAbAttr", { ...abAttrParams, // Same parameters as before, except we are applying the ally's ability @@ -4090,7 +3872,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { */ getCriticalHitResult(source: Pokemon, move: Move): boolean { if (move.hasAttr("FixedDamageAttr")) { - // fixed damage moves (Dragon Rage, etc.) will nevet crit + // fixed damage moves (Dragon Rage, etc.) will never crit return false; } @@ -4118,12 +3900,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Called by damageAndUpdate() - * @param damage integer - * @param ignoreSegments boolean, not currently used - * @param preventEndure used to update damage if endure or sturdy - * @param ignoreFaintPhase flag on whether to add FaintPhase if pokemon after applying damage faints - * @returns integer representing damage dealt + * Submethod called by {@linkcode damageAndUpdate} to apply damage to this Pokemon and adjust its HP. + * @param damage - The damage to deal + * @param _ignoreSegments - Whether to ignore boss segments; default `false` + * @param preventEndure - Whether to allow the damage to bypass an Endure/Sturdy effect + * @param ignoreFaintPhase - Whether to ignore adding a FaintPhase if this damage causes a faint + * @returns The actual damage dealt */ damage(damage: number, _ignoreSegments = false, preventEndure = false, ignoreFaintPhase = false): number { if (this.isFainted()) { @@ -4151,15 +3933,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { damage = Math.min(damage, this.hp); this.hp = this.hp - damage; if (this.isFainted() && !ignoreFaintPhase) { - /** - * When adding the FaintPhase, want to toggle future unshiftPhase() and queueMessage() calls - * to appear before the FaintPhase (as FaintPhase will potentially end the encounter and add Phases such as - * GameOverPhase, VictoryPhase, etc.. that will interfere with anything else that happens during this MoveEffectPhase) - * - * Once the MoveEffectPhase is over (and calls it's .end() function, shiftPhase() will reset the PhaseQueueSplice via clearPhaseQueueSplice() ) - */ - globalScene.phaseManager.setPhaseQueueSplice(); - globalScene.phaseManager.unshiftNew("FaintPhase", this.getBattlerIndex(), preventEndure); + globalScene.phaseManager.queueFaintPhase(this.getBattlerIndex(), preventEndure); this.destroySubstitute(); this.lapseTag(BattlerTagType.COMMANDED); } @@ -4169,14 +3943,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Given the damage, adds a new DamagePhase and update HP values, etc. * - * Checks for 'Indirect' HitResults to account for Endure/Reviver Seed applying correctly - * @param damage integer - passed to damage() - * @param result an enum if it's super effective, not very, etc. - * @param isCritical boolean if move is a critical hit - * @param ignoreSegments boolean, passed to damage() and not used currently - * @param preventEndure boolean, ignore endure properties of pokemon, passed to damage() - * @param ignoreFaintPhase boolean to ignore adding a FaintPhase, passsed to damage() - * @returns integer of damage done + * @remarks + * Checks for {@linkcode HitResult.INDIRECT | Indirect} hits to account for Endure/Reviver Seed applying correctly + * @param damage - The damage to inflict on this Pokémon + * @param __namedParameters.source - Needed for proper typedoc rendering + * @returns Amount of damage actually done */ damageAndUpdate( damage: number, @@ -4187,10 +3958,17 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { ignoreFaintPhase = false, source, }: { + /** + * An enum if it's super effective, not very effective, etc; default {@linkcode HitResult.EFFECTIVE} + */ result?: DamageResult; + /** Whether the attack was a critical hit */ isCritical?: boolean; + /** Whether to ignore boss segments */ ignoreSegments?: boolean; + /** Whether to ignore adding a FaintPhase if this damage causes a faint; default `false` */ ignoreFaintPhase?: boolean; + /** The Pokémon inflicting the damage, or undefined if not caused by a Pokémon */ source?: Pokemon; } = {}, ): number { @@ -4207,11 +3985,6 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { damage = 0; } damage = this.damage(damage, ignoreSegments, isIndirectDamage, ignoreFaintPhase); - // Ensure the battle-info bar's HP is updated, though only if the battle info is visible - // TODO: When battle-info UI is refactored, make this only update the HP bar - if (this.battleInfo.visible) { - this.updateInfo(); - } // Damage amount may have changed, but needed to be queued before calling damage function damagePhase.updateAmount(damage); /** @@ -4224,17 +3997,25 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return damage; } - heal(amount: number): number { + /** + * Restore a specific amount of HP to this Pokémon + * @param amount - The amount of HP to restore + * @returns The true amount of HP restored; may be less than `amount` if `amount` would overheal + */ + public heal(amount: number): number { const healAmount = Math.min(amount, this.getMaxHp() - this.hp); this.hp += healAmount; return healAmount; } - isBossImmune(): boolean { + public isBossImmune(): boolean { return this.isBoss(); } - isMax(): boolean { + /** + * @returns Whether this Pokémon is in a Dynamax or Gigantamax form + */ + public isMax(): boolean { const maxForms = [ SpeciesFormKey.GIGANTAMAX, SpeciesFormKey.GIGANTAMAX_RAPID, @@ -4246,7 +4027,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { ); } - isMega(): boolean { + /** + * @returns Whether this Pokémon is in a Mega or Primal form + */ + public isMega(): boolean { const megaForms = [ SpeciesFormKey.MEGA, SpeciesFormKey.MEGA_X, @@ -4259,7 +4043,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { ); } - canAddTag(tagType: BattlerTagType): boolean { + /** + * Check whether a battler tag can be added to this Pokémon + * + * @param tagType - The tag to check + * @returns - Whether the tag can be added + * @see {@linkcode addTag} + */ + public canAddTag(tagType: BattlerTagType): boolean { if (this.getTag(tagType)) { return false; } @@ -4283,7 +4074,20 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return !cancelled.value; } - addTag(tagType: BattlerTagType, turnCount = 0, sourceMove?: MoveId, sourceId?: number): boolean { + /** + * Add a new {@linkcode BattlerTag} of the specified `tagType` + * + * @remarks + * Also ensures the tag is able to be applied, similar to {@linkcode canAddTag} + * + * @param tagType - The type of tag to add + * @param turnCount - The number of turns the tag should last; default `0` + * @param sourceMove - The id of the move that causing the tag to be added, if caused by a move + * @param sourceId - The {@linkcode Pokemon#id | id} of the pokemon causing the tag to be added, if caused by a Pokémon + * @returns Whether the tag was successfully added + * @see {@linkcode canAddTag} + */ + public addTag(tagType: BattlerTagType, turnCount = 0, sourceMove?: MoveId, sourceId?: number): boolean { const existingTag = this.getTag(tagType); if (existingTag) { existingTag.onOverlap(this); @@ -4292,6 +4096,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { const newTag = getBattlerTag(tagType, turnCount, sourceMove!, sourceId!); // TODO: are the bangs correct? + // TODO: Just call canAddTag() here? Can possibly overload it to accept an actual tag instead of just a type const cancelled = new BooleanHolder(false); applyAbAttrs("BattlerTagImmunityAbAttr", { pokemon: this, tag: newTag, cancelled }); if (cancelled.value) { @@ -4314,31 +4119,52 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return false; } - /**@overload */ - getTag(tagType: BattlerTagType.GRUDGE): GrudgeTag | undefined; - - /** @overload */ - getTag(tagType: BattlerTagType.SUBSTITUTE): SubstituteTag | undefined; - - /** @overload */ - getTag(tagType: BattlerTagType): BattlerTag | undefined; - - /** @overload */ - getTag(tagType: Constructor): T | undefined; - - getTag(tagType: BattlerTagType | Constructor): BattlerTag | undefined { + // TODO: Utilize a type map for these so we can avoid overloads + public getTag(tagType: BattlerTagType.GRUDGE): GrudgeTag | undefined; + public getTag(tagType: BattlerTagType.SUBSTITUTE): SubstituteTag | undefined; + public getTag(tagType: BattlerTagType): BattlerTag | undefined; + public getTag(tagType: Constructor): T | undefined; + public getTag(tagType: BattlerTagType | Constructor): BattlerTag | undefined { return typeof tagType === "function" ? this.summonData.tags.find(t => t instanceof tagType) : this.summonData.tags.find(t => t.tagType === tagType); } + /** + * Find the first `BattlerTag` matching the specified predicate. + * @param tagFilter - The predicate to match against + * @returns The first matching tag, or `undefined` if none match. + * @remarks + * Equivalent to `this.summonData.tags.find(tagFilter)`. + */ findTag(tagFilter: (tag: BattlerTag) => tag is T): T | undefined; + /** + * Find the first `BattlerTag` matching the specified predicate. + * @param tagFilter - The predicate to match against + * @returns The first matching tag, or `undefined` if none match. + * @remarks + * Equivalent to `this.summonData.tags.find(tagFilter)`. + */ findTag(tagFilter: (tag: BattlerTag) => boolean): BattlerTag | undefined; findTag(tagFilter: (tag: BattlerTag) => boolean) { return this.summonData.tags.find(t => tagFilter(t)); } + /** + * Return all `BattlerTag`s satisfying the given predicate. + * @param tagFilter - The predicate to match against + * @returns The filtered list of tags. + * @remarks + * Equivalent to `this.summonData.tags.filter(tagFilter)`. + */ findTags(tagFilter: (tag: BattlerTag) => tag is T): T[]; + /** + * Return all `BattlerTag`s satisfying the given predicate. + * @param tagFilter - The predicate to match against + * @returns The filtered list of tags. + * @remarks + * Equivalent to `this.summonData.tags.filter(tagFilter)`. + */ findTags(tagFilter: (tag: BattlerTag) => boolean): BattlerTag[]; findTags(tagFilter: (tag: BattlerTag) => boolean): BattlerTag[] { return this.summonData.tags.filter(t => tagFilter(t)); @@ -4347,10 +4173,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Tick down the first {@linkcode BattlerTag} found matching the given {@linkcode BattlerTagType}, * removing it if its duration goes below 0. - * @param tagType the {@linkcode BattlerTagType} to check against - * @returns `true` if the tag was present + * @param tagType - The `BattlerTagType` to lapse + * @returns Whether the tag was present */ - lapseTag(tagType: BattlerTagType): boolean { + public lapseTag(tagType: BattlerTagType): boolean { const tags = this.summonData.tags; const tag = tags.find(t => t.tagType === tagType); if (!tag) { @@ -4365,11 +4191,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Tick down all {@linkcode BattlerTags} matching the given {@linkcode BattlerTagLapseType}, - * removing any whose durations fall below 0. - * @param tagType the {@linkcode BattlerTagLapseType} to tick down + * Tick down all {@linkcode BattlerTags} that lapse on the provided + * `lapseType`, removing any whose durations fall below 0. + * @param lapseType - The type of lapse to process */ - lapseTags(lapseType: BattlerTagLapseType): void { + public lapseTags(lapseType: BattlerTagLapseType): void { const tags = this.summonData.tags; tags .filter( @@ -4384,10 +4210,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Remove the first tag matching the given {@linkcode BattlerTagType}. - * @param tagType the {@linkcode BattlerTagType} to search for and remove + * Remove the first tag matching `tagType` and invoke its + * {@linkcode BattlerTag#onRemove | onRemove} method. + * @remarks + * Only removes the first matching tag, if multiple are present; to remove all + * matching tags, use {@linkcode findAndRemoveTags} instead. + * @param tagType - The tag type to search for and remove */ - removeTag(tagType: BattlerTagType): void { + public removeTag(tagType: BattlerTagType): void { const tags = this.summonData.tags; const tag = tags.find(t => t.tagType === tagType); if (tag) { @@ -4397,10 +4227,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Find and remove all {@linkcode BattlerTag}s matching the given function. - * @param tagFilter a function dictating which tags to remove + * Find and remove all {@linkcode BattlerTag}s matching the given function and + * invoke their {@linkcode BattlerTag#onRemove | onRemove} methods. + * @remarks + * Removes all matching tags; to remove only the first matching tag, use + * {@linkcode removeTag} instead. + * @param tagFilter - A function dictating which tags to remove */ - findAndRemoveTags(tagFilter: (tag: BattlerTag) => boolean): void { + public findAndRemoveTags(tagFilter: (tag: BattlerTag) => boolean): void { const tags = this.summonData.tags; const tagsToRemove = tags.filter(t => tagFilter(t)); for (const tag of tagsToRemove) { @@ -4410,11 +4244,22 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } } - removeTagsBySourceId(sourceId: number): void { + /** + * Remove all tags that were applied by a Pokémon with the given `sourceId`, + * invoking their {@linkcode BattlerTag#onRemove | onRemove} methods. + * @param sourceId - Tags with this {@linkcode Pokemon#id | id} as their {@linkcode BattlerTag#sourceId | sourceId} will be removed + * @see {@linkcode findAndRemoveTags} + */ + public removeTagsBySourceId(sourceId: number): void { this.findAndRemoveTags(t => t.isSourceLinked() && t.sourceId === sourceId); } - transferTagsBySourceId(sourceId: number, newSourceId: number): void { + /** + * Change the `sourceId` of all tags on this Pokémon with the given `sourceId` to `newSourceId`. + * @param sourceId - The {@linkcode Pokemon#id | id} of the pokemon whose tags are to be transferred + * @param newSourceId - The {@linkcode Pokemon#id | id} of the pokemon to which the tags are being transferred + */ + public transferTagsBySourceId(sourceId: number, newSourceId: number): void { this.summonData.tags.forEach(t => { if (t.sourceId === sourceId) { t.sourceId = newSourceId; @@ -4423,11 +4268,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Transferring stat changes and Tags - * @param source {@linkcode Pokemon} the pokemon whose stats/Tags are to be passed on from, ie: the Pokemon using Baton Pass + * Transfer stat changes and Tags from another Pokémon + * + * @remarks + * Used to implement Baton Pass and switching via the Baton item. + * + * @param source - The pokemon whose stats/Tags are to be passed on from, ie: the Pokemon using Baton Pass */ - transferSummon(source: Pokemon): void { - // Copy all stat stages + public transferSummon(source: Pokemon): void { for (const s of BATTLE_STATS) { const sourceStage = source.getStatStage(s); if (this.isPlayer() && sourceStage === 6) { @@ -4457,9 +4305,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Gets whether the given move is currently disabled for this Pokemon. + * Get whether the given move is currently disabled for this Pokémon * - * @param moveId - The {@linkcode MoveId} ID of the move to check + * @param moveId - The ID of the move to check * @returns `true` if the move is disabled for this Pokemon, otherwise `false` * * @see {@linkcode MoveRestrictionBattlerTag} @@ -4469,13 +4317,13 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Gets whether the given move is currently disabled for the user based on the player's target selection + * Get whether the given move is currently disabled for the user based on the player's target selection * - * @param moveId - The {@linkcode MoveId} ID of the move to check + * @param moveId - The ID of the move to check * @param user - The move user * @param target - The target of the move * - * @returns {boolean} `true` if the move is disabled for this Pokemon due to the player's target selection + * @returns `true` if the move is disabled for this Pokemon due to the player's target selection * * @see {@linkcode MoveRestrictionBattlerTag} */ @@ -4489,11 +4337,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Gets the {@link MoveRestrictionBattlerTag} that is restricting a move, if it exists. + * Get the {@link MoveRestrictionBattlerTag} that is restricting a move, if it exists. * - * @param moveId - {@linkcode MoveId} ID of the move to check - * @param user - {@linkcode Pokemon} the move user, optional and used when the target is a factor in the move's restricted status - * @param target - {@linkcode Pokemon} the target of the move, optional and used when the target is a factor in the move's restricted status + * @param moveId - The ID of the move to check + * @param user - The move user, optional and used when the target is a factor in the move's restricted status + * @param target - The target of the move; optional, and used when the target is a factor in the move's restricted status * @returns The first tag on this Pokemon that restricts the move, or `null` if the move is not restricted. */ getRestrictingTag(moveId: MoveId, user?: Pokemon, target?: Pokemon): MoveRestrictionBattlerTag | null { @@ -4518,6 +4366,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return this.summonData.moveHistory; } + /** + * Add a new entry to this Pokemon's move history + * @remarks + * Does nothing if this Pokemon is not currently on the field. + * @param turnMove - The move to add to the history + */ public pushMoveHistory(turnMove: TurnMove): void { if (!this.isOnField()) { return; @@ -4535,7 +4389,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @returns An array of {@linkcode TurnMove}, as specified above. */ // TODO: Update documentation in dancer PR to mention "getLastNonVirtualMove" - getLastXMoves(moveCount = 1): TurnMove[] { + public getLastXMoves(moveCount = 1): TurnMove[] { const moveHistory = this.getMoveHistory(); if (moveCount > 0) { return moveHistory.slice(Math.max(moveHistory.length - moveCount, 0)).reverse(); @@ -4553,7 +4407,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @returns The last move this Pokemon has used satisfying the aforementioned conditions, * or `undefined` if no applicable moves have been used since switching in. */ - getLastNonVirtualMove(ignoreStruggle = false, ignoreFollowUp = true): TurnMove | undefined { + public getLastNonVirtualMove(ignoreStruggle = false, ignoreFollowUp = true): TurnMove | undefined { return this.getLastXMoves(-1).find( m => m.move !== MoveId.NONE @@ -4566,7 +4420,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * Return this Pokemon's move queue, consisting of all the moves it is slated to perform. * @returns An array of {@linkcode TurnMove}, as described above */ - getMoveQueue(): TurnMove[] { + public getMoveQueue(): TurnMove[] { return this.summonData.moveQueue; } @@ -4574,11 +4428,17 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * Add a new entry to the end of this Pokemon's move queue. * @param queuedMove - A {@linkcode TurnMove} to push to this Pokemon's queue. */ - pushMoveQueue(queuedMove: TurnMove): void { + public pushMoveQueue(queuedMove: TurnMove): void { this.summonData.moveQueue.push(queuedMove); } - changeForm(formChange: SpeciesFormChange): Promise { + /** + * Change this Pokémon's form to the specified form, loading the required + * assets and updating its stats and info display. + * @param formChange - The form to change to + * @returns A Promise that resolves once the form change has completed. + */ + public async changeForm(formChange: SpeciesFormChange): Promise { return new Promise(resolve => { this.formIndex = Math.max( this.species.forms.findIndex(f => f.formKey === formChange.formKey), @@ -4600,7 +4460,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { }); } - cry(soundConfig?: Phaser.Types.Sound.SoundConfig, sceneOverride?: BattleScene): AnySound | null { + /** + * Play this Pokémon's cry sound + * @param soundConfig - Optional sound configuration to apply to the cry + * @param sceneOverride - Optional scene to use instead of the global scene + */ + public cry(soundConfig?: Phaser.Types.Sound.SoundConfig, sceneOverride?: BattleScene): AnySound | null { const scene = sceneOverride ?? globalScene; // TODO: is `sceneOverride` needed? const cry = this.getSpeciesForm(undefined, true).cry(soundConfig); if (!cry) { @@ -4639,8 +4504,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return cry; } - // biome-ignore lint: there are a ton of issues.. - faintCry(callback: Function): void { + /** + * Play this Pokémon's faint cry, pausing its animation until the cry is finished. + * @param callback - A function to be called once the cry has finished playing + */ + public faintCry(callback: () => any): void { if (this.fusionSpecies && this.getSpeciesForm() !== this.getFusionSpeciesForm()) { this.fusionFaintCry(callback); return; @@ -4712,8 +4580,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { }); } - // biome-ignore lint/complexity/noBannedTypes: Consider refactoring to change type of Function - private fusionFaintCry(callback: Function): void { + /** + * Play this Pokémon's fusion faint cry, which is a mixture of the faint cries + * for both of its species + * @param callback - A function to be called once the cry has finished playing + */ + private fusionFaintCry(callback: () => any): void { const key = this.species.getCryKey(this.formIndex); let i = 0; let rate = 0.85; @@ -4823,7 +4695,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { }); } - isOppositeGender(pokemon: Pokemon): boolean { + /** + * Check the specified pokemon is considered to be the opposite gender as this pokemon + * @param pokemon - The Pokémon to compare against + * @returns Whether the pokemon are considered to be opposite genders + */ + public isOppositeGender(pokemon: Pokemon): boolean { return ( this.gender !== Gender.GENDERLESS && pokemon.gender === (this.gender === Gender.MALE ? Gender.FEMALE : Gender.MALE) @@ -4833,12 +4710,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Display an immunity message for a failed status application. * @param quiet - Whether to suppress message and return early - * @param reason - The reason for the status application failure - + * @param reason - The reason for the status application failure; * can be "overlap" (already has same status), "other" (generic fail message) * or a {@linkcode TerrainType} for terrain-based blockages. * Default `"other"` */ - queueStatusImmuneMessage( + public queueStatusImmuneMessage( quiet: boolean, reason: "overlap" | "other" | Exclude = "other", ): void { @@ -5065,7 +4942,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * * ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller. */ - doSetStatus(effect: Exclude): void; + public doSetStatus(effect: Exclude): void; /** * Set this Pokemon's {@linkcode status | non-volatile status condition} to the specified effect. * @param effect - {@linkcode StatusEffect.SLEEP} @@ -5075,7 +4952,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * * ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller. */ - doSetStatus(effect: StatusEffect.SLEEP, sleepTurnsRemaining?: number): void; + public doSetStatus(effect: StatusEffect.SLEEP, sleepTurnsRemaining?: number): void; /** * Set this Pokemon's {@linkcode status | non-volatile status condition} to the specified effect. * @param effect - The {@linkcode StatusEffect} to set @@ -5086,7 +4963,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * * ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller. */ - doSetStatus(effect: StatusEffect, sleepTurnsRemaining?: number): void; + public doSetStatus(effect: StatusEffect, sleepTurnsRemaining?: number): void; /** * Set this Pokemon's {@linkcode status | non-volatile status condition} to the specified effect. * @param effect - The {@linkcode StatusEffect} to set @@ -5098,7 +4975,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller. * @todo Make this and all related fields private and change tests to use a field-based helper or similar */ - doSetStatus( + public doSetStatus( effect: StatusEffect, sleepTurnsRemaining = effect !== StatusEffect.SLEEP ? 0 : this.randBattleSeedIntRange(2, 4), ): void { @@ -5147,11 +5024,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Resets the status of a pokemon. - * @param revive Whether revive should be cured; defaults to true. - * @param confusion Whether resetStatus should include confusion or not; defaults to false. - * @param reloadAssets Whether to reload the assets or not; defaults to false. - * @param asPhase Whether to reset the status in a phase or immediately + * Reset this Pokémon's status + * @param revive - Whether revive should be cured; default `true` + * @param confusion - Whether to also cure confusion; default `false` + * @param reloadAssets - Whether to reload the assets or not; default `false` + * @param asPhase - Whether to reset the status in a phase or immediately; default `true` */ resetStatus(revive = true, confusion = false, reloadAssets = false, asPhase = true): void { const lastStatus = this.status?.effect; @@ -5167,9 +5044,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Performs the action of clearing a Pokemon's status - * + * Perform the action of clearing a Pokemon's status + * @remarks * This is a helper to {@linkcode resetStatus}, which should be called directly instead of this method + * @param confusion - Whether to also clear this Pokémon's confusion + * @param reloadAssets - Whether to reload this pokemon's assets */ public clearStatus(confusion: boolean, reloadAssets: boolean) { const lastStatus = this.status?.effect; @@ -5188,11 +5067,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Checks if this Pokemon is protected by Safeguard - * @param attacker the {@linkcode Pokemon} inflicting status on this Pokemon - * @returns `true` if this Pokemon is protected by Safeguard; `false` otherwise. + * Check if this Pokémon is protected by Safeguard + * @param attacker - The Pokémon responsible for the interaction that needs to check against Safeguard + * @returns Whether this Pokémon is protected by Safeguard */ - isSafeguarded(attacker: Pokemon): boolean { + public isSafeguarded(attacker: Pokemon): boolean { const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; if (globalScene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, defendingSide)) { const bypassed = new BooleanHolder(false); @@ -5205,11 +5084,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Performs miscellaneous setup for when the Pokemon is summoned, like generating the substitute sprite + * Perform miscellaneous setup for when the Pokemon is summoned, like generating the substitute sprite * @param resetSummonData - Whether to additionally reset the Pokemon's summon data (default: `false`) */ public fieldSetup(resetSummonData?: boolean): void { - this.setSwitchOutStatus(false); + this.switchOutStatus = false; if (globalScene) { globalScene.triggerPokemonFormChange(this, SpeciesFormChangePostMoveTrigger, true); } @@ -5237,7 +5116,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * Reset this Pokemon's {@linkcode PokemonSummonData | SummonData} and {@linkcode PokemonTempSummonData | TempSummonData} * in preparation for switching pokemon, as well as removing any relevant on-switch tags. */ - resetSummonData(): void { + public resetSummonData(): void { const illusion: IllusionData | null = this.summonData.illusion; if (this.summonData.speciesForm) { this.summonData.speciesForm = null; @@ -5250,25 +5129,39 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Reset a {@linkcode Pokemon}'s per-battle {@linkcode PokemonBattleData | battleData}, + * Reset this Pokémon's per-battle {@linkcode PokemonBattleData | battleData} * as well as any transient {@linkcode PokemonWaveData | waveData} for the current wave. + * + * @remarks * Should be called once per arena transition (new biome/trainer battle/Mystery Encounter). */ - resetBattleAndWaveData(): void { + public resetBattleAndWaveData(): void { this.battleData = new PokemonBattleData(); this.resetWaveData(); } /** - * Reset a {@linkcode Pokemon}'s {@linkcode PokemonWaveData | waveData}. + * Reset this Pokémon's {@linkcode PokemonWaveData | waveData} + * + * @remarks * Should be called upon starting a new wave in addition to whenever an arena transition occurs. - * @see {@linkcode resetBattleAndWaveData()} + * @see {@linkcode resetBattleAndWaveData} */ resetWaveData(): void { this.waveData = new PokemonWaveData(); this.tempSummonData.waveTurnCount = 1; } + /** + * Reset this Pokémon's Terastallization state + * + * @remarks + * Responsible for all of the cleanup required when a pokemon goes from being + * terastallized to no longer terastallized: + * - Resetting stellar type boosts + * - Updating the Pokémon's terastallization-dependent form + * - Adjusting the sprite pipeline to remove the Tera effect + */ resetTera(): void { const wasTerastallized = this.isTerastallized; this.isTerastallized = false; @@ -5279,6 +5172,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } } + /** + * Clear this Pokémon's transient turn data + */ resetTurnData(): void { this.turnData = new PokemonTurnData(); } @@ -5288,6 +5184,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return (this.getSpeciesForm().getBaseExp() * this.level) / 5 + 1; } + //#region Sprite and Animation Methods setFrameRate(frameRate: number) { globalScene.anims.get(this.getBattleSpriteKey()).frameRate = frameRate; try { @@ -5364,6 +5261,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } } + /** Play the shiny sparkle animation and effects, if applicable */ sparkle(): void { if (this.shinySparkle) { doShinySparkleAnim(this.shinySparkle, this.variant); @@ -5695,16 +5593,19 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { fusionCanvas.remove(); } + //#endregion Sprite and Animation Methods + /** - * Generates a random number using the current battle's seed, or the global seed if `globalScene.currentBattle` is falsy - * + * Generate a random number using the current battle's seed, or the global seed if `globalScene.currentBattle` is falsy + * + * @remarks * This calls either {@linkcode BattleScene.randBattleSeedInt}({@linkcode range}, {@linkcode min}) in `src/battle-scene.ts` * which calls {@linkcode Battle.randSeedInt}({@linkcode range}, {@linkcode min}) in `src/battle.ts` * which calls {@linkcode randSeedInt randSeedInt}({@linkcode range}, {@linkcode min}) in `src/utils.ts`, * or it directly calls {@linkcode randSeedInt randSeedInt}({@linkcode range}, {@linkcode min}) in `src/utils.ts` if there is no current battle * - * @param range How large of a range of random numbers to choose from. If {@linkcode range} <= 1, returns {@linkcode min} - * @param min The minimum integer to pick, default `0` + * @param range - How large of a range of random numbers to choose from. If {@linkcode range} <= 1, returns {@linkcode min} + * @param min - The minimum integer to pick; default `0` * @returns A random integer between {@linkcode min} and ({@linkcode min} + {@linkcode range} - 1) */ randBattleSeedInt(range: number, min = 0): number { @@ -5712,10 +5613,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Generates a random number using the current battle's seed, or the global seed if `globalScene.currentBattle` is falsy - * @param min The minimum integer to generate - * @param max The maximum integer to generate - * @returns a random integer between {@linkcode min} and {@linkcode max} inclusive + * Generate a random number using the current battle's seed, or the global seed if `globalScene.currentBattle` is falsy + * @param min - The minimum integer to generate + * @param max - The maximum integer to generate + * @returns A random integer between {@linkcode min} and {@linkcode max} (inclusive) */ randBattleSeedIntRange(min: number, max: number): number { return globalScene.currentBattle ? globalScene.randBattleSeedInt(max - min + 1, min) : randSeedIntRange(min, max); @@ -5723,9 +5624,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Causes a Pokemon to leave the field (such as in preparation for a switch out/escape). - * @param clearEffects Indicates if effects should be cleared (true) or passed - * to the next pokemon, such as during a baton pass (false) - * @param hideInfo Indicates if this should also play the animation to hide the Pokemon's + * @param clearEffects - Indicates if effects should be cleared (true) or passed + * to the next pokemon, such as during a baton pass (false) + * @param hideInfo - Indicates if this should also play the animation to hide the Pokemon's * info container. */ leaveField(clearEffects = true, hideInfo = true, destroy = false) { @@ -5745,25 +5646,33 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } // Trigger abilities that activate upon leaving the field applyAbAttrs("PreLeaveFieldAbAttr", { pokemon: this }); - this.setSwitchOutStatus(true); + this.switchOutStatus = true; globalScene.triggerPokemonFormChange(this, SpeciesFormChangeActiveTrigger, true); globalScene.field.remove(this, destroy); } + /** + * @inheritdoc {@linkcode Phaser.GameObjects.Container#destroy} + * + * ### Custom Behavior + * In addition to the base `destroy` behavior, this also destroys the Pokemon's + * {@linkcode battleInfo} and substitute sprite (as applicable). + */ destroy(): void { this.battleInfo?.destroy(); this.destroySubstitute(); super.destroy(); } + // TODO: Turn this into a getter getBattleInfo(): BattleInfo { return this.battleInfo; } /** - * Checks whether or not the Pokemon's root form has the same ability - * @param abilityIndex the given ability index we are checking - * @returns true if the abilities are the same + * Check whether or not this Pokémon's root form has the same ability + * @param abilityIndex - The ability index to check + * @returns Whether the Pokemon's root form has the same ability */ hasSameAbilityInRootForm(abilityIndex: number): boolean { const currentAbilityIndex = this.abilityIndex; @@ -5772,9 +5681,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Helper function to check if the player already owns the starter data of the Pokemon's + * Helper function to check if the player already owns the starter data of this Pokémon's * current ability - * @param ownedAbilityAttrs the owned abilityAttr of this Pokemon's root form + * @param ownedAbilityAttrs - The owned abilityAttr of this Pokemon's root form * @returns true if the player already has it, false otherwise */ checkIfPlayerHasAbilityOfStarter(ownedAbilityAttrs: number): boolean { @@ -5815,8 +5724,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Record a berry being eaten for ability and move triggers. * Only tracks things that proc _every_ time a berry is eaten. - * @param berryType The type of berry being eaten. - * @param updateHarvest Whether to track the berry for harvest; default `true`. + * @param berryType - The type of berry being eaten. + * @param updateHarvest - Whether to track the berry for harvest; default `true`. */ public recordEatenBerry(berryType: BerryType, updateHarvest = true) { this.battleData.hasEatenBerry = true; @@ -5827,6 +5736,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { this.turnData.berriesEaten.push(berryType); } + /** + * Get the number of persistent treasure items this Pokemon has + * @remarks + * Persistent treasure items are defined as held items that give money + * after battle, such as the Lucky Egg or the Amulet Coin. + * Used exclusively for Gimmighoul's evolution condition + * @returns The number of persistent treasure items this Pokémon has + */ getPersistentTreasureCount(): number { return ( this.getHeldItems().filter(m => m.is("DamageMoneyRewardModifier")).length @@ -5957,10 +5874,10 @@ export class PlayerPokemon extends Pokemon { } /** - * Causes this mon to leave the field (via {@linkcode leaveField}) and then - * opens the party switcher UI to switch a new mon in - * @param switchType the {@linkcode SwitchType} for this switch-out. If this is - * `BATON_PASS` or `SHED_TAIL`, this Pokemon's effects are not cleared upon leaving + * Cause this Pokémon to leave the field (via {@linkcode leaveField}) and then + * open the party switcher UI to switch in a new Pokémon + * @param switchType - The type of this switch-out. If this is + * `BATON_PASS` or `SHED_TAIL`, this Pokémon's effects are not cleared upon leaving * the field. */ switchOut(switchType: SwitchType = SwitchType.SWITCH): Promise { @@ -5973,8 +5890,7 @@ export class PlayerPokemon extends Pokemon { this.getFieldIndex(), (slotIndex: number, _option: PartyOption) => { if (slotIndex >= globalScene.currentBattle.getBattlerCount() && slotIndex < 6) { - globalScene.phaseManager.prependNewToPhase( - "MoveEndPhase", + globalScene.phaseManager.queueDeferred( "SwitchSummonPhase", switchType, this.getFieldIndex(), @@ -6294,8 +6210,8 @@ export class PlayerPokemon extends Pokemon { } /** - * Returns a Promise to fuse two PlayerPokemon together - * @param pokemon The PlayerPokemon to fuse to this one + * Fuse another PlayerPokemon into this one + * @param pokemon - The PlayerPokemon to fuse to this one */ fuse(pokemon: PlayerPokemon): void { this.fusionSpecies = pokemon.species; @@ -6433,13 +6349,13 @@ export class EnemyPokemon extends Pokemon { if ( speciesId in Overrides.ENEMY_FORM_OVERRIDES - && !isNullOrUndefined(Overrides.ENEMY_FORM_OVERRIDES[speciesId]) + && Overrides.ENEMY_FORM_OVERRIDES[speciesId] != null && this.species.forms[Overrides.ENEMY_FORM_OVERRIDES[speciesId]] ) { this.formIndex = Overrides.ENEMY_FORM_OVERRIDES[speciesId]; } else if (globalScene.gameMode.isDaily && globalScene.gameMode.isWaveFinal(globalScene.currentBattle.waveIndex)) { const eventBoss = getDailyEventSeedBoss(globalScene.seed); - if (!isNullOrUndefined(eventBoss)) { + if (eventBoss != null) { this.formIndex = eventBoss.formIndex; } } @@ -6786,7 +6702,7 @@ export class EnemyPokemon extends Pokemon { /** * Determines the Pokemon the given move would target if used by this Pokemon - * @param moveId {@linkcode MoveId} The move to be used + * @param moveId - The move to be used * @returns The indexes of the Pokemon the given move would target */ getNextTargets(moveId: MoveId): BattlerIndex[] { @@ -6899,7 +6815,11 @@ export class EnemyPokemon extends Pokemon { return 0; } - damage(damage: number, ignoreSegments = false, preventEndure = false, ignoreFaintPhase = false): number { + /** + * @inheritdoc + * @param ignoreSegments - Whether to ignore boss segments when applying damage + */ + public damage(damage: number, ignoreSegments = false, preventEndure = false, ignoreFaintPhase = false): number { if (this.isFainted()) { return 0; } @@ -6955,7 +6875,7 @@ export class EnemyPokemon extends Pokemon { return ret; } - canBypassBossSegments(segmentCount = 1): boolean { + private canBypassBossSegments(segmentCount = 1): boolean { if ( globalScene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS && !this.formIndex @@ -6969,18 +6889,28 @@ export class EnemyPokemon extends Pokemon { /** * Go through a boss' health segments and give stats boosts for each newly cleared segment + * + * @remarks * The base boost is 1 to a random stat that's not already maxed out per broken shield * For Pokemon with 3 health segments or more, breaking the last shield gives +2 instead * For Pokemon with 5 health segments or more, breaking the last two shields give +2 each - * @param segmentIndex index of the segment to get down to (0 = no shield left, 1 = 1 shield left, etc.) + * @param segmentIndex - index of the segment to get down to (0 = no shield left, 1 = 1 shield left, etc.) */ handleBossSegmentCleared(segmentIndex: number): void { + let doStatBoost = !this.hasTrainer(); + // TODO: Rewrite this bespoke logic to improve clarity while (this.bossSegmentIndex > 0 && segmentIndex - 1 < this.bossSegmentIndex) { + this.bossSegmentIndex--; + + // Continue, _not_ break here, to ensure that each segment is still broken + if (!doStatBoost) { + continue; + } + let boostedStat: EffectiveStat | undefined; // Filter out already maxed out stat stages and weigh the rest based on existing stats const leftoverStats = EFFECTIVE_STATS.filter((s: EffectiveStat) => this.getStatStage(s) < 6); const statWeights = leftoverStats.map((s: EffectiveStat) => this.getStat(s, false)); - let boostedStat: EffectiveStat | undefined; const statThresholds: number[] = []; let totalWeight = 0; @@ -6999,18 +6929,18 @@ export class EnemyPokemon extends Pokemon { } if (boostedStat === undefined) { - this.bossSegmentIndex--; - return; + doStatBoost = false; + continue; } let stages = 1; // increase the boost if the boss has at least 3 segments and we passed last shield - if (this.bossSegments >= 3 && this.bossSegmentIndex === 1) { + if (this.bossSegments >= 3 && this.bossSegmentIndex === 0) { stages++; } // increase the boost if the boss has at least 5 segments and we passed the second to last shield - if (this.bossSegments >= 5 && this.bossSegmentIndex === 2) { + if (this.bossSegments >= 5 && this.bossSegmentIndex === 1) { stages++; } @@ -7023,26 +6953,25 @@ export class EnemyPokemon extends Pokemon { true, true, ); - this.bossSegmentIndex--; } } - getFieldIndex(): number { + public getFieldIndex(): number { return globalScene.getEnemyField().indexOf(this); } - getBattlerIndex(): BattlerIndex { + public getBattlerIndex(): BattlerIndex { return BattlerIndex.ENEMY + this.getFieldIndex(); } /** * Add a new pokemon to the player's party (at `slotIndex` if set). * The new pokemon's visibility will be set to `false`. - * @param pokeballType the type of pokeball the pokemon was caught with - * @param slotIndex an optional index to place the pokemon in the party - * @returns the pokemon that was added or null if the pokemon could not be added + * @param pokeballType - The type of pokeball the pokemon was caught with + * @param slotIndex - An optional index to place the pokemon in the party + * @returns The pokemon that was added or null if the pokemon could not be added */ - addToParty(pokeballType: PokeballType, slotIndex = -1) { + public addToParty(pokeballType: PokeballType, slotIndex = -1) { const party = globalScene.getPlayerParty(); let ret: PlayerPokemon | null = null; @@ -7085,11 +7014,11 @@ export class EnemyPokemon extends Pokemon { * Show or hide the type effectiveness multiplier window * Passing undefined will hide the window */ - updateEffectiveness(effectiveness?: string) { + public updateEffectiveness(effectiveness?: string) { this.battleInfo.updateEffectiveness(effectiveness); } - toggleFlyout(visible: boolean): void { + public toggleFlyout(visible: boolean): void { this.battleInfo.toggleFlyout(visible); } } diff --git a/src/game-mode.ts b/src/game-mode.ts index ebe1d4f4bff..bee7ccfd88e 100644 --- a/src/game-mode.ts +++ b/src/game-mode.ts @@ -14,7 +14,7 @@ import { SpeciesId } from "#enums/species-id"; import type { Arena } from "#field/arena"; import { classicFixedBattles, type FixedBattleConfigs } from "#trainers/fixed-battle-configs"; import { applyChallenges } from "#utils/challenge-utils"; -import { BooleanHolder, isNullOrUndefined, randSeedInt, randSeedItem } from "#utils/common"; +import { BooleanHolder, randSeedInt, randSeedItem } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import i18next from "i18next"; @@ -155,7 +155,7 @@ export class GameMode implements GameModeConfig { * - Town */ getStartingBiome(): BiomeId { - if (!isNullOrUndefined(Overrides.STARTING_BIOME_OVERRIDE)) { + if (Overrides.STARTING_BIOME_OVERRIDE != null) { return Overrides.STARTING_BIOME_OVERRIDE; } @@ -243,7 +243,7 @@ export class GameMode implements GameModeConfig { getOverrideSpecies(waveIndex: number): PokemonSpecies | null { if (this.isDaily && this.isWaveFinal(waveIndex)) { const eventBoss = getDailyEventSeedBoss(globalScene.seed); - if (!isNullOrUndefined(eventBoss)) { + if (eventBoss != null) { // Cannot set form index here, it will be overriden when adding it as enemy pokemon. return getPokemonSpecies(eventBoss.speciesId); } @@ -324,8 +324,8 @@ export class GameMode implements GameModeConfig { /** * Checks whether there is a fixed battle on this gamemode on a given wave. - * @param {number} waveIndex The wave to check. - * @returns {boolean} If this game mode has a fixed battle on this wave + * @param waveIndex The wave to check. + * @returns If this game mode has a fixed battle on this wave */ isFixedBattle(waveIndex: number): boolean { const dummyConfig = new FixedBattleConfig(); @@ -337,8 +337,8 @@ export class GameMode implements GameModeConfig { /** * Returns the config for the fixed battle for a particular wave. - * @param {number} waveIndex The wave to check. - * @returns {boolean} The fixed battle for this wave. + * @param waveIndex The wave to check. + * @returns The fixed battle for this wave. */ getFixedBattle(waveIndex: number): FixedBattleConfig { const challengeConfig = new FixedBattleConfig(); diff --git a/src/init/init.ts b/src/init/init.ts index b717664b654..8452278b3f1 100644 --- a/src/init/init.ts +++ b/src/init/init.ts @@ -11,7 +11,7 @@ import { initMoves } from "#moves/move"; import { initMysteryEncounters } from "#mystery-encounters/mystery-encounters"; import { initAchievements } from "#system/achv"; import { initVouchers } from "#system/voucher"; -import { initStatsKeys } from "#ui/handlers/game-stats-ui-handler"; +import { initStatsKeys } from "#ui/game-stats-ui-handler"; /** Initialize the game. */ export function initializeGame() { diff --git a/src/loading-scene.ts b/src/loading-scene.ts index c01c5287dc5..a4cbaf9ae64 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -154,6 +154,7 @@ export class LoadingScene extends SceneBase { this.loadImage("select_gen_cursor", "ui"); this.loadImage("select_gen_cursor_highlight", "ui"); + this.loadImage("language_icon", "ui"); this.loadImage("saving_icon", "ui"); this.loadImage("discord", "ui"); this.loadImage("google", "ui"); diff --git a/src/messages.ts b/src/messages.ts index 177b4cc9b05..c9673345110 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -6,8 +6,8 @@ import i18next from "i18next"; /** * Retrieves the Pokemon's name, potentially with an affix indicating its role (wild or foe) in the current battle context, translated * @param pokemon {@linkcode Pokemon} name and battle context will be retrieved from this instance - * @param {boolean} useIllusion - Whether we want the name of the illusion or not. Default value : true - * @returns {string} ex: "Wild Gengar", "Ectoplasma sauvage" + * @param useIllusion - Whether we want the name of the illusion or not. Default value : true + * @returns ex: "Wild Gengar", "Ectoplasma sauvage" */ export function getPokemonNameWithAffix(pokemon: Pokemon | undefined, useIllusion = true): string { if (!pokemon) { diff --git a/src/modifier/init-modifier-pools.ts b/src/modifier/init-modifier-pools.ts index ba12920407d..e6ec69eac7f 100644 --- a/src/modifier/init-modifier-pools.ts +++ b/src/modifier/init-modifier-pools.ts @@ -31,7 +31,6 @@ import { } from "#modifiers/modifier-pools"; import { WeightedModifierType } from "#modifiers/modifier-type"; import type { WeightedModifierTypeWeightFunc } from "#types/modifier-types"; -import { isNullOrUndefined } from "#utils/common"; /** * Initialize the wild modifier pool @@ -409,7 +408,7 @@ function initUltraModifierPool() { if (!isHoldingOrb) { const moveset = p .getMoveset(true) - .filter(m => !isNullOrUndefined(m)) + .filter(m => m != null) .map(m => m.moveId); const canSetStatus = p.canSetStatus(StatusEffect.TOXIC, true, true, null, true); @@ -455,7 +454,7 @@ function initUltraModifierPool() { if (!isHoldingOrb) { const moveset = p .getMoveset(true) - .filter(m => !isNullOrUndefined(m)) + .filter(m => m != null) .map(m => m.moveId); const canSetStatus = p.canSetStatus(StatusEffect.BURN, true, true, null, true); diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index 4b7c40e1a7f..d67011bc145 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -115,19 +115,11 @@ import { import type { PokemonMove } from "#moves/pokemon-move"; import { getVoucherTypeIcon, getVoucherTypeName, VoucherType } from "#system/voucher"; import type { ModifierTypeFunc, WeightedModifierTypeWeightFunc } from "#types/modifier-types"; -import type { PokemonMoveSelectFilter, PokemonSelectFilter } from "#ui/handlers/party-ui-handler"; -import { PartyUiHandler } from "#ui/handlers/party-ui-handler"; +import type { PokemonMoveSelectFilter, PokemonSelectFilter } from "#ui/party-ui-handler"; +import { PartyUiHandler } from "#ui/party-ui-handler"; import { getModifierTierTextTint } from "#ui/text"; import { applyChallenges } from "#utils/challenge-utils"; -import { - BooleanHolder, - formatMoney, - isNullOrUndefined, - NumberHolder, - padInt, - randSeedInt, - randSeedItem, -} from "#utils/common"; +import { BooleanHolder, formatMoney, NumberHolder, padInt, randSeedInt, randSeedItem } from "#utils/common"; import { getEnumKeys, getEnumValues } from "#utils/enums"; import { getModifierPoolForType, getModifierType } from "#utils/modifier-utils"; import { toCamelCase } from "#utils/strings"; @@ -150,7 +142,7 @@ export class ModifierType { /** * Checks if the modifier type is of a specific type * @param modifierType - The type to check against - * @return Whether the modifier type is of the specified type + * @returns Whether the modifier type is of the specified type */ public is(modifierType: K): this is ModifierTypeInstanceMap[K] { const targetType = ModifierTypeConstructorMap[modifierType]; @@ -263,7 +255,7 @@ export class ModifierType { this.tier = modifier.modifierType.tier; return this; } - if (isNullOrUndefined(defaultTier)) { + if (defaultTier == null) { // If weight is 0, keep track of the first tier where the item was found defaultTier = modifier.modifierType.tier; } @@ -874,11 +866,7 @@ export class AttackTypeBoosterModifierType export type SpeciesStatBoosterItem = keyof typeof SpeciesStatBoosterModifierTypeGenerator.items; -/** - * Modifier type for {@linkcode SpeciesStatBoosterModifier} - * @extends PokemonHeldItemModifierType - * @implements GeneratedPersistentModifierType - */ +/** Modifier type for {@linkcode SpeciesStatBoosterModifier} */ export class SpeciesStatBoosterModifierType extends PokemonHeldItemModifierType implements GeneratedPersistentModifierType @@ -1396,7 +1384,6 @@ class TempStatStageBoosterModifierTypeGenerator extends ModifierTypeGenerator { * Modifier type generator for {@linkcode SpeciesStatBoosterModifierType}, which * encapsulates the logic for weighting the most useful held item from * the current list of {@linkcode items}. - * @extends ModifierTypeGenerator */ class SpeciesStatBoosterModifierTypeGenerator extends ModifierTypeGenerator { /** Object comprised of the currently available species-based stat boosting held items */ @@ -2925,7 +2912,7 @@ export function getPartyLuckValue(party: Pokemon[]): number { globalScene.executeWithSeedOffset( () => { const eventLuck = getDailyEventSeedLuck(globalScene.seed); - if (!isNullOrUndefined(eventLuck)) { + if (eventLuck != null) { DailyLuck.value = eventLuck; return; } diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 97291d4ed66..19ddc77d436 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -13,7 +13,6 @@ import { getStatusEffectHealText } from "#data/status-effect"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; import { Color, ShadowColor } from "#enums/color"; -import { Command } from "#enums/command"; import type { FormChangeItem } from "#enums/form-change-item"; import { LearnMoveType } from "#enums/learn-move-type"; import type { MoveId } from "#enums/move-id"; @@ -42,7 +41,7 @@ import type { import type { VoucherType } from "#system/voucher"; import type { ModifierInstanceMap, ModifierString } from "#types/modifier-types"; import { addTextObject } from "#ui/text"; -import { BooleanHolder, hslToHex, isNullOrUndefined, NumberHolder, randSeedFloat, toDmgValue } from "#utils/common"; +import { BooleanHolder, hslToHex, NumberHolder, randSeedFloat, toDmgValue } from "#utils/common"; import { getModifierType } from "#utils/modifier-utils"; import i18next from "i18next"; @@ -73,8 +72,8 @@ export class ModifierBar extends Phaser.GameObjects.Container { /** * Method to update content displayed in {@linkcode ModifierBar} - * @param {PersistentModifier[]} modifiers - The list of modifiers to be displayed in the {@linkcode ModifierBar} - * @param {boolean} hideHeldItems - If set to "true", only modifiers not assigned to a Pokémon are displayed + * @param modifiers - The list of modifiers to be displayed in the {@linkcode ModifierBar} + * @param hideHeldItems - If set to "true", only modifiers not assigned to a Pokémon are displayed */ updateModifiers(modifiers: PersistentModifier[], hideHeldItems = false) { this.removeAll(true); @@ -345,9 +344,6 @@ export class AddVoucherModifier extends ConsumableModifier { * modifier will be removed. If a modifier of the same type is to be added, it * will reset {@linkcode battleCount} back to {@linkcode maxBattles} of the * existing modifier instead of adding that modifier directly. - * @extends PersistentModifier - * @abstract - * @see {@linkcode add} */ export abstract class LapsingPersistentModifier extends PersistentModifier { /** The maximum amount of battles the modifier will exist for */ @@ -458,8 +454,6 @@ export abstract class LapsingPersistentModifier extends PersistentModifier { /** * Modifier used for passive items, specifically lures, that * temporarily increases the chance of a double battle. - * @extends LapsingPersistentModifier - * @see {@linkcode apply} */ export class DoubleBattleChanceBoosterModifier extends LapsingPersistentModifier { public declare type: DoubleBattleChanceBoosterModifierType; @@ -495,8 +489,6 @@ export class DoubleBattleChanceBoosterModifier extends LapsingPersistentModifier * Modifier used for party-wide items, specifically the X items, that * temporarily increases the stat stage multiplier of the corresponding * {@linkcode TempBattleStat}. - * @extends LapsingPersistentModifier - * @see {@linkcode apply} */ export class TempStatStageBoosterModifier extends LapsingPersistentModifier { /** The stat whose stat stage multiplier will be temporarily increased */ @@ -562,8 +554,6 @@ export class TempStatStageBoosterModifier extends LapsingPersistentModifier { /** * Modifier used for party-wide items, namely Dire Hit, that * temporarily increments the critical-hit stage - * @extends LapsingPersistentModifier - * @see {@linkcode apply} */ export class TempCritBoosterModifier extends LapsingPersistentModifier { clone() { @@ -737,7 +727,7 @@ export abstract class PokemonHeldItemModifier extends PersistentModifier { } getPokemon(): Pokemon | undefined { - return globalScene.getPokemonById(this.pokemonId) ?? undefined; + return globalScene.getPokemonById(this.pokemonId); } getScoreMultiplier(): number { @@ -818,8 +808,6 @@ export abstract class LapsingPokemonHeldItemModifier extends PokemonHeldItemModi /** * Modifier used for held items, specifically vitamins like Carbos, Hp Up, etc., that * increase the value of a given {@linkcode PermanentStat}. - * @extends PokemonHeldItemModifier - * @see {@linkcode apply} */ export class BaseStatModifier extends PokemonHeldItemModifier { protected stat: PermanentStat; @@ -1126,8 +1114,6 @@ export class PokemonIncrementingStatModifier extends PokemonHeldItemModifier { /** * Modifier used for held items that Applies {@linkcode Stat} boost(s) * using a multiplier. - * @extends PokemonHeldItemModifier - * @see {@linkcode apply} */ export class StatBoosterModifier extends PokemonHeldItemModifier { /** The stats that the held item boosts */ @@ -1194,8 +1180,6 @@ export class StatBoosterModifier extends PokemonHeldItemModifier { /** * Modifier used for held items, specifically Eviolite, that apply * {@linkcode Stat} boost(s) using a multiplier if the holder can evolve. - * @extends StatBoosterModifier - * @see {@linkcode apply} */ export class EvolutionStatBoosterModifier extends StatBoosterModifier { matchType(modifier: Modifier): boolean { @@ -1216,13 +1200,16 @@ export class EvolutionStatBoosterModifier extends StatBoosterModifier { /** * Boosts the incoming stat value by a {@linkcode EvolutionStatBoosterModifier.multiplier} if the holder - * can evolve. Note that, if the holder is a fusion, they will receive + * can evolve + * + * @remarks + * Note that, if the holder is a fusion, they will receive * only half of the boost if either of the fused members are fully * evolved. However, if they are both unevolved, the full boost * will apply. - * @param pokemon {@linkcode Pokemon} that holds the item - * @param _stat {@linkcode Stat} The {@linkcode Stat} to be boosted - * @param statValue{@linkcode NumberHolder} that holds the resulting value of the stat + * @param pokemon - The `Pokemon` holding the item + * @param _stat - The `Stat` to be boosted + * @param statValue - Holds the resulting value of the stat * @returns `true` if the stat boost applies successfully, false otherwise * @see shouldApply */ @@ -1246,8 +1233,6 @@ export class EvolutionStatBoosterModifier extends StatBoosterModifier { /** * Modifier used for held items that Applies {@linkcode Stat} boost(s) using a * multiplier if the holder is of a specific {@linkcode SpeciesId}. - * @extends StatBoosterModifier - * @see {@linkcode apply} */ export class SpeciesStatBoosterModifier extends StatBoosterModifier { /** The species that the held item's stat boost(s) apply to */ @@ -1321,8 +1306,6 @@ export class SpeciesStatBoosterModifier extends StatBoosterModifier { /** * Modifier used for held items that apply critical-hit stage boost(s). - * @extends PokemonHeldItemModifier - * @see {@linkcode apply} */ export class CritBoosterModifier extends PokemonHeldItemModifier { /** The amount of stages by which the held item increases the current critical-hit stage value */ @@ -1369,8 +1352,6 @@ export class CritBoosterModifier extends PokemonHeldItemModifier { /** * Modifier used for held items that apply critical-hit stage boost(s) * if the holder is of a specific {@linkcode SpeciesId}. - * @extends CritBoosterModifier - * @see {@linkcode shouldApply} */ export class SpeciesCritBoosterModifier extends CritBoosterModifier { /** The species that the held item's critical-hit stage boost applies to */ @@ -1560,30 +1541,16 @@ export class BypassSpeedChanceModifier extends PokemonHeldItemModifier { return new BypassSpeedChanceModifier(this.type, this.pokemonId, this.stackCount); } - /** - * Checks if {@linkcode BypassSpeedChanceModifier} should be applied - * @param pokemon the {@linkcode Pokemon} that holds the item - * @param doBypassSpeed {@linkcode BooleanHolder} that is `true` if speed should be bypassed - * @returns `true` if {@linkcode BypassSpeedChanceModifier} should be applied - */ - override shouldApply(pokemon?: Pokemon, doBypassSpeed?: BooleanHolder): boolean { - return super.shouldApply(pokemon, doBypassSpeed) && !!doBypassSpeed; - } - /** * Applies {@linkcode BypassSpeedChanceModifier} * @param pokemon the {@linkcode Pokemon} that holds the item - * @param doBypassSpeed {@linkcode BooleanHolder} that is `true` if speed should be bypassed * @returns `true` if {@linkcode BypassSpeedChanceModifier} has been applied */ - override apply(pokemon: Pokemon, doBypassSpeed: BooleanHolder): boolean { - if (!doBypassSpeed.value && pokemon.randBattleSeedInt(10) < this.getStackCount()) { - doBypassSpeed.value = true; - const isCommandFight = - globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()]?.command === Command.FIGHT; + override apply(pokemon: Pokemon): boolean { + if (pokemon.randBattleSeedInt(10) < this.getStackCount() && pokemon.addTag(BattlerTagType.BYPASS_SPEED)) { const hasQuickClaw = this.type.is("PokemonHeldItemModifierType") && this.type.id === "QUICK_CLAW"; - if (isCommandFight && hasQuickClaw) { + if (hasQuickClaw) { globalScene.phaseManager.queueMessage( i18next.t("modifier:bypassSpeedChanceApply", { pokemonName: getPokemonNameWithAffix(pokemon), @@ -1694,8 +1661,6 @@ export class TurnHealModifier extends PokemonHeldItemModifier { /** * Modifier used for held items, namely Toxic Orb and Flame Orb, that apply a * set {@linkcode StatusEffect} at the end of a turn. - * @extends PokemonHeldItemModifier - * @see {@linkcode apply} */ export class TurnStatusEffectModifier extends PokemonHeldItemModifier { /** The status effect to be applied by the held item */ @@ -1721,7 +1686,7 @@ export class TurnStatusEffectModifier extends PokemonHeldItemModifier { * would be the only item able to {@linkcode apply} successfully. * @override * @param modifier {@linkcode Modifier} being type tested - * @return `true` if {@linkcode modifier} is an instance of + * @returns `true` if {@linkcode modifier} is an instance of * TurnStatusEffectModifier, false otherwise */ matchType(modifier: Modifier): boolean { @@ -1966,8 +1931,6 @@ export class PokemonInstantReviveModifier extends PokemonHeldItemModifier { /** * Modifier used for held items, namely White Herb, that restore adverse stat * stages in battle. - * @extends PokemonHeldItemModifier - * @see {@linkcode apply} */ export class ResetNegativeStatStageModifier extends PokemonHeldItemModifier { matchType(modifier: Modifier) { @@ -2013,8 +1976,6 @@ export class ResetNegativeStatStageModifier extends PokemonHeldItemModifier { /** * Modifier used for held items, namely Mystical Rock, that extend the * duration of weather and terrain effects. - * @extends PokemonHeldItemModifier - * @see {@linkcode apply} */ export class FieldEffectModifier extends PokemonHeldItemModifier { /** @@ -2137,10 +2098,7 @@ export class PokemonHpRestoreModifier extends ConsumablePokemonModifier { * @returns `true` if the {@linkcode PokemonHpRestoreModifier} should be applied */ override shouldApply(playerPokemon?: PlayerPokemon, multiplier?: number): boolean { - return ( - super.shouldApply(playerPokemon) - && (this.fainted || (!isNullOrUndefined(multiplier) && typeof multiplier === "number")) - ); + return super.shouldApply(playerPokemon) && (this.fainted || (multiplier != null && typeof multiplier === "number")); } /** @@ -2777,10 +2735,10 @@ export class PokemonMultiHitModifier extends PokemonHeldItemModifier { return false; } - if (!isNullOrUndefined(count)) { + if (count != null) { return this.applyHitCountBoost(count); } - if (!isNullOrUndefined(damageMultiplier)) { + if (damageMultiplier != null) { return this.applyDamageModifier(pokemon, damageMultiplier); } @@ -3400,8 +3358,6 @@ export class ExtraModifierModifier extends PersistentModifier { /** * Modifier used for timed boosts to the player's shop item rewards. - * @extends LapsingPersistentModifier - * @see {@linkcode apply} */ export class TempExtraModifierModifier extends LapsingPersistentModifier { /** diff --git a/src/overrides.ts b/src/overrides.ts index b8212ea8fd6..3f61196f0b4 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -1,4 +1,4 @@ -import { type PokeballCounts } from "#app/battle-scene"; +import type { PokeballCounts } from "#app/battle-scene"; import { EvolutionItem } from "#balance/pokemon-evolutions"; import { Gender } from "#data/gender"; import { AbilityId } from "#enums/ability-id"; @@ -18,10 +18,11 @@ import { Stat } from "#enums/stat"; import { StatusEffect } from "#enums/status-effect"; import { TimeOfDay } from "#enums/time-of-day"; import { TrainerType } from "#enums/trainer-type"; +import { TrainerVariant } from "#enums/trainer-variant"; import { Unlockables } from "#enums/unlockables"; import { VariantTier } from "#enums/variant-tier"; import { WeatherType } from "#enums/weather-type"; -import { type ModifierOverride } from "#modifiers/modifier-type"; +import type { ModifierOverride } from "#modifiers/modifier-type"; import { Variant } from "#sprites/variant"; /** @@ -311,8 +312,12 @@ export type BattleStyle = "double" | "single" | "even-doubles" | "odd-doubles"; export type RandomTrainerOverride = { /** The Type of trainer to force */ trainerType: Exclude; - /* If the selected trainer type has a double version, it will always use its double version. */ - alwaysDouble?: boolean; + /** + * The {@linkcode TrainerVariant} to force. + * @remarks + * `TrainerVariant.DOUBLE` cannot be forced on the first wave of a game due to issues with trainer party generation. + */ + trainerVariant?: TrainerVariant; }; /** The type of the {@linkcode DefaultOverrides} class */ diff --git a/src/phase-manager.ts b/src/phase-manager.ts index 4bb7e0a4b37..350e77e52eb 100644 --- a/src/phase-manager.ts +++ b/src/phase-manager.ts @@ -1,10 +1,21 @@ +/** + * Manager for phases used by battle scene. + * + * @remarks + * **This file must not be imported or used directly.** + * The manager is exclusively used by the Battle Scene and is NOT intended for external use. + * @module + */ + import { PHASE_START_COLOR } from "#app/constants/colors"; +import { DynamicQueueManager } from "#app/dynamic-queue-manager"; import { globalScene } from "#app/global-scene"; import type { Phase } from "#app/phase"; -import { type PhasePriorityQueue, PostSummonPhasePriorityQueue } from "#data/phase-priority-queue"; -import type { DynamicPhaseType } from "#enums/dynamic-phase-type"; +import { PhaseTree } from "#app/phase-tree"; +import { BattleType } from "#enums/battle-type"; +import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; import type { Pokemon } from "#field/pokemon"; -import { ActivatePriorityQueuePhase } from "#phases/activate-priority-queue-phase"; +import type { PokemonMove } from "#moves/pokemon-move"; import { AddEnemyBuffModifierPhase } from "#phases/add-enemy-buff-modifier-phase"; import { AttemptCapturePhase } from "#phases/attempt-capture-phase"; import { AttemptRunPhase } from "#phases/attempt-run-phase"; @@ -16,6 +27,7 @@ import { CheckSwitchPhase } from "#phases/check-switch-phase"; import { CommandPhase } from "#phases/command-phase"; import { CommonAnimPhase } from "#phases/common-anim-phase"; import { DamageAnimPhase } from "#phases/damage-anim-phase"; +import { DynamicPhaseMarker } from "#phases/dynamic-phase-marker"; import { EggHatchPhase } from "#phases/egg-hatch-phase"; import { EggLapsePhase } from "#phases/egg-lapse-phase"; import { EggSummaryPhase } from "#phases/egg-summary-phase"; @@ -100,17 +112,7 @@ import { UnavailablePhase } from "#phases/unavailable-phase"; import { UnlockPhase } from "#phases/unlock-phase"; import { VictoryPhase } from "#phases/victory-phase"; import { WeatherEffectPhase } from "#phases/weather-effect-phase"; -import type { PhaseMap, PhaseString } from "#types/phase-types"; -import { type Constructor, coerceArray } from "#utils/common"; - -/** - * @module - * Manager for phases used by battle scene. - * - * @remarks - * **This file must not be imported or used directly.** - * The manager is exclusively used by the Battle Scene and is NOT intended for external use. - */ +import type { PhaseConditionFunc, PhaseMap, PhaseString } from "#types/phase-types"; /** * Object that holds all of the phase constructors. @@ -121,7 +123,6 @@ import { type Constructor, coerceArray } from "#utils/common"; * This allows for easy creation of new phases without needing to import each phase individually. */ const PHASES = Object.freeze({ - ActivatePriorityQueuePhase, AddEnemyBuffModifierPhase, AttemptCapturePhase, AttemptRunPhase, @@ -133,6 +134,7 @@ const PHASES = Object.freeze({ CommandPhase, CommonAnimPhase, DamageAnimPhase, + DynamicPhaseMarker, EggHatchPhase, EggLapsePhase, EggSummaryPhase, @@ -221,33 +223,30 @@ const PHASES = Object.freeze({ /** Maps Phase strings to their constructors */ export type PhaseConstructorMap = typeof PHASES; +/** Phases pushed at the end of each {@linkcode TurnStartPhase} */ +const turnEndPhases: readonly PhaseString[] = [ + "WeatherEffectPhase", + "PositionalTagPhase", + "BerryPhase", + "CheckStatusEffectPhase", + "TurnEndPhase", +] as const; + /** * PhaseManager is responsible for managing the phases in the battle scene */ export class PhaseManager { /** PhaseQueue: dequeue/remove the first element to get the next phase */ - public phaseQueue: Phase[] = []; - public conditionalQueue: Array<[() => boolean, Phase]> = []; - /** PhaseQueuePrepend: is a temp storage of what will be added to PhaseQueue */ - private phaseQueuePrepend: Phase[] = []; + private readonly phaseQueue: PhaseTree = new PhaseTree(); - /** overrides default of inserting phases to end of phaseQueuePrepend array. Useful for inserting Phases "out of order" */ - private phaseQueuePrependSpliceIndex = -1; - private nextCommandPhaseQueue: Phase[] = []; - - /** Storage for {@linkcode PhasePriorityQueue}s which hold phases whose order dynamically changes */ - private dynamicPhaseQueues: PhasePriorityQueue[]; - /** Parallel array to {@linkcode dynamicPhaseQueues} - matches phase types to their queues */ - private dynamicPhaseTypes: Constructor[]; + /** Holds priority queues for dynamically ordered phases */ + public dynamicQueueManager = new DynamicQueueManager(); + /** The currently-running phase */ private currentPhase: Phase; + /** The phase put on standby if {@linkcode overridePhase} is called */ private standbyPhase: Phase | null = null; - constructor() { - this.dynamicPhaseQueues = [new PostSummonPhasePriorityQueue()]; - this.dynamicPhaseTypes = [PostSummonPhase]; - } - /** * Clear all previously set phases, then add a new {@linkcode TitlePhase} to transition to the title screen. * @param addLogin - Whether to add a new {@linkcode LoginPhase} before the {@linkcode TitlePhase} @@ -275,123 +274,76 @@ export class PhaseManager { } /** - * Adds a phase to the conditional queue and ensures it is executed only when the specified condition is met. - * - * This method allows deferring the execution of a phase until certain conditions are met, which is useful for handling - * situations like abilities and entry hazards that depend on specific game states. - * - * @param phase - The phase to be added to the conditional queue. - * @param condition - A function that returns a boolean indicating whether the phase should be executed. - * + * Adds a phase to the end of the queue + * @param phase - The {@linkcode Phase} to add */ - pushConditionalPhase(phase: Phase, condition: () => boolean): void { - this.conditionalQueue.push([condition, phase]); + public pushPhase(phase: Phase): void { + this.phaseQueue.pushPhase(this.checkDynamic(phase)); } /** - * Adds a phase to nextCommandPhaseQueue, as long as boolean passed in is false - * @param phase {@linkcode Phase} the phase to add - * @param defer boolean on which queue to add to, defaults to false, and adds to phaseQueue + * Queue a phase to be run immediately after the current phase finishes. \ + * Unshifted phases are run in FIFO order if multiple are queued during a single phase's execution. + * @param phase - The {@linkcode Phase} to add */ - pushPhase(phase: Phase, defer = false): void { - if (this.getDynamicPhaseType(phase) !== undefined) { - this.pushDynamicPhase(phase); - } else { - (!defer ? this.phaseQueue : this.nextCommandPhaseQueue).push(phase); - } + public unshiftPhase(phase: Phase): void { + const toAdd = this.checkDynamic(phase); + phase.is("MovePhase") ? this.phaseQueue.addAfter(toAdd, "MoveEndPhase") : this.phaseQueue.addPhase(toAdd); } /** - * Adds Phase(s) to the end of phaseQueuePrepend, or at phaseQueuePrependSpliceIndex - * @param phases {@linkcode Phase} the phase(s) to add + * Helper method to queue a phase as dynamic if necessary + * @param phase - The phase to check + * @returns The {@linkcode Phase} or a {@linkcode DynamicPhaseMarker} to be used in its place */ - unshiftPhase(...phases: Phase[]): void { - if (this.phaseQueuePrependSpliceIndex === -1) { - this.phaseQueuePrepend.push(...phases); - } else { - this.phaseQueuePrepend.splice(this.phaseQueuePrependSpliceIndex, 0, ...phases); + private checkDynamic(phase: Phase): Phase { + if (this.dynamicQueueManager.queueDynamicPhase(phase)) { + return new DynamicPhaseMarker(phase.phaseName); } + return phase; } /** * Clears the phaseQueue + * @param leaveUnshifted - If `true`, leaves the top level of the tree intact; default `false` */ - clearPhaseQueue(): void { - this.phaseQueue.splice(0, this.phaseQueue.length); + public clearPhaseQueue(leaveUnshifted = false): void { + this.phaseQueue.clear(leaveUnshifted); } - /** - * Clears all phase-related stuff, including all phase queues, the current and standby phases, and a splice index - */ - clearAllPhases(): void { - for (const queue of [this.phaseQueue, this.phaseQueuePrepend, this.conditionalQueue, this.nextCommandPhaseQueue]) { - queue.splice(0, queue.length); - } - this.dynamicPhaseQueues.forEach(queue => queue.clear()); + /** Clears all phase queues and the standby phase */ + public clearAllPhases(): void { + this.clearPhaseQueue(); + this.dynamicQueueManager.clearQueues(); this.standbyPhase = null; - this.clearPhaseQueueSplice(); } /** - * Used by function unshiftPhase(), sets index to start inserting at current length instead of the end of the array, useful if phaseQueuePrepend gets longer with Phases + * Determines the next phase to run and starts it. + * @privateRemarks + * This is called by {@linkcode Phase.end} by default, and should not be called by other methods. */ - setPhaseQueueSplice(): void { - this.phaseQueuePrependSpliceIndex = this.phaseQueuePrepend.length; - } - - /** - * Resets phaseQueuePrependSpliceIndex to -1, implies that calls to unshiftPhase will insert at end of phaseQueuePrepend - */ - clearPhaseQueueSplice(): void { - this.phaseQueuePrependSpliceIndex = -1; - } - - /** - * Is called by each Phase implementations "end()" by default - * We dump everything from phaseQueuePrepend to the start of of phaseQueue - * then removes first Phase and starts it - */ - shiftPhase(): void { + public shiftPhase(): void { if (this.standbyPhase) { this.currentPhase = this.standbyPhase; this.standbyPhase = null; return; } - if (this.phaseQueuePrependSpliceIndex > -1) { - this.clearPhaseQueueSplice(); - } - this.phaseQueue.unshift(...this.phaseQueuePrepend); - this.phaseQueuePrepend.splice(0); + let nextPhase = this.phaseQueue.getNextPhase(); - const unactivatedConditionalPhases: [() => boolean, Phase][] = []; - // Check if there are any conditional phases queued - for (const [condition, phase] of this.conditionalQueue) { - // Evaluate the condition associated with the phase - if (condition()) { - // If the condition is met, add the phase to the phase queue - this.pushPhase(phase); - } else { - // If the condition is not met, re-add the phase back to the end of the conditional queue - unactivatedConditionalPhases.push([condition, phase]); - } + if (nextPhase?.is("DynamicPhaseMarker")) { + nextPhase = this.dynamicQueueManager.popNextPhase(nextPhase.phaseType); } - this.conditionalQueue = unactivatedConditionalPhases; - - // If no phases are left, unshift phases to start a new turn. - if (this.phaseQueue.length === 0) { - this.populatePhaseQueue(); - // Clear the conditionalQueue if there are no phases left in the phaseQueue - this.conditionalQueue = []; + if (nextPhase == null) { + this.turnStart(); + } else { + this.currentPhase = nextPhase; } - // Bang is justified as `populatePhaseQueue` ensures we always have _something_ in the queue at all times - this.currentPhase = this.phaseQueue.shift()!; - this.startCurrentPhase(); } - /** * Helper method to start and log the current phase. */ @@ -400,7 +352,14 @@ export class PhaseManager { this.currentPhase.start(); } - overridePhase(phase: Phase): boolean { + /** + * Overrides the currently running phase with another + * @param phase - The {@linkcode Phase} to override the current one with + * @returns If the override succeeded + * + * @todo This is antithetical to the phase structure and used a single time. Remove it. + */ + public overridePhase(phase: Phase): boolean { if (this.standbyPhase) { return false; } @@ -413,173 +372,47 @@ export class PhaseManager { } /** - * Find a specific {@linkcode Phase} in the phase queue. + * Determine if there is a queued {@linkcode Phase} meeting the specified conditions. + * @param type - The {@linkcode PhaseString | type} of phase to search for + * @param condition - An optional {@linkcode PhaseConditionFunc} to add conditions to the search + * @returns Whether a matching phase exists + */ + public hasPhaseOfType(type: T, condition?: PhaseConditionFunc): boolean { + return this.dynamicQueueManager.exists(type, condition) || this.phaseQueue.exists(type, condition); + } + + /** + * Attempt to find and remove the first queued {@linkcode Phase} matching the given conditions. + * @param type - The {@linkcode PhaseString | type} of phase to search for + * @param phaseFilter - An optional {@linkcode PhaseConditionFunc} to add conditions to the search + * @returns Whether a phase was successfully removed + */ + public tryRemovePhase(type: T, phaseFilter?: PhaseConditionFunc): boolean { + if (this.dynamicQueueManager.removePhase(type, phaseFilter)) { + return true; + } + return this.phaseQueue.remove(type, phaseFilter); + } + + /** + * Removes all {@linkcode Phase}s of the given type from the queue + * @param phaseType - The {@linkcode PhaseString | type} of phase to search for * - * @param phaseFilter filter function to use to find the wanted phase - * @returns the found phase or undefined if none found + * @remarks + * This is not intended to be used with dynamically ordered phases, and does not operate on the dynamic queue. \ + * However, it does remove {@linkcode DynamicPhaseMarker}s and so would prevent such phases from activating. */ - findPhase

(phaseFilter: (phase: P) => boolean): P | undefined { - return this.phaseQueue.find(phaseFilter) as P | undefined; - } - - tryReplacePhase(phaseFilter: (phase: Phase) => boolean, phase: Phase): boolean { - const phaseIndex = this.phaseQueue.findIndex(phaseFilter); - if (phaseIndex > -1) { - this.phaseQueue[phaseIndex] = phase; - return true; - } - return false; - } - - tryRemovePhase(phaseFilter: (phase: Phase) => boolean): boolean { - const phaseIndex = this.phaseQueue.findIndex(phaseFilter); - if (phaseIndex > -1) { - this.phaseQueue.splice(phaseIndex, 1); - return true; - } - return false; + public removeAllPhasesOfType(type: PhaseString): void { + this.phaseQueue.removeAll(type); } /** - * Will search for a specific phase in {@linkcode phaseQueuePrepend} via filter, and remove the first result if a match is found. - * @param phaseFilter filter function - */ - tryRemoveUnshiftedPhase(phaseFilter: (phase: Phase) => boolean): boolean { - const phaseIndex = this.phaseQueuePrepend.findIndex(phaseFilter); - if (phaseIndex > -1) { - this.phaseQueuePrepend.splice(phaseIndex, 1); - return true; - } - return false; - } - - /** - * Tries to add the input phase to index before target phase in the phaseQueue, else simply calls unshiftPhase() - * @param phase - The phase to be added - * @param targetPhase - The phase to search for in phaseQueue - * @returns boolean if a targetPhase was found and added - */ - prependToPhase(phase: Phase | Phase[], targetPhase: PhaseString): boolean { - phase = coerceArray(phase); - const target = PHASES[targetPhase]; - const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof target); - - if (targetIndex !== -1) { - this.phaseQueue.splice(targetIndex, 0, ...phase); - return true; - } - this.unshiftPhase(...phase); - return false; - } - - /** - * Tries to add the input phase(s) to index after target phase in the {@linkcode phaseQueue}, else simply calls {@linkcode unshiftPhase()} - * @param phase {@linkcode Phase} the phase(s) to be added - * @param targetPhase {@linkcode Phase} the type of phase to search for in {@linkcode phaseQueue} - * @param condition Condition the target phase must meet to be appended to - * @returns `true` if a `targetPhase` was found to append to - */ - appendToPhase(phase: Phase | Phase[], targetPhase: PhaseString, condition?: (p: Phase) => boolean): boolean { - phase = coerceArray(phase); - const target = PHASES[targetPhase]; - const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof target && (!condition || condition(ph))); - - if (targetIndex !== -1 && this.phaseQueue.length > targetIndex) { - this.phaseQueue.splice(targetIndex + 1, 0, ...phase); - return true; - } - this.unshiftPhase(...phase); - return false; - } - - /** - * Checks a phase and returns the matching {@linkcode DynamicPhaseType}, or undefined if it does not match one - * @param phase The phase to check - * @returns The corresponding {@linkcode DynamicPhaseType} or `undefined` - */ - public getDynamicPhaseType(phase: Phase | null): DynamicPhaseType | undefined { - let phaseType: DynamicPhaseType | undefined; - this.dynamicPhaseTypes.forEach((cls, index) => { - if (phase instanceof cls) { - phaseType = index; - } - }); - - return phaseType; - } - - /** - * Pushes a phase onto its corresponding dynamic queue and marks the activation point in {@linkcode phaseQueue} - * - * The {@linkcode ActivatePriorityQueuePhase} will run the top phase in the dynamic queue (not necessarily {@linkcode phase}) - * @param phase The phase to push - */ - public pushDynamicPhase(phase: Phase): void { - const type = this.getDynamicPhaseType(phase); - if (type === undefined) { - return; - } - - this.pushPhase(new ActivatePriorityQueuePhase(type)); - this.dynamicPhaseQueues[type].push(phase); - } - - /** - * Attempt to remove one or more Phases from the given DynamicPhaseQueue, removing the equivalent amount of {@linkcode ActivatePriorityQueuePhase}s from the queue. - * @param type - The {@linkcode DynamicPhaseType} to check - * @param phaseFilter - The function to select phases for removal - * @param removeCount - The maximum number of phases to remove, or `all` to remove all matching phases; - * default `1` - * @todo Remove this eventually once the patchwork bug this is used for is fixed - */ - public tryRemoveDynamicPhase( - type: DynamicPhaseType, - phaseFilter: (phase: Phase) => boolean, - removeCount: number | "all" = 1, - ): void { - const numRemoved = this.dynamicPhaseQueues[type].tryRemovePhase(phaseFilter, removeCount); - for (let x = 0; x < numRemoved; x++) { - this.tryRemovePhase(p => p.is("ActivatePriorityQueuePhase")); - } - } - - /** - * Unshifts the top phase from the corresponding dynamic queue onto {@linkcode phaseQueue} - * @param type {@linkcode DynamicPhaseType} The type of dynamic phase to start - */ - public startDynamicPhaseType(type: DynamicPhaseType): void { - const phase = this.dynamicPhaseQueues[type].pop(); - if (phase) { - this.unshiftPhase(phase); - } - } - - /** - * Unshifts an {@linkcode ActivatePriorityQueuePhase} for {@linkcode phase}, then pushes {@linkcode phase} to its dynamic queue - * - * This is the same as {@linkcode pushDynamicPhase}, except the activation phase is unshifted - * - * {@linkcode phase} is not guaranteed to be the next phase from the queue to run (if the queue is not empty) - * @param phase The phase to add - * @returns - */ - public startDynamicPhase(phase: Phase): void { - const type = this.getDynamicPhaseType(phase); - if (type === undefined) { - return; - } - - this.unshiftPhase(new ActivatePriorityQueuePhase(type)); - this.dynamicPhaseQueues[type].push(phase); - } - - /** - * Adds a MessagePhase, either to PhaseQueuePrepend or nextCommandPhaseQueue + * Adds a `MessagePhase` to the queue * @param message - string for MessagePhase * @param callbackDelay - optional param for MessagePhase constructor * @param prompt - optional param for MessagePhase constructor * @param promptDelay - optional param for MessagePhase constructor - * @param defer - Whether to allow the phase to be deferred + * @param defer - If `true`, push the phase instead of unshifting; default `false` * * @see {@linkcode MessagePhase} for more details on the parameters */ @@ -591,20 +424,18 @@ export class PhaseManager { defer?: boolean | null, ) { const phase = new MessagePhase(message, callbackDelay, prompt, promptDelay); - if (!defer) { - // adds to the end of PhaseQueuePrepend - this.unshiftPhase(phase); - } else { - //remember that pushPhase adds it to nextCommandPhaseQueue + if (defer) { this.pushPhase(phase); + } else { + this.unshiftPhase(phase); } } /** - * Queue a phase to show or hide the ability flyout bar. + * Queues an ability bar flyout phase via {@linkcode unshiftPhase} * @param pokemon - The {@linkcode Pokemon} whose ability is being activated * @param passive - Whether the ability is a passive - * @param show - Whether to show or hide the bar + * @param show - If `true`, show the bar. Otherwise, hide it */ public queueAbilityDisplay(pokemon: Pokemon, passive: boolean, show: boolean): void { this.unshiftPhase(show ? new ShowAbilityPhase(pokemon.getBattlerIndex(), passive) : new HideAbilityPhase()); @@ -620,14 +451,12 @@ export class PhaseManager { } /** - * Moves everything from nextCommandPhaseQueue to phaseQueue (keeping order) + * Clear all dynamic queues and begin a new {@linkcode TurnInitPhase} for the new turn. + * Called whenever the current phase queue is empty. */ - private populatePhaseQueue(): void { - if (this.nextCommandPhaseQueue.length > 0) { - this.phaseQueue.push(...this.nextCommandPhaseQueue); - this.nextCommandPhaseQueue.splice(0, this.nextCommandPhaseQueue.length); - } - this.phaseQueue.push(new TurnInitPhase()); + private turnStart(): void { + this.dynamicQueueManager.clearQueues(); + this.currentPhase = new TurnInitPhase(); } /** @@ -669,50 +498,119 @@ export class PhaseManager { } /** - * Create a new phase and immediately prepend it to an existing phase in the phase queue. - * Equivalent to calling {@linkcode create} followed by {@linkcode prependToPhase}. - * @param targetPhase - The phase to search for in phaseQueue - * @param phase - The name of the phase to create + * Add a {@linkcode FaintPhase} to the queue * @param args - The arguments to pass to the phase constructor - * @returns `true` if a `targetPhase` was found to prepend to + * + * @remarks + * + * Faint phases are ordered in a special way to allow battle effects to settle before the pokemon faints. + * @see {@linkcode PhaseTree.addPhase} */ - public prependNewToPhase( - targetPhase: PhaseString, - phase: T, - ...args: ConstructorParameters - ): boolean { - return this.prependToPhase(this.create(phase, ...args), targetPhase); + public queueFaintPhase(...args: ConstructorParameters): void { + this.phaseQueue.addPhase(this.create("FaintPhase", ...args), true); } /** - * Create a new phase and immediately append it to an existing phase the phase queue. - * Equivalent to calling {@linkcode create} followed by {@linkcode appendToPhase}. - * @param targetPhase - The phase to search for in phaseQueue - * @param phase - The name of the phase to create - * @param args - The arguments to pass to the phase constructor - * @returns `true` if a `targetPhase` was found to append to + * Attempts to add {@linkcode PostSummonPhase}s for the enemy pokemon + * + * This is used to ensure that wild pokemon (which have no {@linkcode SummonPhase}) do not queue a {@linkcode PostSummonPhase} + * until all pokemon are on the field. */ - public appendNewToPhase( - targetPhase: PhaseString, - phase: T, - ...args: ConstructorParameters - ): boolean { - return this.appendToPhase(this.create(phase, ...args), targetPhase); + public tryAddEnemyPostSummonPhases(): void { + if ( + ![BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(globalScene.currentBattle.battleType) + && !this.phaseQueue.exists("SummonPhase") + ) { + globalScene.getEnemyField().forEach(p => { + this.pushPhase(new PostSummonPhase(p.getBattlerIndex(), "SummonPhase")); + }); + } } - public startNewDynamicPhase( + /** + * Create a new phase and queue it to run after all others queued by the currently running phase. + * @param phase - The name of the phase to create + * @param args - The arguments to pass to the phase constructor + * + * @deprecated Only used for switches and should be phased out eventually. + */ + public queueDeferred( phase: T, ...args: ConstructorParameters ): void { - this.startDynamicPhase(this.create(phase, ...args)); + this.phaseQueue.unshiftToCurrent(this.create(phase, ...args)); + } + + /** + * Finds the first {@linkcode MovePhase} meeting the condition + * @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function + * @returns The MovePhase, or `undefined` if it does not exist + */ + public getMovePhase(phaseCondition: PhaseConditionFunc<"MovePhase">): MovePhase | undefined { + return this.dynamicQueueManager.getMovePhase(phaseCondition); + } + + /** + * Finds and cancels the first {@linkcode MovePhase} meeting the condition + * @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function + */ + public cancelMove(phaseCondition: PhaseConditionFunc<"MovePhase">): void { + this.dynamicQueueManager.cancelMovePhase(phaseCondition); + } + + /** + * Finds the first {@linkcode MovePhase} meeting the condition and forces it next + * @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function + */ + public forceMoveNext(phaseCondition: PhaseConditionFunc<"MovePhase">): void { + this.dynamicQueueManager.setMoveTimingModifier(phaseCondition, MovePhaseTimingModifier.FIRST); + } + + /** + * Finds the first {@linkcode MovePhase} meeting the condition and forces it last + * @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function + */ + public forceMoveLast(phaseCondition: PhaseConditionFunc<"MovePhase">): void { + this.dynamicQueueManager.setMoveTimingModifier(phaseCondition, MovePhaseTimingModifier.LAST); + } + + /** + * Finds the first {@linkcode MovePhase} meeting the condition and changes its move + * @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function + * @param move - The {@linkcode PokemonMove | move} to use in replacement + */ + public changePhaseMove(phaseCondition: PhaseConditionFunc<"MovePhase">, move: PokemonMove): void { + this.dynamicQueueManager.setMoveForPhase(phaseCondition, move); + } + + /** + * Redirects moves which were targeted at a {@linkcode Pokemon} that has been removed + * @param removedPokemon - The removed {@linkcode Pokemon} + * @param allyPokemon - The ally of the removed pokemon + */ + public redirectMoves(removedPokemon: Pokemon, allyPokemon: Pokemon): void { + this.dynamicQueueManager.redirectMoves(removedPokemon, allyPokemon); + } + + /** Queues phases which run at the end of each turn */ + public queueTurnEndPhases(): void { + turnEndPhases.forEach(p => { + this.pushNew(p); + }); } /** Prevents end of turn effects from triggering when transitioning to a new biome on a X0 wave */ public onInterlude(): void { - const phasesToRemove = ["WeatherEffectPhase", "BerryPhase", "CheckStatusEffectPhase"]; - this.phaseQueue = this.phaseQueue.filter(p => !phasesToRemove.includes(p.phaseName)); + const phasesToRemove: readonly PhaseString[] = [ + "WeatherEffectPhase", + "BerryPhase", + "CheckStatusEffectPhase", + ] as const; + for (const phaseType of phasesToRemove) { + this.phaseQueue.removeAll(phaseType); + } - const turnEndPhase = this.findPhase(p => p.phaseName === "TurnEndPhase"); + const turnEndPhase = this.phaseQueue.find("TurnEndPhase"); if (turnEndPhase) { turnEndPhase.upcomingInterlude = true; } diff --git a/src/phase-tree.ts b/src/phase-tree.ts new file mode 100644 index 00000000000..69bb72ca4f0 --- /dev/null +++ b/src/phase-tree.ts @@ -0,0 +1,205 @@ +// biome-ignore-start lint/correctness/noUnusedImports: TSDoc imports +import type { PhaseManager } from "#app/@types/phase-types"; +import type { DynamicPhaseMarker } from "#phases/dynamic-phase-marker"; +// biome-ignore-end lint/correctness/noUnusedImports: TSDoc imports + +import type { PhaseMap, PhaseString } from "#app/@types/phase-types"; +import type { Phase } from "#app/phase"; +import type { PhaseConditionFunc } from "#types/phase-types"; + +/** + * The PhaseTree is the central storage location for {@linkcode Phase}s by the {@linkcode PhaseManager}. + * + * It has a tiered structure, where unshifted phases are added one level above the currently running Phase. Phases are generally popped from the Tree in FIFO order. + * + * Dynamically ordered phases are queued into the Tree only as {@linkcode DynamicPhaseMarker | Marker}s and as such are not guaranteed to run FIFO (otherwise, they would not be dynamic) + */ +export class PhaseTree { + /** Storage for all levels in the tree. This is a simple array because only one Phase may have "children" at a time. */ + private levels: Phase[][] = [[]]; + /** The level of the currently running {@linkcode Phase} in the Tree (note that such phase is not actually in the Tree while it is running) */ + private currentLevel = 0; + /** + * True if a "deferred" level exists + * @see {@linkcode addPhase} + */ + private deferredActive = false; + + /** + * Adds a {@linkcode Phase} to the specified level + * @param phase - The phase to add + * @param level - The numeric level to add the phase + * @throws Error if `level` is out of legal bounds + */ + private add(phase: Phase, level: number): void { + const addLevel = this.levels[level]; + if (addLevel == null) { + throw new Error("Attempted to add a phase to a nonexistent level of the PhaseTree!\nLevel: " + level.toString()); + } + this.levels[level].push(phase); + } + + /** + * Used by the {@linkcode PhaseManager} to add phases to the Tree + * @param phase - The {@linkcode Phase} to be added + * @param defer - Whether to defer the execution of this phase by allowing subsequently-added phases to run before it + * + * @privateRemarks + * Deferral is implemented by moving the queue at {@linkcode currentLevel} up one level and inserting the new phase below it. + * {@linkcode deferredActive} is set until the moved queue (and anything added to it) is exhausted. + * + * If {@linkcode deferredActive} is `true` when a deferred phase is added, the phase will be pushed to the second-highest level queue. + * That is, it will execute after the originally deferred phase, but there is no possibility for nesting with deferral. + * + * @todo `setPhaseQueueSplice` had strange behavior. This is simpler, but there are probably some remnant edge cases with the current implementation + */ + public addPhase(phase: Phase, defer = false): void { + if (defer && !this.deferredActive) { + this.deferredActive = true; + this.levels.splice(-1, 0, []); + } + this.add(phase, this.levels.length - 1 - +defer); + } + + /** + * Adds a {@linkcode Phase} after the first occurence of the given type, or to the top of the Tree if no such phase exists + * @param phase - The {@linkcode Phase} to be added + * @param type - A {@linkcode PhaseString} representing the type to search for + */ + public addAfter(phase: Phase, type: PhaseString): void { + for (let i = this.levels.length - 1; i >= 0; i--) { + const insertIdx = this.levels[i].findIndex(p => p.is(type)) + 1; + if (insertIdx !== 0) { + this.levels[i].splice(insertIdx, 0, phase); + return; + } + } + + this.addPhase(phase); + } + + /** + * Unshifts a {@linkcode Phase} to the current level. + * This is effectively the same as if the phase were added immediately after the currently-running phase, before it started. + * @param phase - The {@linkcode Phase} to be added + */ + public unshiftToCurrent(phase: Phase): void { + this.levels[this.currentLevel].unshift(phase); + } + + /** + * Pushes a {@linkcode Phase} to the last level of the queue. It will run only after all previously queued phases have been executed. + * @param phase - The {@linkcode Phase} to be added + */ + public pushPhase(phase: Phase): void { + this.add(phase, 0); + } + + /** + * Removes and returns the first {@linkcode Phase} from the topmost level of the tree + * @returns - The next {@linkcode Phase}, or `undefined` if the Tree is empty + */ + public getNextPhase(): Phase | undefined { + this.currentLevel = this.levels.length - 1; + while (this.currentLevel > 0 && this.levels[this.currentLevel].length === 0) { + this.deferredActive = false; + this.levels.pop(); + this.currentLevel--; + } + + // TODO: right now, this is preventing properly marking when one set of unshifted phases ends + this.levels.push([]); + return this.levels[this.currentLevel].shift(); + } + + /** + * Finds a particular {@linkcode Phase} in the Tree by searching in pop order + * @param phaseType - The {@linkcode PhaseString | type} of phase to search for + * @param phaseFilter - A {@linkcode PhaseConditionFunc} to specify conditions for the phase + * @returns The matching {@linkcode Phase}, or `undefined` if none exists + */ + public find

(phaseType: P, phaseFilter?: PhaseConditionFunc

): PhaseMap[P] | undefined { + for (let i = this.levels.length - 1; i >= 0; i--) { + const level = this.levels[i]; + const phase = level.find((p): p is PhaseMap[P] => p.is(phaseType) && (!phaseFilter || phaseFilter(p))); + if (phase) { + return phase; + } + } + } + + /** + * Finds a particular {@linkcode Phase} in the Tree by searching in pop order + * @param phaseType - The {@linkcode PhaseString | type} of phase to search for + * @param phaseFilter - A {@linkcode PhaseConditionFunc} to specify conditions for the phase + * @returns The matching {@linkcode Phase}, or `undefined` if none exists + */ + public findAll

(phaseType: P, phaseFilter?: PhaseConditionFunc

): PhaseMap[P][] { + const phases: PhaseMap[P][] = []; + for (let i = this.levels.length - 1; i >= 0; i--) { + const level = this.levels[i]; + const levelPhases = level.filter((p): p is PhaseMap[P] => p.is(phaseType) && (!phaseFilter || phaseFilter(p))); + phases.push(...levelPhases); + } + return phases; + } + + /** + * Clears the Tree + * @param leaveFirstLevel - If `true`, leaves the top level of the tree intact + * + * @privateremarks + * The parameter on this method exists because {@linkcode PhaseManager.clearPhaseQueue} previously (probably by mistake) ignored `phaseQueuePrepend`. + * + * This is (probably by mistake) relied upon by certain ME functions. + */ + public clear(leaveFirstLevel = false) { + this.levels = [leaveFirstLevel ? (this.levels.at(-1) ?? []) : []]; + } + + /** + * Finds and removes a single {@linkcode Phase} from the Tree + * @param phaseType - The {@linkcode PhaseString | type} of phase to search for + * @param phaseFilter - A {@linkcode PhaseConditionFunc} to specify conditions for the phase + * @returns Whether a removal occurred + */ + public remove

(phaseType: P, phaseFilter?: PhaseConditionFunc

): boolean { + for (let i = this.levels.length - 1; i >= 0; i--) { + const level = this.levels[i]; + const phaseIndex = level.findIndex(p => p.is(phaseType) && (!phaseFilter || phaseFilter(p))); + if (phaseIndex !== -1) { + level.splice(phaseIndex, 1); + return true; + } + } + return false; + } + + /** + * Removes all occurrences of {@linkcode Phase}s of the given type + * @param phaseType - The {@linkcode PhaseString | type} of phase to search for + */ + public removeAll(phaseType: PhaseString): void { + for (let i = 0; i < this.levels.length; i++) { + const level = this.levels[i].filter(phase => !phase.is(phaseType)); + this.levels[i] = level; + } + } + + /** + * Determines if a particular phase exists in the Tree + * @param phaseType - The {@linkcode PhaseString | type} of phase to search for + * @param phaseFilter - A {@linkcode PhaseConditionFunc} to specify conditions for the phase + * @returns Whether a matching phase exists + */ + public exists

(phaseType: P, phaseFilter?: PhaseConditionFunc

): boolean { + for (const level of this.levels) { + for (const phase of level) { + if (phase.is(phaseType) && (!phaseFilter || phaseFilter(phase))) { + return true; + } + } + } + return false; + } +} diff --git a/src/phases/activate-priority-queue-phase.ts b/src/phases/activate-priority-queue-phase.ts deleted file mode 100644 index a31d3291a60..00000000000 --- a/src/phases/activate-priority-queue-phase.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { globalScene } from "#app/global-scene"; -import { Phase } from "#app/phase"; -import type { DynamicPhaseType } from "#enums/dynamic-phase-type"; - -export class ActivatePriorityQueuePhase extends Phase { - public readonly phaseName = "ActivatePriorityQueuePhase"; - private type: DynamicPhaseType; - - constructor(type: DynamicPhaseType) { - super(); - this.type = type; - } - - override start() { - super.start(); - globalScene.phaseManager.startDynamicPhaseType(this.type); - this.end(); - } - - public getType(): DynamicPhaseType { - return this.type; - } -} diff --git a/src/phases/attempt-capture-phase.ts b/src/phases/attempt-capture-phase.ts index 81f85850e88..2eadae244d5 100644 --- a/src/phases/attempt-capture-phase.ts +++ b/src/phases/attempt-capture-phase.ts @@ -21,9 +21,9 @@ import type { EnemyPokemon } from "#field/pokemon"; import { PokemonHeldItemModifier } from "#modifiers/modifier"; import { PokemonPhase } from "#phases/pokemon-phase"; import { achvs } from "#system/achv"; -import type { PartyOption } from "#ui/handlers/party-ui-handler"; -import { PartyUiMode } from "#ui/handlers/party-ui-handler"; -import { SummaryUiMode } from "#ui/handlers/summary-ui-handler"; +import type { PartyOption } from "#ui/party-ui-handler"; +import { PartyUiMode } from "#ui/party-ui-handler"; +import { SummaryUiMode } from "#ui/summary-ui-handler"; import { applyChallenges } from "#utils/challenge-utils"; import { BooleanHolder } from "#utils/common"; import i18next from "i18next"; diff --git a/src/phases/battle-end-phase.ts b/src/phases/battle-end-phase.ts index 8a798d67554..45b0db76ced 100644 --- a/src/phases/battle-end-phase.ts +++ b/src/phases/battle-end-phase.ts @@ -18,23 +18,11 @@ export class BattleEndPhase extends BattlePhase { super.start(); // cull any extra `BattleEnd` phases from the queue. - globalScene.phaseManager.phaseQueue = globalScene.phaseManager.phaseQueue.filter(phase => { - if (phase.is("BattleEndPhase")) { - this.isVictory ||= phase.isVictory; - return false; - } - return true; - }); - // `phaseQueuePrepend` is private, so we have to use this inefficient loop. - while ( - globalScene.phaseManager.tryRemoveUnshiftedPhase(phase => { - if (phase.is("BattleEndPhase")) { - this.isVictory ||= phase.isVictory; - return true; - } - return false; - }) - ) {} + this.isVictory ||= globalScene.phaseManager.hasPhaseOfType( + "BattleEndPhase", + (phase: BattleEndPhase) => phase.isVictory, + ); + globalScene.phaseManager.removeAllPhasesOfType("BattleEndPhase"); globalScene.gameData.gameStats.battles++; if ( diff --git a/src/phases/check-status-effect-phase.ts b/src/phases/check-status-effect-phase.ts index bdaa536986a..5955cd42c55 100644 --- a/src/phases/check-status-effect-phase.ts +++ b/src/phases/check-status-effect-phase.ts @@ -1,20 +1,14 @@ import { globalScene } from "#app/global-scene"; import { Phase } from "#app/phase"; -import type { BattlerIndex } from "#enums/battler-index"; export class CheckStatusEffectPhase extends Phase { public readonly phaseName = "CheckStatusEffectPhase"; - private order: BattlerIndex[]; - constructor(order: BattlerIndex[]) { - super(); - this.order = order; - } start() { const field = globalScene.getField(); - for (const o of this.order) { - if (field[o].status?.isPostTurn()) { - globalScene.phaseManager.unshiftNew("PostTurnStatusEffectPhase", o); + for (const p of field) { + if (p?.status?.isPostTurn()) { + globalScene.phaseManager.unshiftNew("PostTurnStatusEffectPhase", p.getBattlerIndex()); } } this.end(); diff --git a/src/phases/check-switch-phase.ts b/src/phases/check-switch-phase.ts index 504bb6eb4bd..a55db4203bc 100644 --- a/src/phases/check-switch-phase.ts +++ b/src/phases/check-switch-phase.ts @@ -28,7 +28,8 @@ export class CheckSwitchPhase extends BattlePhase { // ...if the user is playing in Set Mode if (globalScene.battleStyle === BattleStyle.SET) { - return super.end(); + this.end(true); + return; } // ...if the checked Pokemon is somehow not on the field @@ -44,7 +45,8 @@ export class CheckSwitchPhase extends BattlePhase { .slice(1) .filter(p => p.isActive()).length === 0 ) { - return super.end(); + this.end(true); + return; } // ...or if any player Pokemon has an effect that prevents the checked Pokemon from switching @@ -53,7 +55,8 @@ export class CheckSwitchPhase extends BattlePhase { || pokemon.isTrapped() || globalScene.getPlayerField().some(p => p.getTag(BattlerTagType.COMMANDED)) ) { - return super.end(); + this.end(true); + return; } globalScene.ui.showText( @@ -71,10 +74,17 @@ export class CheckSwitchPhase extends BattlePhase { }, () => { globalScene.ui.setMode(UiMode.MESSAGE); - this.end(); + this.end(true); }, ); }, ); } + + public override end(queuePostSummon = false): void { + if (queuePostSummon) { + globalScene.phaseManager.unshiftNew("PostSummonPhase", this.fieldIndex); + } + super.end(); + } } diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index b9867e22522..2bf845776ca 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -220,7 +220,7 @@ export class CommandPhase extends FieldPhase { if (!moveStatus.value) { cannotSelectKey = "battle:moveCannotUseChallenge"; } else if (move.getPpRatio() === 0) { - cannotSelectKey = "battle:moveNoPP"; + cannotSelectKey = "battle:moveNoPp"; } else if (move.getName().endsWith(" (N)")) { cannotSelectKey = "battle:moveNotImplemented"; } else if (user.isMoveRestricted(move.moveId, user)) { diff --git a/src/phases/dynamic-phase-marker.ts b/src/phases/dynamic-phase-marker.ts new file mode 100644 index 00000000000..e2b241f29de --- /dev/null +++ b/src/phases/dynamic-phase-marker.ts @@ -0,0 +1,17 @@ +import type { PhaseString } from "#app/@types/phase-types"; +import { Phase } from "#app/phase"; + +/** + * This phase exists for the sole purpose of marking the location and type of a dynamic phase for the phase manager + */ +export class DynamicPhaseMarker extends Phase { + public override readonly phaseName = "DynamicPhaseMarker"; + + /** The type of phase which this phase is a marker for */ + public phaseType: PhaseString; + + constructor(type: PhaseString) { + super(); + this.phaseType = type; + } +} diff --git a/src/phases/egg-hatch-phase.ts b/src/phases/egg-hatch-phase.ts index 68c60abc0dc..3f9b999e0c1 100644 --- a/src/phases/egg-hatch-phase.ts +++ b/src/phases/egg-hatch-phase.ts @@ -9,9 +9,9 @@ import { doShinySparkleAnim } from "#field/anims"; import type { PlayerPokemon } from "#field/pokemon"; import type { EggLapsePhase } from "#phases/egg-lapse-phase"; import { achvs } from "#system/achv"; -import { EggCounterContainer } from "#ui/containers/egg-counter-container"; -import { PokemonInfoContainer } from "#ui/containers/pokemon-info-container"; -import type { EggHatchSceneHandler } from "#ui/handlers/egg-hatch-scene-handler"; +import { EggCounterContainer } from "#ui/egg-counter-container"; +import type { EggHatchSceneUiHandler } from "#ui/egg-hatch-scene-ui-handler"; +import { PokemonInfoContainer } from "#ui/pokemon-info-container"; import { fixedInt, getFrameMs, randInt } from "#utils/common"; import i18next from "i18next"; import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; @@ -32,7 +32,7 @@ export class EggHatchPhase extends Phase { private eggCounterContainer: EggCounterContainer; /** The scene handler for egg hatching */ - private eggHatchHandler: EggHatchSceneHandler; + private eggHatchHandler: EggHatchSceneUiHandler; /** The phaser gameobject container that holds everything */ private eggHatchContainer: Phaser.GameObjects.Container; /** The phaser image that is the background */ @@ -92,7 +92,7 @@ export class EggHatchPhase extends Phase { globalScene.fadeOutBgm(undefined, false); - this.eggHatchHandler = globalScene.ui.getHandler() as EggHatchSceneHandler; + this.eggHatchHandler = globalScene.ui.getHandler() as EggHatchSceneUiHandler; this.eggHatchContainer = this.eggHatchHandler.eggHatchContainer; @@ -225,7 +225,7 @@ export class EggHatchPhase extends Phase { } end() { - if (globalScene.phaseManager.findPhase(p => p.is("EggHatchPhase"))) { + if (globalScene.phaseManager.hasPhaseOfType("EggHatchPhase")) { this.eggHatchHandler.clear(); } else { globalScene.time.delayedCall(250, () => globalScene.setModifiersVisible(true)); diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index 0918ced65e5..9345170e718 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -565,29 +565,6 @@ export class EncounterPhase extends BattlePhase { }); if (![BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(globalScene.currentBattle.battleType)) { - enemyField.map(p => - globalScene.phaseManager.pushConditionalPhase( - globalScene.phaseManager.create("PostSummonPhase", p.getBattlerIndex()), - () => { - // if there is not a player party, we can't continue - if (globalScene.getPlayerParty().length === 0) { - return false; - } - // how many player pokemon are on the field ? - const pokemonsOnFieldCount = globalScene.getPlayerParty().filter(p => p.isOnField()).length; - // if it's a 2vs1, there will never be a 2nd pokemon on our field even - const requiredPokemonsOnField = Math.min( - globalScene.getPlayerParty().filter(p => !p.isFainted()).length, - 2, - ); - // if it's a double, there should be 2, otherwise 1 - if (globalScene.currentBattle.double) { - return pokemonsOnFieldCount === requiredPokemonsOnField; - } - return pokemonsOnFieldCount === 1; - }, - ), - ); const ivScannerModifier = globalScene.findModifier(m => m instanceof IvScannerModifier); if (ivScannerModifier) { enemyField.map(p => globalScene.phaseManager.pushNew("ScanIvsPhase", p.getBattlerIndex())); @@ -596,36 +573,30 @@ export class EncounterPhase extends BattlePhase { if (!this.loaded) { const availablePartyMembers = globalScene.getPokemonAllowedInBattle(); + const minPartySize = globalScene.currentBattle.double ? 2 : 1; + const currentBattle = globalScene.currentBattle; + const checkSwitch = + currentBattle.battleType !== BattleType.TRAINER + && (currentBattle.waveIndex > 1 || !globalScene.gameMode.isDaily) + && availablePartyMembers.length > minPartySize; + const phaseManager = globalScene.phaseManager; if (!availablePartyMembers[0].isOnField()) { - globalScene.phaseManager.pushNew("SummonPhase", 0); + phaseManager.pushNew("SummonPhase", 0, true, false, checkSwitch); } - if (globalScene.currentBattle.double) { + if (currentBattle.double) { if (availablePartyMembers.length > 1) { - globalScene.phaseManager.pushNew("ToggleDoublePositionPhase", true); + phaseManager.pushNew("ToggleDoublePositionPhase", true); if (!availablePartyMembers[1].isOnField()) { - globalScene.phaseManager.pushNew("SummonPhase", 1); + phaseManager.pushNew("SummonPhase", 1, true, false, checkSwitch); } } } else { if (availablePartyMembers.length > 1 && availablePartyMembers[1].isOnField()) { globalScene.phaseManager.pushNew("ReturnPhase", 1); } - globalScene.phaseManager.pushNew("ToggleDoublePositionPhase", false); - } - - if ( - globalScene.currentBattle.battleType !== BattleType.TRAINER - && (globalScene.currentBattle.waveIndex > 1 || !globalScene.gameMode.isDaily) - ) { - const minPartySize = globalScene.currentBattle.double ? 2 : 1; - if (availablePartyMembers.length > minPartySize) { - globalScene.phaseManager.pushNew("CheckSwitchPhase", 0, globalScene.currentBattle.double); - if (globalScene.currentBattle.double) { - globalScene.phaseManager.pushNew("CheckSwitchPhase", 1, globalScene.currentBattle.double); - } - } + phaseManager.pushNew("ToggleDoublePositionPhase", false); } } handleTutorial(Tutorial.Access_Menu).then(() => super.end()); diff --git a/src/phases/evolution-phase.ts b/src/phases/evolution-phase.ts index 7d7301bbeca..80750beeb68 100644 --- a/src/phases/evolution-phase.ts +++ b/src/phases/evolution-phase.ts @@ -10,7 +10,7 @@ import { LearnMoveSituation } from "#enums/learn-move-situation"; import { UiMode } from "#enums/ui-mode"; import { cos, sin } from "#field/anims"; import type { PlayerPokemon, Pokemon } from "#field/pokemon"; -import type { EvolutionSceneHandler } from "#ui/handlers/evolution-scene-handler"; +import type { EvolutionSceneUiHandler } from "#ui/evolution-scene-ui-handler"; import { fixedInt, getFrameMs, randInt } from "#utils/common"; import i18next from "i18next"; import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; @@ -29,7 +29,7 @@ export class EvolutionPhase extends Phase { private evolution: SpeciesFormEvolution | null; private fusionSpeciesEvolved: boolean; // Whether the evolution is of the fused species private evolutionBgm: AnySound | null; - private evolutionHandler: EvolutionSceneHandler; + private evolutionHandler: EvolutionSceneUiHandler; /** Container for all assets used by the scene. When the scene is cleared, the children within this are destroyed. */ protected evolutionContainer: Phaser.GameObjects.Container; @@ -79,7 +79,7 @@ export class EvolutionPhase extends Phase { * */ private setupEvolutionAssets(): void { - this.evolutionHandler = globalScene.ui.getHandler() as EvolutionSceneHandler; + this.evolutionHandler = globalScene.ui.getHandler() as EvolutionSceneUiHandler; this.evolutionContainer = this.evolutionHandler.evolutionContainer; this.evolutionBaseBg = globalScene.add.image(0, 0, "default_bg").setOrigin(0); diff --git a/src/phases/faint-phase.ts b/src/phases/faint-phase.ts index 349dfcfa8e5..821d16c6546 100644 --- a/src/phases/faint-phase.ts +++ b/src/phases/faint-phase.ts @@ -17,7 +17,6 @@ import type { EnemyPokemon, PlayerPokemon, Pokemon } from "#field/pokemon"; import { PokemonInstantReviveModifier } from "#modifiers/modifier"; import { PokemonMove } from "#moves/pokemon-move"; import { PokemonPhase } from "#phases/pokemon-phase"; -import { isNullOrUndefined } from "#utils/common"; import i18next from "i18next"; export class FaintPhase extends PokemonPhase { @@ -187,7 +186,7 @@ export class FaintPhase extends PokemonPhase { // in double battles redirect potential moves off fainted pokemon const allyPokemon = pokemon.getAlly(); - if (globalScene.currentBattle.double && !isNullOrUndefined(allyPokemon)) { + if (globalScene.currentBattle.double && allyPokemon != null) { globalScene.redirectPokemonMoves(pokemon, allyPokemon); } diff --git a/src/phases/form-change-phase.ts b/src/phases/form-change-phase.ts index 7521adee6c9..4fb34079367 100644 --- a/src/phases/form-change-phase.ts +++ b/src/phases/form-change-phase.ts @@ -8,7 +8,7 @@ import { UiMode } from "#enums/ui-mode"; import type { PlayerPokemon, Pokemon } from "#field/pokemon"; import { EvolutionPhase } from "#phases/evolution-phase"; import { achvs } from "#system/achv"; -import type { PartyUiHandler } from "#ui/handlers/party-ui-handler"; +import type { PartyUiHandler } from "#ui/party-ui-handler"; import { fixedInt } from "#utils/common"; export class FormChangePhase extends EvolutionPhase { diff --git a/src/phases/game-over-phase.ts b/src/phases/game-over-phase.ts index 57ebd40b559..f229f872958 100644 --- a/src/phases/game-over-phase.ts +++ b/src/phases/game-over-phase.ts @@ -16,13 +16,13 @@ import type { EndCardPhase } from "#phases/end-card-phase"; import { achvs, ChallengeAchv } from "#system/achv"; import { ArenaData } from "#system/arena-data"; import { ChallengeData } from "#system/challenge-data"; -import type { SessionSaveData } from "#system/game-data"; import { ModifierData as PersistentModifierData } from "#system/modifier-data"; import { PokemonData } from "#system/pokemon-data"; import { RibbonData, type RibbonFlag } from "#system/ribbons/ribbon-data"; import { awardRibbonsToSpeciesLine } from "#system/ribbons/ribbon-methods"; import { TrainerData } from "#system/trainer-data"; import { trainerConfigs } from "#trainers/trainer-config"; +import type { SessionSaveData } from "#types/save-data"; import { checkSpeciesValidForChallenge, isNuzlockeChallenge } from "#utils/challenge-utils"; import { isLocal, isLocalServerConnected } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; @@ -84,19 +84,12 @@ export class GameOverPhase extends BattlePhase { globalScene.phaseManager.pushNew("EncounterPhase", true); const availablePartyMembers = globalScene.getPokemonAllowedInBattle().length; - - globalScene.phaseManager.pushNew("SummonPhase", 0); - if (globalScene.currentBattle.double && availablePartyMembers > 1) { - globalScene.phaseManager.pushNew("SummonPhase", 1); - } - if ( + const checkSwitch = globalScene.currentBattle.waveIndex > 1 - && globalScene.currentBattle.battleType !== BattleType.TRAINER - ) { - globalScene.phaseManager.pushNew("CheckSwitchPhase", 0, globalScene.currentBattle.double); - if (globalScene.currentBattle.double && availablePartyMembers > 1) { - globalScene.phaseManager.pushNew("CheckSwitchPhase", 1, globalScene.currentBattle.double); - } + && globalScene.currentBattle.battleType !== BattleType.TRAINER; + globalScene.phaseManager.pushNew("SummonPhase", 0, true, false, checkSwitch); + if (globalScene.currentBattle.double && availablePartyMembers > 1) { + globalScene.phaseManager.pushNew("SummonPhase", 1, true, false, checkSwitch); } globalScene.ui.fadeIn(1250); @@ -267,7 +260,6 @@ export class GameOverPhase extends BattlePhase { .then(success => doGameOver(!globalScene.gameMode.isDaily || !!success)) .catch(_err => { globalScene.phaseManager.clearPhaseQueue(); - globalScene.phaseManager.clearPhaseQueueSplice(); globalScene.phaseManager.unshiftNew("MessagePhase", i18next.t("menu:serverCommunicationFailed"), 2500); // force the game to reload after 2 seconds. setTimeout(() => { diff --git a/src/phases/learn-move-phase.ts b/src/phases/learn-move-phase.ts index a16e12ba058..bbd1d0f5a2e 100644 --- a/src/phases/learn-move-phase.ts +++ b/src/phases/learn-move-phase.ts @@ -10,8 +10,8 @@ import { UiMode } from "#enums/ui-mode"; import type { Pokemon } from "#field/pokemon"; import type { Move } from "#moves/move"; import { PlayerPartyMemberPokemonPhase } from "#phases/player-party-member-pokemon-phase"; -import { EvolutionSceneHandler } from "#ui/handlers/evolution-scene-handler"; -import { SummaryUiMode } from "#ui/handlers/summary-ui-handler"; +import { EvolutionSceneUiHandler } from "#ui/evolution-scene-ui-handler"; +import { SummaryUiMode } from "#ui/summary-ui-handler"; import i18next from "i18next"; export class LearnMovePhase extends PlayerPartyMemberPokemonPhase { @@ -47,7 +47,7 @@ export class LearnMovePhase extends PlayerPartyMemberPokemonPhase { } this.messageMode = - globalScene.ui.getHandler() instanceof EvolutionSceneHandler ? UiMode.EVOLUTION_SCENE : UiMode.MESSAGE; + globalScene.ui.getHandler() instanceof EvolutionSceneUiHandler ? UiMode.EVOLUTION_SCENE : UiMode.MESSAGE; globalScene.ui.setMode(this.messageMode); // If the Pokemon has less than 4 moves, the new move is added to the largest empty moveset index // If it has 4 moves, the phase then checks if the player wants to replace the move itself. @@ -187,7 +187,7 @@ export class LearnMovePhase extends PlayerPartyMemberPokemonPhase { pokemon.usedTMs = []; } pokemon.usedTMs.push(this.moveId); - globalScene.phaseManager.tryRemovePhase(phase => phase.is("SelectModifierPhase")); + globalScene.phaseManager.tryRemovePhase("SelectModifierPhase"); } else if (this.learnMoveType === LearnMoveType.MEMORY) { if (this.cost !== -1) { if (!Overrides.WAIVE_ROLL_FEE_OVERRIDE) { @@ -197,7 +197,7 @@ export class LearnMovePhase extends PlayerPartyMemberPokemonPhase { } globalScene.playSound("se/buy"); } else { - globalScene.phaseManager.tryRemovePhase(phase => phase.is("SelectModifierPhase")); + globalScene.phaseManager.tryRemovePhase("SelectModifierPhase"); } } pokemon.setMove(index, this.moveId); diff --git a/src/phases/login-phase.ts b/src/phases/login-phase.ts index f310c60b0d4..d81b9b614f2 100644 --- a/src/phases/login-phase.ts +++ b/src/phases/login-phase.ts @@ -33,7 +33,7 @@ export class LoginPhase extends Phase { globalScene.ui.showText(i18next.t("menu:logInOrCreateAccount")); } - globalScene.playSound("menu_open"); + globalScene.playSound("ui/menu_open"); const loadData = () => { updateUserInfo().then(success => { @@ -53,7 +53,7 @@ export class LoginPhase extends Phase { loadData(); }, () => { - globalScene.playSound("menu_open"); + globalScene.playSound("ui/menu_open"); globalScene.ui.setMode(UiMode.REGISTRATION_FORM, { buttonActions: [ () => { diff --git a/src/phases/move-charge-phase.ts b/src/phases/move-charge-phase.ts index 0c83db10511..5dd75f4bab8 100644 --- a/src/phases/move-charge-phase.ts +++ b/src/phases/move-charge-phase.ts @@ -75,7 +75,7 @@ export class MoveChargePhase extends PokemonPhase { // Otherwise, add the attack portion to the user's move queue to execute next turn. // TODO: This checks status twice for a single-turn usage... if (instantCharge.value) { - globalScene.phaseManager.tryRemovePhase(phase => phase.is("MoveEndPhase") && phase.getPokemon() === user); + globalScene.phaseManager.tryRemovePhase("MoveEndPhase", phase => phase.getPokemon() === user); globalScene.phaseManager.unshiftNew("MovePhase", user, [this.targetIndex], this.move, this.useMode); } else { user.pushMoveQueue({ move: move.id, targets: [this.targetIndex], useMode: this.useMode }); diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 0600f7d5ecf..be6d0164698 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -1,7 +1,6 @@ import { applyAbAttrs } from "#abilities/apply-ab-attrs"; import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; -import type { Phase } from "#app/phase"; import { ConditionalProtectTag } from "#data/arena-tag"; import { MoveAnim } from "#data/battle-anims"; import { DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag, TypeBoostTag } from "#data/battler-tags"; @@ -17,6 +16,7 @@ import { MoveCategory } from "#enums/move-category"; import { MoveEffectTrigger } from "#enums/move-effect-trigger"; import { MoveFlags } from "#enums/move-flags"; import { MoveId } from "#enums/move-id"; +import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; import { MoveResult } from "#enums/move-result"; import { MoveTarget } from "#enums/move-target"; import { isReflected, MoveUseMode } from "#enums/move-use-mode"; @@ -40,7 +40,7 @@ import { DamageAchv } from "#system/achv"; import type { DamageResult } from "#types/damage-result"; import type { TurnMove } from "#types/turn-move"; import type { nil } from "#utils/common"; -import { BooleanHolder, isNullOrUndefined, NumberHolder } from "#utils/common"; +import { BooleanHolder, NumberHolder } from "#utils/common"; import i18next from "i18next"; export type HitCheckEntry = [HitCheckResult, TypeDamageMultiplier]; @@ -67,12 +67,6 @@ export class MoveEffectPhase extends PokemonPhase { /** Is this the last strike of a move? */ private lastHit: boolean; - /** - * Phases queued during moves; used to add a new MovePhase for reflected moves after triggering. - * TODO: Remove this and move the reflection logic to ability-side - */ - private queuedPhases: Phase[] = []; - /** * @param useMode - The {@linkcode MoveUseMode} corresponding to how this move was used. */ @@ -148,7 +142,7 @@ export class MoveEffectPhase extends PokemonPhase { } /** - * Queue the phaes that should occur when the target reflects the move back to the user + * Queue the phases that should occur when the target reflects the move back to the user * @param user - The {@linkcode Pokemon} using this phase's invoked move * @param target - The {@linkcode Pokemon} that is reflecting the move * TODO: Rework this to use `onApply` of Magic Coat @@ -159,24 +153,21 @@ export class MoveEffectPhase extends PokemonPhase { : [user.getBattlerIndex()]; // TODO: ability displays should be handled by the ability if (!target.getTag(BattlerTagType.MAGIC_COAT)) { - this.queuedPhases.push( - globalScene.phaseManager.create( - "ShowAbilityPhase", - target.getBattlerIndex(), - target.getPassiveAbility().hasAttr("ReflectStatusMoveAbAttr"), - ), + globalScene.phaseManager.unshiftNew( + "ShowAbilityPhase", + target.getBattlerIndex(), + target.getPassiveAbility().hasAttr("ReflectStatusMoveAbAttr"), ); - this.queuedPhases.push(globalScene.phaseManager.create("HideAbilityPhase")); + globalScene.phaseManager.unshiftNew("HideAbilityPhase"); } - this.queuedPhases.push( - globalScene.phaseManager.create( - "MovePhase", - target, - newTargets, - new PokemonMove(this.move.id), - MoveUseMode.REFLECTED, - ), + globalScene.phaseManager.unshiftNew( + "MovePhase", + target, + newTargets, + new PokemonMove(this.move.id), + MoveUseMode.REFLECTED, + MovePhaseTimingModifier.FIRST, ); } @@ -344,9 +335,6 @@ export class MoveEffectPhase extends PokemonPhase { return; } - if (this.queuedPhases.length > 0) { - globalScene.phaseManager.appendToPhase(this.queuedPhases, "MoveEndPhase"); - } const moveType = user.getMoveType(this.move, true); if (this.move.category !== MoveCategory.STATUS && !user.stellarTypesBoosted.includes(moveType)) { user.stellarTypesBoosted.push(moveType); @@ -645,13 +633,18 @@ export class MoveEffectPhase extends PokemonPhase { return move.getAttrs("HitsTagAttr").some(hta => hta.tagType === semiInvulnerableTag.tagType); } - /** @returns The {@linkcode Pokemon} using this phase's invoked move */ - public getUserPokemon(): Pokemon | null { + /** + * @todo Investigate why this doesn't use `BattlerIndex` + * @returns The {@linkcode Pokemon} using this phase's invoked move + */ + public getUserPokemon(): Pokemon | undefined { // TODO: Make this purely a battler index if (this.battlerIndex > BattlerIndex.ENEMY_2) { return globalScene.getPokemonById(this.battlerIndex); } - return (this.player ? globalScene.getPlayerField() : globalScene.getEnemyField())[this.fieldIndex]; + // TODO: Figure out why this uses `fieldIndex` instead of `BattlerIndex` + // TODO: Remove `?? undefined` once field pokemon getters are made sane + return (this.player ? globalScene.getPlayerField() : globalScene.getEnemyField())[this.fieldIndex] ?? undefined; } /** @@ -740,7 +733,7 @@ export class MoveEffectPhase extends PokemonPhase { (attr: MoveAttr) => attr.is("MoveEffectAttr") && attr.trigger === triggerType - && (isNullOrUndefined(selfTarget) || attr.selfTarget === selfTarget) + && (selfTarget == null || attr.selfTarget === selfTarget) && (!attr.firstHitOnly || this.firstHit) && (!attr.lastHitOnly || this.lastHit) && (!attr.firstTargetOnly || (firstTarget ?? true)), @@ -765,7 +758,7 @@ export class MoveEffectPhase extends PokemonPhase { */ protected applyMoveEffects(target: Pokemon, effectiveness: TypeDamageMultiplier, firstTarget: boolean): void { const user = this.getUserPokemon(); - if (isNullOrUndefined(user)) { + if (user == null) { return; } @@ -900,10 +893,7 @@ export class MoveEffectPhase extends PokemonPhase { * @param target - The {@linkcode Pokemon} that fainted */ protected onFaintTarget(user: Pokemon, target: Pokemon): void { - // set splice index here, so future scene queues happen before FaintedPhase - globalScene.phaseManager.setPhaseQueueSplice(); - - globalScene.phaseManager.unshiftNew("FaintPhase", target.getBattlerIndex(), false, user); + globalScene.phaseManager.queueFaintPhase(target.getBattlerIndex(), false, user); target.destroySubstitute(); target.lapseTag(BattlerTagType.COMMANDED); @@ -932,7 +922,7 @@ export class MoveEffectPhase extends PokemonPhase { msg = i18next.t("battle:hitResultNotVeryEffective"); break; case HitResult.ONE_HIT_KO: - msg = i18next.t("battle:hitResultOneHitKO"); + msg = i18next.t("battle:hitResultOneHitKo"); break; } if (msg) { diff --git a/src/phases/move-header-phase.ts b/src/phases/move-header-phase.ts index 5c69dcd1217..5b8a6f998a1 100644 --- a/src/phases/move-header-phase.ts +++ b/src/phases/move-header-phase.ts @@ -5,8 +5,8 @@ import { BattlePhase } from "#phases/battle-phase"; export class MoveHeaderPhase extends BattlePhase { public readonly phaseName = "MoveHeaderPhase"; - public pokemon: Pokemon; public move: PokemonMove; + public pokemon: Pokemon; constructor(pokemon: Pokemon, move: PokemonMove) { super(); @@ -15,6 +15,10 @@ export class MoveHeaderPhase extends BattlePhase { this.move = move; } + public getPokemon(): Pokemon { + return this.pokemon; + } + canMove(): boolean { return this.pokemon.isActive(true) && this.move.isUsable(this.pokemon); } diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 96943065ff0..5e85401db77 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -3,6 +3,7 @@ import { MOVE_COLOR } from "#app/constants/colors"; import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; import Overrides from "#app/overrides"; +import { PokemonPhase } from "#app/phases/pokemon-phase"; import { CenterOfAttentionTag } from "#data/battler-tags"; import { SpeciesFormChangePreMoveTrigger } from "#data/form-change-triggers"; import { getStatusEffectActivationText, getStatusEffectHealText } from "#data/status-effect"; @@ -15,6 +16,7 @@ import { BattlerTagType } from "#enums/battler-tag-type"; import { CommonAnim } from "#enums/move-anims-common"; import { MoveFlags } from "#enums/move-flags"; import { MoveId } from "#enums/move-id"; +import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; import { MoveResult } from "#enums/move-result"; import { isIgnorePP, isIgnoreStatus, isReflected, isVirtual, MoveUseMode } from "#enums/move-use-mode"; import { PokemonType } from "#enums/pokemon-type"; @@ -24,20 +26,19 @@ import type { Pokemon } from "#field/pokemon"; import { applyMoveAttrs } from "#moves/apply-attrs"; import { frenzyMissFunc } from "#moves/move-utils"; import type { PokemonMove } from "#moves/pokemon-move"; -import { BattlePhase } from "#phases/battle-phase"; import type { TurnMove } from "#types/turn-move"; import { NumberHolder } from "#utils/common"; import { enumValueToKey } from "#utils/enums"; import i18next from "i18next"; -export class MovePhase extends BattlePhase { +export class MovePhase extends PokemonPhase { public readonly phaseName = "MovePhase"; protected _pokemon: Pokemon; - protected _move: PokemonMove; + public move: PokemonMove; protected _targets: BattlerIndex[]; public readonly useMode: MoveUseMode; // Made public for quash - /** Whether the current move is forced last (used for Quash). */ - protected forcedLast: boolean; + /** The timing modifier of the move (used by Quash and to force called moves to the front of their queue) */ + public timingModifier: MovePhaseTimingModifier; /** Whether the current move should fail but still use PP. */ protected failed = false; /** Whether the current move should fail and retain PP. */ @@ -59,14 +60,6 @@ export class MovePhase extends BattlePhase { this._pokemon = pokemon; } - public get move(): PokemonMove { - return this._move; - } - - protected set move(move: PokemonMove) { - this._move = move; - } - public get targets(): BattlerIndex[] { return this._targets; } @@ -81,16 +74,22 @@ export class MovePhase extends BattlePhase { * @param move - The {@linkcode PokemonMove} to use * @param useMode - The {@linkcode MoveUseMode} corresponding to this move's means of execution (usually `MoveUseMode.NORMAL`). * Not marked optional to ensure callers correctly pass on `useModes`. - * @param forcedLast - Whether to force this phase to occur last in order (for {@linkcode MoveId.QUASH}); default `false` + * @param timingModifier - The {@linkcode MovePhaseTimingModifier} for the move; Default {@linkcode MovePhaseTimingModifier.NORMAL} */ - constructor(pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, useMode: MoveUseMode, forcedLast = false) { - super(); + constructor( + pokemon: Pokemon, + targets: BattlerIndex[], + move: PokemonMove, + useMode: MoveUseMode, + timingModifier: MovePhaseTimingModifier = MovePhaseTimingModifier.NORMAL, + ) { + super(pokemon.getBattlerIndex()); this.pokemon = pokemon; this.targets = targets; this.move = move; this.useMode = useMode; - this.forcedLast = forcedLast; + this.timingModifier = timingModifier; this.moveHistoryEntry = { move: MoveId.NONE, targets, @@ -121,14 +120,6 @@ export class MovePhase extends BattlePhase { this.cancelled = true; } - /** - * Shows whether the current move has been forced to the end of the turn - * Needed for speed order, see {@linkcode MoveId.QUASH} - */ - public isForcedLast(): boolean { - return this.forcedLast; - } - public start(): void { super.start(); diff --git a/src/phases/mystery-encounter-phases.ts b/src/phases/mystery-encounter-phases.ts index 6a0c01f857a..bb3f4a92033 100644 --- a/src/phases/mystery-encounter-phases.ts +++ b/src/phases/mystery-encounter-phases.ts @@ -14,7 +14,7 @@ import type { OptionSelectSettings } from "#mystery-encounters/encounter-phase-u import { transitionMysteryEncounterIntroVisuals } from "#mystery-encounters/encounter-phase-utils"; import type { MysteryEncounterOption, OptionPhaseCallback } from "#mystery-encounters/mystery-encounter-option"; import { SeenEncounterData } from "#mystery-encounters/mystery-encounter-save-data"; -import { isNullOrUndefined, randSeedItem } from "#utils/common"; +import { randSeedItem } from "#utils/common"; import i18next from "i18next"; /** @@ -48,7 +48,6 @@ export class MysteryEncounterPhase extends Phase { // Clears out queued phases that are part of standard battle globalScene.phaseManager.clearPhaseQueue(); - globalScene.phaseManager.clearPhaseQueueSplice(); const encounter = globalScene.currentBattle.mysteryEncounter!; encounter.updateSeedOffset(); @@ -93,7 +92,7 @@ export class MysteryEncounterPhase extends Phase { if (option.onPreOptionPhase) { globalScene.executeWithSeedOffset(async () => { return await option.onPreOptionPhase!().then(result => { - if (isNullOrUndefined(result) || result) { + if (result == null || result) { this.continueEncounter(); } }); @@ -233,9 +232,7 @@ export class MysteryEncounterBattleStartCleanupPhase extends Phase { }); // Remove any status tick phases - while (globalScene.phaseManager.findPhase(p => p.is("PostTurnStatusEffectPhase"))) { - globalScene.phaseManager.tryRemovePhase(p => p.is("PostTurnStatusEffectPhase")); - } + globalScene.phaseManager.removeAllPhasesOfType("PostTurnStatusEffectPhase"); // The total number of Pokemon in the player's party that can legally fight const legalPlayerPokemon = globalScene.getPokemonAllowedInBattle(); @@ -289,10 +286,7 @@ export class MysteryEncounterBattlePhase extends Phase { this.doMysteryEncounterBattle(); } - /** - * Gets intro battle message for new battle - * @private - */ + /** Get intro battle message for new battle */ private getBattleMessage(): string { const enemyField = globalScene.getEnemyField(); const encounterMode = globalScene.currentBattle.mysteryEncounter!.encounterMode; @@ -323,8 +317,7 @@ export class MysteryEncounterBattlePhase extends Phase { } /** - * Queues {@linkcode SummonPhase}s for the new battle, and handles trainer animations/dialogue if it's a Trainer battle - * @private + * Queue {@linkcode SummonPhase}s for the new battle and handle trainer animations/dialogue for Trainer battles */ private doMysteryEncounterBattle() { const encounterMode = globalScene.currentBattle.mysteryEncounter!.encounterMode; @@ -401,7 +394,6 @@ export class MysteryEncounterBattlePhase extends Phase { /** * Initiate {@linkcode SummonPhase}s, {@linkcode ScanIvsPhase}, {@linkcode PostSummonPhase}s, etc. - * @private */ private endBattleSetup() { const enemyField = globalScene.getEnemyField(); @@ -417,16 +409,21 @@ export class MysteryEncounterBattlePhase extends Phase { } const availablePartyMembers = globalScene.getPlayerParty().filter(p => p.isAllowedInBattle()); + const minPartySize = globalScene.currentBattle.double ? 2 : 1; + const checkSwitch = + encounterMode !== MysteryEncounterMode.TRAINER_BATTLE + && !this.disableSwitch + && availablePartyMembers.length > minPartySize; if (!availablePartyMembers[0].isOnField()) { - globalScene.phaseManager.pushNew("SummonPhase", 0); + globalScene.phaseManager.pushNew("SummonPhase", 0, true, false, checkSwitch); } if (globalScene.currentBattle.double) { if (availablePartyMembers.length > 1) { globalScene.phaseManager.pushNew("ToggleDoublePositionPhase", true); if (!availablePartyMembers[1].isOnField()) { - globalScene.phaseManager.pushNew("SummonPhase", 1); + globalScene.phaseManager.pushNew("SummonPhase", 1, true, false, checkSwitch); } } } else { @@ -437,23 +434,10 @@ export class MysteryEncounterBattlePhase extends Phase { globalScene.phaseManager.pushNew("ToggleDoublePositionPhase", false); } - if (encounterMode !== MysteryEncounterMode.TRAINER_BATTLE && !this.disableSwitch) { - const minPartySize = globalScene.currentBattle.double ? 2 : 1; - if (availablePartyMembers.length > minPartySize) { - globalScene.phaseManager.pushNew("CheckSwitchPhase", 0, globalScene.currentBattle.double); - if (globalScene.currentBattle.double) { - globalScene.phaseManager.pushNew("CheckSwitchPhase", 1, globalScene.currentBattle.double); - } - } - } - this.end(); } - /** - * Ease in enemy trainer - * @private - */ + /** Ease in enemy trainer */ private showEnemyTrainer(): void { // Show enemy trainer const trainer = globalScene.currentBattle.trainer; @@ -548,7 +532,7 @@ export class MysteryEncounterRewardsPhase extends Phase { if (encounter.doEncounterRewards) { encounter.doEncounterRewards(); } else if (this.addHealPhase) { - globalScene.phaseManager.tryRemovePhase(p => p.is("SelectModifierPhase")); + globalScene.phaseManager.removeAllPhasesOfType("SelectModifierPhase"); globalScene.phaseManager.unshiftNew("SelectModifierPhase", 0, undefined, { fillRemaining: false, rerollMultiplier: -1, @@ -586,7 +570,7 @@ export class PostMysteryEncounterPhase extends Phase { if (this.onPostOptionSelect) { globalScene.executeWithSeedOffset(async () => { return await this.onPostOptionSelect!().then(result => { - if (isNullOrUndefined(result) || result) { + if (result == null || result) { this.continueEncounter(); } }); diff --git a/src/phases/new-battle-phase.ts b/src/phases/new-battle-phase.ts index b9a57161bd0..7b5d132ccd2 100644 --- a/src/phases/new-battle-phase.ts +++ b/src/phases/new-battle-phase.ts @@ -6,12 +6,7 @@ export class NewBattlePhase extends BattlePhase { start() { super.start(); - // cull any extra `NewBattle` phases from the queue. - globalScene.phaseManager.phaseQueue = globalScene.phaseManager.phaseQueue.filter( - phase => !phase.is("NewBattlePhase"), - ); - // `phaseQueuePrepend` is private, so we have to use this inefficient loop. - while (globalScene.phaseManager.tryRemoveUnshiftedPhase(phase => phase.is("NewBattlePhase"))) {} + globalScene.phaseManager.removeAllPhasesOfType("NewBattlePhase"); globalScene.newBattle(); diff --git a/src/phases/obtain-status-effect-phase.ts b/src/phases/obtain-status-effect-phase.ts index 4846130cf4d..b9f3e266d87 100644 --- a/src/phases/obtain-status-effect-phase.ts +++ b/src/phases/obtain-status-effect-phase.ts @@ -23,6 +23,7 @@ export class ObtainStatusEffectPhase extends PokemonPhase { * @param sourceText - The text to show for the source of the status effect, if any; default `null`. * @param statusMessage - A string containing text to be displayed upon status setting; * defaults to normal key for status if empty or omitted. + * @todo stop passing `null` to the phase */ constructor( battlerIndex: BattlerIndex, diff --git a/src/phases/party-member-pokemon-phase.ts b/src/phases/party-member-pokemon-phase.ts index 9536dafda60..545799cf36a 100644 --- a/src/phases/party-member-pokemon-phase.ts +++ b/src/phases/party-member-pokemon-phase.ts @@ -22,4 +22,8 @@ export abstract class PartyMemberPokemonPhase extends FieldPhase { getPokemon(): Pokemon { return this.getParty()[this.partyMemberIndex]; } + + isPlayer(): boolean { + return this.player; + } } diff --git a/src/phases/pokemon-anim-phase.ts b/src/phases/pokemon-anim-phase.ts index 39e9c609aec..c45f201641c 100644 --- a/src/phases/pokemon-anim-phase.ts +++ b/src/phases/pokemon-anim-phase.ts @@ -4,7 +4,6 @@ import { PokemonAnimType } from "#enums/pokemon-anim-type"; import { SpeciesId } from "#enums/species-id"; import type { Pokemon } from "#field/pokemon"; import { BattlePhase } from "#phases/battle-phase"; -import { isNullOrUndefined } from "#utils/common"; export class PokemonAnimPhase extends BattlePhase { public readonly phaseName = "PokemonAnimPhase"; @@ -52,7 +51,7 @@ export class PokemonAnimPhase extends BattlePhase { private doSubstituteAddAnim(): void { const substitute = this.pokemon.getTag(SubstituteTag); - if (isNullOrUndefined(substitute)) { + if (substitute == null) { this.end(); return; } @@ -336,7 +335,7 @@ export class PokemonAnimPhase extends BattlePhase { // Note: unlike the other Commander animation, this is played through the // Dondozo instead of the Tatsugiri. const tatsugiri = this.pokemon.getAlly(); - if (isNullOrUndefined(tatsugiri)) { + if (tatsugiri == null) { console.warn("Aborting COMMANDER_REMOVE anim: Tatsugiri is undefined"); this.end(); return; diff --git a/src/phases/pokemon-heal-phase.ts b/src/phases/pokemon-heal-phase.ts index 02bb3a0b968..258ddb0b624 100644 --- a/src/phases/pokemon-heal-phase.ts +++ b/src/phases/pokemon-heal-phase.ts @@ -64,7 +64,8 @@ export class PokemonHealPhase extends CommonAnimPhase { } const hasMessage = !!this.message; - const healOrDamage = !pokemon.isFullHp() || this.hpHealed < 0; + const canRestorePP = this.fullRestorePP && pokemon.getMoveset().some(mv => mv.ppUsed > 0); + const healOrDamage = !pokemon.isFullHp() || this.hpHealed < 0 || canRestorePP; const healBlock = pokemon.getTag(BattlerTagType.HEAL_BLOCK) as HealBlockTag; let lastStatusEffect = StatusEffect.NONE; diff --git a/src/phases/pokemon-phase.ts b/src/phases/pokemon-phase.ts index 1a1a7e2efa3..92b29889079 100644 --- a/src/phases/pokemon-phase.ts +++ b/src/phases/pokemon-phase.ts @@ -9,7 +9,9 @@ export abstract class PokemonPhase extends FieldPhase { * TODO: Make this either use IDs or `BattlerIndex`es, not a weird mix of both */ protected battlerIndex: BattlerIndex | number; + // TODO: Why is this needed? public player: boolean; + /** @todo Remove in favor of `battlerIndex` pleas for fuck's sake */ public fieldIndex: number; constructor(battlerIndex?: BattlerIndex | number) { @@ -32,10 +34,11 @@ export abstract class PokemonPhase extends FieldPhase { this.fieldIndex = battlerIndex % 2; } + // TODO: This should have `undefined` in its signature getPokemon(): Pokemon { if (this.battlerIndex > BattlerIndex.ENEMY_2) { - return globalScene.getPokemonById(this.battlerIndex)!; //TODO: is this bang correct? + return globalScene.getPokemonById(this.battlerIndex)!; } - return globalScene.getField()[this.battlerIndex]!; //TODO: is this bang correct? + return globalScene.getField()[this.battlerIndex]!; } } diff --git a/src/phases/post-summon-activate-ability-phase.ts b/src/phases/post-summon-activate-ability-phase.ts index 5f790c01ad1..a2b6c059bee 100644 --- a/src/phases/post-summon-activate-ability-phase.ts +++ b/src/phases/post-summon-activate-ability-phase.ts @@ -6,8 +6,8 @@ import { PostSummonPhase } from "#phases/post-summon-phase"; * Helper to {@linkcode PostSummonPhase} which applies abilities */ export class PostSummonActivateAbilityPhase extends PostSummonPhase { - private priority: number; - private passive: boolean; + private readonly priority: number; + private readonly passive: boolean; constructor(battlerIndex: BattlerIndex, priority: number, passive: boolean) { super(battlerIndex); diff --git a/src/phases/post-summon-phase.ts b/src/phases/post-summon-phase.ts index 5de068f2ae5..136f2fbd601 100644 --- a/src/phases/post-summon-phase.ts +++ b/src/phases/post-summon-phase.ts @@ -1,21 +1,35 @@ import { applyAbAttrs } from "#abilities/apply-ab-attrs"; +import type { PhaseString } from "#app/@types/phase-types"; import { globalScene } from "#app/global-scene"; import { EntryHazardTag } from "#data/arena-tag"; import { MysteryEncounterPostSummonTag } from "#data/battler-tags"; +import { ArenaTagType } from "#enums/arena-tag-type"; +import type { BattlerIndex } from "#enums/battler-index"; import { BattlerTagType } from "#enums/battler-tag-type"; import { StatusEffect } from "#enums/status-effect"; import { PokemonPhase } from "#phases/pokemon-phase"; export class PostSummonPhase extends PokemonPhase { public readonly phaseName = "PostSummonPhase"; + /** Used to determine whether to push or unshift {@linkcode PostSummonActivateAbilityPhase}s */ + public readonly source: PhaseString; + + constructor(battlerIndex?: BattlerIndex | number, source: PhaseString = "SwitchSummonPhase") { + super(battlerIndex); + this.source = source; + } + start() { super.start(); const pokemon = this.getPokemon(); - + console.log("Ran PSP for:", pokemon.name); if (pokemon.status?.effect === StatusEffect.TOXIC) { pokemon.status.toxicTurnCount = 0; } + + globalScene.arena.applyTags(ArenaTagType.PENDING_HEAL, false, pokemon); + globalScene.arena.applyTags(EntryHazardTag, false, pokemon); // If this is mystery encounter and has post summon phase tag, apply post summon effects @@ -25,8 +39,7 @@ export class PostSummonPhase extends PokemonPhase { ) { pokemon.lapseTag(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON); } - - const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); + const field = pokemon.isPlayer() ? globalScene.getPlayerField(true) : globalScene.getEnemyField(true); for (const p of field) { applyAbAttrs("CommanderAbAttr", { pokemon: p }); } diff --git a/src/phases/quiet-form-change-phase.ts b/src/phases/quiet-form-change-phase.ts index ef53b16cc56..920ff2252b8 100644 --- a/src/phases/quiet-form-change-phase.ts +++ b/src/phases/quiet-form-change-phase.ts @@ -9,7 +9,6 @@ import { BattleSpec } from "#enums/battle-spec"; import { BattlerTagType } from "#enums/battler-tag-type"; import type { Pokemon } from "#field/pokemon"; import { BattlePhase } from "#phases/battle-phase"; -import type { MovePhase } from "#phases/move-phase"; export class QuietFormChangePhase extends BattlePhase { public readonly phaseName = "QuietFormChangePhase"; @@ -170,12 +169,7 @@ export class QuietFormChangePhase extends BattlePhase { this.pokemon.initBattleInfo(); this.pokemon.cry(); - const movePhase = globalScene.phaseManager.findPhase( - p => p.is("MovePhase") && p.pokemon === this.pokemon, - ) as MovePhase; - if (movePhase) { - movePhase.cancel(); - } + globalScene.phaseManager.cancelMove(p => p.pokemon === this.pokemon); } if (this.formChange.trigger instanceof SpeciesFormChangeTeraTrigger) { const params = { pokemon: this.pokemon }; diff --git a/src/phases/revival-blessing-phase.ts b/src/phases/revival-blessing-phase.ts index a9dedf4c325..5d75f2c9b47 100644 --- a/src/phases/revival-blessing-phase.ts +++ b/src/phases/revival-blessing-phase.ts @@ -3,9 +3,9 @@ import { SwitchType } from "#enums/switch-type"; import { UiMode } from "#enums/ui-mode"; import type { PlayerPokemon } from "#field/pokemon"; import { BattlePhase } from "#phases/battle-phase"; -import type { PartyOption } from "#ui/handlers/party-ui-handler"; -import { PartyUiHandler, PartyUiMode } from "#ui/handlers/party-ui-handler"; -import { isNullOrUndefined, toDmgValue } from "#utils/common"; +import type { PartyOption } from "#ui/party-ui-handler"; +import { PartyUiHandler, PartyUiMode } from "#ui/party-ui-handler"; +import { toDmgValue } from "#utils/common"; import i18next from "i18next"; /** @@ -42,11 +42,7 @@ export class RevivalBlessingPhase extends BattlePhase { ); const allyPokemon = this.user.getAlly(); - if ( - globalScene.currentBattle.double - && globalScene.getPlayerParty().length > 1 - && !isNullOrUndefined(allyPokemon) - ) { + if (globalScene.currentBattle.double && globalScene.getPlayerParty().length > 1 && allyPokemon != null) { if (slotIndex <= 1) { // Revived ally pokemon globalScene.phaseManager.unshiftNew( diff --git a/src/phases/select-biome-phase.ts b/src/phases/select-biome-phase.ts index 3276c34306c..21c0cfade94 100644 --- a/src/phases/select-biome-phase.ts +++ b/src/phases/select-biome-phase.ts @@ -5,7 +5,7 @@ import { ChallengeType } from "#enums/challenge-type"; import { UiMode } from "#enums/ui-mode"; import { MapModifier, MoneyInterestModifier } from "#modifiers/modifier"; import { BattlePhase } from "#phases/battle-phase"; -import type { OptionSelectItem } from "#ui/handlers/abstract-option-select-ui-handler"; +import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; import { applyChallenges } from "#utils/challenge-utils"; import { BooleanHolder, randSeedInt } from "#utils/common"; diff --git a/src/phases/select-modifier-phase.ts b/src/phases/select-modifier-phase.ts index 51adeb21af0..2031fc5c5f1 100644 --- a/src/phases/select-modifier-phase.ts +++ b/src/phases/select-modifier-phase.ts @@ -24,10 +24,10 @@ import { TmModifierType, } from "#modifiers/modifier-type"; import { BattlePhase } from "#phases/battle-phase"; -import type { ModifierSelectUiHandler } from "#ui/handlers/modifier-select-ui-handler"; -import { SHOP_OPTIONS_ROW_LIMIT } from "#ui/handlers/modifier-select-ui-handler"; -import { PartyOption, PartyUiHandler, PartyUiMode } from "#ui/handlers/party-ui-handler"; -import { isNullOrUndefined, NumberHolder } from "#utils/common"; +import type { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; +import { SHOP_OPTIONS_ROW_LIMIT } from "#ui/modifier-select-ui-handler"; +import { PartyOption, PartyUiHandler, PartyUiMode } from "#ui/party-ui-handler"; +import { NumberHolder } from "#utils/common"; import i18next from "i18next"; export type ModifierSelectCallback = (rowCursor: number, cursor: number) => boolean; @@ -429,7 +429,7 @@ export class SelectModifierPhase extends BattlePhase { } let multiplier = 1; - if (!isNullOrUndefined(this.customModifierSettings?.rerollMultiplier)) { + if (this.customModifierSettings?.rerollMultiplier != null) { if (this.customModifierSettings.rerollMultiplier < 0) { // Completely overrides reroll cost to -1 and early exits return -1; diff --git a/src/phases/select-starter-phase.ts b/src/phases/select-starter-phase.ts index 50c06f96a77..e923efaa678 100644 --- a/src/phases/select-starter-phase.ts +++ b/src/phases/select-starter-phase.ts @@ -4,13 +4,11 @@ import { Phase } from "#app/phase"; import { SpeciesFormChangeMoveLearnedTrigger } from "#data/form-change-triggers"; import { Gender } from "#data/gender"; import { ChallengeType } from "#enums/challenge-type"; -import type { SpeciesId } from "#enums/species-id"; import { UiMode } from "#enums/ui-mode"; import { overrideHeldItems, overrideModifiers } from "#modifiers/modifier"; +import type { Starter } from "#types/save-data"; import { SaveSlotUiMode } from "#ui/handlers/save-slot-select-ui-handler"; -import type { Starter } from "#ui/handlers/starter-select-ui-handler"; import { applyChallenges } from "#utils/challenge-utils"; -import { isNullOrUndefined } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; @@ -45,33 +43,32 @@ export class SelectStarterPhase extends Phase { const loadPokemonAssets: Promise[] = []; starters.forEach((starter: Starter, i: number) => { if (!i && Overrides.STARTER_SPECIES_OVERRIDE) { - starter.species = getPokemonSpecies(Overrides.STARTER_SPECIES_OVERRIDE as SpeciesId); + starter.speciesId = Overrides.STARTER_SPECIES_OVERRIDE; } - const starterProps = globalScene.gameData.getSpeciesDexAttrProps(starter.species, starter.dexAttr); - let starterFormIndex = Math.min(starterProps.formIndex, Math.max(starter.species.forms.length - 1, 0)); + const species = getPokemonSpecies(starter.speciesId); + let starterFormIndex = starter.formIndex; if ( - starter.species.speciesId in Overrides.STARTER_FORM_OVERRIDES - && !isNullOrUndefined(Overrides.STARTER_FORM_OVERRIDES[starter.species.speciesId]) - && starter.species.forms[Overrides.STARTER_FORM_OVERRIDES[starter.species.speciesId]!] + starter.speciesId in Overrides.STARTER_FORM_OVERRIDES + && Overrides.STARTER_FORM_OVERRIDES[starter.speciesId] != null + && species.forms[Overrides.STARTER_FORM_OVERRIDES[starter.speciesId]!] ) { - starterFormIndex = Overrides.STARTER_FORM_OVERRIDES[starter.species.speciesId]!; + starterFormIndex = Overrides.STARTER_FORM_OVERRIDES[starter.speciesId]!; } let starterGender = - starter.species.malePercent !== null ? (!starterProps.female ? Gender.MALE : Gender.FEMALE) : Gender.GENDERLESS; + species.malePercent !== null ? (!starter.female ? Gender.MALE : Gender.FEMALE) : Gender.GENDERLESS; if (Overrides.GENDER_OVERRIDE !== null) { starterGender = Overrides.GENDER_OVERRIDE; } - const starterIvs = globalScene.gameData.dexData[starter.species.speciesId].ivs.slice(0); const starterPokemon = globalScene.addPlayerPokemon( - starter.species, + species, globalScene.gameMode.getStartingLevel(), starter.abilityIndex, starterFormIndex, starterGender, - starterProps.shiny, - starterProps.variant, - starterIvs, + starter.shiny, + starter.variant, + starter.ivs, starter.nature, ); starter.moveset && starterPokemon.tryPopulateMoveset(starter.moveset); @@ -79,7 +76,7 @@ export class SelectStarterPhase extends Phase { starterPokemon.passive = true; } starterPokemon.luck = globalScene.gameData.getDexAttrLuck( - globalScene.gameData.dexData[starter.species.speciesId].caughtAttr, + globalScene.gameData.dexData[species.speciesId].caughtAttr, ); if (starter.pokerus) { starterPokemon.pokerus = true; @@ -89,7 +86,7 @@ export class SelectStarterPhase extends Phase { starterPokemon.nickname = starter.nickname; } - if (!isNullOrUndefined(starter.teraType)) { + if (starter.teraType != null) { starterPokemon.teraType = starter.teraType; } else { starterPokemon.teraType = starterPokemon.species.type1; diff --git a/src/phases/stat-stage-change-phase.ts b/src/phases/stat-stage-change-phase.ts index 6c15342ddeb..3c2d1cb5fad 100644 --- a/src/phases/stat-stage-change-phase.ts +++ b/src/phases/stat-stage-change-phase.ts @@ -13,7 +13,7 @@ import type { Pokemon } from "#field/pokemon"; import { ResetNegativeStatStageModifier } from "#modifiers/modifier"; import { PokemonPhase } from "#phases/pokemon-phase"; import type { ConditionalUserFieldProtectStatAbAttrParams, PreStatStageChangeAbAttrParams } from "#types/ability-types"; -import { BooleanHolder, isNullOrUndefined, NumberHolder } from "#utils/common"; +import { BooleanHolder, NumberHolder } from "#utils/common"; import i18next from "i18next"; export type StatStageChangeCallback = ( @@ -153,7 +153,7 @@ export class StatStageChangePhase extends PokemonPhase { applyAbAttrs("ConditionalUserFieldProtectStatAbAttr", abAttrParams); // TODO: Consider skipping this call if `cancelled` is false. const ally = pokemon.getAlly(); - if (!isNullOrUndefined(ally)) { + if (ally != null) { applyAbAttrs("ConditionalUserFieldProtectStatAbAttr", { ...abAttrParams, pokemon: ally }); } @@ -223,10 +223,7 @@ export class StatStageChangePhase extends PokemonPhase { }); // Look for any other stat change phases; if this is the last one, do White Herb check - const existingPhase = globalScene.phaseManager.findPhase( - p => p.is("StatStageChangePhase") && p.battlerIndex === this.battlerIndex, - ); - if (!existingPhase?.is("StatStageChangePhase")) { + if (!globalScene.phaseManager.hasPhaseOfType("StatStageChangePhase", p => p.battlerIndex === this.battlerIndex)) { // Apply White Herb if needed const whiteHerb = globalScene.applyModifier( ResetNegativeStatStageModifier, @@ -297,49 +294,6 @@ export class StatStageChangePhase extends PokemonPhase { } } - aggregateStatStageChanges(): void { - const accEva: BattleStat[] = [Stat.ACC, Stat.EVA]; - const isAccEva = accEva.some(s => this.stats.includes(s)); - let existingPhase: StatStageChangePhase; - if (this.stats.length === 1) { - while ( - (existingPhase = globalScene.phaseManager.findPhase( - p => - p.is("StatStageChangePhase") - && p.battlerIndex === this.battlerIndex - && p.stats.length === 1 - && p.stats[0] === this.stats[0] - && p.selfTarget === this.selfTarget - && p.showMessage === this.showMessage - && p.ignoreAbilities === this.ignoreAbilities, - ) as StatStageChangePhase) - ) { - this.stages += existingPhase.stages; - - if (!globalScene.phaseManager.tryRemovePhase(p => p === existingPhase)) { - break; - } - } - } - while ( - (existingPhase = globalScene.phaseManager.findPhase( - p => - p.is("StatStageChangePhase") - && p.battlerIndex === this.battlerIndex - && p.selfTarget === this.selfTarget - && accEva.some(s => p.stats.includes(s)) === isAccEva - && p.stages === this.stages - && p.showMessage === this.showMessage - && p.ignoreAbilities === this.ignoreAbilities, - ) as StatStageChangePhase) - ) { - this.stats.push(...existingPhase.stats); - if (!globalScene.phaseManager.tryRemovePhase(p => p === existingPhase)) { - break; - } - } - } - getStatStageChangeMessages(stats: BattleStat[], stages: number, relStages: number[]): string[] { const messages: string[] = []; diff --git a/src/phases/summon-phase.ts b/src/phases/summon-phase.ts index dda70f46ec9..26a8ba40ffc 100644 --- a/src/phases/summon-phase.ts +++ b/src/phases/summon-phase.ts @@ -16,12 +16,14 @@ import i18next from "i18next"; export class SummonPhase extends PartyMemberPokemonPhase { // The union type is needed to keep typescript happy as these phases extend from SummonPhase public readonly phaseName: "SummonPhase" | "SummonMissingPhase" | "SwitchSummonPhase" | "ReturnPhase" = "SummonPhase"; - private loaded: boolean; + private readonly loaded: boolean; + private readonly checkSwitch: boolean; - constructor(fieldIndex: number, player = true, loaded = false) { + constructor(fieldIndex: number, player = true, loaded = false, checkSwitch = false) { super(fieldIndex, player); this.loaded = loaded; + this.checkSwitch = checkSwitch; } start() { @@ -288,7 +290,17 @@ export class SummonPhase extends PartyMemberPokemonPhase { } queuePostSummon(): void { - globalScene.phaseManager.pushNew("PostSummonPhase", this.getPokemon().getBattlerIndex()); + if (this.checkSwitch) { + globalScene.phaseManager.pushNew( + "CheckSwitchPhase", + this.getPokemon().getFieldIndex(), + globalScene.currentBattle.double, + ); + } else { + globalScene.phaseManager.pushNew("PostSummonPhase", this.getPokemon().getBattlerIndex(), this.phaseName); + } + + globalScene.phaseManager.tryAddEnemyPostSummonPhases(); } end() { @@ -296,4 +308,8 @@ export class SummonPhase extends PartyMemberPokemonPhase { super.end(); } + + public getFieldIndex(): number { + return this.fieldIndex; + } } diff --git a/src/phases/switch-phase.ts b/src/phases/switch-phase.ts index 8b03f5ec5ce..9ab06ec827c 100644 --- a/src/phases/switch-phase.ts +++ b/src/phases/switch-phase.ts @@ -1,9 +1,8 @@ import { globalScene } from "#app/global-scene"; -import { DynamicPhaseType } from "#enums/dynamic-phase-type"; import { SwitchType } from "#enums/switch-type"; import { UiMode } from "#enums/ui-mode"; import { BattlePhase } from "#phases/battle-phase"; -import { PartyOption, PartyUiHandler, PartyUiMode } from "#ui/handlers/party-ui-handler"; +import { PartyOption, PartyUiHandler, PartyUiMode } from "#ui/party-ui-handler"; /** * Opens the party selector UI and transitions into a {@linkcode SwitchSummonPhase} @@ -77,14 +76,6 @@ export class SwitchPhase extends BattlePhase { fieldIndex, (slotIndex: number, option: PartyOption) => { if (slotIndex >= globalScene.currentBattle.getBattlerCount() && slotIndex < 6) { - // Remove any pre-existing PostSummonPhase under the same field index. - // Pre-existing PostSummonPhases may occur when this phase is invoked during a prompt to switch at the start of a wave. - // TODO: Separate the animations from `SwitchSummonPhase` and co. into another phase and use that on initial switch - this is a band-aid fix - globalScene.phaseManager.tryRemoveDynamicPhase( - DynamicPhaseType.POST_SUMMON, - p => p.is("PostSummonPhase") && p.player && p.fieldIndex === this.fieldIndex, - "all", - ); const switchType = option === PartyOption.PASS_BATON ? SwitchType.BATON_PASS : this.switchType; globalScene.phaseManager.unshiftNew("SwitchSummonPhase", switchType, fieldIndex, slotIndex, this.doReturn); } diff --git a/src/phases/switch-summon-phase.ts b/src/phases/switch-summon-phase.ts index ac47068c619..8cc7843b55f 100644 --- a/src/phases/switch-summon-phase.ts +++ b/src/phases/switch-summon-phase.ts @@ -241,11 +241,11 @@ export class SwitchSummonPhase extends SummonPhase { globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true); // Reverts to weather-based forms when weather suppressors (Cloud Nine/Air Lock) are switched out - globalScene.arena.triggerWeatherBasedFormChanges(); + globalScene.arena.triggerWeatherBasedFormChanges(pokemon); } queuePostSummon(): void { - globalScene.phaseManager.startNewDynamicPhase("PostSummonPhase", this.getPokemon().getBattlerIndex()); + globalScene.phaseManager.unshiftNew("PostSummonPhase", this.getPokemon().getBattlerIndex()); } /** diff --git a/src/phases/title-phase.ts b/src/phases/title-phase.ts index fd12ec3bd6b..9535ea1c8e9 100644 --- a/src/phases/title-phase.ts +++ b/src/phases/title-phase.ts @@ -14,11 +14,12 @@ import { Unlockables } from "#enums/unlockables"; import { getBiomeKey } from "#field/arena"; import type { Modifier } from "#modifiers/modifier"; import { getDailyRunStarterModifiers, regenerateModifierPoolThresholds } from "#modifiers/modifier-type"; -import type { SessionSaveData } from "#system/game-data"; import { vouchers } from "#system/voucher"; -import type { OptionSelectConfig, OptionSelectItem } from "#ui/handlers/abstract-option-select-ui-handler"; -import { SaveSlotUiMode } from "#ui/handlers/save-slot-select-ui-handler"; -import { isLocal, isLocalServerConnected, isNullOrUndefined } from "#utils/common"; +import type { SessionSaveData } from "#types/save-data"; +import type { OptionSelectConfig, OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; +import { SaveSlotUiMode } from "#ui/save-slot-select-ui-handler"; +import { isLocal, isLocalServerConnected } from "#utils/common"; +import { getPokemonSpecies } from "#utils/pokemon-utils"; import i18next from "i18next"; export class TitlePhase extends Phase { @@ -218,23 +219,19 @@ export class TitlePhase extends Phase { const party = globalScene.getPlayerParty(); const loadPokemonAssets: Promise[] = []; for (const starter of starters) { - const starterProps = globalScene.gameData.getSpeciesDexAttrProps(starter.species, starter.dexAttr); - const starterFormIndex = Math.min(starterProps.formIndex, Math.max(starter.species.forms.length - 1, 0)); + const species = getPokemonSpecies(starter.speciesId); + const starterFormIndex = starter.formIndex; const starterGender = - starter.species.malePercent !== null - ? !starterProps.female - ? Gender.MALE - : Gender.FEMALE - : Gender.GENDERLESS; + species.malePercent !== null ? (starter.female ? Gender.FEMALE : Gender.MALE) : Gender.GENDERLESS; const starterPokemon = globalScene.addPlayerPokemon( - starter.species, + species, startingLevel, starter.abilityIndex, starterFormIndex, starterGender, - starterProps.shiny, - starterProps.variant, - undefined, + starter.shiny, + starter.variant, + starter.ivs, starter.nature, ); starterPokemon.setVisible(false); @@ -289,7 +286,7 @@ export class TitlePhase extends Phase { } else { // Grab first 10 chars of ISO date format (YYYY-MM-DD) and convert to base64 let seed: string = btoa(new Date().toISOString().substring(0, 10)); - if (!isNullOrUndefined(Overrides.DAILY_RUN_SEED_OVERRIDE)) { + if (Overrides.DAILY_RUN_SEED_OVERRIDE != null) { seed = Overrides.DAILY_RUN_SEED_OVERRIDE; } generateDaily(seed); @@ -315,23 +312,15 @@ export class TitlePhase extends Phase { if (this.loaded) { const availablePartyMembers = globalScene.getPokemonAllowedInBattle().length; - - globalScene.phaseManager.pushNew("SummonPhase", 0, true, true); - if (globalScene.currentBattle.double && availablePartyMembers > 1) { - globalScene.phaseManager.pushNew("SummonPhase", 1, true, true); - } - - if ( + const minPartySize = globalScene.currentBattle.double ? 2 : 1; + const checkSwitch = globalScene.currentBattle.battleType !== BattleType.TRAINER && (globalScene.currentBattle.waveIndex > 1 || !globalScene.gameMode.isDaily) - ) { - const minPartySize = globalScene.currentBattle.double ? 2 : 1; - if (availablePartyMembers > minPartySize) { - globalScene.phaseManager.pushNew("CheckSwitchPhase", 0, globalScene.currentBattle.double); - if (globalScene.currentBattle.double) { - globalScene.phaseManager.pushNew("CheckSwitchPhase", 1, globalScene.currentBattle.double); - } - } + && availablePartyMembers > minPartySize; + + globalScene.phaseManager.pushNew("SummonPhase", 0, true, true, checkSwitch); + if (globalScene.currentBattle.double && availablePartyMembers > 1) { + globalScene.phaseManager.pushNew("SummonPhase", 1, true, true, checkSwitch); } } diff --git a/src/phases/turn-end-phase.ts b/src/phases/turn-end-phase.ts index 463f26e73a2..22ebbd2607b 100644 --- a/src/phases/turn-end-phase.ts +++ b/src/phases/turn-end-phase.ts @@ -25,6 +25,7 @@ export class TurnEndPhase extends FieldPhase { globalScene.currentBattle.incrementTurn(); globalScene.eventTarget.dispatchEvent(new TurnEndEvent(globalScene.currentBattle.turn)); + globalScene.phaseManager.dynamicQueueManager.clearLastTurnOrder(); globalScene.phaseManager.hideAbilityBar(); diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts index 1733901d527..cd45a73c813 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -1,89 +1,31 @@ import { applyAbAttrs } from "#abilities/apply-ab-attrs"; import type { TurnCommand } from "#app/battle"; import { globalScene } from "#app/global-scene"; -import { TrickRoomTag } from "#data/arena-tag"; -import { allMoves } from "#data/data-lists"; -import { BattlerIndex } from "#enums/battler-index"; +import { ArenaTagSide } from "#enums/arena-tag-side"; +import type { BattlerIndex } from "#enums/battler-index"; import { Command } from "#enums/command"; -import { Stat } from "#enums/stat"; import { SwitchType } from "#enums/switch-type"; import type { Pokemon } from "#field/pokemon"; import { BypassSpeedChanceModifier } from "#modifiers/modifier"; import { PokemonMove } from "#moves/pokemon-move"; import { FieldPhase } from "#phases/field-phase"; -import { BooleanHolder, randSeedShuffle } from "#utils/common"; +import { inSpeedOrder } from "#utils/speed-order-generator"; export class TurnStartPhase extends FieldPhase { public readonly phaseName = "TurnStartPhase"; /** - * Helper method to retrieve the current speed order of the combattants. - * It also checks for Trick Room and reverses the array if it is present. - * @returns The {@linkcode BattlerIndex}es of all on-field Pokemon, sorted in speed order. - * @todo Make this private - */ - getSpeedOrder(): BattlerIndex[] { - const playerField = globalScene.getPlayerField().filter(p => p.isActive()); - const enemyField = globalScene.getEnemyField().filter(p => p.isActive()); - - // Shuffle the list before sorting so speed ties produce random results - // This is seeded with the current turn to prevent turn order varying - // based on how long since you last reloaded. - let orderedTargets = (playerField as Pokemon[]).concat(enemyField); - globalScene.executeWithSeedOffset( - () => { - orderedTargets = randSeedShuffle(orderedTargets); - }, - globalScene.currentBattle.turn, - globalScene.waveSeed, - ); - - // Check for Trick Room and reverse sort order if active. - // Notably, Pokerogue does NOT have the "outspeed trick room" glitch at >1809 spd. - const speedReversed = new BooleanHolder(false); - globalScene.arena.applyTags(TrickRoomTag, false, speedReversed); - - orderedTargets.sort((a: Pokemon, b: Pokemon) => { - const aSpeed = a.getEffectiveStat(Stat.SPD); - const bSpeed = b.getEffectiveStat(Stat.SPD); - - return speedReversed.value ? aSpeed - bSpeed : bSpeed - aSpeed; - }); - - return orderedTargets.map(t => t.getFieldIndex() + (t.isEnemy() ? BattlerIndex.ENEMY : BattlerIndex.PLAYER)); - } - - /** - * This takes the result of {@linkcode getSpeedOrder} and applies priority / bypass speed attributes to it. - * This also considers the priority levels of various commands and changes the result of `getSpeedOrder` based on such. - * @returns The `BattlerIndex`es of all on-field Pokemon sorted in action order. + * Returns an ordering of the current field based on command priority + * @returns The sequence of commands for this turn */ getCommandOrder(): BattlerIndex[] { - let moveOrder = this.getSpeedOrder(); - // The creation of the battlerBypassSpeed object contains checks for the ability Quick Draw and the held item Quick Claw - // The ability Mycelium Might disables Quick Claw's activation when using a status move - // This occurs before the main loop because of battles with more than two Pokemon - const battlerBypassSpeed = {}; - - globalScene.getField(true).forEach(p => { - const bypassSpeed = new BooleanHolder(false); - const canCheckHeldItems = new BooleanHolder(true); - applyAbAttrs("BypassSpeedChanceAbAttr", { pokemon: p, bypass: bypassSpeed }); - applyAbAttrs("PreventBypassSpeedChanceAbAttr", { - pokemon: p, - bypass: bypassSpeed, - canCheckHeldItems, - }); - if (canCheckHeldItems.value) { - globalScene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p, bypassSpeed); - } - battlerBypassSpeed[p.getBattlerIndex()] = bypassSpeed; - }); + const playerField = globalScene.getPlayerField(true).map(p => p.getBattlerIndex()); + const enemyField = globalScene.getEnemyField(true).map(p => p.getBattlerIndex()); + const orderedTargets: BattlerIndex[] = playerField.concat(enemyField); // The function begins sorting orderedTargets based on command priority, move priority, and possible speed bypasses. // Non-FIGHT commands (SWITCH, BALL, RUN) have a higher command priority and will always occur before any FIGHT commands. - moveOrder = moveOrder.slice(0); - moveOrder.sort((a, b) => { + orderedTargets.sort((a, b) => { const aCommand = globalScene.currentBattle.turnCommands[a]; const bCommand = globalScene.currentBattle.turnCommands[b]; @@ -94,41 +36,14 @@ export class TurnStartPhase extends FieldPhase { if (bCommand?.command === Command.FIGHT) { return -1; } - } else if (aCommand?.command === Command.FIGHT) { - const aMove = allMoves[aCommand.move!.move]; - const bMove = allMoves[bCommand!.move!.move]; - - const aUser = globalScene.getField(true).find(p => p.getBattlerIndex() === a)!; - const bUser = globalScene.getField(true).find(p => p.getBattlerIndex() === b)!; - - const aPriority = aMove.getPriority(aUser, false); - const bPriority = bMove.getPriority(bUser, false); - - // The game now checks for differences in priority levels. - // If the moves share the same original priority bracket, it can check for differences in battlerBypassSpeed and return the result. - // This conditional is used to ensure that Quick Claw can still activate with abilities like Stall and Mycelium Might (attack moves only) - // Otherwise, the game returns the user of the move with the highest priority. - const isSameBracket = Math.ceil(aPriority) - Math.ceil(bPriority) === 0; - if (aPriority !== bPriority) { - if (isSameBracket && battlerBypassSpeed[a].value !== battlerBypassSpeed[b].value) { - return battlerBypassSpeed[a].value ? -1 : 1; - } - return aPriority < bPriority ? 1 : -1; - } } - // If there is no difference between the move's calculated priorities, - // check for differences in battlerBypassSpeed and returns the result. - if (battlerBypassSpeed[a].value !== battlerBypassSpeed[b].value) { - return battlerBypassSpeed[a].value ? -1 : 1; - } - - const aIndex = moveOrder.indexOf(a); - const bIndex = moveOrder.indexOf(b); + const aIndex = orderedTargets.indexOf(a); + const bIndex = orderedTargets.indexOf(b); return aIndex < bIndex ? -1 : aIndex > bIndex ? 1 : 0; }); - return moveOrder; + return orderedTargets; } // TODO: Refactor this alongside `CommandPhase.handleCommand` to use SEPARATE METHODS @@ -139,9 +54,8 @@ export class TurnStartPhase extends FieldPhase { const field = globalScene.getField(); const moveOrder = this.getCommandOrder(); - for (const o of this.getSpeedOrder()) { - const pokemon = field[o]; - const preTurnCommand = globalScene.currentBattle.preTurnCommands[o]; + for (const pokemon of inSpeedOrder(ArenaTagSide.BOTH)) { + const preTurnCommand = globalScene.currentBattle.preTurnCommands[pokemon.getBattlerIndex()]; if (preTurnCommand?.skip) { continue; @@ -154,6 +68,10 @@ export class TurnStartPhase extends FieldPhase { } const phaseManager = globalScene.phaseManager; + for (const pokemon of inSpeedOrder(ArenaTagSide.BOTH)) { + applyAbAttrs("BypassSpeedChanceAbAttr", { pokemon }); + globalScene.applyModifiers(BypassSpeedChanceModifier, pokemon.isPlayer(), pokemon); + } moveOrder.forEach((o, index) => { const pokemon = field[o]; @@ -178,13 +96,8 @@ export class TurnStartPhase extends FieldPhase { // TODO: Re-order these phases to be consistent with mainline turn order: // https://www.smogon.com/forums/threads/sword-shield-battle-mechanics-research.3655528/page-64#post-9244179 - phaseManager.pushNew("WeatherEffectPhase"); - phaseManager.pushNew("PositionalTagPhase"); - phaseManager.pushNew("BerryPhase"); - - phaseManager.pushNew("CheckStatusEffectPhase", moveOrder); - - phaseManager.pushNew("TurnEndPhase"); + // TODO: In an ideal world, this is handled by the phase manager. The change is nontrivial due to the ordering of post-turn phases like those queued by VictoryPhase + globalScene.phaseManager.queueTurnEndPhases(); /* * `this.end()` will call `PhaseManager#shiftPhase()`, which dumps everything from `phaseQueuePrepend` diff --git a/src/plugins/api/pokerogue-daily-api.ts b/src/plugins/api/pokerogue-daily-api.ts index dfde4720730..5ea3846e60e 100644 --- a/src/plugins/api/pokerogue-daily-api.ts +++ b/src/plugins/api/pokerogue-daily-api.ts @@ -1,6 +1,6 @@ import { ApiBase } from "#api/api-base"; import type { GetDailyRankingsPageCountRequest, GetDailyRankingsRequest } from "#types/api/pokerogue-daily-api"; -import type { RankingEntry } from "#ui/containers/daily-run-scoreboard"; +import type { RankingEntry } from "#ui/daily-run-scoreboard"; /** * A wrapper for daily-run PokéRogue API requests. diff --git a/src/plugins/api/pokerogue-session-savedata-api.ts b/src/plugins/api/pokerogue-session-savedata-api.ts index 87960339671..e1c67ef7245 100644 --- a/src/plugins/api/pokerogue-session-savedata-api.ts +++ b/src/plugins/api/pokerogue-session-savedata-api.ts @@ -1,5 +1,4 @@ import { ApiBase } from "#api/api-base"; -import type { SessionSaveData } from "#system/game-data"; import type { ClearSessionSavedataRequest, ClearSessionSavedataResponse, @@ -8,6 +7,7 @@ import type { NewClearSessionSavedataRequest, UpdateSessionSavedataRequest, } from "#types/api/pokerogue-session-save-data-api"; +import type { SessionSaveData } from "#types/save-data"; /** * A wrapper for PokéRogue session savedata API requests. diff --git a/src/queues/move-phase-priority-queue.ts b/src/queues/move-phase-priority-queue.ts new file mode 100644 index 00000000000..5f0b20c3c2e --- /dev/null +++ b/src/queues/move-phase-priority-queue.ts @@ -0,0 +1,103 @@ +import type { PokemonMove } from "#app/data/moves/pokemon-move"; +import type { Pokemon } from "#app/field/pokemon"; +import { globalScene } from "#app/global-scene"; +import type { MovePhase } from "#app/phases/move-phase"; +import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-queue"; +import type { BattlerIndex } from "#enums/battler-index"; +import type { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; +import type { PhaseConditionFunc } from "#types/phase-types"; + +/** A priority queue responsible for the ordering of {@linkcode MovePhase}s */ +export class MovePhasePriorityQueue extends PokemonPhasePriorityQueue { + private lastTurnOrder: Pokemon[] = []; + + protected override reorder(): void { + super.reorder(); + this.sortPostSpeed(); + } + + public cancelMove(condition: PhaseConditionFunc<"MovePhase">): void { + this.queue.find(p => condition(p))?.cancel(); + } + + public setTimingModifier(condition: PhaseConditionFunc<"MovePhase">, modifier: MovePhaseTimingModifier): void { + const phase = this.queue.find(p => condition(p)); + if (phase != null) { + phase.timingModifier = modifier; + } + } + + public setMoveForPhase(condition: PhaseConditionFunc<"MovePhase">, move: PokemonMove) { + const phase = this.queue.find(p => condition(p)); + if (phase != null) { + phase.move = move; + } + } + + public redirectMoves(removedPokemon: Pokemon, allyPokemon: Pokemon): void { + // failsafe: if not a double battle just return + if (!globalScene.currentBattle.double) { + return; + } + + // TODO: simplify later + if (allyPokemon?.isActive(true)) { + this.queue + .filter( + mp => + mp.targets.length === 1 + && mp.targets[0] === removedPokemon.getBattlerIndex() + && mp.pokemon.isPlayer() !== allyPokemon.isPlayer(), + ) + .forEach(targetingMovePhase => { + if (targetingMovePhase && targetingMovePhase.targets[0] !== allyPokemon.getBattlerIndex()) { + targetingMovePhase.targets[0] = allyPokemon.getBattlerIndex(); + } + }); + } + } + + public setMoveOrder(order: BattlerIndex[]) { + this.setOrder = order; + } + + public override pop(): MovePhase | undefined { + this.reorder(); + const phase = this.queue.shift(); + if (phase) { + this.lastTurnOrder.push(phase.pokemon); + } + return phase; + } + + public getTurnOrder(): Pokemon[] { + return this.lastTurnOrder; + } + + public clearTurnOrder(): void { + this.lastTurnOrder = []; + } + + public override clear(): void { + this.setOrder = undefined; + this.lastTurnOrder = []; + super.clear(); + } + + private sortPostSpeed(): void { + this.queue.sort((a: MovePhase, b: MovePhase) => { + const priority = [a, b].map(movePhase => { + const move = movePhase.move.getMove(); + return move.getPriority(movePhase.pokemon, true); + }); + + const timingModifiers = [a, b].map(movePhase => movePhase.timingModifier); + + if (timingModifiers[0] !== timingModifiers[1]) { + return timingModifiers[1] - timingModifiers[0]; + } + + return priority[1] - priority[0]; + }); + } +} diff --git a/src/queues/pokemon-phase-priority-queue.ts b/src/queues/pokemon-phase-priority-queue.ts new file mode 100644 index 00000000000..3098c5be435 --- /dev/null +++ b/src/queues/pokemon-phase-priority-queue.ts @@ -0,0 +1,20 @@ +import type { DynamicPhase } from "#app/@types/phase-types"; +import { PriorityQueue } from "#app/queues/priority-queue"; +import { sortInSpeedOrder } from "#app/utils/speed-order"; +import type { BattlerIndex } from "#enums/battler-index"; + +/** A generic speed-based priority queue of {@linkcode DynamicPhase}s */ +export class PokemonPhasePriorityQueue extends PriorityQueue { + protected setOrder: BattlerIndex[] | undefined; + protected override reorder(): void { + const setOrder = this.setOrder; + if (setOrder) { + this.queue.sort( + (a, b) => + setOrder.indexOf(a.getPokemon().getBattlerIndex()) - setOrder.indexOf(b.getPokemon().getBattlerIndex()), + ); + } else { + this.queue = sortInSpeedOrder(this.queue); + } + } +} diff --git a/src/queues/pokemon-priority-queue.ts b/src/queues/pokemon-priority-queue.ts new file mode 100644 index 00000000000..597bfb32c0d --- /dev/null +++ b/src/queues/pokemon-priority-queue.ts @@ -0,0 +1,10 @@ +import type { Pokemon } from "#app/field/pokemon"; +import { PriorityQueue } from "#app/queues/priority-queue"; +import { sortInSpeedOrder } from "#app/utils/speed-order"; + +/** A priority queue of {@linkcode Pokemon}s */ +export class PokemonPriorityQueue extends PriorityQueue { + protected override reorder(): void { + this.queue = sortInSpeedOrder(this.queue); + } +} diff --git a/src/queues/post-summon-phase-priority-queue.ts b/src/queues/post-summon-phase-priority-queue.ts new file mode 100644 index 00000000000..37da90a1427 --- /dev/null +++ b/src/queues/post-summon-phase-priority-queue.ts @@ -0,0 +1,45 @@ +import { globalScene } from "#app/global-scene"; +import { PostSummonActivateAbilityPhase } from "#app/phases/post-summon-activate-ability-phase"; +import type { PostSummonPhase } from "#app/phases/post-summon-phase"; +import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-queue"; +import { sortInSpeedOrder } from "#app/utils/speed-order"; + +/** + * Priority Queue for {@linkcode PostSummonPhase} and {@linkcode PostSummonActivateAbilityPhase} + * + * Orders phases first by ability priority, then by the {@linkcode Pokemon}'s effective speed + */ +export class PostSummonPhasePriorityQueue extends PokemonPhasePriorityQueue { + protected override reorder(): void { + this.queue = sortInSpeedOrder(this.queue, false); + this.queue.sort((phaseA, phaseB) => phaseB.getPriority() - phaseA.getPriority()); + } + + public override push(phase: PostSummonPhase): void { + super.push(phase); + this.queueAbilityPhase(phase); + } + + /** + * Queues all necessary {@linkcode PostSummonActivateAbilityPhase}s for each pushed {@linkcode PostSummonPhase} + * @param phase - The {@linkcode PostSummonPhase} that was pushed onto the queue + */ + private queueAbilityPhase(phase: PostSummonPhase): void { + if (phase instanceof PostSummonActivateAbilityPhase) { + return; + } + + const phasePokemon = phase.getPokemon(); + + phasePokemon.getAbilityPriorities().forEach((priority, idx) => { + const activateAbilityPhase = new PostSummonActivateAbilityPhase( + phasePokemon.getBattlerIndex(), + priority, + idx !== 0, + ); + phase.source === "SummonPhase" + ? globalScene.phaseManager.pushPhase(activateAbilityPhase) + : globalScene.phaseManager.unshiftPhase(activateAbilityPhase); + }); + } +} diff --git a/src/queues/priority-queue.ts b/src/queues/priority-queue.ts new file mode 100644 index 00000000000..b53cfec3f4d --- /dev/null +++ b/src/queues/priority-queue.ts @@ -0,0 +1,78 @@ +/** + * Stores a list of elements. + * + * Dynamically updates ordering to always pop the highest "priority", based on implementation of {@linkcode reorder}. + */ +export abstract class PriorityQueue { + protected queue: T[] = []; + + /** + * Sorts the elements in the queue + */ + protected abstract reorder(): void; + + /** + * Calls {@linkcode reorder} and shifts the queue + * @returns The front element of the queue after sorting, or `undefined` if the queue is empty + * @sealed + */ + public pop(): T | undefined { + if (this.isEmpty()) { + return; + } + + this.reorder(); + return this.queue.shift(); + } + + /** + * Adds an element to the queue + * @param element The element to add + */ + public push(element: T): void { + this.queue.push(element); + } + + /** + * Removes all elements from the queue + * @sealed + */ + public clear(): void { + this.queue.splice(0, this.queue.length); + } + + /** + * @returns Whether the queue is empty + * @sealed + */ + public isEmpty(): boolean { + return this.queue.length === 0; + } + + /** + * Removes the first element matching the condition + * @param condition - An optional condition function (defaults to a function that always returns `true`) + * @returns Whether a removal occurred + */ + public remove(condition: (t: T) => boolean = () => true): boolean { + // Reorder to remove the first element + this.reorder(); + const index = this.queue.findIndex(condition); + if (index === -1) { + return false; + } + + this.queue.splice(index, 1); + return true; + } + + /** @returns An element matching the condition function */ + public find(condition?: (t: T) => boolean): T | undefined { + return this.queue.find(e => !condition || condition(e)); + } + + /** @returns Whether an element matching the condition function exists */ + public has(condition?: (t: T) => boolean): boolean { + return this.queue.some(e => !condition || condition(e)); + } +} diff --git a/src/sprites/variant.ts b/src/sprites/variant.ts index 28d7ed13839..9d7a20bc058 100644 --- a/src/sprites/variant.ts +++ b/src/sprites/variant.ts @@ -2,7 +2,6 @@ import { globalScene } from "#app/global-scene"; import { VariantTier } from "#enums/variant-tier"; import type { Pokemon } from "#field/pokemon"; import { hasExpSprite } from "#sprites/sprite-utils"; -import { isNullOrUndefined } from "#utils/common"; export type Variant = 0 | 1 | 2; @@ -138,7 +137,7 @@ export async function populateVariantColorCache( return fallbackVariantColor(cacheKey, spritePath, useExpSprite, battleSpritePath, error); }) .then(c => { - if (!isNullOrUndefined(c)) { + if (c != null) { variantColorCache[cacheKey] = c; } }); diff --git a/src/system/arena-data.ts b/src/system/arena-data.ts index 18620e15223..0d40a9c6234 100644 --- a/src/system/arena-data.ts +++ b/src/system/arena-data.ts @@ -47,8 +47,12 @@ export class ArenaData { } this.biome = source.biome; - this.weather = source.weather ? new Weather(source.weather.weatherType, source.weather.turnsLeft) : null; - this.terrain = source.terrain ? new Terrain(source.terrain.terrainType, source.terrain.turnsLeft) : null; + this.weather = source.weather + ? new Weather(source.weather.weatherType, source.weather.turnsLeft, source.weather.maxDuration) + : null; + this.terrain = source.terrain + ? new Terrain(source.terrain.terrainType, source.terrain.turnsLeft, source.terrain.maxDuration) + : null; this.positionalTags = source.positionalTags ?? []; } } diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 3442748c9ab..3ffa7482706 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -1,6 +1,5 @@ import { pokerogueApi } from "#api/pokerogue-api"; import { clientSessionId, loggedInUser, updateUserInfo } from "#app/account"; -import type { PokeballCounts } from "#app/battle-scene"; import { defaultStarterSpecies, saveKey } from "#app/constants"; import { getGameMode } from "#app/game-mode"; import { globalScene } from "#app/global-scene"; @@ -24,11 +23,9 @@ import { Device } from "#enums/devices"; import { DexAttr } from "#enums/dex-attr"; import { GameDataType } from "#enums/game-data-type"; import { GameModes } from "#enums/game-modes"; -import type { MoveId } from "#enums/move-id"; import type { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Nature } from "#enums/nature"; import { PlayerGender } from "#enums/player-gender"; -import type { PokemonType } from "#enums/pokemon-type"; import { SpeciesId } from "#enums/species-id"; import { StatusEffect } from "#enums/status-effect"; import { TrainerVariant } from "#enums/trainer-variant"; @@ -62,7 +59,20 @@ import { import { VoucherType, vouchers } from "#system/voucher"; import { trainerConfigs } from "#trainers/trainer-config"; import type { DexData, DexEntry } from "#types/dex-data"; -import { RUN_HISTORY_LIMIT } from "#ui/handlers/run-history-ui-handler"; +import type { + AchvUnlocks, + DexAttrProps, + RunHistoryData, + SeenDialogues, + SessionSaveData, + StarterData, + SystemSaveData, + TutorialFlags, + Unlocks, + VoucherCounts, + VoucherUnlocks, +} from "#types/save-data"; +import { RUN_HISTORY_LIMIT } from "#ui/run-history-ui-handler"; import { applyChallenges } from "#utils/challenge-utils"; import { executeIf, fixedInt, isLocal, NumberHolder, randInt, randSeedItem } from "#utils/common"; import { decrypt, encrypt } from "#utils/data"; @@ -94,132 +104,6 @@ function getDataTypeKey(dataType: GameDataType, slotId = 0): string { } } -// TODO: Move all these exported interfaces to @types -export interface SystemSaveData { - trainerId: number; - secretId: number; - gender: PlayerGender; - dexData: DexData; - starterData: StarterData; - gameStats: GameStats; - unlocks: Unlocks; - achvUnlocks: AchvUnlocks; - voucherUnlocks: VoucherUnlocks; - voucherCounts: VoucherCounts; - eggs: EggData[]; - gameVersion: string; - timestamp: number; - eggPity: number[]; - unlockPity: number[]; -} - -export interface SessionSaveData { - seed: string; - playTime: number; - gameMode: GameModes; - party: PokemonData[]; - enemyParty: PokemonData[]; - modifiers: PersistentModifierData[]; - enemyModifiers: PersistentModifierData[]; - arena: ArenaData; - pokeballCounts: PokeballCounts; - money: number; - score: number; - waveIndex: number; - battleType: BattleType; - trainer: TrainerData; - gameVersion: string; - /** The player-chosen name of the run */ - name: string; - timestamp: number; - challenges: ChallengeData[]; - mysteryEncounterType: MysteryEncounterType | -1; // Only defined when current wave is ME, - mysteryEncounterSaveData: MysteryEncounterSaveData; - /** - * Counts the amount of pokemon fainted in your party during the current arena encounter. - */ - playerFaints: number; -} - -interface Unlocks { - [key: number]: boolean; -} - -export interface AchvUnlocks { - [key: string]: number; -} - -export interface VoucherUnlocks { - [key: string]: number; -} - -export interface VoucherCounts { - [type: string]: number; -} - -export type StarterMoveset = [MoveId] | [MoveId, MoveId] | [MoveId, MoveId, MoveId] | [MoveId, MoveId, MoveId, MoveId]; - -export interface StarterFormMoveData { - [key: number]: StarterMoveset; -} - -export interface StarterMoveData { - [key: number]: StarterMoveset | StarterFormMoveData; -} - -export interface StarterAttributes { - nature?: number; - ability?: number; - variant?: number; - form?: number; - female?: boolean; - shiny?: boolean; - favorite?: boolean; - nickname?: string; - tera?: PokemonType; -} - -export interface DexAttrProps { - shiny: boolean; - female: boolean; - variant: Variant; - formIndex: number; -} - -export type RunHistoryData = Record; - -export interface RunEntry { - entry: SessionSaveData; - isVictory: boolean; - /*Automatically set to false at the moment - implementation TBD*/ - isFavorite: boolean; -} - -export interface StarterDataEntry { - moveset: StarterMoveset | StarterFormMoveData | null; - eggMoves: number; - candyCount: number; - friendship: number; - abilityAttr: number; - passiveAttr: number; - valueReduction: number; - classicWinCount: number; -} - -export interface StarterData { - [key: number]: StarterDataEntry; -} - -// TODO: Rework into a bitmask -export type TutorialFlags = { - [key in Tutorial]: boolean; -}; - -// TODO: Rework into a bitmask -export interface SeenDialogues { - [key: string]: boolean; -} - const systemShortKeys = { seenAttr: "$sa", caughtAttr: "$ca", @@ -1137,6 +1021,7 @@ export class GameData { WeatherType.NONE, globalScene.arena.weather?.weatherType!, globalScene.arena.weather?.turnsLeft!, + globalScene.arena.weather?.maxDuration!, ), ); // TODO: is this bang correct? @@ -1146,6 +1031,7 @@ export class GameData { TerrainType.NONE, globalScene.arena.terrain?.terrainType!, globalScene.arena.terrain?.turnsLeft!, + globalScene.arena.terrain?.maxDuration!, ), ); // TODO: is this bang correct? @@ -1155,12 +1041,14 @@ export class GameData { if (globalScene.arena.tags) { for (const tag of globalScene.arena.tags) { if (tag instanceof EntryHazardTag) { - const { tagType, side, turnCount, layers, maxLayers } = tag as EntryHazardTag; + const { tagType, side, turnCount, maxDuration, layers, maxLayers } = tag as EntryHazardTag; globalScene.arena.eventTarget.dispatchEvent( - new TagAddedEvent(tagType, side, turnCount, layers, maxLayers), + new TagAddedEvent(tagType, side, turnCount, maxDuration, layers, maxLayers), ); } else { - globalScene.arena.eventTarget.dispatchEvent(new TagAddedEvent(tag.tagType, tag.side, tag.turnCount)); + globalScene.arena.eventTarget.dispatchEvent( + new TagAddedEvent(tag.tagType, tag.side, tag.turnCount, tag.maxDuration), + ); } } } @@ -2002,9 +1890,7 @@ export class GameData { }); } - /** - * Checks whether the root species of a given {@PokemonSpecies} has been unlocked in the dex - */ + /** Return whether the root species of a given `PokemonSpecies` has been unlocked in the dex */ isRootSpeciesUnlocked(species: PokemonSpecies): boolean { return !!this.dexData[species.getRootSpeciesId()]?.caughtAttr; } diff --git a/src/system/ribbons/ribbon-methods.ts b/src/system/ribbons/ribbon-methods.ts index 138c0be7b51..f1aeb9fefc2 100644 --- a/src/system/ribbons/ribbon-methods.ts +++ b/src/system/ribbons/ribbon-methods.ts @@ -2,7 +2,6 @@ import { globalScene } from "#app/global-scene"; import { pokemonPrevolutions } from "#balance/pokemon-evolutions"; import type { SpeciesId } from "#enums/species-id"; import type { RibbonFlag } from "#system/ribbons/ribbon-data"; -import { isNullOrUndefined } from "#utils/common"; /** * Award one or more ribbons to a species and its pre-evolutions @@ -14,7 +13,7 @@ export function awardRibbonsToSpeciesLine(id: SpeciesId, ribbons: RibbonFlag): v const dexData = globalScene.gameData.dexData; dexData[id].ribbons.award(ribbons); // Mark all pre-evolutions of the Pokémon with the same ribbon flags. - for (let prevoId = pokemonPrevolutions[id]; !isNullOrUndefined(prevoId); prevoId = pokemonPrevolutions[prevoId]) { + for (let prevoId = pokemonPrevolutions[id]; prevoId != null; prevoId = pokemonPrevolutions[prevoId]) { dexData[prevoId].ribbons.award(ribbons); } } diff --git a/src/system/settings/settings-language.ts b/src/system/settings/settings-language.ts new file mode 100644 index 00000000000..3fd90477e65 --- /dev/null +++ b/src/system/settings/settings-language.ts @@ -0,0 +1,101 @@ +import { globalScene } from "#app/global-scene"; +import type { SettingsDisplayUiHandler } from "#ui/settings-display-ui-handler"; +import i18next from "i18next"; + +const cancelHandler = () => { + globalScene.ui.revertMode(); + const handler = globalScene.ui.getHandler(); + // Reset the cursor to the current language, if in the settings menu + if (handler && typeof (handler as SettingsDisplayUiHandler).setOptionCursor === "function") { + (handler as SettingsDisplayUiHandler).setOptionCursor(-1, 0, true); + } +}; + +const changeLocaleHandler = (locale: string): boolean => { + try { + i18next.changeLanguage(locale); + localStorage.setItem("prLang", locale); + cancelHandler(); + // Reload the whole game to apply the new locale since also some constants are translated + window.location.reload(); + return true; + } catch (error) { + console.error("Error changing locale:", error); + return false; + } +}; + +export const languageOptions = [ + { + label: "English", + handler: () => changeLocaleHandler("en"), + }, + { + label: "Español (ES)", + handler: () => changeLocaleHandler("es-ES"), + }, + { + label: "Español (LATAM)", + handler: () => changeLocaleHandler("es-MX"), + }, + { + label: "Français", + handler: () => changeLocaleHandler("fr"), + }, + { + label: "Deutsch", + handler: () => changeLocaleHandler("de"), + }, + { + label: "Italiano", + handler: () => changeLocaleHandler("it"), + }, + { + label: "Português (BR)", + handler: () => changeLocaleHandler("pt-BR"), + }, + { + label: "한국어", + handler: () => changeLocaleHandler("ko"), + }, + { + label: "日本語", + handler: () => changeLocaleHandler("ja"), + }, + { + label: "简体中文", + handler: () => changeLocaleHandler("zh-CN"), + }, + { + label: "繁體中文", + handler: () => changeLocaleHandler("zh-TW"), + }, + { + label: "Català (Needs Help)", + handler: () => changeLocaleHandler("ca"), + }, + { + label: "Türkçe (Needs Help)", + handler: () => changeLocaleHandler("tr"), + }, + { + label: "Русский (Needs Help)", + handler: () => changeLocaleHandler("ru"), + }, + { + label: "Dansk (Needs Help)", + handler: () => changeLocaleHandler("da"), + }, + { + label: "Română (Needs Help)", + handler: () => changeLocaleHandler("ro"), + }, + { + label: "Tagalog (Needs Help)", + handler: () => changeLocaleHandler("tl"), + }, + { + label: i18next.t("settings:back"), + handler: () => cancelHandler(), + }, +]; diff --git a/src/system/settings/settings.ts b/src/system/settings/settings.ts index cf3a5fb0eee..78b6044b0fc 100644 --- a/src/system/settings/settings.ts +++ b/src/system/settings/settings.ts @@ -6,10 +6,10 @@ import { PlayerGender } from "#enums/player-gender"; import { ShopCursorTarget } from "#enums/shop-cursor-target"; import { UiMode } from "#enums/ui-mode"; import { CandyUpgradeNotificationChangedEvent } from "#events/battle-scene"; -import type { SettingsUiHandler } from "#ui/settings-ui-handler"; import { updateWindowType } from "#ui/ui-theme"; import { isLocal } from "#utils/common"; import i18next from "i18next"; +import { languageOptions } from "./settings-language"; const VOLUME_OPTIONS: SettingOption[] = [ { @@ -120,6 +120,14 @@ export interface Setting { default: number; type: SettingType; requireReload?: boolean; + /** + * Specifies the behavior when navigating left/right at the boundaries of the option + * + * - `true`: the cursor will stay on the boundary instead of moving + * - `false`: the cursor will wrap to the other end of the options list + * @defaultValue `false` + */ + clamp?: boolean; /** Whether the setting can be activated or not */ activatable?: boolean; /** Determines whether the setting should be hidden from the UI */ @@ -230,6 +238,7 @@ export const Setting: Array = [ ], default: 3, type: SettingType.GENERAL, + clamp: false, }, { key: SettingKeys.HP_Bar_Speed, @@ -639,6 +648,7 @@ export const Setting: Array = [ options: VOLUME_OPTIONS, default: 5, type: SettingType.AUDIO, + clamp: true, }, { key: SettingKeys.BGM_Volume, @@ -646,6 +656,7 @@ export const Setting: Array = [ options: VOLUME_OPTIONS, default: 10, type: SettingType.AUDIO, + clamp: true, }, { key: SettingKeys.Field_Volume, @@ -653,6 +664,7 @@ export const Setting: Array = [ options: VOLUME_OPTIONS, default: 10, type: SettingType.AUDIO, + clamp: true, }, { key: SettingKeys.SE_Volume, @@ -660,6 +672,7 @@ export const Setting: Array = [ options: VOLUME_OPTIONS, default: 10, type: SettingType.AUDIO, + clamp: true, }, { key: SettingKeys.UI_Volume, @@ -667,6 +680,7 @@ export const Setting: Array = [ options: VOLUME_OPTIONS, default: 10, type: SettingType.AUDIO, + clamp: true, }, { key: SettingKeys.Battle_Music, @@ -897,98 +911,8 @@ export function setSetting(setting: string, value: number): boolean { break; case SettingKeys.Language: if (value && globalScene.ui) { - const cancelHandler = () => { - globalScene.ui.revertMode(); - (globalScene.ui.getHandler() as SettingsUiHandler).setOptionCursor(-1, 0, true); - }; - const changeLocaleHandler = (locale: string): boolean => { - try { - i18next.changeLanguage(locale); - localStorage.setItem("prLang", locale); - cancelHandler(); - // Reload the whole game to apply the new locale since also some constants are translated - window.location.reload(); - return true; - } catch (error) { - console.error("Error changing locale:", error); - return false; - } - }; globalScene.ui.setOverlayMode(UiMode.OPTION_SELECT, { - options: [ - { - label: "English", - handler: () => changeLocaleHandler("en"), - }, - { - label: "Español (ES)", - handler: () => changeLocaleHandler("es-ES"), - }, - { - label: "Español (LATAM)", - handler: () => changeLocaleHandler("es-MX"), - }, - { - label: "Français", - handler: () => changeLocaleHandler("fr"), - }, - { - label: "Deutsch", - handler: () => changeLocaleHandler("de"), - }, - { - label: "Italiano", - handler: () => changeLocaleHandler("it"), - }, - { - label: "Português (BR)", - handler: () => changeLocaleHandler("pt-BR"), - }, - { - label: "한국어", - handler: () => changeLocaleHandler("ko"), - }, - { - label: "日本語", - handler: () => changeLocaleHandler("ja"), - }, - { - label: "简体中文", - handler: () => changeLocaleHandler("zh-CN"), - }, - { - label: "繁體中文", - handler: () => changeLocaleHandler("zh-TW"), - }, - { - label: "Català (Needs Help)", - handler: () => changeLocaleHandler("ca"), - }, - { - label: "Türkçe (Needs Help)", - handler: () => changeLocaleHandler("tr"), - }, - { - label: "Русский (Needs Help)", - handler: () => changeLocaleHandler("ru"), - }, - { - label: "Dansk (Needs Help)", - handler: () => changeLocaleHandler("da"), - }, - { - label: "Română (Needs Help)", - handler: () => changeLocaleHandler("ro"), - }, - { - label: "Tagalog (Needs Help)", - handler: () => changeLocaleHandler("tl"), - }, - { - label: i18next.t("settings:back"), - handler: () => cancelHandler(), - }, - ], + options: languageOptions, maxOptions: 7, }); return false; diff --git a/src/system/version-migration/version-converter.ts b/src/system/version-migration/version-converter.ts index 6dde611ce84..269e577ca3f 100644 --- a/src/system/version-migration/version-converter.ts +++ b/src/system/version-migration/version-converter.ts @@ -1,7 +1,7 @@ /** biome-ignore-all lint/performance/noNamespaceImport: Convenience */ import { version } from "#package.json"; -import type { SessionSaveData, SystemSaveData } from "#system/game-data"; +import type { SessionSaveData, SystemSaveData } from "#types/save-data"; import type { SessionSaveMigrator } from "#types/session-save-migrator"; import type { SettingsSaveMigrator } from "#types/settings-save-migrator"; import type { SystemSaveMigrator } from "#types/system-save-migrator"; diff --git a/src/system/version-migration/versions/v1_0_4.ts b/src/system/version-migration/versions/v1_0_4.ts index 6d65df29970..5342396d576 100644 --- a/src/system/version-migration/versions/v1_0_4.ts +++ b/src/system/version-migration/versions/v1_0_4.ts @@ -3,12 +3,11 @@ import { allSpecies } from "#data/data-lists"; import { CustomPokemonData } from "#data/pokemon-data"; import { AbilityAttr } from "#enums/ability-attr"; import { DexAttr } from "#enums/dex-attr"; -import type { SessionSaveData, SystemSaveData } from "#system/game-data"; import { SettingKeys } from "#system/settings"; +import type { SessionSaveData, SystemSaveData } from "#types/save-data"; import type { SessionSaveMigrator } from "#types/session-save-migrator"; import type { SettingsSaveMigrator } from "#types/settings-save-migrator"; import type { SystemSaveMigrator } from "#types/system-save-migrator"; -import { isNullOrUndefined } from "#utils/common"; /** * Migrate ability starter data if empty for caught species. @@ -82,7 +81,7 @@ const fixLegendaryStats: SystemSaveMigrator = { const fixStarterData: SystemSaveMigrator = { version: "1.0.4", migrate: (data: SystemSaveData): void => { - if (!isNullOrUndefined(data.starterData)) { + if (data.starterData != null) { for (const starterId of defaultStarterSpecies) { if (data.starterData[starterId]?.abilityAttr) { data.starterData[starterId].abilityAttr |= AbilityAttr.ABILITY_1; @@ -198,7 +197,7 @@ const migrateCustomPokemonData: SessionSaveMigrator = { pokemon["fusionMysteryEncounterPokemonData"] = null; } pokemon.customPokemonData = pokemon.customPokemonData ?? new CustomPokemonData(); - if (!isNullOrUndefined(pokemon["natureOverride"]) && pokemon["natureOverride"] >= 0) { + if (pokemon["natureOverride"] != null && pokemon["natureOverride"] >= 0) { pokemon.customPokemonData.nature = pokemon["natureOverride"]; pokemon["natureOverride"] = -1; } diff --git a/src/system/version-migration/versions/v1_10_0.ts b/src/system/version-migration/versions/v1_10_0.ts index 5c13acd4508..eab0b0cc78e 100644 --- a/src/system/version-migration/versions/v1_10_0.ts +++ b/src/system/version-migration/versions/v1_10_0.ts @@ -2,7 +2,7 @@ import type { BattlerIndex } from "#enums/battler-index"; import type { MoveId } from "#enums/move-id"; import type { MoveResult } from "#enums/move-result"; import { MoveUseMode } from "#enums/move-use-mode"; -import type { SessionSaveData } from "#system/game-data"; +import type { SessionSaveData } from "#types/save-data"; import type { SessionSaveMigrator } from "#types/session-save-migrator"; import type { TurnMove } from "#types/turn-move"; diff --git a/src/system/version-migration/versions/v1_7_0.ts b/src/system/version-migration/versions/v1_7_0.ts index 69c640756ea..e526ccd2c2b 100644 --- a/src/system/version-migration/versions/v1_7_0.ts +++ b/src/system/version-migration/versions/v1_7_0.ts @@ -1,9 +1,8 @@ import { globalScene } from "#app/global-scene"; import { DexAttr } from "#enums/dex-attr"; -import type { SessionSaveData, SystemSaveData } from "#system/game-data"; +import type { SessionSaveData, SystemSaveData } from "#types/save-data"; import type { SessionSaveMigrator } from "#types/session-save-migrator"; import type { SystemSaveMigrator } from "#types/system-save-migrator"; -import { isNullOrUndefined } from "#utils/common"; import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils"; /** @@ -68,13 +67,13 @@ const migrateTera: SessionSaveMigrator = { } data.party.forEach(p => { - if (isNullOrUndefined(p.teraType)) { + if (p.teraType == null) { p.teraType = getPokemonSpeciesForm(p.species, p.formIndex).type1; } }); data.enemyParty.forEach(p => { - if (isNullOrUndefined(p.teraType)) { + if (p.teraType == null) { p.teraType = getPokemonSpeciesForm(p.species, p.formIndex).type1; } }); diff --git a/src/system/version-migration/versions/v1_8_3.ts b/src/system/version-migration/versions/v1_8_3.ts index 4907b4e5e57..0d6fd87a56c 100644 --- a/src/system/version-migration/versions/v1_8_3.ts +++ b/src/system/version-migration/versions/v1_8_3.ts @@ -1,6 +1,6 @@ import { DexAttr } from "#enums/dex-attr"; import { SpeciesId } from "#enums/species-id"; -import type { SystemSaveData } from "#system/game-data"; +import type { SystemSaveData } from "#types/save-data"; import type { SystemSaveMigrator } from "#types/system-save-migrator"; import { getPokemonSpecies } from "#utils/pokemon-utils"; diff --git a/src/system/version-migration/versions/v1_9_0.ts b/src/system/version-migration/versions/v1_9_0.ts index 60e299ed458..eee60571884 100644 --- a/src/system/version-migration/versions/v1_9_0.ts +++ b/src/system/version-migration/versions/v1_9_0.ts @@ -1,7 +1,7 @@ import { MoveId } from "#enums/move-id"; import { PokemonMove } from "#moves/pokemon-move"; -import type { SessionSaveData } from "#system/game-data"; import type { PokemonData } from "#system/pokemon-data"; +import type { SessionSaveData } from "#types/save-data"; import type { SessionSaveMigrator } from "#types/session-save-migrator"; /** diff --git a/src/timed-event-manager.ts b/src/timed-event-manager.ts index ed92a1c9ca5..7db89b2a0ef 100644 --- a/src/timed-event-manager.ts +++ b/src/timed-event-manager.ts @@ -9,7 +9,6 @@ import { TextStyle } from "#enums/text-style"; import { WeatherType } from "#enums/weather-type"; import { addTextObject } from "#ui/text"; import type { nil } from "#utils/common"; -import { isNullOrUndefined } from "#utils/common"; import i18next from "i18next"; export enum EventType { @@ -428,7 +427,7 @@ export class TimedEventManager { getEventBannerLangs(): string[] { const ret: string[] = []; - ret.push(...timedEvents.find(te => this.isActive(te) && !isNullOrUndefined(te.availableLangs))?.availableLangs!); + ret.push(...timedEvents.find(te => this.isActive(te) && te.availableLangs != null)?.availableLangs!); return ret; } @@ -437,7 +436,7 @@ export class TimedEventManager { timedEvents .filter(te => this.isActive(te)) .map(te => { - if (!isNullOrUndefined(te.eventEncounters)) { + if (te.eventEncounters != null) { ret.push(...te.eventEncounters); } }); @@ -452,7 +451,7 @@ export class TimedEventManager { let multiplier = CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER; const classicFriendshipEvents = timedEvents.filter(te => this.isActive(te)); for (const fe of classicFriendshipEvents) { - if (!isNullOrUndefined(fe.classicFriendshipMultiplier) && fe.classicFriendshipMultiplier > multiplier) { + if (fe.classicFriendshipMultiplier != null && fe.classicFriendshipMultiplier > multiplier) { multiplier = fe.classicFriendshipMultiplier; } } @@ -476,7 +475,7 @@ export class TimedEventManager { timedEvents .filter(te => this.isActive(te)) .map(te => { - if (!isNullOrUndefined(te.delibirdyBuff)) { + if (te.delibirdyBuff != null) { ret.push(...te.delibirdyBuff); } }); @@ -492,7 +491,7 @@ export class TimedEventManager { timedEvents .filter(te => this.isActive(te)) .map(te => { - if (!isNullOrUndefined(te.weather)) { + if (te.weather != null) { ret.push(...te.weather); } }); @@ -504,7 +503,7 @@ export class TimedEventManager { timedEvents .filter(te => this.isActive(te)) .map(te => { - if (!isNullOrUndefined(te.mysteryEncounterTierChanges)) { + if (te.mysteryEncounterTierChanges != null) { ret.push(...te.mysteryEncounterTierChanges); } }); @@ -514,7 +513,7 @@ export class TimedEventManager { getEventMysteryEncountersDisabled(): MysteryEncounterType[] { const ret: MysteryEncounterType[] = []; timedEvents - .filter(te => this.isActive(te) && !isNullOrUndefined(te.mysteryEncounterTierChanges)) + .filter(te => this.isActive(te) && te.mysteryEncounterTierChanges != null) .map(te => { te.mysteryEncounterTierChanges?.map(metc => { if (metc.disable) { @@ -531,7 +530,7 @@ export class TimedEventManager { ): MysteryEncounterTier { let ret = normal; timedEvents - .filter(te => this.isActive(te) && !isNullOrUndefined(te.mysteryEncounterTierChanges)) + .filter(te => this.isActive(te) && te.mysteryEncounterTierChanges != null) .map(te => { te.mysteryEncounterTierChanges?.map(metc => { if (metc.mysteryEncounter === encounterType) { @@ -544,7 +543,7 @@ export class TimedEventManager { getEventLuckBoost(): number { let ret = 0; - const luckEvents = timedEvents.filter(te => this.isActive(te) && !isNullOrUndefined(te.luckBoost)); + const luckEvents = timedEvents.filter(te => this.isActive(te) && te.luckBoost != null); for (const le of luckEvents) { ret += le.luckBoost!; } @@ -556,7 +555,7 @@ export class TimedEventManager { timedEvents .filter(te => this.isActive(te)) .map(te => { - if (!isNullOrUndefined(te.luckBoostedSpecies)) { + if (te.luckBoostedSpecies != null) { ret.push(...te.luckBoostedSpecies.filter(s => !ret.includes(s))); } }); @@ -576,7 +575,7 @@ export class TimedEventManager { getFixedBattleEventRewards(wave: number): string[] { const ret: string[] = []; timedEvents - .filter(te => this.isActive(te) && !isNullOrUndefined(te.classicWaveRewards)) + .filter(te => this.isActive(te) && te.classicWaveRewards != null) .map(te => { ret.push(...te.classicWaveRewards!.filter(cwr => cwr.wave === wave).map(cwr => cwr.type)); }); @@ -586,7 +585,7 @@ export class TimedEventManager { // Gets the extra shiny chance for trainers due to event (odds/65536) getClassicTrainerShinyChance(): number { let ret = 0; - const tsEvents = timedEvents.filter(te => this.isActive(te) && !isNullOrUndefined(te.trainerShinyChance)); + const tsEvents = timedEvents.filter(te => this.isActive(te) && te.trainerShinyChance != null); tsEvents.map(t => (ret += t.trainerShinyChance!)); return ret; } @@ -594,7 +593,7 @@ export class TimedEventManager { getEventBgmReplacement(bgm: string): string { let ret = bgm; timedEvents.map(te => { - if (this.isActive(te) && !isNullOrUndefined(te.music)) { + if (this.isActive(te) && te.music != null) { te.music.map(mr => { if (mr[0] === bgm) { console.log(`it is ${te.name} so instead of ${mr[0]} we play ${mr[1]}`); diff --git a/src/touch-controls.ts b/src/touch-controls.ts index 370b1748653..d031af9ef4c 100644 --- a/src/touch-controls.ts +++ b/src/touch-controls.ts @@ -5,9 +5,9 @@ import type Phaser from "phaser"; const repeatInputDelayMillis = 250; export class TouchControl { - events: Phaser.Events.EventEmitter; + readonly events: Phaser.Events.EventEmitter; private buttonLock: string[] = []; - private inputInterval: NodeJS.Timeout[] = []; + private readonly inputInterval: NodeJS.Timeout[] = []; /** Whether touch controls are disabled */ private disabled = false; /** Whether the last touch event has finished before disabling */ @@ -61,12 +61,46 @@ export class TouchControl { * event, removes the keydown state, and removes the 'active' class from the node and the last touched element. */ bindKey(node: HTMLElement, key: string) { + node.addEventListener("touchstart", (event: TouchEvent) => { + // Handle touch events for touch devices + this.touchButtonDown(node, key); + event.preventDefault(); + + // prevent pointer event from also firing (undefined just sets presence of custom attribute) + if (event.currentTarget instanceof HTMLElement) { + event.currentTarget.dataset.skipPointerEvent = undefined; + } + }); node.addEventListener("pointerdown", event => { + const currentTarget = event.currentTarget; + if (currentTarget instanceof HTMLElement && "skipPointerDown" in currentTarget.dataset) { + return; + } event.preventDefault(); this.touchButtonDown(node, key); }); + node.addEventListener("touchcancel", (event: TouchEvent) => { + if (event.currentTarget instanceof HTMLElement && "skipPointerDown" in event.currentTarget.dataset) { + delete event.currentTarget.dataset.skipPointerEvent; + } + }); + + node.addEventListener("touchend", (event: TouchEvent) => { + event.preventDefault(); + this.touchButtonUp(node, key, event.target?.["id"]); + if (event.currentTarget instanceof HTMLElement && "skipPointerDown" in event.currentTarget.dataset) { + // allow pointer event to once again fire + delete event.currentTarget.dataset.skipPointerEvent; + event.currentTarget.dataset.skipPointerUp = undefined; + } + }); + node.addEventListener("pointerup", event => { + if (event.currentTarget instanceof HTMLElement && "skipPointerUp" in event.currentTarget.dataset) { + delete event.currentTarget.dataset.skipPointerUp; + return; + } event.preventDefault(); this.touchButtonUp(node, key, event.target?.["id"]); }); @@ -143,7 +177,7 @@ export class TouchControl { * {@link https://stackoverflow.com/a/39778831/4622620|Source} * * Prevent zoom on specified element - * @param {HTMLElement} element + * @param element */ preventElementZoom(element: HTMLElement | null): void { if (!element) { diff --git a/src/tutorial.ts b/src/tutorial.ts index 5ab0be116f8..018d0927da0 100644 --- a/src/tutorial.ts +++ b/src/tutorial.ts @@ -1,8 +1,8 @@ import { globalScene } from "#app/global-scene"; import Overrides from "#app/overrides"; import { UiMode } from "#enums/ui-mode"; -import { AwaitableUiHandler } from "#ui/handlers/awaitable-ui-handler"; -import type { UiHandler } from "#ui/handlers/ui-handler"; +import { AwaitableUiHandler } from "#ui/awaitable-ui-handler"; +import type { UiHandler } from "#ui/ui-handler"; import i18next from "i18next"; export enum Tutorial { diff --git a/src/ui-inputs.ts b/src/ui-inputs.ts index 34a08db8fbc..fd7883d3136 100644 --- a/src/ui-inputs.ts +++ b/src/ui-inputs.ts @@ -3,16 +3,16 @@ import type { InputsController } from "#app/inputs-controller"; import { Button } from "#enums/buttons"; import { UiMode } from "#enums/ui-mode"; import { Setting, SettingKeys, settingIndex } from "#system/settings"; -import { PokedexPageUiHandler } from "#ui/containers/pokedex-page-ui-handler"; -import type { MessageUiHandler } from "#ui/handlers/message-ui-handler"; -import { PokedexUiHandler } from "#ui/handlers/pokedex-ui-handler"; -import { RunInfoUiHandler } from "#ui/handlers/run-info-ui-handler"; -import { StarterSelectUiHandler } from "#ui/handlers/starter-select-ui-handler"; +import type { MessageUiHandler } from "#ui/message-ui-handler"; +import { PokedexPageUiHandler } from "#ui/pokedex-page-ui-handler"; +import { PokedexUiHandler } from "#ui/pokedex-ui-handler"; +import { RunInfoUiHandler } from "#ui/run-info-ui-handler"; import { SettingsAudioUiHandler } from "#ui/settings-audio-ui-handler"; import { SettingsDisplayUiHandler } from "#ui/settings-display-ui-handler"; import { SettingsGamepadUiHandler } from "#ui/settings-gamepad-ui-handler"; import { SettingsKeyboardUiHandler } from "#ui/settings-keyboard-ui-handler"; import { SettingsUiHandler } from "#ui/settings-ui-handler"; +import { StarterSelectUiHandler } from "#ui/starter-select-ui-handler"; import Phaser from "phaser"; type ActionKeys = Record void>; diff --git a/src/ui/battle-info/enemy-battle-info.ts b/src/ui/battle-info/enemy-battle-info.ts index ad72afedc38..1a16a1dd934 100644 --- a/src/ui/battle-info/enemy-battle-info.ts +++ b/src/ui/battle-info/enemy-battle-info.ts @@ -3,9 +3,9 @@ import { Stat } from "#enums/stat"; import { TextStyle } from "#enums/text-style"; import { UiTheme } from "#enums/ui-theme"; import type { EnemyPokemon } from "#field/pokemon"; +import { BattleFlyout } from "#ui/battle-flyout"; import type { BattleInfoParamList } from "#ui/battle-info"; import { BattleInfo } from "#ui/battle-info"; -import { BattleFlyout } from "#ui/containers/battle-flyout"; import { addTextObject } from "#ui/text"; import { addWindow, WindowVariant } from "#ui/ui-theme"; import { getLocalizedSpriteKey } from "#utils/common"; diff --git a/src/ui/containers/arena-flyout.ts b/src/ui/containers/arena-flyout.ts index 3555694760d..355f3edb293 100644 --- a/src/ui/containers/arena-flyout.ts +++ b/src/ui/containers/arena-flyout.ts @@ -15,8 +15,8 @@ import { } from "#events/arena"; import type { TurnEndEvent } from "#events/battle-scene"; import { BattleSceneEventType } from "#events/battle-scene"; -import { TimeOfDayWidget } from "#ui/containers/time-of-day-widget"; import { addTextObject } from "#ui/text"; +import { TimeOfDayWidget } from "#ui/time-of-day-widget"; import { addWindow, WindowVariant } from "#ui/ui-theme"; import { fixedInt } from "#utils/common"; import { toCamelCase, toTitleCase } from "#utils/strings"; @@ -285,6 +285,12 @@ export class ArenaFlyout extends Phaser.GameObjects.Container { switch (arenaEffectChangedEvent.constructor) { case TagAddedEvent: { const tagAddedEvent = arenaEffectChangedEvent as TagAddedEvent; + + const excludedTags = [ArenaTagType.PENDING_HEAL]; + if (excludedTags.includes(tagAddedEvent.arenaTagType)) { + return; + } + const isArenaTrapTag = globalScene.arena.getTag(tagAddedEvent.arenaTagType) instanceof EntryHazardTag; let arenaEffectType: ArenaEffectType; @@ -317,7 +323,7 @@ export class ArenaFlyout extends Phaser.GameObjects.Container { this.fieldEffectInfo.push({ name, effectType: arenaEffectType, - maxDuration: tagAddedEvent.duration, + maxDuration: tagAddedEvent.maxDuration, duration: tagAddedEvent.duration, tagType: tagAddedEvent.arenaTagType, }); @@ -353,7 +359,7 @@ export class ArenaFlyout extends Phaser.GameObjects.Container { ), effectType: fieldEffectChangedEvent instanceof WeatherChangedEvent ? ArenaEffectType.WEATHER : ArenaEffectType.TERRAIN, - maxDuration: fieldEffectChangedEvent.duration, + maxDuration: fieldEffectChangedEvent.maxDuration, duration: fieldEffectChangedEvent.duration, }; diff --git a/src/ui/containers/bgm-bar.ts b/src/ui/containers/bgm-bar.ts index f24f372a804..9c9e761e26b 100644 --- a/src/ui/containers/bgm-bar.ts +++ b/src/ui/containers/bgm-bar.ts @@ -53,7 +53,7 @@ export class BgmBar extends Phaser.GameObjects.Container { /* * Set the BGM Name to the BGM bar. - * @param {string} bgmName The name of the BGM to set. + * @param bgmName The name of the BGM to set. */ setBgmToBgmBar(bgmName: string): void { this.musicText.setText(`${i18next.t("bgmName:music")}${this.getRealBgmName(bgmName)}`); @@ -71,7 +71,7 @@ export class BgmBar extends Phaser.GameObjects.Container { /* Show or hide the BGM bar. - @param {boolean} visible Whether to show or hide the BGM bar. + @param visible Whether to show or hide the BGM bar. */ public toggleBgmBar(visible: boolean): void { /* diff --git a/src/ui/containers/daily-run-scoreboard.ts b/src/ui/containers/daily-run-scoreboard.ts index 9391d02859c..456c8edde01 100644 --- a/src/ui/containers/daily-run-scoreboard.ts +++ b/src/ui/containers/daily-run-scoreboard.ts @@ -202,8 +202,8 @@ export class DailyRunScoreboard extends Phaser.GameObjects.Container { * The method fetches the total page count if necessary, followed by fetching the rankings for the specified category * and page. It updates the UI with the fetched rankings or shows an appropriate message if no rankings are found. * - * @param {ScoreboardCategory} [category=this.category] - The category to fetch rankings for. Defaults to the current category. - * @param {number} [page=this.page] - The page number to fetch. Defaults to the current page. + * @param [category=this.category] - The category to fetch rankings for. Defaults to the current category. + * @param [page=this.page] - The page number to fetch. Defaults to the current page. */ update(category: ScoreboardCategory = this.category, page: number = this.page) { if (this.isUpdating) { @@ -249,7 +249,7 @@ export class DailyRunScoreboard extends Phaser.GameObjects.Container { /** * Sets the state of the navigation buttons. - * @param {boolean} [enabled=true] - Whether the buttons should be enabled or disabled. + * @param [enabled=true] - Whether the buttons should be enabled or disabled. */ setButtonsState(enabled = true) { const buttons = [ diff --git a/src/ui/containers/dropdown.ts b/src/ui/containers/dropdown.ts index bf589085d2e..2244aa0e5ce 100644 --- a/src/ui/containers/dropdown.ts +++ b/src/ui/containers/dropdown.ts @@ -1,6 +1,6 @@ import { globalScene } from "#app/global-scene"; import { TextStyle } from "#enums/text-style"; -import { ScrollBar } from "#ui/containers/scroll-bar"; +import { ScrollBar } from "#ui/scroll-bar"; import { addTextObject } from "#ui/text"; import { addWindow, WindowVariant } from "#ui/ui-theme"; import i18next from "i18next"; diff --git a/src/ui/containers/egg-counter-container.ts b/src/ui/containers/egg-counter-container.ts index 811b6b3bc3a..385480fc91d 100644 --- a/src/ui/containers/egg-counter-container.ts +++ b/src/ui/containers/egg-counter-container.ts @@ -2,14 +2,11 @@ import { globalScene } from "#app/global-scene"; import { TextStyle } from "#enums/text-style"; import type { EggCountChangedEvent } from "#events/egg"; import { EggEventType } from "#events/egg"; -import type { EggHatchSceneHandler } from "#ui/handlers/egg-hatch-scene-handler"; +import type { EggHatchSceneUiHandler } from "#ui/egg-hatch-scene-ui-handler"; import { addTextObject } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; -/** - * A container that displays the count of hatching eggs. - * @extends Phaser.GameObjects.Container - */ +/** A container that displays the count of hatching eggs */ export class EggCounterContainer extends Phaser.GameObjects.Container { private readonly WINDOW_DEFAULT_WIDTH = 37; private readonly WINDOW_MEDIUM_WIDTH = 42; @@ -27,7 +24,7 @@ export class EggCounterContainer extends Phaser.GameObjects.Container { super(globalScene, 0, 0); this.eggCount = eggCount; - const uiHandler = globalScene.ui.getHandler() as EggHatchSceneHandler; + const uiHandler = globalScene.ui.getHandler() as EggHatchSceneUiHandler; uiHandler.eventTarget.addEventListener(EggEventType.EGG_COUNT_CHANGED, this.onEggCountChangedEvent); this.setup(); diff --git a/src/ui/containers/filter-bar.ts b/src/ui/containers/filter-bar.ts index bbca38c3f53..3f164c4fcbb 100644 --- a/src/ui/containers/filter-bar.ts +++ b/src/ui/containers/filter-bar.ts @@ -2,9 +2,9 @@ import { globalScene } from "#app/global-scene"; import type { DropDownColumn } from "#enums/drop-down-column"; import { TextStyle } from "#enums/text-style"; import type { UiTheme } from "#enums/ui-theme"; -import type { DropDown } from "#ui/containers/dropdown"; -import { DropDownType } from "#ui/containers/dropdown"; -import type { StarterContainer } from "#ui/containers/starter-container"; +import type { DropDown } from "#ui/dropdown"; +import { DropDownType } from "#ui/dropdown"; +import type { StarterContainer } from "#ui/starter-container"; import { addTextObject, getTextColor } from "#ui/text"; import { addWindow, WindowVariant } from "#ui/ui-theme"; diff --git a/src/ui/containers/filter-text.ts b/src/ui/containers/filter-text.ts index 0d15aca8530..8601a86defa 100644 --- a/src/ui/containers/filter-text.ts +++ b/src/ui/containers/filter-text.ts @@ -2,8 +2,8 @@ import { globalScene } from "#app/global-scene"; import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; import type { UiTheme } from "#enums/ui-theme"; -import type { StarterContainer } from "#ui/containers/starter-container"; -import type { AwaitableUiHandler } from "#ui/handlers/awaitable-ui-handler"; +import type { AwaitableUiHandler } from "#ui/awaitable-ui-handler"; +import type { StarterContainer } from "#ui/starter-container"; import { addTextObject, getTextColor } from "#ui/text"; import type { UI } from "#ui/ui"; import { addWindow, WindowVariant } from "#ui/ui-theme"; diff --git a/src/ui/containers/hatched-pokemon-container.ts b/src/ui/containers/hatched-pokemon-container.ts index 4ed67477a3f..b1bc66c21ec 100644 --- a/src/ui/containers/hatched-pokemon-container.ts +++ b/src/ui/containers/hatched-pokemon-container.ts @@ -4,8 +4,8 @@ import { Gender } from "#data/gender"; import type { PokemonSpecies } from "#data/pokemon-species"; import { DexAttr } from "#enums/dex-attr"; import { getVariantTint } from "#sprites/variant"; -import type { PokemonIconAnimHandler } from "#ui/handlers/pokemon-icon-anim-handler"; -import { PokemonIconAnimMode } from "#ui/handlers/pokemon-icon-anim-handler"; +import type { PokemonIconAnimHelper } from "#ui/pokemon-icon-anim-helper"; +import { PokemonIconAnimMode } from "#ui/pokemon-icon-anim-helper"; /** * A container for a Pokemon's sprite and icons to get displayed in the egg summary screen @@ -81,9 +81,9 @@ export class HatchedPokemonContainer extends Phaser.GameObjects.Container { * Animates the pokemon icon if it has a new form or shiny variant * * @param hatchData the {@linkcode EggHatchData} to base the icons on - * @param iconAnimHandler the {@linkcode PokemonIconAnimHandler} to use to animate the sprites + * @param iconAnimHandler the {@linkcode PokemonIconAnimHelper} to use to animate the sprites */ - updateAndAnimate(hatchData: EggHatchData, iconAnimHandler: PokemonIconAnimHandler) { + updateAndAnimate(hatchData: EggHatchData, iconAnimHandler: PokemonIconAnimHelper) { const displayPokemon = hatchData.pokemon; this.species = displayPokemon.species; diff --git a/src/ui/containers/pokedex-mon-container.ts b/src/ui/containers/pokedex-mon-container.ts index 15ef6c9b5c8..158f42dd42a 100644 --- a/src/ui/containers/pokedex-mon-container.ts +++ b/src/ui/containers/pokedex-mon-container.ts @@ -3,7 +3,6 @@ import type { PokemonSpecies } from "#data/pokemon-species"; import { TextStyle } from "#enums/text-style"; import type { Variant } from "#sprites/variant"; import { addTextObject } from "#ui/text"; -import { isNullOrUndefined } from "#utils/common"; interface SpeciesDetails { shiny?: boolean; @@ -177,16 +176,16 @@ export class PokedexMonContainer extends Phaser.GameObjects.Container { const defaultDexAttr = globalScene.gameData.getSpeciesDefaultDexAttr(species, false, true); const defaultProps = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr); - if (!isNullOrUndefined(formIndex)) { + if (formIndex != null) { defaultProps.formIndex = formIndex; } - if (!isNullOrUndefined(shiny)) { + if (shiny != null) { defaultProps.shiny = shiny; } - if (!isNullOrUndefined(variant)) { + if (variant != null) { defaultProps.variant = variant; } - if (!isNullOrUndefined(female)) { + if (female != null) { defaultProps.female = female; } diff --git a/src/ui/containers/pokemon-hatch-info-container.ts b/src/ui/containers/pokemon-hatch-info-container.ts index 8ddd9df9836..ad09eb39d42 100644 --- a/src/ui/containers/pokemon-hatch-info-container.ts +++ b/src/ui/containers/pokemon-hatch-info-container.ts @@ -9,11 +9,11 @@ import { PokemonType } from "#enums/pokemon-type"; import { SpeciesId } from "#enums/species-id"; import { TextStyle } from "#enums/text-style"; import type { PlayerPokemon } from "#field/pokemon"; -import { PokemonInfoContainer } from "#ui/containers/pokemon-info-container"; import { addTextObject } from "#ui/text"; import { padInt, rgbHexToRgba } from "#utils/common"; import { getPokemonSpeciesForm } from "#utils/pokemon-utils"; import { argbFromRgba } from "@material/material-color-utilities"; +import { PokemonInfoContainer } from "./pokemon-info-container"; /** * Class for the hatch info summary of each pokemon diff --git a/src/ui/containers/pokemon-info-container.ts b/src/ui/containers/pokemon-info-container.ts index 2b214229992..3b6e5bc2fc1 100644 --- a/src/ui/containers/pokemon-info-container.ts +++ b/src/ui/containers/pokemon-info-container.ts @@ -6,15 +6,15 @@ import { PokemonType } from "#enums/pokemon-type"; import { TextStyle } from "#enums/text-style"; import type { Pokemon } from "#field/pokemon"; import { getVariantTint } from "#sprites/variant"; -import type { StarterDataEntry } from "#system/game-data"; import type { DexEntry } from "#types/dex-data"; -import { StatsContainer } from "#ui/containers/stats-container"; -import { ConfirmUiHandler } from "#ui/handlers/confirm-ui-handler"; +import type { StarterDataEntry } from "#types/save-data"; +import { ConfirmUiHandler } from "#ui/confirm-ui-handler"; import { addBBCodeTextObject, addTextObject, getTextColor } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; import { fixedInt, getShinyDescriptor } from "#utils/common"; import i18next from "i18next"; import type BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext"; +import { StatsContainer } from "./stats-container"; interface LanguageSetting { infoContainerTextSize: string; diff --git a/src/ui/containers/saving-icon-handler.ts b/src/ui/containers/saving-icon-handler.ts index 00c8b8b526c..aad37bca97f 100644 --- a/src/ui/containers/saving-icon-handler.ts +++ b/src/ui/containers/saving-icon-handler.ts @@ -1,7 +1,7 @@ import { globalScene } from "#app/global-scene"; import { fixedInt } from "#utils/common"; -export class SavingIconHandler extends Phaser.GameObjects.Container { +export class SavingIconContainer extends Phaser.GameObjects.Container { private icon: Phaser.GameObjects.Sprite; private animActive: boolean; diff --git a/src/ui/handlers/abstract-binding-ui-handler.ts b/src/ui/handlers/abstract-binding-ui-handler.ts index d106ff6f914..6b747a10d2b 100644 --- a/src/ui/handlers/abstract-binding-ui-handler.ts +++ b/src/ui/handlers/abstract-binding-ui-handler.ts @@ -2,9 +2,9 @@ import { globalScene } from "#app/global-scene"; import { Button } from "#enums/buttons"; import { TextStyle } from "#enums/text-style"; import type { UiMode } from "#enums/ui-mode"; -import { UiHandler } from "#ui/handlers/ui-handler"; import { NavigationManager } from "#ui/navigation-menu"; import { addTextObject, getTextColor } from "#ui/text"; +import { UiHandler } from "#ui/ui-handler"; import { addWindow } from "#ui/ui-theme"; import i18next from "i18next"; diff --git a/src/ui/handlers/abstract-option-select-ui-handler.ts b/src/ui/handlers/abstract-option-select-ui-handler.ts index 1e102010e4a..b86d5c33a0d 100644 --- a/src/ui/handlers/abstract-option-select-ui-handler.ts +++ b/src/ui/handlers/abstract-option-select-ui-handler.ts @@ -2,8 +2,8 @@ import { globalScene } from "#app/global-scene"; import { Button } from "#enums/buttons"; import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; -import { UiHandler } from "#ui/handlers/ui-handler"; import { addBBCodeTextObject, getTextColor, getTextStyleOptions } from "#ui/text"; +import { UiHandler } from "#ui/ui-handler"; import { addWindow } from "#ui/ui-theme"; import { fixedInt, rgbHexToRgba } from "#utils/common"; import { argbFromRgba } from "@material/material-color-utilities"; diff --git a/src/ui/handlers/achvs-ui-handler.ts b/src/ui/handlers/achvs-ui-handler.ts index f8f73dd2078..dea1829499e 100644 --- a/src/ui/handlers/achvs-ui-handler.ts +++ b/src/ui/handlers/achvs-ui-handler.ts @@ -5,11 +5,11 @@ import { TextStyle } from "#enums/text-style"; import type { UiMode } from "#enums/ui-mode"; import type { Achv } from "#system/achv"; import { achvs, getAchievementDescription } from "#system/achv"; -import type { AchvUnlocks, VoucherUnlocks } from "#system/game-data"; import type { Voucher } from "#system/voucher"; import { getVoucherTypeIcon, getVoucherTypeName, vouchers } from "#system/voucher"; -import { ScrollBar } from "#ui/containers/scroll-bar"; -import { MessageUiHandler } from "#ui/handlers/message-ui-handler"; +import type { AchvUnlocks, VoucherUnlocks } from "#types/save-data"; +import { MessageUiHandler } from "#ui/message-ui-handler"; +import { ScrollBar } from "#ui/scroll-bar"; import { addTextObject } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; import i18next from "i18next"; diff --git a/src/ui/handlers/admin-ui-handler.ts b/src/ui/handlers/admin-ui-handler.ts index 9ca30e35313..38420c61010 100644 --- a/src/ui/handlers/admin-ui-handler.ts +++ b/src/ui/handlers/admin-ui-handler.ts @@ -3,9 +3,9 @@ import { globalScene } from "#app/global-scene"; import { Button } from "#enums/buttons"; import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; -import type { InputFieldConfig } from "#ui/handlers/form-modal-ui-handler"; -import { FormModalUiHandler } from "#ui/handlers/form-modal-ui-handler"; -import type { ModalConfig } from "#ui/handlers/modal-ui-handler"; +import type { InputFieldConfig } from "#ui/form-modal-ui-handler"; +import { FormModalUiHandler } from "#ui/form-modal-ui-handler"; +import type { ModalConfig } from "#ui/modal-ui-handler"; import { getTextColor } from "#ui/text"; import { toTitleCase } from "#utils/strings"; diff --git a/src/ui/handlers/autocomplete-ui-handler.ts b/src/ui/handlers/autocomplete-ui-handler.ts index 914fe23a123..337b17048dc 100644 --- a/src/ui/handlers/autocomplete-ui-handler.ts +++ b/src/ui/handlers/autocomplete-ui-handler.ts @@ -1,6 +1,6 @@ import { Button } from "#enums/buttons"; import { UiMode } from "#enums/ui-mode"; -import { AbstractOptionSelectUiHandler } from "#ui/handlers/abstract-option-select-ui-handler"; +import { AbstractOptionSelectUiHandler } from "#ui/abstract-option-select-ui-handler"; export class AutoCompleteUiHandler extends AbstractOptionSelectUiHandler { modalContainer: Phaser.GameObjects.Container; diff --git a/src/ui/handlers/awaitable-ui-handler.ts b/src/ui/handlers/awaitable-ui-handler.ts index 9dcd3377da2..e8513b4acc1 100644 --- a/src/ui/handlers/awaitable-ui-handler.ts +++ b/src/ui/handlers/awaitable-ui-handler.ts @@ -1,7 +1,7 @@ import { globalScene } from "#app/global-scene"; import { Button } from "#enums/buttons"; import type { UiMode } from "#enums/ui-mode"; -import { UiHandler } from "#ui/handlers/ui-handler"; +import { UiHandler } from "#ui/ui-handler"; export abstract class AwaitableUiHandler extends UiHandler { protected awaitingActionInput: boolean; diff --git a/src/ui/handlers/ball-ui-handler.ts b/src/ui/handlers/ball-ui-handler.ts index 3d1868c207e..3d8efca96b8 100644 --- a/src/ui/handlers/ball-ui-handler.ts +++ b/src/ui/handlers/ball-ui-handler.ts @@ -5,8 +5,8 @@ import { Command } from "#enums/command"; import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; import type { CommandPhase } from "#phases/command-phase"; -import { UiHandler } from "#ui/handlers/ui-handler"; import { addTextObject, getTextStyleOptions } from "#ui/text"; +import { UiHandler } from "#ui/ui-handler"; import { addWindow } from "#ui/ui-theme"; import i18next from "i18next"; diff --git a/src/ui/handlers/battle-message-ui-handler.ts b/src/ui/handlers/battle-message-ui-handler.ts index 6912109c7e7..f845f22a730 100644 --- a/src/ui/handlers/battle-message-ui-handler.ts +++ b/src/ui/handlers/battle-message-ui-handler.ts @@ -3,7 +3,7 @@ import { Button } from "#enums/buttons"; import { getStatKey, PERMANENT_STATS } from "#enums/stat"; import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; -import { MessageUiHandler } from "#ui/handlers/message-ui-handler"; +import { MessageUiHandler } from "#ui/message-ui-handler"; import { addBBCodeTextObject, addTextObject, getTextColor } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; import i18next from "i18next"; diff --git a/src/ui/handlers/challenges-select-ui-handler.ts b/src/ui/handlers/challenges-select-ui-handler.ts index 5cc91285a74..62f9d578667 100644 --- a/src/ui/handlers/challenges-select-ui-handler.ts +++ b/src/ui/handlers/challenges-select-ui-handler.ts @@ -5,8 +5,8 @@ import { Challenges } from "#enums/challenges"; import { Color, ShadowColor } from "#enums/color"; import { TextStyle } from "#enums/text-style"; import type { UiMode } from "#enums/ui-mode"; -import { UiHandler } from "#ui/handlers/ui-handler"; import { addTextObject } from "#ui/text"; +import { UiHandler } from "#ui/ui-handler"; import { addWindow } from "#ui/ui-theme"; import { getLocalizedSpriteKey } from "#utils/common"; import i18next from "i18next"; diff --git a/src/ui/handlers/change-password-form-ui-handler.ts b/src/ui/handlers/change-password-form-ui-handler.ts index f4fdf349978..eccc67ffb04 100644 --- a/src/ui/handlers/change-password-form-ui-handler.ts +++ b/src/ui/handlers/change-password-form-ui-handler.ts @@ -1,9 +1,9 @@ import { globalScene } from "#app/global-scene"; import { pokerogueApi } from "#app/plugins/api/pokerogue-api"; import { UiMode } from "#enums/ui-mode"; -import type { InputFieldConfig } from "#ui/handlers/form-modal-ui-handler"; -import { FormModalUiHandler } from "#ui/handlers/form-modal-ui-handler"; -import type { ModalConfig } from "#ui/handlers/modal-ui-handler"; +import type { InputFieldConfig } from "#ui/form-modal-ui-handler"; +import { FormModalUiHandler } from "#ui/form-modal-ui-handler"; +import type { ModalConfig } from "#ui/modal-ui-handler"; import i18next from "i18next"; export class ChangePasswordFormUiHandler extends FormModalUiHandler { diff --git a/src/ui/handlers/command-ui-handler.ts b/src/ui/handlers/command-ui-handler.ts index de5e51a4210..693fe0eefef 100644 --- a/src/ui/handlers/command-ui-handler.ts +++ b/src/ui/handlers/command-ui-handler.ts @@ -9,9 +9,9 @@ import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; import { TerastallizeAccessModifier } from "#modifiers/modifier"; import type { CommandPhase } from "#phases/command-phase"; -import { PartyUiHandler, PartyUiMode } from "#ui/handlers/party-ui-handler"; -import { UiHandler } from "#ui/handlers/ui-handler"; +import { PartyUiHandler, PartyUiMode } from "#ui/party-ui-handler"; import { addTextObject } from "#ui/text"; +import { UiHandler } from "#ui/ui-handler"; import i18next from "i18next"; export class CommandUiHandler extends UiHandler { diff --git a/src/ui/handlers/confirm-ui-handler.ts b/src/ui/handlers/confirm-ui-handler.ts index 77f1f182514..64a0bb7028a 100644 --- a/src/ui/handlers/confirm-ui-handler.ts +++ b/src/ui/handlers/confirm-ui-handler.ts @@ -1,8 +1,8 @@ import { globalScene } from "#app/global-scene"; import { Button } from "#enums/buttons"; import { UiMode } from "#enums/ui-mode"; -import type { OptionSelectConfig } from "#ui/handlers/abstract-option-select-ui-handler"; -import { AbstractOptionSelectUiHandler } from "#ui/handlers/abstract-option-select-ui-handler"; +import type { OptionSelectConfig } from "#ui/abstract-option-select-ui-handler"; +import { AbstractOptionSelectUiHandler } from "#ui/abstract-option-select-ui-handler"; import i18next from "i18next"; export class ConfirmUiHandler extends AbstractOptionSelectUiHandler { diff --git a/src/ui/handlers/egg-gacha-ui-handler.ts b/src/ui/handlers/egg-gacha-ui-handler.ts index bd96b4d9392..f24c8c04fdb 100644 --- a/src/ui/handlers/egg-gacha-ui-handler.ts +++ b/src/ui/handlers/egg-gacha-ui-handler.ts @@ -9,7 +9,7 @@ import { GachaType } from "#enums/gacha-types"; import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; import { getVoucherTypeIcon, VoucherType } from "#system/voucher"; -import { MessageUiHandler } from "#ui/handlers/message-ui-handler"; +import { MessageUiHandler } from "#ui/message-ui-handler"; import { addTextObject, getEggTierTextTint, getTextStyleOptions } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; import { fixedInt, randSeedShuffle } from "#utils/common"; @@ -96,7 +96,7 @@ export class EggGachaUiHandler extends MessageUiHandler { legendaryLabelY = 0; } - const gachaUpLabel = addTextObject(gachaX, gachaY, i18next.t("egg:legendaryUPGacha"), gachaTextStyle).setOrigin(0); + const gachaUpLabel = addTextObject(gachaX, gachaY, i18next.t("egg:legendaryUpGacha"), gachaTextStyle).setOrigin(0); gachaInfoContainer.add(gachaUpLabel); switch (gachaType as GachaType) { @@ -124,14 +124,14 @@ export class EggGachaUiHandler extends MessageUiHandler { gachaUpLabel.setAlign("center").setY(0); } - gachaUpLabel.setText(i18next.t("egg:moveUPGacha")).setX(0).setOrigin(0.5, 0); + gachaUpLabel.setText(i18next.t("egg:moveUpGacha")).setX(0).setOrigin(0.5, 0); break; case GachaType.SHINY: if (["de", "fr", "ko", "ru"].includes(currentLanguage)) { gachaUpLabel.setAlign("center").setY(0); } - gachaUpLabel.setText(i18next.t("egg:shinyUPGacha")).setX(0).setOrigin(0.5, 0); + gachaUpLabel.setText(i18next.t("egg:shinyUpGacha")).setX(0).setOrigin(0.5, 0); break; } diff --git a/src/ui/handlers/egg-hatch-scene-handler.ts b/src/ui/handlers/egg-hatch-scene-ui-handler.ts similarity index 93% rename from src/ui/handlers/egg-hatch-scene-handler.ts rename to src/ui/handlers/egg-hatch-scene-ui-handler.ts index b7b5b78641f..d0827532e14 100644 --- a/src/ui/handlers/egg-hatch-scene-handler.ts +++ b/src/ui/handlers/egg-hatch-scene-ui-handler.ts @@ -1,9 +1,9 @@ import { globalScene } from "#app/global-scene"; import { Button } from "#enums/buttons"; import { UiMode } from "#enums/ui-mode"; -import { UiHandler } from "#ui/handlers/ui-handler"; +import { UiHandler } from "#ui/ui-handler"; -export class EggHatchSceneHandler extends UiHandler { +export class EggHatchSceneUiHandler extends UiHandler { public eggHatchContainer: Phaser.GameObjects.Container; /** diff --git a/src/ui/handlers/egg-list-ui-handler.ts b/src/ui/handlers/egg-list-ui-handler.ts index 2161073e6b1..3ec509261bb 100644 --- a/src/ui/handlers/egg-list-ui-handler.ts +++ b/src/ui/handlers/egg-list-ui-handler.ts @@ -2,10 +2,10 @@ import { globalScene } from "#app/global-scene"; import { Button } from "#enums/buttons"; import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; -import { ScrollBar } from "#ui/containers/scroll-bar"; -import { MessageUiHandler } from "#ui/handlers/message-ui-handler"; -import { PokemonIconAnimHandler, PokemonIconAnimMode } from "#ui/handlers/pokemon-icon-anim-handler"; -import { ScrollableGridUiHandler } from "#ui/handlers/scrollable-grid-handler"; +import { MessageUiHandler } from "#ui/message-ui-handler"; +import { PokemonIconAnimHelper, PokemonIconAnimMode } from "#ui/pokemon-icon-anim-helper"; +import { ScrollBar } from "#ui/scroll-bar"; +import { ScrollableGridHelper } from "#ui/scrollable-grid-helper"; import { addTextObject } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; import i18next from "i18next"; @@ -25,9 +25,9 @@ export class EggListUiHandler extends MessageUiHandler { private eggListMessageBoxContainer: Phaser.GameObjects.Container; private cursorObj: Phaser.GameObjects.Image; - private scrollGridHandler: ScrollableGridUiHandler; + private scrollGridHandler: ScrollableGridHelper; - private iconAnimHandler: PokemonIconAnimHandler; + private iconAnimHandler: PokemonIconAnimHelper; constructor() { super(UiMode.EGG_LIST); @@ -45,7 +45,7 @@ export class EggListUiHandler extends MessageUiHandler { const eggListBg = globalScene.add.image(0, 0, "egg_list_bg").setOrigin(0); - this.iconAnimHandler = new PokemonIconAnimHandler(); + this.iconAnimHandler = new PokemonIconAnimHelper(); this.iconAnimHandler.setup(); this.eggNameText = addTextObject(8, 68, "", TextStyle.SUMMARY).setOrigin(0); @@ -64,7 +64,7 @@ export class EggListUiHandler extends MessageUiHandler { const scrollBar = new ScrollBar(310, 5, 4, 170, this.ROWS); - this.scrollGridHandler = new ScrollableGridUiHandler(this, this.ROWS, this.COLUMNS) + this.scrollGridHandler = new ScrollableGridHelper(this, this.ROWS, this.COLUMNS) .withScrollBar(scrollBar) .withUpdateGridCallBack(() => this.updateEggIcons()) .withUpdateSingleElementCallback((i: number) => this.setEggDetails(i)); diff --git a/src/ui/handlers/egg-summary-ui-handler.ts b/src/ui/handlers/egg-summary-ui-handler.ts index 046f3b80ea4..35dc9c5176f 100644 --- a/src/ui/handlers/egg-summary-ui-handler.ts +++ b/src/ui/handlers/egg-summary-ui-handler.ts @@ -3,12 +3,12 @@ import { getEggTierForSpecies } from "#data/egg"; import type { EggHatchData } from "#data/egg-hatch-data"; import { Button } from "#enums/buttons"; import { UiMode } from "#enums/ui-mode"; -import { HatchedPokemonContainer } from "#ui/containers/hatched-pokemon-container"; -import { PokemonHatchInfoContainer } from "#ui/containers/pokemon-hatch-info-container"; -import { ScrollBar } from "#ui/containers/scroll-bar"; -import { MessageUiHandler } from "#ui/handlers/message-ui-handler"; -import { PokemonIconAnimHandler, PokemonIconAnimMode } from "#ui/handlers/pokemon-icon-anim-handler"; -import { ScrollableGridUiHandler } from "#ui/handlers/scrollable-grid-handler"; +import { HatchedPokemonContainer } from "#ui/hatched-pokemon-container"; +import { MessageUiHandler } from "#ui/message-ui-handler"; +import { PokemonHatchInfoContainer } from "#ui/pokemon-hatch-info-container"; +import { PokemonIconAnimHelper, PokemonIconAnimMode } from "#ui/pokemon-icon-anim-helper"; +import { ScrollBar } from "#ui/scroll-bar"; +import { ScrollableGridHelper } from "#ui/scrollable-grid-helper"; const iconContainerX = 112; const iconContainerY = 9; @@ -34,11 +34,11 @@ export class EggSummaryUiHandler extends MessageUiHandler { /** hatch info container that displays the current pokemon / hatch (main element on left hand side) */ private infoContainer: PokemonHatchInfoContainer; /** handles jumping animations for the pokemon sprite icons */ - private iconAnimHandler: PokemonIconAnimHandler; + private iconAnimHandler: PokemonIconAnimHelper; private eggHatchBg: Phaser.GameObjects.Image; private eggHatchData: EggHatchData[]; - private scrollGridHandler: ScrollableGridUiHandler; + private scrollGridHandler: ScrollableGridHelper; private cursorObj: Phaser.GameObjects.Image; /** used to add a delay before which it is not possible to exit the summary */ @@ -67,7 +67,7 @@ export class EggSummaryUiHandler extends MessageUiHandler { this.eggHatchContainer.setVisible(false); ui.add(this.eggHatchContainer); - this.iconAnimHandler = new PokemonIconAnimHandler(); + this.iconAnimHandler = new PokemonIconAnimHelper(); this.iconAnimHandler.setup(); this.eggHatchBg = globalScene.add.image(0, 0, "egg_summary_bg"); @@ -97,7 +97,7 @@ export class EggSummaryUiHandler extends MessageUiHandler { ); this.summaryContainer.add(scrollBar); - this.scrollGridHandler = new ScrollableGridUiHandler(this, numRows, numCols) + this.scrollGridHandler = new ScrollableGridHelper(this, numRows, numCols) .withScrollBar(scrollBar) .withUpdateGridCallBack(() => this.updatePokemonIcons()) .withUpdateSingleElementCallback((i: number) => this.infoContainer.showHatchInfo(this.eggHatchData[i])); diff --git a/src/ui/handlers/evolution-scene-handler.ts b/src/ui/handlers/evolution-scene-ui-handler.ts similarity index 94% rename from src/ui/handlers/evolution-scene-handler.ts rename to src/ui/handlers/evolution-scene-ui-handler.ts index 55405d8f437..ba3d8f8f57f 100644 --- a/src/ui/handlers/evolution-scene-handler.ts +++ b/src/ui/handlers/evolution-scene-ui-handler.ts @@ -2,10 +2,10 @@ import { globalScene } from "#app/global-scene"; import { Button } from "#enums/buttons"; import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; -import { MessageUiHandler } from "#ui/handlers/message-ui-handler"; +import { MessageUiHandler } from "#ui/message-ui-handler"; import { addTextObject } from "#ui/text"; -export class EvolutionSceneHandler extends MessageUiHandler { +export class EvolutionSceneUiHandler extends MessageUiHandler { public evolutionContainer: Phaser.GameObjects.Container; public messageBg: Phaser.GameObjects.Image; public messageContainer: Phaser.GameObjects.Container; diff --git a/src/ui/handlers/fight-ui-handler.ts b/src/ui/handlers/fight-ui-handler.ts index 9dd00a90b66..72b6949eb9c 100644 --- a/src/ui/handlers/fight-ui-handler.ts +++ b/src/ui/handlers/fight-ui-handler.ts @@ -12,9 +12,9 @@ import { UiMode } from "#enums/ui-mode"; import type { EnemyPokemon, Pokemon } from "#field/pokemon"; import type { PokemonMove } from "#moves/pokemon-move"; import type { CommandPhase } from "#phases/command-phase"; -import { MoveInfoOverlay } from "#ui/containers/move-info-overlay"; -import { UiHandler } from "#ui/handlers/ui-handler"; +import { MoveInfoOverlay } from "#ui/move-info-overlay"; import { addTextObject, getTextColor } from "#ui/text"; +import { UiHandler } from "#ui/ui-handler"; import { fixedInt, getLocalizedSpriteKey, padInt } from "#utils/common"; import i18next from "i18next"; diff --git a/src/ui/handlers/form-modal-ui-handler.ts b/src/ui/handlers/form-modal-ui-handler.ts index af1d8653df7..2efd39ca359 100644 --- a/src/ui/handlers/form-modal-ui-handler.ts +++ b/src/ui/handlers/form-modal-ui-handler.ts @@ -2,8 +2,8 @@ import { globalScene } from "#app/global-scene"; import { Button } from "#enums/buttons"; import { TextStyle } from "#enums/text-style"; import type { UiMode } from "#enums/ui-mode"; -import type { ModalConfig } from "#ui/handlers/modal-ui-handler"; -import { ModalUiHandler } from "#ui/handlers/modal-ui-handler"; +import type { ModalConfig } from "#ui/modal-ui-handler"; +import { ModalUiHandler } from "#ui/modal-ui-handler"; import { addTextInputObject, addTextObject, getTextColor } from "#ui/text"; import { addWindow, WindowVariant } from "#ui/ui-theme"; import { fixedInt } from "#utils/common"; diff --git a/src/ui/handlers/game-stats-ui-handler.ts b/src/ui/handlers/game-stats-ui-handler.ts index 9ffb7346b4d..24ff842a902 100644 --- a/src/ui/handlers/game-stats-ui-handler.ts +++ b/src/ui/handlers/game-stats-ui-handler.ts @@ -7,8 +7,8 @@ import { PlayerGender } from "#enums/player-gender"; import { TextStyle } from "#enums/text-style"; import { UiTheme } from "#enums/ui-theme"; import type { GameData } from "#system/game-data"; -import { UiHandler } from "#ui/handlers/ui-handler"; import { addTextObject } from "#ui/text"; +import { UiHandler } from "#ui/ui-handler"; import { addWindow } from "#ui/ui-theme"; import { formatFancyLargeNumber, getPlayTimeString } from "#utils/common"; import { toTitleCase } from "#utils/strings"; @@ -108,7 +108,7 @@ const displayStats: DisplayStats = { sourceFunc: gameData => gameData.gameStats.highestDamage.toString(), }, highestHeal: { - label_key: "highestHPHealed", + label_key: "highestHpHealed", sourceFunc: gameData => gameData.gameStats.highestHeal.toString(), }, pokemonSeen: { diff --git a/src/ui/handlers/loading-modal-ui-handler.ts b/src/ui/handlers/loading-modal-ui-handler.ts index 9b401e17f91..de00d911c47 100644 --- a/src/ui/handlers/loading-modal-ui-handler.ts +++ b/src/ui/handlers/loading-modal-ui-handler.ts @@ -1,6 +1,6 @@ import { TextStyle } from "#enums/text-style"; import type { UiMode } from "#enums/ui-mode"; -import { ModalUiHandler } from "#ui/handlers/modal-ui-handler"; +import { ModalUiHandler } from "#ui/modal-ui-handler"; import { addTextObject } from "#ui/text"; import i18next from "i18next"; diff --git a/src/ui/handlers/login-form-ui-handler.ts b/src/ui/handlers/login-form-ui-handler.ts index aeebd23ce43..44c5b93131f 100644 --- a/src/ui/handlers/login-form-ui-handler.ts +++ b/src/ui/handlers/login-form-ui-handler.ts @@ -2,10 +2,11 @@ import { pokerogueApi } from "#api/pokerogue-api"; import { globalScene } from "#app/global-scene"; import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; -import type { OptionSelectItem } from "#ui/handlers/abstract-option-select-ui-handler"; -import type { InputFieldConfig } from "#ui/handlers/form-modal-ui-handler"; -import { FormModalUiHandler } from "#ui/handlers/form-modal-ui-handler"; -import type { ModalConfig } from "#ui/handlers/modal-ui-handler"; +import { languageOptions } from "#system/settings-language"; +import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; +import type { InputFieldConfig } from "#ui/form-modal-ui-handler"; +import { FormModalUiHandler } from "#ui/form-modal-ui-handler"; +import type { ModalConfig } from "#ui/modal-ui-handler"; import { addTextObject } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; import { fixedInt } from "#utils/common"; @@ -31,6 +32,7 @@ export class LoginFormUiHandler extends FormModalUiHandler { private discordImage: Phaser.GameObjects.Image; private usernameInfoImage: Phaser.GameObjects.Image; private saveDownloadImage: Phaser.GameObjects.Image; + private changeLanguageImage: Phaser.GameObjects.Image; private externalPartyContainer: Phaser.GameObjects.Container; private infoContainer: Phaser.GameObjects.Container; private externalPartyBg: Phaser.GameObjects.NineSlice; @@ -82,8 +84,14 @@ export class LoginFormUiHandler extends FormModalUiHandler { scale: 0.75, }); + this.changeLanguageImage = this.buildInteractableImage("language_icon", "change-language-icon", { + x: 40, + scale: 0.5, + }); + this.infoContainer.add(this.usernameInfoImage); this.infoContainer.add(this.saveDownloadImage); + this.infoContainer.add(this.changeLanguageImage); this.getUi().add(this.infoContainer); this.infoContainer.setVisible(false); this.infoContainer.disableInteractive(); @@ -163,13 +171,18 @@ export class LoginFormUiHandler extends FormModalUiHandler { const [usernameInput, passwordInput] = this.inputs; - pokerogueApi.account.login({ username: usernameInput.text, password: passwordInput.text }).then(error => { - if (!error && originalLoginAction) { - originalLoginAction(); - } else { - onFail(error); - } - }); + pokerogueApi.account + .login({ + username: usernameInput.text, + password: passwordInput.text, + }) + .then(error => { + if (!error && originalLoginAction) { + originalLoginAction(); + } else { + onFail(error); + } + }); } }; @@ -185,9 +198,13 @@ export class LoginFormUiHandler extends FormModalUiHandler { this.infoContainer.setVisible(false); this.setMouseCursorStyle("default"); //reset cursor - [this.discordImage, this.googleImage, this.usernameInfoImage, this.saveDownloadImage].forEach(img => - img.off("pointerdown"), - ); + [ + this.discordImage, + this.googleImage, + this.usernameInfoImage, + this.saveDownloadImage, + this.changeLanguageImage, + ].forEach(img => img.off("pointerdown")); } private processExternalProvider(config: ModalConfig): void { @@ -206,6 +223,7 @@ export class LoginFormUiHandler extends FormModalUiHandler { this.getUi().moveTo(this.infoContainer, this.getUi().length - 1); this.usernameInfoImage.setPositionRelative(this.infoContainer, 0, 0); this.saveDownloadImage.setPositionRelative(this.infoContainer, 20, 0); + this.changeLanguageImage.setPositionRelative(this.infoContainer, 40, 0); this.discordImage.on("pointerdown", () => { const redirectUri = encodeURIComponent(`${import.meta.env.VITE_SERVER_URL}/auth/discord/callback`); @@ -288,6 +306,14 @@ export class LoginFormUiHandler extends FormModalUiHandler { } }); + this.changeLanguageImage.on("pointerdown", () => { + globalScene.ui.setOverlayMode(UiMode.OPTION_SELECT, { + options: languageOptions, + maxOptions: 7, + delay: 1000, + }); + }); + this.externalPartyContainer.setAlpha(0); globalScene.tweens.add({ targets: this.externalPartyContainer, diff --git a/src/ui/handlers/menu-ui-handler.ts b/src/ui/handlers/menu-ui-handler.ts index df1908bae39..419f2489818 100644 --- a/src/ui/handlers/menu-ui-handler.ts +++ b/src/ui/handlers/menu-ui-handler.ts @@ -7,10 +7,10 @@ import { Button } from "#enums/buttons"; import { GameDataType } from "#enums/game-data-type"; import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; -import { BgmBar } from "#ui/containers/bgm-bar"; -import type { OptionSelectConfig, OptionSelectItem } from "#ui/handlers/abstract-option-select-ui-handler"; -import type { AwaitableUiHandler } from "#ui/handlers/awaitable-ui-handler"; -import { MessageUiHandler } from "#ui/handlers/message-ui-handler"; +import type { OptionSelectConfig, OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; +import type { AwaitableUiHandler } from "#ui/awaitable-ui-handler"; +import { BgmBar } from "#ui/bgm-bar"; +import { MessageUiHandler } from "#ui/message-ui-handler"; import { addTextObject, getTextStyleOptions } from "#ui/text"; import { addWindow, WindowVariant } from "#ui/ui-theme"; import { fixedInt, isLocal, sessionIdKey } from "#utils/common"; diff --git a/src/ui/handlers/message-ui-handler.ts b/src/ui/handlers/message-ui-handler.ts index b8e3f983cca..1deaca78493 100644 --- a/src/ui/handlers/message-ui-handler.ts +++ b/src/ui/handlers/message-ui-handler.ts @@ -1,6 +1,6 @@ import { globalScene } from "#app/global-scene"; import type { UiMode } from "#enums/ui-mode"; -import { AwaitableUiHandler } from "#ui/handlers/awaitable-ui-handler"; +import { AwaitableUiHandler } from "#ui/awaitable-ui-handler"; import { getFrameMs } from "#utils/common"; export abstract class MessageUiHandler extends AwaitableUiHandler { diff --git a/src/ui/handlers/modal-ui-handler.ts b/src/ui/handlers/modal-ui-handler.ts index e6eecece9a2..c145363b244 100644 --- a/src/ui/handlers/modal-ui-handler.ts +++ b/src/ui/handlers/modal-ui-handler.ts @@ -2,8 +2,8 @@ import { globalScene } from "#app/global-scene"; import type { Button } from "#enums/buttons"; import { TextStyle } from "#enums/text-style"; import type { UiMode } from "#enums/ui-mode"; -import { UiHandler } from "#ui/handlers/ui-handler"; import { addTextObject } from "#ui/text"; +import { UiHandler } from "#ui/ui-handler"; import { addWindow, WindowVariant } from "#ui/ui-theme"; export interface ModalConfig { diff --git a/src/ui/handlers/modifier-select-ui-handler.ts b/src/ui/handlers/modifier-select-ui-handler.ts index a721bf9e7db..95bc30fb97c 100644 --- a/src/ui/handlers/modifier-select-ui-handler.ts +++ b/src/ui/handlers/modifier-select-ui-handler.ts @@ -11,8 +11,8 @@ import { UiMode } from "#enums/ui-mode"; import { HealShopCostModifier, LockModifierTiersModifier, PokemonHeldItemModifier } from "#modifiers/modifier"; import type { ModifierTypeOption } from "#modifiers/modifier-type"; import { getPlayerShopModifierTypeOptionsForWave, TmModifierType } from "#modifiers/modifier-type"; -import { MoveInfoOverlay } from "#ui/containers/move-info-overlay"; -import { AwaitableUiHandler } from "#ui/handlers/awaitable-ui-handler"; +import { AwaitableUiHandler } from "#ui/awaitable-ui-handler"; +import { MoveInfoOverlay } from "#ui/move-info-overlay"; import { addTextObject, getModifierTierTextTint, getTextColor, getTextStyleOptions } from "#ui/text"; import { formatMoney, NumberHolder } from "#utils/common"; import i18next from "i18next"; diff --git a/src/ui/handlers/mystery-encounter-ui-handler.ts b/src/ui/handlers/mystery-encounter-ui-handler.ts index 9bc6f0681ee..e4c9dfbfee3 100644 --- a/src/ui/handlers/mystery-encounter-ui-handler.ts +++ b/src/ui/handlers/mystery-encounter-ui-handler.ts @@ -9,11 +9,11 @@ import { getEncounterText } from "#mystery-encounters/encounter-dialogue-utils"; import type { OptionSelectSettings } from "#mystery-encounters/encounter-phase-utils"; import type { MysteryEncounterOption } from "#mystery-encounters/mystery-encounter-option"; import type { MysteryEncounterPhase } from "#phases/mystery-encounter-phases"; -import { PartyUiMode } from "#ui/handlers/party-ui-handler"; -import { UiHandler } from "#ui/handlers/ui-handler"; +import { PartyUiMode } from "#ui/party-ui-handler"; import { addBBCodeTextObject, getBBCodeFrag } from "#ui/text"; +import { UiHandler } from "#ui/ui-handler"; import { addWindow, WindowVariant } from "#ui/ui-theme"; -import { fixedInt, isNullOrUndefined } from "#utils/common"; +import { fixedInt } from "#utils/common"; import i18next from "i18next"; import type BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext"; @@ -95,12 +95,10 @@ export class MysteryEncounterUiHandler extends UiHandler { super.show(args); this.overrideSettings = (args[0] as OptionSelectSettings) ?? {}; - const showDescriptionContainer = isNullOrUndefined(this.overrideSettings?.hideDescription) - ? true - : !this.overrideSettings.hideDescription; - const slideInDescription = isNullOrUndefined(this.overrideSettings?.slideInDescription) - ? true - : this.overrideSettings.slideInDescription; + const showDescriptionContainer = + this.overrideSettings?.hideDescription == null ? true : !this.overrideSettings.hideDescription; + const slideInDescription = + this.overrideSettings?.slideInDescription == null ? true : this.overrideSettings.slideInDescription; const startingCursorIndex = this.overrideSettings?.startingCursorIndex ?? 0; this.cursorContainer.setVisible(true); @@ -567,7 +565,7 @@ export class MysteryEncounterUiHandler extends UiHandler { } this.tooltipContainer.setVisible(true); - if (isNullOrUndefined(cursor) || cursor > this.optionsContainer.length - 2) { + if (cursor == null || cursor > this.optionsContainer.length - 2) { // Ignore hovers on view party button // Hide dex progress if visible this.showHideDexProgress(false); diff --git a/src/ui/handlers/party-ui-handler.ts b/src/ui/handlers/party-ui-handler.ts index ef5ed099153..d4014cc0288 100644 --- a/src/ui/handlers/party-ui-handler.ts +++ b/src/ui/handlers/party-ui-handler.ts @@ -21,9 +21,9 @@ import type { PokemonMove } from "#moves/pokemon-move"; import type { CommandPhase } from "#phases/command-phase"; import { getVariantTint } from "#sprites/variant"; import type { TurnMove } from "#types/turn-move"; -import { MoveInfoOverlay } from "#ui/containers/move-info-overlay"; -import { MessageUiHandler } from "#ui/handlers/message-ui-handler"; -import { PokemonIconAnimHandler, PokemonIconAnimMode } from "#ui/handlers/pokemon-icon-anim-handler"; +import { MessageUiHandler } from "#ui/message-ui-handler"; +import { MoveInfoOverlay } from "#ui/move-info-overlay"; +import { PokemonIconAnimHelper, PokemonIconAnimMode } from "#ui/pokemon-icon-anim-helper"; import { addBBCodeTextObject, addTextObject, getTextColor } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; import { applyChallenges } from "#utils/challenge-utils"; @@ -201,7 +201,7 @@ export class PartyUiHandler extends MessageUiHandler { private tmMoveId: MoveId; private showMovePp: boolean; - private iconAnimHandler: PokemonIconAnimHandler; + private iconAnimHandler: PokemonIconAnimHelper; private blockInput: boolean; @@ -320,7 +320,7 @@ export class PartyUiHandler extends MessageUiHandler { this.optionsContainer = globalScene.add.container(globalScene.scaledCanvas.width - 1, -1); partyContainer.add(this.optionsContainer); - this.iconAnimHandler = new PokemonIconAnimHandler(); + this.iconAnimHandler = new PokemonIconAnimHelper(); this.iconAnimHandler.setup(); const partyDiscardModeButton = new PartyDiscardModeButton(DISCARD_BUTTON_X, DISCARD_BUTTON_Y, this); @@ -1892,12 +1892,12 @@ class PartySlot extends Phaser.GameObjects.Container { private slotBgKey: string; private pokemonIcon: Phaser.GameObjects.Container; - private iconAnimHandler: PokemonIconAnimHandler; + private iconAnimHandler: PokemonIconAnimHelper; constructor( slotIndex: number, pokemon: PlayerPokemon, - iconAnimHandler: PokemonIconAnimHandler, + iconAnimHandler: PokemonIconAnimHelper, partyUiMode: PartyUiMode, tmMoveId: MoveId, ) { diff --git a/src/ui/containers/pokedex-page-ui-handler.ts b/src/ui/handlers/pokedex-page-ui-handler.ts similarity index 99% rename from src/ui/containers/pokedex-page-ui-handler.ts rename to src/ui/handlers/pokedex-page-ui-handler.ts index ef1c1a6896c..31e2998b850 100644 --- a/src/ui/containers/pokedex-page-ui-handler.ts +++ b/src/ui/handlers/pokedex-page-ui-handler.ts @@ -43,18 +43,18 @@ import { TimeOfDay } from "#enums/time-of-day"; import { UiMode } from "#enums/ui-mode"; import type { Variant } from "#sprites/variant"; import { getVariantIcon, getVariantTint } from "#sprites/variant"; -import type { StarterAttributes } from "#system/game-data"; import { SettingKeyboard } from "#system/settings-keyboard"; import type { DexEntry } from "#types/dex-data"; -import { BaseStatsOverlay } from "#ui/containers/base-stats-overlay"; -import { MoveInfoOverlay } from "#ui/containers/move-info-overlay"; -import { PokedexInfoOverlay } from "#ui/containers/pokedex-info-overlay"; -import { StatsContainer } from "#ui/containers/stats-container"; -import type { OptionSelectItem } from "#ui/handlers/abstract-option-select-ui-handler"; -import { MessageUiHandler } from "#ui/handlers/message-ui-handler"; +import type { StarterAttributes } from "#types/save-data"; +import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; +import { BaseStatsOverlay } from "#ui/base-stats-overlay"; +import { MessageUiHandler } from "#ui/message-ui-handler"; +import { MoveInfoOverlay } from "#ui/move-info-overlay"; +import { PokedexInfoOverlay } from "#ui/pokedex-info-overlay"; +import { StatsContainer } from "#ui/stats-container"; import { addBBCodeTextObject, addTextObject, getTextColor, getTextStyleOptions } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; -import { BooleanHolder, getLocalizedSpriteKey, isNullOrUndefined, padInt, rgbHexToRgba } from "#utils/common"; +import { BooleanHolder, getLocalizedSpriteKey, padInt, rgbHexToRgba } from "#utils/common"; import { getEnumValues } from "#utils/enums"; import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils"; import { toCamelCase, toTitleCase } from "#utils/strings"; @@ -2424,11 +2424,7 @@ export class PokedexPageUiHandler extends MessageUiHandler { // We will only update the sprite if there is a change to form, shiny/variant // or gender for species with gender sprite differences const shouldUpdateSprite = - (species?.genderDiffs && !isNullOrUndefined(female)) - || !isNullOrUndefined(formIndex) - || !isNullOrUndefined(shiny) - || !isNullOrUndefined(variant) - || forceUpdate; + (species?.genderDiffs && female != null) || formIndex != null || shiny != null || variant != null || forceUpdate; if (this.activeTooltip === "CANDY") { if (this.species && this.pokemonCandyContainer.visible) { diff --git a/src/ui/handlers/pokedex-scan-ui-handler.ts b/src/ui/handlers/pokedex-scan-ui-handler.ts index bb3cec5bb56..18afd0598c2 100644 --- a/src/ui/handlers/pokedex-scan-ui-handler.ts +++ b/src/ui/handlers/pokedex-scan-ui-handler.ts @@ -1,12 +1,11 @@ import { allAbilities, allMoves, allSpecies } from "#data/data-lists"; import { UiMode } from "#enums/ui-mode"; import type { PlayerPokemon } from "#field/pokemon"; -import { FilterTextRow } from "#ui/containers/filter-text"; -import type { OptionSelectItem } from "#ui/handlers/abstract-option-select-ui-handler"; -import type { InputFieldConfig } from "#ui/handlers/form-modal-ui-handler"; -import { FormModalUiHandler } from "#ui/handlers/form-modal-ui-handler"; -import type { ModalConfig } from "#ui/handlers/modal-ui-handler"; -import { isNullOrUndefined } from "#utils/common"; +import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; +import { FilterTextRow } from "#ui/filter-text"; +import type { InputFieldConfig } from "#ui/form-modal-ui-handler"; +import { FormModalUiHandler } from "#ui/form-modal-ui-handler"; +import type { ModalConfig } from "#ui/modal-ui-handler"; import i18next from "i18next"; export class PokedexScanUiHandler extends FormModalUiHandler { @@ -132,7 +131,7 @@ export class PokedexScanUiHandler extends FormModalUiHandler { return { label: value, handler: () => { - if (!isNullOrUndefined(evt.data) || evt.inputType?.toLowerCase() === "deletecontentbackward") { + if (evt.data != null || evt.inputType?.toLowerCase() === "deletecontentbackward") { inputObject.setText(value); } ui.revertMode(); diff --git a/src/ui/handlers/pokedex-ui-handler.ts b/src/ui/handlers/pokedex-ui-handler.ts index 3500fa97436..c6f9dbee448 100644 --- a/src/ui/handlers/pokedex-ui-handler.ts +++ b/src/ui/handlers/pokedex-ui-handler.ts @@ -31,24 +31,17 @@ import { UiMode } from "#enums/ui-mode"; import { UiTheme } from "#enums/ui-theme"; import type { Variant } from "#sprites/variant"; import { getVariantIcon, getVariantTint } from "#sprites/variant"; -import type { DexAttrProps, StarterAttributes } from "#system/game-data"; import { SettingKeyboard } from "#system/settings-keyboard"; import type { DexEntry } from "#types/dex-data"; -import { - DropDown, - DropDownLabel, - DropDownOption, - DropDownState, - DropDownType, - SortCriteria, -} from "#ui/containers/dropdown"; -import { FilterBar } from "#ui/containers/filter-bar"; -import { FilterText, FilterTextRow } from "#ui/containers/filter-text"; -import { PokedexMonContainer } from "#ui/containers/pokedex-mon-container"; -import { ScrollBar } from "#ui/containers/scroll-bar"; -import type { OptionSelectConfig } from "#ui/handlers/abstract-option-select-ui-handler"; -import { MessageUiHandler } from "#ui/handlers/message-ui-handler"; -import { PokemonIconAnimHandler, PokemonIconAnimMode } from "#ui/handlers/pokemon-icon-anim-handler"; +import type { DexAttrProps, StarterAttributes } from "#types/save-data"; +import type { OptionSelectConfig } from "#ui/abstract-option-select-ui-handler"; +import { DropDown, DropDownLabel, DropDownOption, DropDownState, DropDownType, SortCriteria } from "#ui/dropdown"; +import { FilterBar } from "#ui/filter-bar"; +import { FilterText, FilterTextRow } from "#ui/filter-text"; +import { MessageUiHandler } from "#ui/message-ui-handler"; +import { PokedexMonContainer } from "#ui/pokedex-mon-container"; +import { PokemonIconAnimHelper, PokemonIconAnimMode } from "#ui/pokemon-icon-anim-helper"; +import { ScrollBar } from "#ui/scroll-bar"; import { addTextObject, getTextColor } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; import { BooleanHolder, fixedInt, getLocalizedSpriteKey, padInt, randIntRange, rgbHexToRgba } from "#utils/common"; @@ -198,7 +191,7 @@ export class PokedexUiHandler extends MessageUiHandler { public cursorObj: Phaser.GameObjects.Image; private pokerusCursorObjs: Phaser.GameObjects.Image[]; - private iconAnimHandler: PokemonIconAnimHandler; + private iconAnimHandler: PokemonIconAnimHelper; private starterPreferences: StarterPreferences; @@ -482,7 +475,7 @@ export class PokedexUiHandler extends MessageUiHandler { pokemonContainerWindow.setVisible(false); } - this.iconAnimHandler = new PokemonIconAnimHandler(); + this.iconAnimHandler = new PokemonIconAnimHelper(); this.iconAnimHandler.setup(); this.pokemonNumberText = addTextObject(6, 141, "", TextStyle.SUMMARY); diff --git a/src/ui/handlers/registration-form-ui-handler.ts b/src/ui/handlers/registration-form-ui-handler.ts index d424e44b455..2c8080d534d 100644 --- a/src/ui/handlers/registration-form-ui-handler.ts +++ b/src/ui/handlers/registration-form-ui-handler.ts @@ -2,9 +2,9 @@ import { pokerogueApi } from "#api/pokerogue-api"; import { globalScene } from "#app/global-scene"; import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; -import type { InputFieldConfig } from "#ui/handlers/form-modal-ui-handler"; -import { FormModalUiHandler } from "#ui/handlers/form-modal-ui-handler"; -import type { ModalConfig } from "#ui/handlers/modal-ui-handler"; +import type { InputFieldConfig } from "#ui/form-modal-ui-handler"; +import { FormModalUiHandler } from "#ui/form-modal-ui-handler"; +import type { ModalConfig } from "#ui/modal-ui-handler"; import { addTextObject } from "#ui/text"; import i18next from "i18next"; diff --git a/src/ui/handlers/rename-form-ui-handler.ts b/src/ui/handlers/rename-form-ui-handler.ts index f1d9ae3c981..9da5b0e8554 100644 --- a/src/ui/handlers/rename-form-ui-handler.ts +++ b/src/ui/handlers/rename-form-ui-handler.ts @@ -1,7 +1,7 @@ import type { PlayerPokemon } from "#field/pokemon"; -import type { InputFieldConfig } from "#ui/handlers/form-modal-ui-handler"; -import { FormModalUiHandler } from "#ui/handlers/form-modal-ui-handler"; -import type { ModalConfig } from "#ui/handlers/modal-ui-handler"; +import type { InputFieldConfig } from "#ui/form-modal-ui-handler"; +import { FormModalUiHandler } from "#ui/form-modal-ui-handler"; +import type { ModalConfig } from "#ui/modal-ui-handler"; import i18next from "i18next"; export class RenameFormUiHandler extends FormModalUiHandler { diff --git a/src/ui/handlers/run-history-ui-handler.ts b/src/ui/handlers/run-history-ui-handler.ts index cec2b95cdd5..4dd73d4826b 100644 --- a/src/ui/handlers/run-history-ui-handler.ts +++ b/src/ui/handlers/run-history-ui-handler.ts @@ -6,10 +6,10 @@ import { PlayerGender } from "#enums/player-gender"; import { TextStyle } from "#enums/text-style"; import { TrainerVariant } from "#enums/trainer-variant"; import { UiMode } from "#enums/ui-mode"; -import type { RunEntry } from "#system/game-data"; import type { PokemonData } from "#system/pokemon-data"; -import { MessageUiHandler } from "#ui/handlers/message-ui-handler"; -import { RunDisplayMode } from "#ui/handlers/run-info-ui-handler"; +import type { RunEntry } from "#types/save-data"; +import { MessageUiHandler } from "#ui/message-ui-handler"; +import { RunDisplayMode } from "#ui/run-info-ui-handler"; import { addTextObject } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; import { fixedInt, formatLargeNumber } from "#utils/common"; diff --git a/src/ui/handlers/run-info-ui-handler.ts b/src/ui/handlers/run-info-ui-handler.ts index 3693a58bf48..556884194b1 100644 --- a/src/ui/handlers/run-info-ui-handler.ts +++ b/src/ui/handlers/run-info-ui-handler.ts @@ -19,11 +19,11 @@ import { UiMode } from "#enums/ui-mode"; import * as Modifier from "#modifiers/modifier"; import { getLuckString, getLuckTextTint } from "#modifiers/modifier-type"; import { getVariantTint } from "#sprites/variant"; -import type { SessionSaveData } from "#system/game-data"; import type { PokemonData } from "#system/pokemon-data"; import { SettingKeyboard } from "#system/settings-keyboard"; -import { UiHandler } from "#ui/handlers/ui-handler"; +import type { SessionSaveData } from "#types/save-data"; import { addBBCodeTextObject, addTextObject, getTextColor } from "#ui/text"; +import { UiHandler } from "#ui/ui-handler"; import { addWindow } from "#ui/ui-theme"; import { formatFancyLargeNumber, formatLargeNumber, formatMoney, getPlayTimeString } from "#utils/common"; import { toCamelCase } from "#utils/strings"; @@ -685,7 +685,7 @@ export class RunInfoUiHandler extends UiHandler { /** * This function parses the Challenges section of the Run Entry and returns a list of active challenge. - * @return string[] of active challenge names + * @returns string[] of active challenge names */ private challengeParser(): string[] { const rules: string[] = []; diff --git a/src/ui/handlers/save-slot-select-ui-handler.ts b/src/ui/handlers/save-slot-select-ui-handler.ts index 1b062e964be..194971a005f 100644 --- a/src/ui/handlers/save-slot-select-ui-handler.ts +++ b/src/ui/handlers/save-slot-select-ui-handler.ts @@ -6,14 +6,14 @@ import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; // biome-ignore lint/performance/noNamespaceImport: See `src/system/game-data.ts` import * as Modifier from "#modifiers/modifier"; -import type { SessionSaveData } from "#system/game-data"; import type { PokemonData } from "#system/pokemon-data"; -import type { OptionSelectConfig } from "#ui/handlers/abstract-option-select-ui-handler"; -import { MessageUiHandler } from "#ui/handlers/message-ui-handler"; -import { RunDisplayMode } from "#ui/handlers/run-info-ui-handler"; +import type { SessionSaveData } from "#types/save-data"; +import type { OptionSelectConfig } from "#ui/abstract-option-select-ui-handler"; +import { MessageUiHandler } from "#ui/message-ui-handler"; +import { RunDisplayMode } from "#ui/run-info-ui-handler"; import { addTextObject } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; -import { fixedInt, formatLargeNumber, getPlayTimeString, isNullOrUndefined } from "#utils/common"; +import { fixedInt, formatLargeNumber, getPlayTimeString } from "#utils/common"; import i18next from "i18next"; const SESSION_SLOTS_COUNT = 5; @@ -405,7 +405,7 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler { } this.setArrowVisibility(hasData); } - if (!isNullOrUndefined(prevSlotIndex)) { + if (prevSlotIndex != null) { this.revertSessionSlot(prevSlotIndex); } diff --git a/src/ui/handlers/session-reload-modal-ui-handler.ts b/src/ui/handlers/session-reload-modal-ui-handler.ts index 33c18b1974a..1f5a205f990 100644 --- a/src/ui/handlers/session-reload-modal-ui-handler.ts +++ b/src/ui/handlers/session-reload-modal-ui-handler.ts @@ -1,7 +1,7 @@ import { TextStyle } from "#enums/text-style"; import type { UiMode } from "#enums/ui-mode"; -import type { ModalConfig } from "#ui/handlers/modal-ui-handler"; -import { ModalUiHandler } from "#ui/handlers/modal-ui-handler"; +import type { ModalConfig } from "#ui/modal-ui-handler"; +import { ModalUiHandler } from "#ui/modal-ui-handler"; import { addTextObject } from "#ui/text"; export class SessionReloadModalUiHandler extends ModalUiHandler { diff --git a/src/ui/handlers/starter-select-ui-handler.ts b/src/ui/handlers/starter-select-ui-handler.ts index 60d8a4dc4d6..8c3594546f1 100644 --- a/src/ui/handlers/starter-select-ui-handler.ts +++ b/src/ui/handlers/starter-select-ui-handler.ts @@ -46,26 +46,19 @@ import { BattleSceneEventType } from "#events/battle-scene"; import type { Variant } from "#sprites/variant"; import { getVariantIcon, getVariantTint } from "#sprites/variant"; import { achvs } from "#system/achv"; -import type { DexAttrProps, StarterAttributes, StarterDataEntry, StarterMoveset } from "#system/game-data"; import { RibbonData } from "#system/ribbons/ribbon-data"; import { SettingKeyboard } from "#system/settings-keyboard"; import type { DexEntry } from "#types/dex-data"; -import { - DropDown, - DropDownLabel, - DropDownOption, - DropDownState, - DropDownType, - SortCriteria, -} from "#ui/containers/dropdown"; -import { FilterBar } from "#ui/containers/filter-bar"; -import { MoveInfoOverlay } from "#ui/containers/move-info-overlay"; -import { ScrollBar } from "#ui/containers/scroll-bar"; -import { StarterContainer } from "#ui/containers/starter-container"; -import { StatsContainer } from "#ui/containers/stats-container"; -import type { OptionSelectItem } from "#ui/handlers/abstract-option-select-ui-handler"; -import { MessageUiHandler } from "#ui/handlers/message-ui-handler"; -import { PokemonIconAnimHandler, PokemonIconAnimMode } from "#ui/handlers/pokemon-icon-anim-handler"; +import type { Starter, StarterAttributes, StarterDataEntry, StarterMoveset } from "#types/save-data"; +import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; +import { DropDown, DropDownLabel, DropDownOption, DropDownState, DropDownType, SortCriteria } from "#ui/dropdown"; +import { FilterBar } from "#ui/filter-bar"; +import { MessageUiHandler } from "#ui/message-ui-handler"; +import { MoveInfoOverlay } from "#ui/move-info-overlay"; +import { PokemonIconAnimHelper, PokemonIconAnimMode } from "#ui/pokemon-icon-anim-helper"; +import { ScrollBar } from "#ui/scroll-bar"; +import { StarterContainer } from "#ui/starter-container"; +import { StatsContainer } from "#ui/stats-container"; import { addBBCodeTextObject, addTextObject, getTextColor } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; import { applyChallenges, checkStarterValidForChallenge } from "#utils/challenge-utils"; @@ -73,7 +66,6 @@ import { BooleanHolder, fixedInt, getLocalizedSpriteKey, - isNullOrUndefined, NumberHolder, padInt, randIntRange, @@ -90,18 +82,6 @@ import type BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext"; export type StarterSelectCallback = (starters: Starter[]) => void; -export interface Starter { - species: PokemonSpecies; - dexAttr: bigint; - abilityIndex: number; - passive: boolean; - nature: Nature; - moveset?: StarterMoveset; - pokerus: boolean; - nickname?: string; - teraType?: PokemonType; -} - interface LanguageSetting { starterInfoTextSize: string; instructionTextSize: string; @@ -372,15 +352,13 @@ export class StarterSelectUiHandler extends MessageUiHandler { private allSpecies: PokemonSpecies[] = []; private lastSpecies: PokemonSpecies; private speciesLoaded: Map = new Map(); + + private starters: Starter[] = []; public starterSpecies: PokemonSpecies[] = []; private pokerusSpecies: PokemonSpecies[] = []; - private starterAttr: bigint[] = []; - private starterAbilityIndexes: number[] = []; - private starterNatures: Nature[] = []; - private starterTeras: PokemonType[] = []; - private starterMovesets: StarterMoveset[] = []; private speciesStarterDexEntry: DexEntry | null; private speciesStarterMoves: MoveId[]; + private canCycleShiny: boolean; private canCycleForm: boolean; private canCycleGender: boolean; @@ -398,7 +376,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { private startCursorObj: Phaser.GameObjects.NineSlice; private randomCursorObj: Phaser.GameObjects.NineSlice; - private iconAnimHandler: PokemonIconAnimHandler; + private iconAnimHandler: PokemonIconAnimHelper; //variables to keep track of the dynamically rendered list of instruction prompts for starter select private instructionRowX = 0; @@ -611,7 +589,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { starterContainerWindow.setVisible(false); } - this.iconAnimHandler = new PokemonIconAnimHandler(); + this.iconAnimHandler = new PokemonIconAnimHelper(); this.iconAnimHandler.setup(); this.pokemonSprite = globalScene.add.sprite(53, 63, "pkmn__sub"); @@ -2555,7 +2533,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { case Button.CYCLE_TERA: if (this.canCycleTera) { const speciesForm = getPokemonSpeciesForm(this.lastSpecies.speciesId, starterAttributes.form ?? 0); - if (speciesForm.type1 === this.teraCursor && !isNullOrUndefined(speciesForm.type2)) { + if (speciesForm.type1 === this.teraCursor && speciesForm.type2 != null) { starterAttributes.tera = speciesForm.type2; originalStarterAttributes.tera = starterAttributes.tera; this.setSpeciesDetails(this.lastSpecies, { @@ -2766,12 +2744,26 @@ export class StarterSelectUiHandler extends MessageUiHandler { props.variant, ); + const { dexEntry, starterDataEntry } = this.getSpeciesData(species.speciesId); + + const starter = { + speciesId: species.speciesId, + shiny: props.shiny, + variant: props.variant, + formIndex: props.formIndex, + female: props.female, + abilityIndex, + passive: !(starterDataEntry.passiveAttr ^ (PassiveAttr.ENABLED | PassiveAttr.UNLOCKED)), + nature, + moveset, + pokerus: this.pokerusSpecies.includes(species), + nickname: this.starterPreferences[species.speciesId]?.nickname, + teraType, + ivs: dexEntry.ivs, + }; + + this.starters.push(starter); this.starterSpecies.push(species); - this.starterAttr.push(dexAttr); - this.starterAbilityIndexes.push(abilityIndex); - this.starterNatures.push(nature); - this.starterTeras.push(teraType); - this.starterMovesets.push(moveset); if (this.speciesLoaded.get(species.speciesId) || randomSelection) { getPokemonSpeciesForm(species.speciesId, props.formIndex).cry(); } @@ -2797,7 +2789,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { */ switchMoveHandler(targetIndex: number, newMove: MoveId, previousMove: MoveId) { const starterMoveset = this.starterMoveset; - if (isNullOrUndefined(starterMoveset)) { + if (starterMoveset == null) { console.warn("Trying to update a non-existing moveset"); return; } @@ -2841,7 +2833,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { for (const [index, species] of this.starterSpecies.entries()) { if (species.speciesId === id) { - this.starterMovesets[index] = this.starterMoveset; + this.starters[index].moveset = this.starterMoveset; } } } @@ -3648,20 +3640,20 @@ export class StarterSelectUiHandler extends MessageUiHandler { const starterIndex = this.starterSpecies.indexOf(species); - let props: DexAttrProps; + const props = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr); if (starterIndex > -1) { - props = globalScene.gameData.getSpeciesDexAttrProps(species, this.starterAttr[starterIndex]); + const starter = this.starters[starterIndex]; this.setSpeciesDetails( species, { - shiny: props.shiny, - formIndex: props.formIndex, - female: props.female, - variant: props.variant, - abilityIndex: this.starterAbilityIndexes[starterIndex], - natureIndex: this.starterNatures[starterIndex], - teraType: this.starterTeras[starterIndex], + shiny: starter.shiny, + formIndex: starter.formIndex, + female: starter.female, + variant: starter.variant, + abilityIndex: starter.abilityIndex, + natureIndex: starter.nature, + teraType: starter.teraType, }, false, ); @@ -3672,7 +3664,6 @@ export class StarterSelectUiHandler extends MessageUiHandler { const { dexEntry } = this.getSpeciesData(species.speciesId); const defaultNature = starterAttributes?.nature || globalScene.gameData.getSpeciesDefaultNature(species, dexEntry); - props = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr); if (starterAttributes?.variant && !Number.isNaN(starterAttributes.variant) && props.shiny) { props.variant = starterAttributes.variant as Variant; } @@ -3694,7 +3685,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { ); } - if (!isNullOrUndefined(props.formIndex)) { + if (props.formIndex != null) { // If switching forms while the pokemon is in the team, update its moveset this.updateSelectedStarterMoveset(species.speciesId); } @@ -3816,10 +3807,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { // We will only update the sprite if there is a change to form, shiny/variant // or gender for species with gender sprite differences const shouldUpdateSprite = - (species?.genderDiffs && !isNullOrUndefined(female)) - || !isNullOrUndefined(formIndex) - || !isNullOrUndefined(shiny) - || !isNullOrUndefined(variant); + (species?.genderDiffs && female != null) || formIndex != null || shiny != null || variant != null; const isFreshStartChallenge = globalScene.gameMode.hasChallenge(Challenges.FRESH_START); @@ -3857,7 +3845,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { ); // TODO: is this bang correct? this.abilityCursor = abilityIndex !== undefined ? abilityIndex : (abilityIndex = oldAbilityIndex); this.natureCursor = natureIndex !== undefined ? natureIndex : (natureIndex = oldNatureIndex); - this.teraCursor = !isNullOrUndefined(teraType) ? teraType : (teraType = oldTeraType); + this.teraCursor = teraType != null ? teraType : (teraType = oldTeraType); const [isInParty, partyIndex]: [boolean, number] = this.isInParty(species); // we use this to firstly check if the pokemon is in the party, and if so, to get the party index in order to update the icon image if (isInParty) { this.updatePartyIcon(species, partyIndex); @@ -3921,10 +3909,15 @@ export class StarterSelectUiHandler extends MessageUiHandler { const starterIndex = this.starterSpecies.indexOf(species); if (starterIndex > -1) { - this.starterAttr[starterIndex] = this.dexAttrCursor; - this.starterAbilityIndexes[starterIndex] = this.abilityCursor; - this.starterNatures[starterIndex] = this.natureCursor; - this.starterTeras[starterIndex] = this.teraCursor; + const starter = this.starters[starterIndex]; + const props = globalScene.gameData.getSpeciesDexAttrProps(species, this.dexAttrCursor); + starter.shiny = props.shiny; + starter.variant = props.variant; + starter.female = props.female; + starter.formIndex = props.formIndex; + starter.abilityIndex = this.abilityCursor; + starter.nature = this.natureCursor; + starter.teraType = this.teraCursor; } const assetLoadCancelled = new BooleanHolder(false); @@ -3998,7 +3991,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { this.canCycleTera = !this.statsMode && this.allowTera - && !isNullOrUndefined(getPokemonSpeciesForm(species.speciesId, formIndex ?? 0).type2) + && getPokemonSpeciesForm(species.speciesId, formIndex ?? 0).type2 != null && !globalScene.gameMode.hasChallenge(Challenges.FRESH_START); } @@ -4226,11 +4219,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { popStarter(index: number): void { this.starterSpecies.splice(index, 1); - this.starterAttr.splice(index, 1); - this.starterAbilityIndexes.splice(index, 1); - this.starterNatures.splice(index, 1); - this.starterTeras.splice(index, 1); - this.starterMovesets.splice(index, 1); + this.starters.splice(index, 1); for (let s = 0; s < this.starterSpecies.length; s++) { const species = this.starterSpecies[s]; @@ -4454,27 +4443,11 @@ export class StarterSelectUiHandler extends MessageUiHandler { () => { const startRun = () => { globalScene.money = globalScene.gameMode.getStartingMoney(); + const starters = this.starters.slice(0); ui.setMode(UiMode.STARTER_SELECT); - const thisObj = this; const originalStarterSelectCallback = this.starterSelectCallback; this.starterSelectCallback = null; - originalStarterSelectCallback?.( - new Array(this.starterSpecies.length).fill(0).map((_, i) => { - const starterSpecies = thisObj.starterSpecies[i]; - const { starterDataEntry } = this.getSpeciesData(starterSpecies.speciesId); - return { - species: starterSpecies, - dexAttr: thisObj.starterAttr[i], - abilityIndex: thisObj.starterAbilityIndexes[i], - passive: !(starterDataEntry.passiveAttr ^ (PassiveAttr.ENABLED | PassiveAttr.UNLOCKED)), - nature: thisObj.starterNatures[i] as Nature, - teraType: thisObj.starterTeras[i] as PokemonType, - moveset: thisObj.starterMovesets[i], - pokerus: thisObj.pokerusSpecies.includes(starterSpecies), - nickname: thisObj.starterPreferences[starterSpecies.speciesId]?.nickname, - }; - }), - ); + originalStarterSelectCallback?.(starters); }; startRun(); }, @@ -4503,10 +4476,17 @@ export class StarterSelectUiHandler extends MessageUiHandler { */ isPartyValid(): boolean { let canStart = false; - for (const species of this.starterSpecies) { + for (let s = 0; s < this.starterSpecies.length; s++) { + const species = this.starterSpecies[s]; + const starter = this.starters[s]; const isValidForChallenge = checkStarterValidForChallenge( species, - globalScene.gameData.getSpeciesDexAttrProps(species, this.getCurrentDexProps(species.speciesId)), + { + formIndex: starter.formIndex, + shiny: starter.shiny, + variant: starter.variant, + female: starter.female ?? false, + }, false, ); canStart ||= isValidForChallenge; @@ -4599,7 +4579,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { this.canCycleTera = !this.statsMode && this.allowTera - && !isNullOrUndefined(getPokemonSpeciesForm(this.lastSpecies.speciesId, formIndex ?? 0).type2) + && getPokemonSpeciesForm(this.lastSpecies.speciesId, formIndex ?? 0).type2 != null && !globalScene.gameMode.hasChallenge(Challenges.FRESH_START); this.updateInstructions(); } diff --git a/src/ui/handlers/summary-ui-handler.ts b/src/ui/handlers/summary-ui-handler.ts index e73c5bae431..c9c8229ebfd 100644 --- a/src/ui/handlers/summary-ui-handler.ts +++ b/src/ui/handlers/summary-ui-handler.ts @@ -25,17 +25,9 @@ import type { PokemonMove } from "#moves/pokemon-move"; import type { Variant } from "#sprites/variant"; import { getVariantTint } from "#sprites/variant"; import { achvs } from "#system/achv"; -import { UiHandler } from "#ui/handlers/ui-handler"; import { addBBCodeTextObject, addTextObject, getBBCodeFrag, getTextColor } from "#ui/text"; -import { - fixedInt, - formatStat, - getLocalizedSpriteKey, - getShinyDescriptor, - isNullOrUndefined, - padInt, - rgbHexToRgba, -} from "#utils/common"; +import { UiHandler } from "#ui/ui-handler"; +import { fixedInt, formatStat, getLocalizedSpriteKey, getShinyDescriptor, padInt, rgbHexToRgba } from "#utils/common"; import { getEnumValues } from "#utils/enums"; import { toCamelCase, toTitleCase } from "#utils/strings"; import { argbFromRgba } from "@material/material-color-utilities"; @@ -895,10 +887,7 @@ export class SummaryUiHandler extends UiHandler { profileContainer.add(luckText); } - if ( - globalScene.gameData.achvUnlocks.hasOwnProperty(achvs.TERASTALLIZE.id) - && !isNullOrUndefined(this.pokemon) - ) { + if (globalScene.gameData.achvUnlocks.hasOwnProperty(achvs.TERASTALLIZE.id) && this.pokemon != null) { const teraIcon = globalScene.add.sprite(123, 26, "button_tera"); teraIcon.setName("terastallize-icon"); teraIcon.setFrame(PokemonType[this.pokemon.getTeraType()].toLowerCase()); diff --git a/src/ui/handlers/target-select-ui-handler.ts b/src/ui/handlers/target-select-ui-handler.ts index 777a6734383..bd81278c661 100644 --- a/src/ui/handlers/target-select-ui-handler.ts +++ b/src/ui/handlers/target-select-ui-handler.ts @@ -7,8 +7,8 @@ import { UiMode } from "#enums/ui-mode"; import type { Pokemon } from "#field/pokemon"; import type { ModifierBar } from "#modifiers/modifier"; import { getMoveTargets } from "#moves/move-utils"; -import { UiHandler } from "#ui/handlers/ui-handler"; -import { fixedInt, isNullOrUndefined } from "#utils/common"; +import { UiHandler } from "#ui/ui-handler"; +import { fixedInt } from "#utils/common"; export type TargetSelectCallback = (targets: BattlerIndex[]) => void; @@ -71,7 +71,7 @@ export class TargetSelectUiHandler extends UiHandler { */ resetCursor(cursorN: number, user: Pokemon): void { if ( - !isNullOrUndefined(cursorN) + cursorN != null && ([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2].includes(cursorN) || user.tempSummonData.waveTurnCount === 1) ) { // Reset cursor on the first turn of a fight or if an ally was targeted last turn @@ -90,13 +90,10 @@ export class TargetSelectUiHandler extends UiHandler { this.targetSelectCallback(button === Button.ACTION ? targetIndexes : []); success = true; if (this.fieldIndex === BattlerIndex.PLAYER) { - if (isNullOrUndefined(this.cursor0) || this.cursor0 !== this.cursor) { + if (this.cursor0 == null || this.cursor0 !== this.cursor) { this.cursor0 = this.cursor; } - } else if ( - this.fieldIndex === BattlerIndex.PLAYER_2 - && (isNullOrUndefined(this.cursor1) || this.cursor1 !== this.cursor) - ) { + } else if (this.fieldIndex === BattlerIndex.PLAYER_2 && (this.cursor1 == null || this.cursor1 !== this.cursor)) { this.cursor1 = this.cursor; } } else if (this.isMultipleTargets) { diff --git a/src/ui/handlers/test-dialogue-ui-handler.ts b/src/ui/handlers/test-dialogue-ui-handler.ts index d72de64ef70..b33e6726547 100644 --- a/src/ui/handlers/test-dialogue-ui-handler.ts +++ b/src/ui/handlers/test-dialogue-ui-handler.ts @@ -1,10 +1,9 @@ import { UiMode } from "#enums/ui-mode"; import type { PlayerPokemon } from "#field/pokemon"; -import type { OptionSelectItem } from "#ui/handlers/abstract-option-select-ui-handler"; -import type { InputFieldConfig } from "#ui/handlers/form-modal-ui-handler"; -import { FormModalUiHandler } from "#ui/handlers/form-modal-ui-handler"; -import type { ModalConfig } from "#ui/handlers/modal-ui-handler"; -import { isNullOrUndefined } from "#utils/common"; +import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; +import type { InputFieldConfig } from "#ui/form-modal-ui-handler"; +import { FormModalUiHandler } from "#ui/form-modal-ui-handler"; +import type { ModalConfig } from "#ui/modal-ui-handler"; import i18next from "i18next"; export class TestDialogueUiHandler extends FormModalUiHandler { @@ -18,7 +17,7 @@ export class TestDialogueUiHandler extends FormModalUiHandler { .map((t, i) => { const value = Object.values(object)[i]; - if (typeof value === "object" && !isNullOrUndefined(value)) { + if (typeof value === "object" && value != null) { // we check for not null or undefined here because if the language json file has a null key, the typeof will still be an object, but that object will be null, causing issues // If the value is an object, execute the same process // si el valor es un objeto ejecuta el mismo proceso @@ -27,7 +26,7 @@ export class TestDialogueUiHandler extends FormModalUiHandler { t => t.length > 0, ); } - if (typeof value === "string" || isNullOrUndefined(value)) { + if (typeof value === "string" || value == null) { // we check for null or undefined here as per above - the typeof is still an object but the value is null so we need to exit out of this and pass the null key // Return in the format expected by i18next @@ -109,7 +108,7 @@ export class TestDialogueUiHandler extends FormModalUiHandler { handler: () => { // this is here to make sure that if you try to backspace then enter, the last known evt.data (backspace) is picked up // this is because evt.data is null for backspace, so without this, the autocomplete windows just closes - if (!isNullOrUndefined(evt.data) || evt.inputType?.toLowerCase() === "deletecontentbackward") { + if (evt.data != null || evt.inputType?.toLowerCase() === "deletecontentbackward") { const separatedArray = inputObject.text.split(" "); separatedArray[separatedArray.length - 1] = value; inputObject.setText(separatedArray.join(" ")); diff --git a/src/ui/handlers/unavailable-modal-ui-handler.ts b/src/ui/handlers/unavailable-modal-ui-handler.ts index 5b885bfff77..7ba77dcac23 100644 --- a/src/ui/handlers/unavailable-modal-ui-handler.ts +++ b/src/ui/handlers/unavailable-modal-ui-handler.ts @@ -2,8 +2,8 @@ import { updateUserInfo } from "#app/account"; import { globalScene } from "#app/global-scene"; import { TextStyle } from "#enums/text-style"; import type { UiMode } from "#enums/ui-mode"; -import type { ModalConfig } from "#ui/handlers/modal-ui-handler"; -import { ModalUiHandler } from "#ui/handlers/modal-ui-handler"; +import type { ModalConfig } from "#ui/modal-ui-handler"; +import { ModalUiHandler } from "#ui/modal-ui-handler"; import { addTextObject } from "#ui/text"; import { sessionIdKey } from "#utils/common"; import { removeCookie } from "#utils/cookies"; diff --git a/src/ui/settings/abstract-control-settings-ui-handler.ts b/src/ui/settings/abstract-control-settings-ui-handler.ts index 2d9f3e6a6bd..17812785d1e 100644 --- a/src/ui/settings/abstract-control-settings-ui-handler.ts +++ b/src/ui/settings/abstract-control-settings-ui-handler.ts @@ -5,10 +5,10 @@ import type { Device } from "#enums/devices"; import { TextStyle } from "#enums/text-style"; import type { UiMode } from "#enums/ui-mode"; import { getIconWithSettingName } from "#inputs/config-handler"; -import { ScrollBar } from "#ui/containers/scroll-bar"; -import { UiHandler } from "#ui/handlers/ui-handler"; import { NavigationManager, NavigationMenu } from "#ui/navigation-menu"; +import { ScrollBar } from "#ui/scroll-bar"; import { addTextObject, getTextColor } from "#ui/text"; +import { UiHandler } from "#ui/ui-handler"; import { addWindow } from "#ui/ui-theme"; import { toCamelCase } from "#utils/strings"; import i18next from "i18next"; diff --git a/src/ui/settings/abstract-settings-ui-handler.ts b/src/ui/settings/abstract-settings-ui-handler.ts index ef117fb6a34..e22c28116f5 100644 --- a/src/ui/settings/abstract-settings-ui-handler.ts +++ b/src/ui/settings/abstract-settings-ui-handler.ts @@ -4,10 +4,11 @@ import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; import type { SettingType } from "#system/settings"; import { Setting, SettingKeys } from "#system/settings"; +import type { AnyFn } from "#types/type-helpers"; import type { InputsIcons } from "#ui/abstract-control-settings-ui-handler"; -import { ScrollBar } from "#ui/containers/scroll-bar"; -import { MessageUiHandler } from "#ui/handlers/message-ui-handler"; +import { MessageUiHandler } from "#ui/message-ui-handler"; import { NavigationManager, NavigationMenu } from "#ui/navigation-menu"; +import { ScrollBar } from "#ui/scroll-bar"; import { addTextObject, getTextColor } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; import i18next from "i18next"; @@ -53,54 +54,47 @@ export class AbstractSettingsUiHandler extends MessageUiHandler { */ setup() { const ui = this.getUi(); + const canvasWidth = globalScene.scaledCanvas.width; + const canvasHeight = globalScene.scaledCanvas.height; - this.settingsContainer = globalScene.add.container(1, -globalScene.scaledCanvas.height + 1); - this.settingsContainer.setName(`settings-${this.title}`); - this.settingsContainer.setInteractive( - new Phaser.Geom.Rectangle(0, 0, globalScene.scaledCanvas.width, globalScene.scaledCanvas.height - 20), - Phaser.Geom.Rectangle.Contains, - ); + this.settingsContainer = globalScene.add + .container(1, -canvasHeight + 1) + .setName(`settings-${this.title}`) + .setInteractive(new Phaser.Geom.Rectangle(0, 0, canvasWidth, canvasHeight - 20), Phaser.Geom.Rectangle.Contains); this.navigationIcons = {}; this.navigationContainer = new NavigationMenu(0, 0); + const navWidth = this.navigationContainer.width; + const navHeight = this.navigationContainer.height; - this.optionsBg = addWindow( - 0, - this.navigationContainer.height, - globalScene.scaledCanvas.width - 2, - globalScene.scaledCanvas.height - 16 - this.navigationContainer.height - 2, - ); - this.optionsBg.setName("window-options-bg"); - this.optionsBg.setOrigin(0, 0); + this.optionsBg = addWindow(0, navHeight, canvasWidth - 2, canvasHeight - 16 - navHeight - 2) + .setName("window-options-bg") + .setOrigin(0); - const actionsBg = addWindow( - 0, - globalScene.scaledCanvas.height - this.navigationContainer.height, - globalScene.scaledCanvas.width - 2, - 22, - ); - actionsBg.setOrigin(0, 0); + const actionsBg = addWindow(0, canvasHeight - navHeight, canvasWidth - 2, 22) // formatting + .setOrigin(0); - const iconAction = globalScene.add.sprite(0, 0, "keyboard"); - iconAction.setOrigin(0, -0.1); - iconAction.setPositionRelative(actionsBg, this.navigationContainer.width - 32, 4); + const iconAction = globalScene.add + .sprite(0, 0, "keyboard") + .setOrigin(0, -0.1) + .setPositionRelative(actionsBg, navWidth - 32, 4); this.navigationIcons["BUTTON_ACTION"] = iconAction; - const actionText = addTextObject(0, 0, i18next.t("settings:action"), TextStyle.SETTINGS_LABEL); - actionText.setOrigin(0, 0.15); + const actionText = addTextObject(0, 0, i18next.t("settings:action"), TextStyle.SETTINGS_LABEL).setOrigin(0, 0.15); actionText.setPositionRelative(iconAction, -actionText.width / 6 - 2, 0); - const iconCancel = globalScene.add.sprite(0, 0, "keyboard"); - iconCancel.setOrigin(0, -0.1); - iconCancel.setPositionRelative(actionsBg, actionText.x - 28, 4); + const iconCancel = globalScene.add + .sprite(0, 0, "keyboard") + .setOrigin(0, -0.1) + .setPositionRelative(actionsBg, actionText.x - 28, 4); this.navigationIcons["BUTTON_CANCEL"] = iconCancel; - const cancelText = addTextObject(0, 0, i18next.t("settings:back"), TextStyle.SETTINGS_LABEL); - cancelText.setOrigin(0, 0.15); + const cancelText = addTextObject(0, 0, i18next.t("settings:back"), TextStyle.SETTINGS_LABEL) // formatting + .setOrigin(0, 0.15); cancelText.setPositionRelative(iconCancel, -cancelText.width / 6 - 2, 0); - this.optionsContainer = globalScene.add.container(0, 0); + this.optionsContainer = globalScene.add.container(); this.settingLabels = []; this.optionValueLabels = []; @@ -113,8 +107,7 @@ export class AbstractSettingsUiHandler extends MessageUiHandler { anyReloadRequired = true; } - this.settingLabels[s] = addTextObject(8, 28 + s * 16, settingName, TextStyle.SETTINGS_LABEL); - this.settingLabels[s].setOrigin(0, 0); + this.settingLabels[s] = addTextObject(8, 28 + s * 16, settingName, TextStyle.SETTINGS_LABEL).setOrigin(0); this.optionsContainer.add(this.settingLabels[s]); this.optionValueLabels.push( @@ -125,7 +118,7 @@ export class AbstractSettingsUiHandler extends MessageUiHandler { option.label, setting.default === o ? TextStyle.SETTINGS_SELECTED : TextStyle.SETTINGS_VALUE, ); - valueLabel.setOrigin(0, 0); + valueLabel.setOrigin(0); this.optionsContainer.add(valueLabel); @@ -160,32 +153,33 @@ export class AbstractSettingsUiHandler extends MessageUiHandler { this.scrollBar.setTotalRows(this.settings.length); // Two-lines message box - this.messageBoxContainer = globalScene.add.container(0, globalScene.scaledCanvas.height); - this.messageBoxContainer.setName("settings-message-box"); - this.messageBoxContainer.setVisible(false); + this.messageBoxContainer = globalScene.add + .container(0, globalScene.scaledCanvas.height) + .setName("settings-message-box") + .setVisible(false); const settingsMessageBox = addWindow(0, -1, globalScene.scaledCanvas.width - 2, 48); settingsMessageBox.setOrigin(0, 1); this.messageBoxContainer.add(settingsMessageBox); - const messageText = addTextObject(8, -40, "", TextStyle.WINDOW, { - maxLines: 2, - }); - messageText.setWordWrapWidth(globalScene.game.canvas.width - 60); - messageText.setName("settings-message"); - messageText.setOrigin(0, 0); + const messageText = addTextObject(8, -40, "", TextStyle.WINDOW, { maxLines: 2 }) + .setWordWrapWidth(globalScene.game.canvas.width - 60) + .setName("settings-message") + .setOrigin(0); this.messageBoxContainer.add(messageText); this.message = messageText; - this.settingsContainer.add(this.optionsBg); - this.settingsContainer.add(this.scrollBar); - this.settingsContainer.add(this.navigationContainer); - this.settingsContainer.add(actionsBg); - this.settingsContainer.add(this.optionsContainer); - this.settingsContainer.add(iconAction); - this.settingsContainer.add(iconCancel); - this.settingsContainer.add(actionText); + this.settingsContainer.add([ + this.optionsBg, + this.scrollBar, + this.navigationContainer, + actionsBg, + this.optionsContainer, + iconAction, + iconCancel, + actionText, + ]); // Only add the ReloadRequired text on pages that have settings that require a reload. if (anyReloadRequired) { const reloadRequired = addTextObject(0, 0, `*${i18next.t("settings:requireReload")}`, TextStyle.SETTINGS_LABEL) @@ -194,8 +188,7 @@ export class AbstractSettingsUiHandler extends MessageUiHandler { .setY(actionText.y); this.settingsContainer.add(reloadRequired); } - this.settingsContainer.add(cancelText); - this.settingsContainer.add(this.messageBoxContainer); + this.settingsContainer.add([cancelText, this.messageBoxContainer]); ui.add(this.settingsContainer); @@ -210,17 +203,13 @@ export class AbstractSettingsUiHandler extends MessageUiHandler { updateBindings(): void { for (const settingName of Object.keys(this.navigationIcons)) { if (settingName === "BUTTON_HOME") { - this.navigationIcons[settingName].setTexture("keyboard"); - this.navigationIcons[settingName].setFrame("HOME.png"); - this.navigationIcons[settingName].alpha = 1; + this.navigationIcons[settingName].setTexture("keyboard").setFrame("HOME.png").alpha = 1; continue; } const icon = globalScene.inputController?.getIconForLatestInputRecorded(settingName); if (icon) { const type = globalScene.inputController?.getLastSourceType(); - this.navigationIcons[settingName].setTexture(type); - this.navigationIcons[settingName].setFrame(icon); - this.navigationIcons[settingName].alpha = 1; + this.navigationIcons[settingName].setTexture(type).setFrame(icon).alpha = 1; } else { this.navigationIcons[settingName].alpha = 0; } @@ -242,21 +231,43 @@ export class AbstractSettingsUiHandler extends MessageUiHandler { ? JSON.parse(localStorage.getItem(this.localStorageKey)!) : {}; // TODO: is this bang correct? - this.settings.forEach((setting, s) => - this.setOptionCursor(s, settings.hasOwnProperty(setting.key) ? settings[setting.key] : this.settings[s].default), - ); + this.settings.forEach((setting, s) => { + this.setOptionCursor(s, settings.hasOwnProperty(setting.key) ? settings[setting.key] : this.settings[s].default); + }); this.settingsContainer.setVisible(true); this.setCursor(0); this.setScrollCursor(0); - this.getUi().moveTo(this.settingsContainer, this.getUi().length - 1); + const ui = this.getUi(); - this.getUi().hideTooltip(); + ui.moveTo(this.settingsContainer, ui.length - 1); + + ui.hideTooltip(); return true; } + /** + * Submethod of {@linkcode processInput} to handle left/right input for changing option values + * + * @remarks + * If the cursor is positioned on a boundary option, will apply clamping / wrapping as appropriate + * @param cursor - Current cursor position in the settings menu + * @param dir - Direction to pan when scrolling, -1 for left, 1 for right + * @returns `true` if the action associated with the button was successfully processed, `false` otherwise. + */ + private processLeftRightInput(cursor: number, dir: -1 | 1): boolean { + let boundaryAction = Phaser.Math.Wrap; + let upperBound = this.optionValueLabels[cursor].length; + if (this.settings[cursor]?.clamp) { + boundaryAction = Phaser.Math.Clamp; + // clamping is right inclusive; wrapping isn't + upperBound -= 1; + } + return this.setOptionCursor(cursor, boundaryAction(this.optionCursors[cursor] + dir, 0, upperBound), true); + } + /** * Processes input from a specified button. * This method handles navigation through a UI menu, including movement through menu items @@ -314,20 +325,10 @@ export class AbstractSettingsUiHandler extends MessageUiHandler { } break; case Button.LEFT: - // Cycle to the rightmost position when at the leftmost, otherwise move left - success = this.setOptionCursor( - cursor, - Phaser.Math.Wrap(this.optionCursors[cursor] - 1, 0, this.optionValueLabels[cursor].length), - true, - ); + success = this.processLeftRightInput(cursor, -1); break; case Button.RIGHT: - // Cycle to the leftmost position when at the rightmost, otherwise move right - success = this.setOptionCursor( - cursor, - Phaser.Math.Wrap(this.optionCursors[cursor] + 1, 0, this.optionValueLabels[cursor].length), - true, - ); + success = this.processLeftRightInput(cursor, 1); break; case Button.CYCLE_FORM: case Button.CYCLE_SHINY: @@ -376,8 +377,9 @@ export class AbstractSettingsUiHandler extends MessageUiHandler { if (!this.cursorObj) { const cursorWidth = globalScene.scaledCanvas.width - (this.scrollBar.visible ? 16 : 10); - this.cursorObj = globalScene.add.nineslice(0, 0, "summary_moves_cursor", undefined, cursorWidth, 16, 1, 1, 1, 1); - this.cursorObj.setOrigin(0, 0); + this.cursorObj = globalScene.add + .nineslice(0, 0, "summary_moves_cursor", undefined, cursorWidth, 16, 1, 1, 1, 1) + .setOrigin(0); this.optionsContainer.add(this.cursorObj); } @@ -399,18 +401,21 @@ export class AbstractSettingsUiHandler extends MessageUiHandler { settingIndex = this.cursor + this.scrollCursor; } const setting = this.settings[settingIndex]; - const lastCursor = this.optionCursors[settingIndex]; + // do nothing if the option isn't changing + if (cursor === lastCursor) { + return false; + } - const lastValueLabel = this.optionValueLabels[settingIndex][lastCursor]; - lastValueLabel.setColor(getTextColor(TextStyle.SETTINGS_VALUE)); - lastValueLabel.setShadowColor(getTextColor(TextStyle.SETTINGS_VALUE, true)); + this.optionValueLabels[settingIndex][lastCursor] + .setColor(getTextColor(TextStyle.SETTINGS_VALUE)) + .setShadowColor(getTextColor(TextStyle.SETTINGS_VALUE, true)); this.optionCursors[settingIndex] = cursor; - const newValueLabel = this.optionValueLabels[settingIndex][cursor]; - newValueLabel.setColor(getTextColor(TextStyle.SETTINGS_SELECTED)); - newValueLabel.setShadowColor(getTextColor(TextStyle.SETTINGS_SELECTED, true)); + this.optionValueLabels[settingIndex][cursor] + .setColor(getTextColor(TextStyle.SETTINGS_SELECTED)) + .setShadowColor(getTextColor(TextStyle.SETTINGS_SELECTED, true)); if (save) { const saveSetting = () => { @@ -511,7 +516,7 @@ export class AbstractSettingsUiHandler extends MessageUiHandler { override showText( text: string, delay?: number, - callback?: Function, + callback?: AnyFn, callbackDelay?: number, prompt?: boolean, promptDelay?: number, diff --git a/src/ui/settings/gamepad-binding-ui-handler.ts b/src/ui/settings/gamepad-binding-ui-handler.ts index 4fdc4abdbfe..93923aeb57d 100644 --- a/src/ui/settings/gamepad-binding-ui-handler.ts +++ b/src/ui/settings/gamepad-binding-ui-handler.ts @@ -3,7 +3,7 @@ import { Device } from "#enums/devices"; import { TextStyle } from "#enums/text-style"; import type { UiMode } from "#enums/ui-mode"; import { getIconWithSettingName, getKeyWithKeycode } from "#inputs/config-handler"; -import { AbstractBindingUiHandler } from "#ui/handlers/abstract-binding-ui-handler"; +import { AbstractBindingUiHandler } from "#ui/abstract-binding-ui-handler"; import { addTextObject } from "#ui/text"; import i18next from "i18next"; diff --git a/src/ui/settings/keyboard-binding-ui-handler.ts b/src/ui/settings/keyboard-binding-ui-handler.ts index b1fd153461f..b339ac16188 100644 --- a/src/ui/settings/keyboard-binding-ui-handler.ts +++ b/src/ui/settings/keyboard-binding-ui-handler.ts @@ -3,7 +3,7 @@ import { Device } from "#enums/devices"; import { TextStyle } from "#enums/text-style"; import type { UiMode } from "#enums/ui-mode"; import { getKeyWithKeycode } from "#inputs/config-handler"; -import { AbstractBindingUiHandler } from "#ui/handlers/abstract-binding-ui-handler"; +import { AbstractBindingUiHandler } from "#ui/abstract-binding-ui-handler"; import { addTextObject } from "#ui/text"; import i18next from "i18next"; diff --git a/src/ui/settings/move-touch-controls-handler.ts b/src/ui/settings/move-touch-controls-handler.ts index 248ee76a850..dd7bceec55e 100644 --- a/src/ui/settings/move-touch-controls-handler.ts +++ b/src/ui/settings/move-touch-controls-handler.ts @@ -197,7 +197,7 @@ export class MoveTouchControlsHandler { /** * Returns the current positions of all touch controls that have moved from their default positions of this orientation. - * @returns {ControlPosition[]} The current positions of all touch controls that have moved from their default positions of this orientation + * @returns The current positions of all touch controls that have moved from their default positions of this orientation */ private getModifiedCurrentPositions(): ControlPosition[] { return this.getControlGroupElements() diff --git a/src/ui/settings/option-select-ui-handler.ts b/src/ui/settings/option-select-ui-handler.ts index 235f16e7f09..c989c768244 100644 --- a/src/ui/settings/option-select-ui-handler.ts +++ b/src/ui/settings/option-select-ui-handler.ts @@ -1,5 +1,5 @@ import { UiMode } from "#enums/ui-mode"; -import { AbstractOptionSelectUiHandler } from "#ui/handlers/abstract-option-select-ui-handler"; +import { AbstractOptionSelectUiHandler } from "#ui/abstract-option-select-ui-handler"; export class OptionSelectUiHandler extends AbstractOptionSelectUiHandler { constructor(mode: UiMode = UiMode.OPTION_SELECT) { diff --git a/src/ui/settings/settings-gamepad-ui-handler.ts b/src/ui/settings/settings-gamepad-ui-handler.ts index 57a70411f4c..5c8f187f4f8 100644 --- a/src/ui/settings/settings-gamepad-ui-handler.ts +++ b/src/ui/settings/settings-gamepad-ui-handler.ts @@ -18,11 +18,7 @@ import { addTextObject } from "#ui/text"; import { truncateString } from "#utils/common"; import i18next from "i18next"; -/** - * Class representing the settings UI handler for gamepads. - * - * @extends AbstractControlSettingsUiHandler - */ +/** Class representing the settings UI handler for gamepads */ export class SettingsGamepadUiHandler extends AbstractControlSettingsUiHandler { /** diff --git a/src/ui/settings/settings-keyboard-ui-handler.ts b/src/ui/settings/settings-keyboard-ui-handler.ts index 295a71abe36..f802a4fa9cc 100644 --- a/src/ui/settings/settings-keyboard-ui-handler.ts +++ b/src/ui/settings/settings-keyboard-ui-handler.ts @@ -19,11 +19,7 @@ import { truncateString } from "#utils/common"; import { toPascalSnakeCase } from "#utils/strings"; import i18next from "i18next"; -/** - * Class representing the settings UI handler for keyboards. - * - * @extends AbstractControlSettingsUiHandler - */ +/** Class representing the settings UI handler for keyboards */ export class SettingsKeyboardUiHandler extends AbstractControlSettingsUiHandler { /** * Creates an instance of SettingsKeyboardUiHandler. diff --git a/src/ui/ui.ts b/src/ui/ui.ts index a8e4dbe7318..76b07d7bfa5 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -4,59 +4,59 @@ import { Device } from "#enums/devices"; import { PlayerGender } from "#enums/player-gender"; import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; -import { AchvBar } from "#ui/containers/achv-bar"; -import type { BgmBar } from "#ui/containers/bgm-bar"; -import { PokedexPageUiHandler } from "#ui/containers/pokedex-page-ui-handler"; -import { SavingIconHandler } from "#ui/containers/saving-icon-handler"; +import { AchvBar } from "#ui/achv-bar"; +import { AchvsUiHandler } from "#ui/achvs-ui-handler"; +import { AutoCompleteUiHandler } from "#ui/autocomplete-ui-handler"; +import { AwaitableUiHandler } from "#ui/awaitable-ui-handler"; +import { BallUiHandler } from "#ui/ball-ui-handler"; +import { BattleMessageUiHandler } from "#ui/battle-message-ui-handler"; +import type { BgmBar } from "#ui/bgm-bar"; +import { GameChallengesUiHandler } from "#ui/challenges-select-ui-handler"; +import { ChangePasswordFormUiHandler } from "#ui/change-password-form-ui-handler"; +import { CommandUiHandler } from "#ui/command-ui-handler"; +import { ConfirmUiHandler } from "#ui/confirm-ui-handler"; +import { EggGachaUiHandler } from "#ui/egg-gacha-ui-handler"; +import { EggHatchSceneUiHandler } from "#ui/egg-hatch-scene-ui-handler"; +import { EggListUiHandler } from "#ui/egg-list-ui-handler"; +import { EggSummaryUiHandler } from "#ui/egg-summary-ui-handler"; +import { EvolutionSceneUiHandler } from "#ui/evolution-scene-ui-handler"; +import { FightUiHandler } from "#ui/fight-ui-handler"; +import { GameStatsUiHandler } from "#ui/game-stats-ui-handler"; import { GamepadBindingUiHandler } from "#ui/gamepad-binding-ui-handler"; -import { AchvsUiHandler } from "#ui/handlers/achvs-ui-handler"; -import { AutoCompleteUiHandler } from "#ui/handlers/autocomplete-ui-handler"; -import { AwaitableUiHandler } from "#ui/handlers/awaitable-ui-handler"; -import { BallUiHandler } from "#ui/handlers/ball-ui-handler"; -import { BattleMessageUiHandler } from "#ui/handlers/battle-message-ui-handler"; -import { GameChallengesUiHandler } from "#ui/handlers/challenges-select-ui-handler"; -import { ChangePasswordFormUiHandler } from "#ui/handlers/change-password-form-ui-handler"; -import { CommandUiHandler } from "#ui/handlers/command-ui-handler"; -import { ConfirmUiHandler } from "#ui/handlers/confirm-ui-handler"; -import { EggGachaUiHandler } from "#ui/handlers/egg-gacha-ui-handler"; -import { EggHatchSceneHandler } from "#ui/handlers/egg-hatch-scene-handler"; -import { EggListUiHandler } from "#ui/handlers/egg-list-ui-handler"; -import { EggSummaryUiHandler } from "#ui/handlers/egg-summary-ui-handler"; -import { EvolutionSceneHandler } from "#ui/handlers/evolution-scene-handler"; -import { FightUiHandler } from "#ui/handlers/fight-ui-handler"; -import { GameStatsUiHandler } from "#ui/handlers/game-stats-ui-handler"; -import { LoadingModalUiHandler } from "#ui/handlers/loading-modal-ui-handler"; -import { LoginFormUiHandler } from "#ui/handlers/login-form-ui-handler"; -import { MenuUiHandler } from "#ui/handlers/menu-ui-handler"; -import { MessageUiHandler } from "#ui/handlers/message-ui-handler"; -import { ModifierSelectUiHandler } from "#ui/handlers/modifier-select-ui-handler"; -import { MysteryEncounterUiHandler } from "#ui/handlers/mystery-encounter-ui-handler"; -import { PartyUiHandler } from "#ui/handlers/party-ui-handler"; -import { PokedexScanUiHandler } from "#ui/handlers/pokedex-scan-ui-handler"; -import { PokedexUiHandler } from "#ui/handlers/pokedex-ui-handler"; -import { RegistrationFormUiHandler } from "#ui/handlers/registration-form-ui-handler"; -import { RenameFormUiHandler } from "#ui/handlers/rename-form-ui-handler"; -import { RunHistoryUiHandler } from "#ui/handlers/run-history-ui-handler"; -import { RunInfoUiHandler } from "#ui/handlers/run-info-ui-handler"; -import { SaveSlotSelectUiHandler } from "#ui/handlers/save-slot-select-ui-handler"; -import { SessionReloadModalUiHandler } from "#ui/handlers/session-reload-modal-ui-handler"; -import { StarterSelectUiHandler } from "#ui/handlers/starter-select-ui-handler"; -import { SummaryUiHandler } from "#ui/handlers/summary-ui-handler"; -import { TargetSelectUiHandler } from "#ui/handlers/target-select-ui-handler"; -import { TestDialogueUiHandler } from "#ui/handlers/test-dialogue-ui-handler"; -import { TitleUiHandler } from "#ui/handlers/title-ui-handler"; -import type { UiHandler } from "#ui/handlers/ui-handler"; -import { UnavailableModalUiHandler } from "#ui/handlers/unavailable-modal-ui-handler"; import { KeyboardBindingUiHandler } from "#ui/keyboard-binding-ui-handler"; +import { LoadingModalUiHandler } from "#ui/loading-modal-ui-handler"; +import { LoginFormUiHandler } from "#ui/login-form-ui-handler"; +import { MenuUiHandler } from "#ui/menu-ui-handler"; +import { MessageUiHandler } from "#ui/message-ui-handler"; +import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; +import { MysteryEncounterUiHandler } from "#ui/mystery-encounter-ui-handler"; import { NavigationManager } from "#ui/navigation-menu"; import { OptionSelectUiHandler } from "#ui/option-select-ui-handler"; +import { PartyUiHandler } from "#ui/party-ui-handler"; +import { PokedexPageUiHandler } from "#ui/pokedex-page-ui-handler"; +import { PokedexScanUiHandler } from "#ui/pokedex-scan-ui-handler"; +import { PokedexUiHandler } from "#ui/pokedex-ui-handler"; +import { RegistrationFormUiHandler } from "#ui/registration-form-ui-handler"; +import { RenameFormUiHandler } from "#ui/rename-form-ui-handler"; +import { RunHistoryUiHandler } from "#ui/run-history-ui-handler"; +import { RunInfoUiHandler } from "#ui/run-info-ui-handler"; +import { SaveSlotSelectUiHandler } from "#ui/save-slot-select-ui-handler"; +import { SavingIconContainer } from "#ui/saving-icon-handler"; +import { SessionReloadModalUiHandler } from "#ui/session-reload-modal-ui-handler"; import { SettingsAudioUiHandler } from "#ui/settings-audio-ui-handler"; import { SettingsDisplayUiHandler } from "#ui/settings-display-ui-handler"; import { SettingsGamepadUiHandler } from "#ui/settings-gamepad-ui-handler"; import { SettingsKeyboardUiHandler } from "#ui/settings-keyboard-ui-handler"; import { SettingsUiHandler } from "#ui/settings-ui-handler"; +import { StarterSelectUiHandler } from "#ui/starter-select-ui-handler"; +import { SummaryUiHandler } from "#ui/summary-ui-handler"; +import { TargetSelectUiHandler } from "#ui/target-select-ui-handler"; +import { TestDialogueUiHandler } from "#ui/test-dialogue-ui-handler"; import { addTextObject } from "#ui/text"; +import { TitleUiHandler } from "#ui/title-ui-handler"; +import type { UiHandler } from "#ui/ui-handler"; import { addWindow } from "#ui/ui-theme"; +import { UnavailableModalUiHandler } from "#ui/unavailable-modal-ui-handler"; import { executeIf } from "#utils/common"; import i18next from "i18next"; import { AdminUiHandler } from "./handlers/admin-ui-handler"; @@ -115,7 +115,7 @@ export class UI extends Phaser.GameObjects.Container { private overlay: Phaser.GameObjects.Rectangle; public achvBar: AchvBar; public bgmBar: BgmBar; - public savingIcon: SavingIconHandler; + public savingIcon: SavingIconContainer; private tooltipContainer: Phaser.GameObjects.Container; private tooltipBg: Phaser.GameObjects.NineSlice; @@ -141,8 +141,8 @@ export class UI extends Phaser.GameObjects.Container { new PartyUiHandler(), new SummaryUiHandler(), new StarterSelectUiHandler(), - new EvolutionSceneHandler(), - new EggHatchSceneHandler(), + new EvolutionSceneUiHandler(), + new EggHatchSceneUiHandler(), new EggSummaryUiHandler(), new ConfirmUiHandler(), new OptionSelectUiHandler(), @@ -198,7 +198,7 @@ export class UI extends Phaser.GameObjects.Container { globalScene.uiContainer.add(this.achvBar); - this.savingIcon = new SavingIconHandler(); + this.savingIcon = new SavingIconContainer(); this.savingIcon.setup(); globalScene.uiContainer.add(this.savingIcon); diff --git a/src/ui/handlers/pokemon-icon-anim-handler.ts b/src/ui/utils/pokemon-icon-anim-helper.ts similarity index 98% rename from src/ui/handlers/pokemon-icon-anim-handler.ts rename to src/ui/utils/pokemon-icon-anim-helper.ts index 408e0ebc9d3..0d8de7ce1ca 100644 --- a/src/ui/handlers/pokemon-icon-anim-handler.ts +++ b/src/ui/utils/pokemon-icon-anim-helper.ts @@ -9,7 +9,7 @@ export enum PokemonIconAnimMode { type PokemonIcon = Phaser.GameObjects.Container | Phaser.GameObjects.Sprite; -export class PokemonIconAnimHandler { +export class PokemonIconAnimHelper { private icons: Map; private toggled: boolean; diff --git a/src/ui/handlers/scrollable-grid-handler.ts b/src/ui/utils/scrollable-grid-helper.ts similarity index 96% rename from src/ui/handlers/scrollable-grid-handler.ts rename to src/ui/utils/scrollable-grid-helper.ts index 12bbaa32e98..74ddfdfa412 100644 --- a/src/ui/handlers/scrollable-grid-handler.ts +++ b/src/ui/utils/scrollable-grid-helper.ts @@ -1,6 +1,6 @@ import { Button } from "#enums/buttons"; -import type { ScrollBar } from "#ui/containers/scroll-bar"; -import type { UiHandler } from "#ui/handlers/ui-handler"; +import type { ScrollBar } from "#ui/scroll-bar"; +import type { UiHandler } from "#ui/ui-handler"; type UpdateGridCallbackFunction = () => void; type UpdateDetailsCallbackFunction = (index: number) => void; @@ -16,7 +16,7 @@ type UpdateDetailsCallbackFunction = (index: number) => void; * - in `UiHandler.processInput`: call `processNavigationInput` to have it handle the cursor updates while calling the defined callbacks * - in `UiHandler.clear`: call `reset` */ -export class ScrollableGridUiHandler { +export class ScrollableGridHelper { private readonly ROWS: number; private readonly COLUMNS: number; private handler: UiHandler; @@ -47,7 +47,7 @@ export class ScrollableGridUiHandler { * @param scrollBar {@linkcode ScrollBar} * @returns this */ - withScrollBar(scrollBar: ScrollBar): ScrollableGridUiHandler { + withScrollBar(scrollBar: ScrollBar): ScrollableGridHelper { this.scrollBar = scrollBar; this.scrollBar.setTotalRows(Math.ceil(this.totalElements / this.COLUMNS)); return this; @@ -58,7 +58,7 @@ export class ScrollableGridUiHandler { * @param callback {@linkcode UpdateGridCallbackFunction} * @returns this */ - withUpdateGridCallBack(callback: UpdateGridCallbackFunction): ScrollableGridUiHandler { + withUpdateGridCallBack(callback: UpdateGridCallbackFunction): ScrollableGridHelper { this.updateGridCallback = callback; return this; } @@ -68,7 +68,7 @@ export class ScrollableGridUiHandler { * @param callback {@linkcode UpdateDetailsCallbackFunction} * @returns this */ - withUpdateSingleElementCallback(callback: UpdateDetailsCallbackFunction): ScrollableGridUiHandler { + withUpdateSingleElementCallback(callback: UpdateDetailsCallbackFunction): ScrollableGridHelper { this.updateDetailsCallback = callback; return this; } diff --git a/src/utils/anim-utils.ts b/src/utils/anim-utils.ts new file mode 100644 index 00000000000..f1a06552d38 --- /dev/null +++ b/src/utils/anim-utils.ts @@ -0,0 +1,26 @@ +import { globalScene } from "#app/global-scene"; +import type { SceneBase } from "#app/scene-base"; + +/** + * Plays a Tween animation, resolving once the animation completes. + * @param config - The config for a single Tween + * @param scene - The {@linkcode SceneBase} on which the Tween plays; default {@linkcode globalScene} + * @returns A Promise that resolves once the Tween has been played. + * + * @privateRemarks + * The `config` input should not include an `onComplete` field as that callback is + * used to resolve the Promise containing the Tween animation. + * However, `config`'s type cannot be changed to something like `Omit` + * due to how the type for `TweenBuilderConfig` is defined. + */ +export async function playTween( + config: Phaser.Types.Tweens.TweenBuilderConfig, + scene: SceneBase = globalScene, +): Promise { + await new Promise(resolve => + scene.tweens.add({ + ...config, + onComplete: resolve, + }), + ); +} diff --git a/src/utils/challenge-utils.ts b/src/utils/challenge-utils.ts index 10f834e24b4..f79f48f30d2 100644 --- a/src/utils/challenge-utils.ts +++ b/src/utils/challenge-utils.ts @@ -12,8 +12,8 @@ import type { MoveSourceType } from "#enums/move-source-type"; import type { SpeciesId } from "#enums/species-id"; import type { EnemyPokemon, PlayerPokemon, Pokemon } from "#field/pokemon"; import type { ModifierTypeOption } from "#modifiers/modifier-type"; -import type { DexAttrProps, StarterDataEntry } from "#system/game-data"; import type { DexEntry } from "#types/dex-data"; +import type { DexAttrProps, StarterDataEntry } from "#types/save-data"; import { BooleanHolder, type NumberHolder } from "./common"; import { getPokemonSpecies } from "./pokemon-utils"; @@ -204,7 +204,7 @@ export function applyChallenges(challengeType: ChallengeType.SHOP, status: Boole * @param challengeType - {@linkcode ChallengeType.POKEMON_ADD_TO_PARTY} * @param pokemon - The pokemon being caught * @param status - Whether the pokemon can be added to the party or not - * @return `true` if any challenge was sucessfully applied, `false` otherwise + * @returns `true` if any challenge was sucessfully applied, `false` otherwise */ export function applyChallenges( challengeType: ChallengeType.POKEMON_ADD_TO_PARTY, @@ -217,7 +217,7 @@ export function applyChallenges( * @param challengeType - {@linkcode ChallengeType.POKEMON_FUSION} * @param pokemon - The pokemon being checked * @param status - Whether the selected pokemon is allowed to fuse or not - * @return `true` if any challenge was sucessfully applied, `false` otherwise + * @returns `true` if any challenge was sucessfully applied, `false` otherwise */ export function applyChallenges( challengeType: ChallengeType.POKEMON_FUSION, @@ -230,7 +230,7 @@ export function applyChallenges( * @param challengeType - {@linkcode ChallengeType.POKEMON_MOVE} * @param moveId - The move being checked * @param status - Whether the move can be used or not - * @return `true` if any challenge was sucessfully applied, `false` otherwise + * @returns `true` if any challenge was sucessfully applied, `false` otherwise */ export function applyChallenges( challengeType: ChallengeType.POKEMON_MOVE, @@ -243,7 +243,7 @@ export function applyChallenges( * @param challengeType - {@linkcode ChallengeType.SHOP_ITEM} * @param shopItem - The item being checked * @param status - Whether the item should be added to the shop or not - * @return `true` if any challenge was sucessfully applied, `false` otherwise + * @returns `true` if any challenge was sucessfully applied, `false` otherwise */ export function applyChallenges( challengeType: ChallengeType.SHOP_ITEM, @@ -256,7 +256,7 @@ export function applyChallenges( * @param challengeType - {@linkcode ChallengeType.WAVE_REWARD} * @param reward - The reward being checked * @param status - Whether the reward should be added to the reward options or not - * @return `true` if any challenge was sucessfully applied, `false` otherwise + * @returns `true` if any challenge was sucessfully applied, `false` otherwise */ export function applyChallenges( challengeType: ChallengeType.WAVE_REWARD, @@ -268,7 +268,7 @@ export function applyChallenges( * Apply all challenges that prevent recovery from fainting * @param challengeType - {@linkcode ChallengeType.PREVENT_REVIVE} * @param status - Whether fainting is a permanent status or not - * @return `true` if any challenge was sucessfully applied, `false` otherwise + * @returns `true` if any challenge was sucessfully applied, `false` otherwise */ export function applyChallenges(challengeType: ChallengeType.PREVENT_REVIVE, status: BooleanHolder): boolean; diff --git a/src/utils/common.ts b/src/utils/common.ts index 97e61b902d8..f0166b1e74c 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -135,8 +135,8 @@ export function randSeedItem(items: T[]): T { /** * Shuffle a list using the seeded rng. Utilises the Fisher-Yates algorithm. - * @param {Array} items An array of items. - * @returns {Array} A new shuffled array of items. + * @param items An array of items. + * @returns A new shuffled array of items. */ export function randSeedShuffle(items: T[]): T[] { if (items.length <= 1) { @@ -340,8 +340,8 @@ export function rgbToHsv(r: number, g: number, b: number) { /** * Compare color difference in RGB - * @param {Array} rgb1 First RGB color in array - * @param {Array} rgb2 Second RGB color in array + * @param rgb1 First RGB color in array + * @param rgb2 Second RGB color in array */ export function deltaRgb(rgb1: number[], rgb2: number[]): number { const [r1, g1, b1] = rgb1; @@ -458,15 +458,6 @@ export function truncateString(str: string, maxLength = 10) { return str; } -/** - * Report whether a given value is nullish (`null`/`undefined`). - * @param val - The value whose nullishness is being checked - * @returns `true` if `val` is either `null` or `undefined` - */ -export function isNullOrUndefined(val: any): val is null | undefined { - return val === null || val === undefined; -} - /** * This function is used in the context of a Pokémon battle game to calculate the actual integer damage value from a float result. * Many damage calculation formulas involve various parameters and result in float values. diff --git a/src/utils/cookies.ts b/src/utils/cookies.ts index e82895d1fac..407bd75da14 100644 --- a/src/utils/cookies.ts +++ b/src/utils/cookies.ts @@ -17,17 +17,17 @@ export function removeCookie(cName: string): void { export function getCookie(cName: string): string { // check if there are multiple cookies with the same name and delete them - if (document.cookie.split(";").filter(c => c.includes(cName)).length > 1) { + if (document.cookie.split(";").filter(c => c.trim().includes(cName)).length > 1) { removeCookie(cName); return ""; } const name = `${cName}=`; - const ca = document.cookie.split(";"); + const cookieArray = document.cookie.split(";"); // Check all cookies in the document and see if any of them match, grabbing the first one whose value lines up - for (const c of ca) { - const cTrimmed = c.trim(); - if (cTrimmed.startsWith(name)) { - return c.slice(name.length, c.length); + for (const cookie of cookieArray) { + const cookieTrimmed = cookie.trim(); + if (cookieTrimmed.startsWith(name)) { + return cookieTrimmed.slice(name.length, cookieTrimmed.length); } } return ""; diff --git a/src/utils/data.ts b/src/utils/data.ts index 8ff308cde8f..1383d8e6ff2 100644 --- a/src/utils/data.ts +++ b/src/utils/data.ts @@ -1,6 +1,6 @@ import { loggedInUser } from "#app/account"; import { saveKey } from "#app/constants"; -import type { StarterAttributes } from "#system/game-data"; +import type { StarterAttributes } from "#types/save-data"; import { AES, enc } from "crypto-js"; /** diff --git a/src/utils/speed-order-generator.ts b/src/utils/speed-order-generator.ts new file mode 100644 index 00000000000..24f95de665f --- /dev/null +++ b/src/utils/speed-order-generator.ts @@ -0,0 +1,39 @@ +import { globalScene } from "#app/global-scene"; +import { PokemonPriorityQueue } from "#app/queues/pokemon-priority-queue"; +import { ArenaTagSide } from "#enums/arena-tag-side"; +import type { Pokemon } from "#field/pokemon"; + +/** + * A generator function which uses a priority queue to yield each pokemon from a given side of the field in speed order. + * @param side - The {@linkcode ArenaTagSide | side} of the field to use + * @returns A {@linkcode Generator} of {@linkcode Pokemon} + * + * @remarks + * This should almost always be used by iteration in a `for...of` loop + */ +export function* inSpeedOrder(side: ArenaTagSide = ArenaTagSide.BOTH): Generator { + let pokemonList: Pokemon[]; + switch (side) { + case ArenaTagSide.PLAYER: + pokemonList = globalScene.getPlayerField(true); + break; + case ArenaTagSide.ENEMY: + pokemonList = globalScene.getEnemyField(true); + break; + default: + pokemonList = globalScene.getField(true); + } + + const queue = new PokemonPriorityQueue(); + let i = 0; + pokemonList.forEach(p => { + queue.push(p); + }); + while (!queue.isEmpty()) { + // If the queue is not empty, this can never be undefined + i++; + yield queue.pop()!; + } + + return i; +} diff --git a/src/utils/speed-order.ts b/src/utils/speed-order.ts new file mode 100644 index 00000000000..1d894369bb3 --- /dev/null +++ b/src/utils/speed-order.ts @@ -0,0 +1,57 @@ +import { Pokemon } from "#app/field/pokemon"; +import { globalScene } from "#app/global-scene"; +import { BooleanHolder, randSeedShuffle } from "#app/utils/common"; +import { ArenaTagType } from "#enums/arena-tag-type"; +import { Stat } from "#enums/stat"; + +/** Interface representing an object associated with a specific Pokemon */ +interface hasPokemon { + getPokemon(): Pokemon; +} + +/** + * Sorts an array of {@linkcode Pokemon} by speed, taking Trick Room into account. + * @param pokemonList - The list of Pokemon or objects containing Pokemon + * @param shuffleFirst - Whether to shuffle the list before sorting (to handle speed ties). Default `true`. + * @returns The sorted array of {@linkcode Pokemon} + */ +export function sortInSpeedOrder(pokemonList: T[], shuffleFirst = true): T[] { + pokemonList = shuffleFirst ? shufflePokemonList(pokemonList) : pokemonList; + sortBySpeed(pokemonList); + return pokemonList; +} + +/** + * @param pokemonList - The array of Pokemon or objects containing Pokemon + * @returns The shuffled array + */ +function shufflePokemonList(pokemonList: T[]): T[] { + // This is seeded with the current turn to prevent an inconsistency where it + // was varying based on how long since you last reloaded + globalScene.executeWithSeedOffset( + () => { + pokemonList = randSeedShuffle(pokemonList); + }, + globalScene.currentBattle.turn * 1000 + pokemonList.length, + globalScene.waveSeed, + ); + return pokemonList; +} + +/** Sorts an array of {@linkcode Pokemon} by speed (without shuffling) */ +function sortBySpeed(pokemonList: T[]): void { + pokemonList.sort((a, b) => { + const aSpeed = (a instanceof Pokemon ? a : a.getPokemon()).getEffectiveStat(Stat.SPD); + const bSpeed = (b instanceof Pokemon ? b : b.getPokemon()).getEffectiveStat(Stat.SPD); + + return bSpeed - aSpeed; + }); + + /** 'true' if Trick Room is on the field. */ + const speedReversed = new BooleanHolder(false); + globalScene.arena.applyTags(ArenaTagType.TRICK_ROOM, false, speedReversed); + + if (speedReversed.value) { + pokemonList.reverse(); + } +} diff --git a/src/utils/strings.ts b/src/utils/strings.ts index bf5e5c6473f..b4b2498fe9d 100644 --- a/src/utils/strings.ts +++ b/src/utils/strings.ts @@ -62,7 +62,7 @@ function trimFromStartAndEnd(str: string, charToTrim: string): string { /** * Capitalize the first letter of a string. * @param str - The string whose first letter is to be capitalized - * @return The original string with its first letter capitalized. + * @returns The original string with its first letter capitalized. * @example * ```ts * console.log(capitalizeFirstLetter("consectetur adipiscing elit")); // returns "Consectetur adipiscing elit" diff --git a/src/vite.env.d.ts b/src/vite.env.d.ts index 68159908730..3192b81afd3 100644 --- a/src/vite.env.d.ts +++ b/src/vite.env.d.ts @@ -9,8 +9,9 @@ interface ImportMetaEnv { readonly VITE_DISCORD_CLIENT_ID?: string; readonly VITE_GOOGLE_CLIENT_ID?: string; readonly VITE_I18N_DEBUG?: string; + readonly NODE_ENV?: string; } -interface ImportMeta { +declare interface ImportMeta { readonly env: ImportMetaEnv; } diff --git a/test/@types/vitest.d.ts b/test/@types/vitest.d.ts index b13d9e53101..43e9df190aa 100644 --- a/test/@types/vitest.d.ts +++ b/test/@types/vitest.d.ts @@ -1,7 +1,7 @@ import "vitest"; -import type { Phase } from "#app/phase"; import type Overrides from "#app/overrides"; +import type { Phase } from "#app/phase"; import type { ArenaTag } from "#data/arena-tag"; import type { TerrainType } from "#data/terrain"; import type { AbilityId } from "#enums/ability-id"; @@ -10,10 +10,14 @@ import type { ArenaTagType } from "#enums/arena-tag-type"; import type { BattlerTagType } from "#enums/battler-tag-type"; import type { MoveId } from "#enums/move-id"; import type { PokemonType } from "#enums/pokemon-type"; +import type { PositionalTag } from "#data/positional-tags/positional-tag"; import type { PositionalTagType } from "#enums/positional-tag-type"; import type { BattleStat, EffectiveStat } from "#enums/stat"; import type { WeatherType } from "#enums/weather-type"; +import type { Pokemon } from "#field/pokemon"; +import type { GameManager } from "#test/test-utils/game-manager"; import type { toHaveArenaTagOptions } from "#test/test-utils/matchers/to-have-arena-tag"; +import type { toHaveBattlerTagOptions } from "#test/test-utils/matchers/to-have-battler-tag"; import type { toHaveEffectiveStatOptions } from "#test/test-utils/matchers/to-have-effective-stat"; import type { toHavePositionalTagOptions } from "#test/test-utils/matchers/to-have-positional-tag"; import type { expectedStatusType } from "#test/test-utils/matchers/to-have-status-effect"; @@ -23,164 +27,212 @@ import type { TurnMove } from "#types/turn-move"; import type { AtLeastOne } from "#types/type-helpers"; import type { toDmgValue } from "#utils/common"; import type { expect } from "vitest"; -import type { toHaveBattlerTagOptions } from "#test/test-utils/matchers/to-have-battler-tag"; +// #region Boilerplate/Helpers declare module "vitest" { - interface Assertion { - /** - * Check whether an array contains EXACTLY the given items (in any order). - * - * Different from {@linkcode expect.arrayContaining} as the latter only checks for subset equality - * (as opposed to full equality). - * - * @param expected - The expected contents of the array, in any order - * @see {@linkcode expect.arrayContaining} - */ - toEqualArrayUnsorted(expected: T[]): void; - - /** - * Check if the currently-running {@linkcode Phase} is of the given type. - * @param expectedPhase - The expected {@linkcode PhaseString} - */ - toBeAtPhase(expectedPhase: PhaseString): void; - - // #region Arena Matchers - - /** - * Check whether the current {@linkcode WeatherType} is as expected. - * @param expectedWeatherType - The expected {@linkcode WeatherType} - */ - toHaveWeather(expectedWeatherType: WeatherType): void; - - /** - * Check whether the current {@linkcode TerrainType} is as expected. - * @param expectedTerrainType - The expected {@linkcode TerrainType} - */ - toHaveTerrain(expectedTerrainType: TerrainType): void; - - /** - * Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}. - * @param expectedTag - A partially-filled {@linkcode ArenaTag} containing the desired properties - */ - toHaveArenaTag(expectedTag: toHaveArenaTagOptions): void; - /** - * Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}. - * @param expectedType - The {@linkcode ArenaTagType} of the desired tag - * @param side - The {@linkcode ArenaTagSide | side(s) of the field} the tag should affect; default {@linkcode ArenaTagSide.BOTH} - */ - toHaveArenaTag(expectedType: ArenaTagType, side?: ArenaTagSide): void; - - /** - * Check whether the current {@linkcode Arena} contains the given {@linkcode PositionalTag}. - * @param expectedTag - A partially-filled `PositionalTag` containing the desired properties - */ - toHavePositionalTag

(expectedTag: toHavePositionalTagOptions

): void; - /** - * Check whether the current {@linkcode Arena} contains the given number of {@linkcode PositionalTag}s. - * @param expectedType - The {@linkcode PositionalTagType} of the desired tag - * @param count - The number of instances of {@linkcode expectedType} that should be active; - * defaults to `1` and must be within the range `[0, 4]` - */ - toHavePositionalTag(expectedType: PositionalTagType, count?: number): void; - - // #endregion Arena Matchers - - // #region Pokemon Matchers - - /** - * Check whether a {@linkcode Pokemon}'s current typing includes the given types. - * @param expectedTypes - The expected {@linkcode PokemonType}s to check against; must have length `>0` - * @param options - The {@linkcode toHaveTypesOptions | options} passed to the matcher - */ - toHaveTypes(expectedTypes: PokemonType[], options?: toHaveTypesOptions): void; - - /** - * Check whether a {@linkcode Pokemon} has used a move matching the given criteria. - * @param expectedMove - The {@linkcode MoveId} the Pokemon is expected to have used, - * or a partially filled {@linkcode TurnMove} containing the desired properties to check - * @param index - The index of the move history entry to check, in order from most recent to least recent; default `0` - * @see {@linkcode Pokemon.getLastXMoves} - */ - toHaveUsedMove(expectedMove: MoveId | AtLeastOne, index?: number): void; - - /** - * Check whether a {@linkcode Pokemon}'s effective stat is as expected - * (checked after all stat value modifications). - * @param stat - The {@linkcode EffectiveStat} to check - * @param expectedValue - The expected value of {@linkcode stat} - * @param options - The {@linkcode toHaveEffectiveStatOptions | options} passed to the matcher - * @remarks - * If you want to check the stat **before** modifiers are applied, use {@linkcode Pokemon.getStat} instead. - */ - toHaveEffectiveStat(stat: EffectiveStat, expectedValue: number, options?: toHaveEffectiveStatOptions): void; - - /** - * Check whether a {@linkcode Pokemon} has a specific {@linkcode StatusEffect | non-volatile status effect}. - * @param expectedStatusEffect - The {@linkcode StatusEffect} the Pokemon is expected to have, - * or a partially filled {@linkcode Status} containing the desired properties - */ - toHaveStatusEffect(expectedStatusEffect: expectedStatusType): void; - - /** - * Check whether a {@linkcode Pokemon} has a specific {@linkcode Stat} stage. - * @param stat - The {@linkcode BattleStat} to check - * @param expectedStage - The expected stat stage value of {@linkcode stat} - */ - toHaveStatStage(stat: BattleStat, expectedStage: number): void; - - /** - * Check whether a {@linkcode Pokemon} has the given {@linkcode BattlerTag}. - * @param expectedTag - A partially-filled {@linkcode BattlerTag} containing the desired properties - */ - toHaveBattlerTag(expectedTag: toHaveBattlerTagOptions): void; - /** - * Check whether a {@linkcode Pokemon} has the given {@linkcode BattlerTag}. - * @param expectedType - The expected {@linkcode BattlerTagType} - */ - toHaveBattlerTag(expectedType: BattlerTagType): void; - - /** - * Check whether a {@linkcode Pokemon} has applied a specific {@linkcode AbilityId}. - * @param expectedAbilityId - The `AbilityId` to check for - */ - toHaveAbilityApplied(expectedAbilityId: AbilityId): void; - - /** - * Check whether a {@linkcode Pokemon} has a specific amount of {@linkcode Stat.HP | HP}. - * @param expectedHp - The expected amount of {@linkcode Stat.HP | HP} to have - */ - toHaveHp(expectedHp: number): void; - - /** - * Check whether a {@linkcode Pokemon} has taken a specific amount of damage. - * @param expectedDamageTaken - The expected amount of damage taken - * @param roundDown - Whether to round down `expectedDamageTaken` with {@linkcode toDmgValue}; default `true` - */ - toHaveTakenDamage(expectedDamageTaken: number, roundDown?: boolean): void; - - /** - * Check whether a {@linkcode Pokemon} is currently fainted (as determined by {@linkcode Pokemon.isFainted}). - * @remarks - * When checking whether an enemy wild Pokemon is fainted, one must store a reference to it in a variable _before_ the fainting effect occurs. - * Otherwise, the Pokemon will be removed from the field and garbage collected. - */ - toHaveFainted(): void; - - /** - * Check whether a {@linkcode Pokemon} is at full HP. - */ - toHaveFullHp(): void; - /** - * Check whether a {@linkcode Pokemon} has consumed the given amount of PP for one of its moves. - * @param moveId - The {@linkcode MoveId} corresponding to the {@linkcode PokemonMove} that should have consumed PP - * @param ppUsed - The numerical amount of PP that should have been consumed, - * or `all` to indicate the move should be _out_ of PP - * @remarks - * If the Pokemon's moveset has been set via {@linkcode Overrides.MOVESET_OVERRIDE}/{@linkcode Overrides.ENEMY_MOVESET_OVERRIDE} - * or does not contain exactly one copy of `moveId`, this will fail the test. - */ - toHaveUsedPP(moveId: MoveId, ppUsed: number | "all"): void; - - // #endregion Pokemon Matchers - } + interface Assertion + extends GenericMatchers, + RestrictMatcher, + RestrictMatcher, + RestrictMatcher {} } + +/** + * Utility type to restrict matchers' properties based on the type of `T`. + * If it does not extend `R`, all methods inside `M` will have their types resolved to `never`. + * @typeParam M - The type of the matchers object to restrict + * @typeParam T - The type parameter of the assertion + * @typeParam R - The type to restrict T based off of + * @privateRemarks + * We cannot remove incompatible methods outright as Typescript requires that + * interfaces extend solely off of types with statically known members. + */ +type RestrictMatcher = { + [k in keyof M]: T extends R ? M[k] : never; +}; +// #endregion Boilerplate/Helpers + +// #region Generic Matchers +interface GenericMatchers { + /** + * Check whether an array contains EXACTLY the given items (in any order). + * + * Different from {@linkcode expect.arrayContaining} as the latter only checks for subset equality + * (as opposed to full equality). + * + * @param expected - The expected contents of the array, in any order + * @see {@linkcode expect.arrayContaining} + */ + toEqualUnsorted: T extends (infer U)[] ? (expected: U[]) => void : never; + + /** + * Check whether a {@linkcode Map} contains the given key, disregarding its value. + * @param expectedKey - The key whose inclusion is being checked + * @privateRemarks + * While this functionality _could_ be simulated by writing + * `expect(x.get(y)).toBeDefined()` or + * `expect(x).toContain([y, expect.anything()])`, + * this is still preferred due to being more ergonomic and provides better error messsages. + */ + toHaveKey: T extends Map ? (expectedKey: K) => void : never; +} +// #endregion Generic Matchers + +// #region GameManager Matchers +interface GameManagerMatchers { + /** + * Check if the {@linkcode GameManager} has shown the given message at least once in the current test case. + * @param expectedMessage - The expected message to be displayed + * @remarks + * Strings consumed by this function should _always_ be produced by a call to `i18next.t` + * to avoid hardcoding text into test files. + */ + toHaveShownMessage(expectedMessage: string): void; + + /** + * Check if the currently-running {@linkcode Phase} is of the given type. + * @param expectedPhase - The expected {@linkcode PhaseString | name of the phase} + */ + toBeAtPhase(expectedPhase: PhaseString): void; +} // #endregion GameManager Matchers + +// #region Arena Matchers +interface ArenaMatchers { + /** + * Check whether the current {@linkcode WeatherType} is as expected. + * @param expectedWeatherType - The expected `WeatherType` + */ + toHaveWeather(expectedWeatherType: WeatherType): void; + + /** + * Check whether the current {@linkcode TerrainType} is as expected. + * @param expectedTerrainType - The expected `TerrainType` + */ + toHaveTerrain(expectedTerrainType: TerrainType): void; + + /** + * Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}. + * @param expectedTag - A partially-filled `ArenaTag` containing the desired properties + */ + toHaveArenaTag(expectedTag: toHaveArenaTagOptions): void; + /** + * Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}. + * @param expectedType - The {@linkcode ArenaTagType} of the desired tag + * @param side - The {@linkcode ArenaTagSide | side(s) of the field} the tag should affect; default {@linkcode ArenaTagSide.BOTH} + */ + toHaveArenaTag(expectedType: ArenaTagType, side?: ArenaTagSide): void; + + /** + * Check whether the current {@linkcode Arena} contains the given {@linkcode PositionalTag}. + * @param expectedTag - A partially-filled `PositionalTag` containing the desired properties + */ + toHavePositionalTag

(expectedTag: toHavePositionalTagOptions

): void; + /** + * Check whether the current {@linkcode Arena} contains the given number of {@linkcode PositionalTag}s. + * @param expectedType - The {@linkcode PositionalTagType} of the desired tag + * @param count - The number of instances of `expectedType` that should be active; + * defaults to `1` and must be within the range `[0, 4]` + */ + toHavePositionalTag(expectedType: PositionalTagType, count?: number): void; +} + +// #endregion Arena Matchers + +// #region Pokemon Matchers +interface PokemonMatchers { + /** + * Check whether a {@linkcode Pokemon}'s current typing includes the given types. + * @param expectedTypes - The expected {@linkcode PokemonType}s to check against; must have length `>0` + * @param options - The {@linkcode toHaveTypesOptions | options} passed to the matcher + */ + toHaveTypes(expectedTypes: PokemonType[], options?: toHaveTypesOptions): void; + + /** + * Check whether a {@linkcode Pokemon} has used a move matching the given criteria. + * @param expectedMove - The {@linkcode MoveId} the Pokemon is expected to have used, + * or a partially filled {@linkcode TurnMove} containing the desired properties to check + * @param index - The index of the move history entry to check, in order from most recent to least recent; default `0` + * @see {@linkcode Pokemon.getLastXMoves} + */ + toHaveUsedMove(expectedMove: MoveId | AtLeastOne, index?: number): void; + + /** + * Check whether a {@linkcode Pokemon}'s effective stat is as expected + * (checked after all stat value modifications). + * @param stat - The {@linkcode EffectiveStat} to check + * @param expectedValue - The expected value of {@linkcode stat} + * @param options - The {@linkcode toHaveEffectiveStatOptions | options} passed to the matcher + * @remarks + * If you want to check the stat **before** modifiers are applied, use {@linkcode Pokemon.getStat} instead. + */ + toHaveEffectiveStat(stat: EffectiveStat, expectedValue: number, options?: toHaveEffectiveStatOptions): void; + + /** + * Check whether a {@linkcode Pokemon} has a specific {@linkcode StatusEffect | non-volatile status effect}. + * @param expectedStatusEffect - The {@linkcode StatusEffect} the Pokemon is expected to have, + * or a partially filled {@linkcode Status} containing the desired properties + */ + toHaveStatusEffect(expectedStatusEffect: expectedStatusType): void; + + /** + * Check whether a {@linkcode Pokemon} has a specific {@linkcode Stat} stage. + * @param stat - The {@linkcode BattleStat} to check + * @param expectedStage - The expected stat stage value of {@linkcode stat} + */ + toHaveStatStage(stat: BattleStat, expectedStage: number): void; + + /** + * Check whether a {@linkcode Pokemon} has the given {@linkcode BattlerTag}. + * @param expectedTag - A partially-filled {@linkcode BattlerTag} containing the desired properties + */ + toHaveBattlerTag(expectedTag: toHaveBattlerTagOptions): void; + /** + * Check whether a {@linkcode Pokemon} has the given {@linkcode BattlerTag}. + * @param expectedType - The expected {@linkcode BattlerTagType} + */ + toHaveBattlerTag(expectedType: BattlerTagType): void; + + /** + * Check whether a {@linkcode Pokemon} has applied a specific {@linkcode AbilityId}. + * @param expectedAbilityId - The `AbilityId` to check for + */ + toHaveAbilityApplied(expectedAbilityId: AbilityId): void; + + /** + * Check whether a {@linkcode Pokemon} has a specific amount of {@linkcode Stat.HP | HP}. + * @param expectedHp - The expected amount of {@linkcode Stat.HP | HP} to have + */ + toHaveHp(expectedHp: number): void; + + /** + * Check whether a {@linkcode Pokemon} has taken a specific amount of damage. + * @param expectedDamageTaken - The expected amount of damage taken + * @param roundDown - Whether to round down `expectedDamageTaken` with {@linkcode toDmgValue}; default `true` + */ + toHaveTakenDamage(expectedDamageTaken: number, roundDown?: boolean): void; + + /** + * Check whether a {@linkcode Pokemon} is currently fainted (as determined by {@linkcode Pokemon.isFainted}). + * @remarks + * When checking whether an enemy wild Pokemon is fainted, one must store a reference to it in a variable _before_ the fainting effect occurs. + * Otherwise, the Pokemon will be removed from the field and garbage collected. + */ + toHaveFainted(): void; + + /** + * Check whether a {@linkcode Pokemon} is at full HP. + */ + toHaveFullHp(): void; + + /** + * Check whether a {@linkcode Pokemon} has consumed the given amount of PP for one of its moves. + * @param moveId - The {@linkcode MoveId} corresponding to the {@linkcode PokemonMove} that should have consumed PP + * @param ppUsed - The numerical amount of PP that should have been consumed, + * or `all` to indicate the move should be _out_ of PP + * @remarks + * If the Pokemon's moveset has been set via {@linkcode Overrides.MOVESET_OVERRIDE}/{@linkcode Overrides.ENEMY_MOVESET_OVERRIDE} + * or does not contain exactly one copy of `moveId`, this will fail the test. + */ + toHaveUsedPP(moveId: MoveId, ppUsed: number | "all"): void; +} +// #endregion Pokemon Matchers diff --git a/test/abilities/ability-duplication.test.ts b/test/abilities/ability-duplication.test.ts index da572d94466..f684500ab90 100644 --- a/test/abilities/ability-duplication.test.ts +++ b/test/abilities/ability-duplication.test.ts @@ -30,12 +30,13 @@ describe("Ability Duplication", () => { .enemyMoveset(MoveId.SPLASH); }); + // TODO: Find a cleaner way of checking ability duplication effects than suppressing the ability it("huge power should only be applied once if both normal and passive", async () => { game.override.passiveAbility(AbilityId.HUGE_POWER); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - const [magikarp] = game.scene.getPlayerField(); + const magikarp = game.field.getPlayerPokemon(); const magikarpAttack = magikarp.getEffectiveStat(Stat.ATK); magikarp.summonData.abilitySuppressed = true; @@ -48,7 +49,7 @@ describe("Ability Duplication", () => { await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - const [magikarp] = game.scene.getPlayerField(); + const magikarp = game.field.getPlayerPokemon(); const magikarpAttack = magikarp.getEffectiveStat(Stat.ATK); magikarp.summonData.abilitySuppressed = true; diff --git a/test/abilities/arena-trap.test.ts b/test/abilities/arena-trap.test.ts index d43148dce7b..8f5d820a145 100644 --- a/test/abilities/arena-trap.test.ts +++ b/test/abilities/arena-trap.test.ts @@ -6,7 +6,7 @@ import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { UiMode } from "#enums/ui-mode"; import { GameManager } from "#test/test-utils/game-manager"; -import type { PartyUiHandler } from "#ui/handlers/party-ui-handler"; +import type { PartyUiHandler } from "#ui/party-ui-handler"; import i18next from "i18next"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -57,7 +57,7 @@ describe("Abilities - Arena Trap", () => { await game.phaseInterceptor.to("CommandPhase"); - expect(game.textInterceptor.logs).toContain( + expect(game).toHaveShownMessage( i18next.t("abilityTriggers:arenaTrap", { pokemonNameWithAffix: getPokemonNameWithAffix(enemy), abilityName: allAbilities[AbilityId.ARENA_TRAP].name, diff --git a/test/abilities/commander.test.ts b/test/abilities/commander.test.ts index d485cab83a2..8447b2a7d61 100644 --- a/test/abilities/commander.test.ts +++ b/test/abilities/commander.test.ts @@ -5,8 +5,7 @@ import { MoveId } from "#enums/move-id"; import { MoveResult } from "#enums/move-result"; import { PokemonAnimType } from "#enums/pokemon-anim-type"; import { SpeciesId } from "#enums/species-id"; -import type { EffectiveStat } from "#enums/stat"; -import { Stat } from "#enums/stat"; +import { EFFECTIVE_STATS } from "#enums/stat"; import { StatusEffect } from "#enums/status-effect"; import { WeatherType } from "#enums/weather-type"; import { GameManager } from "#test/test-utils/game-manager"; @@ -48,23 +47,24 @@ describe("Abilities - Commander", () => { const [tatsugiri, dondozo] = game.scene.getPlayerField(); - const affectedStats: EffectiveStat[] = [Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD]; - expect(game.scene.triggerPokemonBattleAnim).toHaveBeenLastCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY); - expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeDefined(); - affectedStats.forEach(stat => expect(dondozo.getStatStage(stat)).toBe(2)); - - game.move.select(MoveId.SPLASH, 1); + expect(dondozo).toHaveBattlerTag(BattlerTagType.COMMANDED); + EFFECTIVE_STATS.forEach(stat => { + expect(dondozo).toHaveStatStage(stat, 2); + }); + game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2); expect(game.scene.currentBattle.turnCommands[0]?.skip).toBeTruthy(); // Force both enemies to target the Tatsugiri - await game.move.selectEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER); - await game.move.selectEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER); + await game.move.forceEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER); + await game.move.forceEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER); - await game.phaseInterceptor.to("BerryPhase", false); - game.scene.getEnemyField().forEach(enemy => expect(enemy.getLastXMoves(1)[0].result).toBe(MoveResult.MISS)); - expect(tatsugiri.isFullHp()).toBeTruthy(); + await game.toEndOfTurn(); + const [enemy1, enemy2] = game.scene.getEnemyField(); + expect(enemy1).toHaveUsedMove({ move: MoveId.TACKLE, result: MoveResult.MISS }); + expect(enemy2).toHaveUsedMove({ move: MoveId.TACKLE, result: MoveResult.MISS }); + expect(tatsugiri).toHaveFullHp(); }); it("should activate when a Dondozo switches in and cancel the source's move", async () => { @@ -72,7 +72,7 @@ describe("Abilities - Commander", () => { await game.classicMode.startBattle([SpeciesId.TATSUGIRI, SpeciesId.MAGIKARP, SpeciesId.DONDOZO]); - const tatsugiri = game.scene.getPlayerField()[0]; + const [tatsugiri, _, dondozo] = game.scene.getPlayerParty(); game.move.select(MoveId.LIQUIDATION, 0, BattlerIndex.ENEMY); game.doSwitchPokemon(2); @@ -80,12 +80,11 @@ describe("Abilities - Commander", () => { await game.phaseInterceptor.to("MovePhase", false); expect(game.scene.triggerPokemonBattleAnim).toHaveBeenCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY); - const dondozo = game.scene.getPlayerField()[1]; expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeDefined(); await game.phaseInterceptor.to("BerryPhase", false); expect(tatsugiri.getMoveHistory()).toHaveLength(0); - expect(game.scene.getEnemyField()[0].isFullHp()).toBeTruthy(); + expect(game.field.getEnemyPokemon()).toHaveFullHp(); }); it("source should reenter the field when Dondozo faints", async () => { @@ -192,26 +191,26 @@ describe("Abilities - Commander", () => { }); it("should interrupt the source's semi-invulnerability", async () => { - game.override.moveset([MoveId.SPLASH, MoveId.DIVE]).enemyMoveset(MoveId.SPLASH); - await game.classicMode.startBattle([SpeciesId.TATSUGIRI, SpeciesId.MAGIKARP, SpeciesId.DONDOZO]); - const tatsugiri = game.scene.getPlayerField()[0]; + const [tatsugiri, , dondozo] = game.scene.getPlayerParty(); - game.move.select(MoveId.DIVE, 0, BattlerIndex.ENEMY); - game.move.select(MoveId.SPLASH, 1); + game.move.use(MoveId.DIVE, BattlerIndex.PLAYER, BattlerIndex.ENEMY); + game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2); + await game.move.forceEnemyMove(MoveId.SPLASH); + await game.move.forceEnemyMove(MoveId.SPLASH); await game.toNextTurn(); - expect(tatsugiri.getTag(BattlerTagType.UNDERWATER)).toBeDefined(); + expect(tatsugiri).toHaveBattlerTag(BattlerTagType.UNDERWATER); + game.doSwitchPokemon(2); - await game.phaseInterceptor.to("MovePhase", false); - const dondozo = game.scene.getPlayerField()[1]; - expect(tatsugiri.getTag(BattlerTagType.UNDERWATER)).toBeUndefined(); - expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeDefined(); - await game.toNextTurn(); - const enemy = game.scene.getEnemyField()[0]; - expect(enemy.isFullHp()).toBeTruthy(); + expect(tatsugiri).not.toHaveBattlerTag(BattlerTagType.UNDERWATER); + expect(dondozo).toHaveBattlerTag(BattlerTagType.COMMANDED); + + await game.toEndOfTurn(); + + expect(game.field.getEnemyPokemon()).toHaveFullHp(); }); }); diff --git a/test/abilities/cud-chew.test.ts b/test/abilities/cud-chew.test.ts index ae3b4ad8765..8d80ba119ca 100644 --- a/test/abilities/cud-chew.test.ts +++ b/test/abilities/cud-chew.test.ts @@ -99,7 +99,7 @@ describe("Abilities - Cud Chew", () => { expect(abDisplaySpy.mock.calls[1][2]).toBe(false); // should display messgae - expect(game.textInterceptor.logs).toContain( + expect(game).toHaveShownMessage( i18next.t("battle:hpIsFull", { pokemonName: getPokemonNameWithAffix(farigiraf), }), diff --git a/test/abilities/dancer.test.ts b/test/abilities/dancer.test.ts index c651a341c42..e206152715e 100644 --- a/test/abilities/dancer.test.ts +++ b/test/abilities/dancer.test.ts @@ -34,7 +34,7 @@ describe("Abilities - Dancer", () => { game.override.enemyAbility(AbilityId.DANCER).enemySpecies(SpeciesId.MAGIKARP).enemyMoveset(MoveId.VICTORY_DANCE); await game.classicMode.startBattle([SpeciesId.ORICORIO, SpeciesId.FEEBAS]); - const [oricorio, feebas] = game.scene.getPlayerField(); + const [oricorio, feebas, magikarp1] = game.scene.getField(); game.move.changeMoveset(oricorio, [MoveId.SWORDS_DANCE, MoveId.VICTORY_DANCE, MoveId.SPLASH]); game.move.changeMoveset(feebas, [MoveId.SWORDS_DANCE, MoveId.SPLASH]); @@ -44,8 +44,9 @@ describe("Abilities - Dancer", () => { await game.phaseInterceptor.to("MovePhase"); // feebas uses swords dance await game.phaseInterceptor.to("MovePhase", false); // oricorio copies swords dance + // Dancer order will be Magikarp, Oricorio, Magikarp based on set turn order let currentPhase = game.scene.phaseManager.getCurrentPhase() as MovePhase; - expect(currentPhase.pokemon).toBe(oricorio); + expect(currentPhase.pokemon).toBe(magikarp1); expect(currentPhase.move.moveId).toBe(MoveId.SWORDS_DANCE); await game.phaseInterceptor.to("MoveEndPhase"); // end oricorio's move @@ -74,8 +75,8 @@ describe("Abilities - Dancer", () => { .enemyLevel(10); await game.classicMode.startBattle([SpeciesId.ORICORIO, SpeciesId.FEEBAS]); - const [oricorio] = game.scene.getPlayerField(); - const [, shuckle2] = game.scene.getEnemyField(); + const oricorio = game.field.getPlayerPokemon(); + const shuckle2 = game.scene.getEnemyField()[1]; game.move.select(MoveId.REVELATION_DANCE, BattlerIndex.PLAYER, BattlerIndex.ENEMY_2); game.move.select(MoveId.FIERY_DANCE, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2); diff --git a/test/abilities/flower-gift.test.ts b/test/abilities/flower-gift.test.ts index 01459cd4e1e..74be845ffed 100644 --- a/test/abilities/flower-gift.test.ts +++ b/test/abilities/flower-gift.test.ts @@ -18,8 +18,8 @@ describe("Abilities - Flower Gift", () => { /** * Tests reverting to normal form when Cloud Nine/Air Lock is active on the field - * @param {GameManager} game The game manager instance - * @param {AbilityId} ability The ability that is active on the field + * @param game The game manager instance + * @param ability The ability that is active on the field */ const testRevertFormAgainstAbility = async (game: GameManager, ability: AbilityId) => { game.override.starterForms({ [SpeciesId.CASTFORM]: SUNSHINE_FORM }).enemyAbility(ability); @@ -58,12 +58,12 @@ describe("Abilities - Flower Gift", () => { const ally_target = allyAttacker ? BattlerIndex.ENEMY : null; await game.classicMode.startBattle([SpeciesId.CHERRIM, SpeciesId.MAGIKARP]); - const target = allyAttacker ? game.scene.getEnemyField()[0] : game.scene.getPlayerField()[1]; + const target = allyAttacker ? game.field.getEnemyPokemon() : game.scene.getPlayerField()[1]; const initialHp = target.getMaxHp(); // Override the ability for the target and attacker only vi.spyOn(game.scene.getPlayerField()[1], "getAbility").mockReturnValue(allAbilities[allyAbility]); - vi.spyOn(game.scene.getEnemyField()[0], "getAbility").mockReturnValue(allAbilities[enemyAbility]); + vi.spyOn(game.field.getEnemyPokemon(), "getAbility").mockReturnValue(allAbilities[enemyAbility]); // turn 1 game.move.select(MoveId.SUNNY_DAY, 0); diff --git a/test/abilities/flower-veil.test.ts b/test/abilities/flower-veil.test.ts index 44274d86a1b..ec34f696bc9 100644 --- a/test/abilities/flower-veil.test.ts +++ b/test/abilities/flower-veil.test.ts @@ -66,7 +66,7 @@ describe("Abilities - Flower Veil", () => { await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.BULBASAUR]); // Clear the ability of the ally to isolate the test - const ally = game.scene.getPlayerField()[1]!; + const ally = game.scene.getPlayerField()[1]; vi.spyOn(ally, "getAbility").mockReturnValue(allAbilities[AbilityId.BALL_FETCH]); game.move.select(MoveId.SPLASH); game.move.select(MoveId.SPLASH); diff --git a/test/abilities/forecast.test.ts b/test/abilities/forecast.test.ts index 87d1d20acdb..9bd40709a94 100644 --- a/test/abilities/forecast.test.ts +++ b/test/abilities/forecast.test.ts @@ -76,7 +76,7 @@ describe("Abilities - Forecast", () => { vi.spyOn(game.scene.getPlayerParty()[5], "getAbility").mockReturnValue(allAbilities[AbilityId.CLOUD_NINE]); - const castform = game.scene.getPlayerField()[0]; + const castform = game.field.getPlayerPokemon(); expect(castform.formIndex).toBe(NORMAL_FORM); game.move.select(MoveId.RAIN_DANCE); diff --git a/test/abilities/gulp-missile.test.ts b/test/abilities/gulp-missile.test.ts index 865a319251f..82f446623f8 100644 --- a/test/abilities/gulp-missile.test.ts +++ b/test/abilities/gulp-missile.test.ts @@ -21,7 +21,7 @@ describe("Abilities - Gulp Missile", () => { /** * Gets the effect damage of Gulp Missile * See Gulp Missile {@link https://bulbapedia.bulbagarden.net/wiki/Gulp_Missile_(Ability)} - * @param {Pokemon} pokemon The pokemon taking the effect damage. + * @param pokemon The pokemon taking the effect damage. * @returns The effect damage of Gulp Missile */ const getEffectDamage = (pokemon: Pokemon): number => { diff --git a/test/abilities/healer.test.ts b/test/abilities/healer.test.ts index 43280ff8271..fb28cd891ac 100644 --- a/test/abilities/healer.test.ts +++ b/test/abilities/healer.test.ts @@ -6,7 +6,6 @@ import { SpeciesId } from "#enums/species-id"; import { StatusEffect } from "#enums/status-effect"; import type { Pokemon } from "#field/pokemon"; import { GameManager } from "#test/test-utils/game-manager"; -import { isNullOrUndefined } from "#utils/common"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -37,7 +36,7 @@ describe("Abilities - Healer", () => { // Mock healer to have a 100% chance of healing its ally vi.spyOn(allAbilities[AbilityId.HEALER].getAttrs("PostTurnResetStatusAbAttr")[0], "getCondition").mockReturnValue( - (pokemon: Pokemon) => !isNullOrUndefined(pokemon.getAlly()), + (pokemon: Pokemon) => pokemon.getAlly() != null, ); }); diff --git a/test/abilities/magic-bounce.test.ts b/test/abilities/magic-bounce.test.ts index c15690c3f5d..6b7bc7453ed 100644 --- a/test/abilities/magic-bounce.test.ts +++ b/test/abilities/magic-bounce.test.ts @@ -64,7 +64,7 @@ describe("Abilities - Magic Bounce", () => { game.move.use(MoveId.SPLASH, 1); await game.phaseInterceptor.to("BerryPhase"); - const user = game.scene.getPlayerField()[0]; + const user = game.field.getPlayerPokemon(); expect(user.getStatStage(Stat.ATK)).toBe(-2); }); diff --git a/test/abilities/mirror-armor.test.ts b/test/abilities/mirror-armor.test.ts index b2bd9be4755..85d821d0683 100644 --- a/test/abilities/mirror-armor.test.ts +++ b/test/abilities/mirror-armor.test.ts @@ -92,8 +92,7 @@ describe("Ability - Mirror Armor", () => { game.override.battleStyle("double").enemyAbility(AbilityId.MIRROR_ARMOR).ability(AbilityId.INTIMIDATE); await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER]); - const [enemy1, enemy2] = game.scene.getEnemyField(); - const [player1, player2] = game.scene.getPlayerField(); + const [player1, player2, enemy1, enemy2] = game.scene.getField(); // Enemy has intimidate, enemy should lose -1 atk game.move.select(MoveId.SPLASH); diff --git a/test/abilities/mycelium-might.test.ts b/test/abilities/mycelium-might.test.ts index c3b7b4753b6..21b856d341e 100644 --- a/test/abilities/mycelium-might.test.ts +++ b/test/abilities/mycelium-might.test.ts @@ -2,8 +2,6 @@ import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; -import { TurnEndPhase } from "#phases/turn-end-phase"; -import { TurnStartPhase } from "#phases/turn-start-phase"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -45,65 +43,50 @@ describe("Abilities - Mycelium Might", () => { it("should move last in its priority bracket and ignore protective abilities", async () => { await game.classicMode.startBattle([SpeciesId.REGIELEKI]); - const enemyPokemon = game.field.getEnemyPokemon(); - const playerIndex = game.field.getPlayerPokemon().getBattlerIndex(); - const enemyIndex = enemyPokemon.getBattlerIndex(); + const enemy = game.field.getEnemyPokemon(); + const player = game.field.getPlayerPokemon(); game.move.select(MoveId.BABY_DOLL_EYES); - await game.phaseInterceptor.to(TurnStartPhase, false); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const speedOrder = phase.getSpeedOrder(); - const commandOrder = phase.getCommandOrder(); + await game.phaseInterceptor.to("MoveEndPhase", false); // The opponent Pokemon (without Mycelium Might) goes first despite having lower speed than the player Pokemon. // The player Pokemon (with Mycelium Might) goes last despite having higher speed than the opponent. - expect(speedOrder).toEqual([playerIndex, enemyIndex]); - expect(commandOrder).toEqual([enemyIndex, playerIndex]); - await game.phaseInterceptor.to(TurnEndPhase); + expect(player.hp).not.toEqual(player.getMaxHp()); + await game.phaseInterceptor.to("TurnEndPhase"); // Despite the opponent's ability (Clear Body), its ATK stat stage is still reduced. - expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1); + expect(enemy.getStatStage(Stat.ATK)).toBe(-1); }); it("should still go first if a status move that is in a higher priority bracket than the opponent's move is used", async () => { game.override.enemyMoveset(MoveId.TACKLE); await game.classicMode.startBattle([SpeciesId.REGIELEKI]); - const enemyPokemon = game.field.getEnemyPokemon(); - const playerIndex = game.field.getPlayerPokemon().getBattlerIndex(); - const enemyIndex = enemyPokemon.getBattlerIndex(); + const enemy = game.field.getEnemyPokemon(); + const player = game.field.getPlayerPokemon(); game.move.select(MoveId.BABY_DOLL_EYES); - await game.phaseInterceptor.to(TurnStartPhase, false); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const speedOrder = phase.getSpeedOrder(); - const commandOrder = phase.getCommandOrder(); + await game.phaseInterceptor.to("MoveEndPhase", false); // The player Pokemon (with M.M.) goes first because its move is still within a higher priority bracket than its opponent. // The enemy Pokemon goes second because its move is in a lower priority bracket. - expect(speedOrder).toEqual([playerIndex, enemyIndex]); - expect(commandOrder).toEqual([playerIndex, enemyIndex]); - await game.phaseInterceptor.to(TurnEndPhase); + expect(player.hp).toEqual(player.getMaxHp()); + await game.phaseInterceptor.to("TurnEndPhase"); // Despite the opponent's ability (Clear Body), its ATK stat stage is still reduced. - expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1); + expect(enemy.getStatStage(Stat.ATK)).toBe(-1); }); it("should not affect non-status moves", async () => { await game.classicMode.startBattle([SpeciesId.REGIELEKI]); - const playerIndex = game.field.getPlayerPokemon().getBattlerIndex(); - const enemyIndex = game.field.getEnemyPokemon().getBattlerIndex(); + const player = game.field.getPlayerPokemon(); game.move.select(MoveId.QUICK_ATTACK); - await game.phaseInterceptor.to(TurnStartPhase, false); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const speedOrder = phase.getSpeedOrder(); - const commandOrder = phase.getCommandOrder(); + await game.phaseInterceptor.to("MoveEndPhase", false); // The player Pokemon (with M.M.) goes first because it has a higher speed and did not use a status move. // The enemy Pokemon (without M.M.) goes second because its speed is lower. // This means that the commandOrder should be identical to the speedOrder - expect(speedOrder).toEqual([playerIndex, enemyIndex]); - expect(commandOrder).toEqual([playerIndex, enemyIndex]); + expect(player.hp).toEqual(player.getMaxHp()); }); }); diff --git a/test/abilities/neutralizing-gas.test.ts b/test/abilities/neutralizing-gas.test.ts index 555e5f8a19c..fd9138e4174 100644 --- a/test/abilities/neutralizing-gas.test.ts +++ b/test/abilities/neutralizing-gas.test.ts @@ -59,7 +59,7 @@ describe("Abilities - Neutralizing Gas", () => { expect(game.field.getPlayerPokemon().getStatStage(Stat.ATK)).toBe(1); }); - it.todo("should activate before other abilities", async () => { + it("should activate before other abilities", async () => { game.override.enemySpecies(SpeciesId.ACCELGOR).enemyLevel(100).enemyAbility(AbilityId.INTIMIDATE); await game.classicMode.startBattle([SpeciesId.FEEBAS]); diff --git a/test/abilities/no-guard.test.ts b/test/abilities/no-guard.test.ts index 9ce12e710e5..9fc308ab9e3 100644 --- a/test/abilities/no-guard.test.ts +++ b/test/abilities/no-guard.test.ts @@ -58,6 +58,6 @@ describe("Abilities - No Guard", () => { await game.classicMode.startBattle(); - expect(game.scene.getEnemyField().length).toBe(2); + expect(game.scene.getEnemyField()).toHaveLength(2); }); }); diff --git a/test/abilities/quick-draw.test.ts b/test/abilities/quick-draw.test.ts index ce5873af3a8..257892145e5 100644 --- a/test/abilities/quick-draw.test.ts +++ b/test/abilities/quick-draw.test.ts @@ -5,7 +5,7 @@ import { SpeciesId } from "#enums/species-id"; import { FaintPhase } from "#phases/faint-phase"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; describe("Abilities - Quick Draw", () => { let phaserGame: Phaser.Game; @@ -25,7 +25,6 @@ describe("Abilities - Quick Draw", () => { game = new GameManager(phaserGame); game.override .battleStyle("single") - .starterSpecies(SpeciesId.MAGIKARP) .ability(AbilityId.QUICK_DRAW) .moveset([MoveId.TACKLE, MoveId.TAIL_WHIP]) .enemyLevel(100) @@ -40,8 +39,8 @@ describe("Abilities - Quick Draw", () => { ).mockReturnValue(100); }); - test("makes pokemon going first in its priority bracket", async () => { - await game.classicMode.startBattle(); + it("makes pokemon go first in its priority bracket", async () => { + await game.classicMode.startBattle([SpeciesId.MAGIKARP]); const pokemon = game.field.getPlayerPokemon(); const enemy = game.field.getEnemyPokemon(); @@ -57,33 +56,27 @@ describe("Abilities - Quick Draw", () => { expect(pokemon.waveData.abilitiesApplied).toContain(AbilityId.QUICK_DRAW); }); - test( - "does not triggered by non damage moves", - { - retry: 5, - }, - async () => { - await game.classicMode.startBattle(); + it("is not triggered by non damaging moves", async () => { + await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - const pokemon = game.field.getPlayerPokemon(); - const enemy = game.field.getEnemyPokemon(); + const pokemon = game.field.getPlayerPokemon(); + const enemy = game.field.getEnemyPokemon(); - pokemon.hp = 1; - enemy.hp = 1; + pokemon.hp = 1; + enemy.hp = 1; - game.move.select(MoveId.TAIL_WHIP); - await game.phaseInterceptor.to(FaintPhase, false); + game.move.select(MoveId.TAIL_WHIP); + await game.phaseInterceptor.to(FaintPhase, false); - expect(pokemon.isFainted()).toBe(true); - expect(enemy.isFainted()).toBe(false); - expect(pokemon.waveData.abilitiesApplied).not.contain(AbilityId.QUICK_DRAW); - }, - ); + expect(pokemon.isFainted()).toBe(true); + expect(enemy.isFainted()).toBe(false); + expect(pokemon.waveData.abilitiesApplied).not.contain(AbilityId.QUICK_DRAW); + }); - test("does not increase priority", async () => { + it("does not increase priority", async () => { game.override.enemyMoveset([MoveId.EXTREME_SPEED]); - await game.classicMode.startBattle(); + await game.classicMode.startBattle([SpeciesId.MAGIKARP]); const pokemon = game.field.getPlayerPokemon(); const enemy = game.field.getEnemyPokemon(); diff --git a/test/abilities/stall.test.ts b/test/abilities/stall.test.ts index 5b4e38f7099..b6a88964e09 100644 --- a/test/abilities/stall.test.ts +++ b/test/abilities/stall.test.ts @@ -1,7 +1,6 @@ import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; -import { TurnStartPhase } from "#phases/turn-start-phase"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -40,56 +39,41 @@ describe("Abilities - Stall", () => { it("Pokemon with Stall should move last in its priority bracket regardless of speed", async () => { await game.classicMode.startBattle([SpeciesId.SHUCKLE]); - const playerIndex = game.field.getPlayerPokemon().getBattlerIndex(); - const enemyIndex = game.field.getEnemyPokemon().getBattlerIndex(); + const player = game.field.getPlayerPokemon(); game.move.select(MoveId.QUICK_ATTACK); - await game.phaseInterceptor.to(TurnStartPhase, false); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const speedOrder = phase.getSpeedOrder(); - const commandOrder = phase.getCommandOrder(); + await game.phaseInterceptor.to("MoveEndPhase", false); // The player Pokemon (without Stall) goes first despite having lower speed than the opponent. // The opponent Pokemon (with Stall) goes last despite having higher speed than the player Pokemon. - expect(speedOrder).toEqual([enemyIndex, playerIndex]); - expect(commandOrder).toEqual([playerIndex, enemyIndex]); + expect(player).toHaveFullHp(); }); it("Pokemon with Stall will go first if a move that is in a higher priority bracket than the opponent's move is used", async () => { await game.classicMode.startBattle([SpeciesId.SHUCKLE]); - const playerIndex = game.field.getPlayerPokemon().getBattlerIndex(); - const enemyIndex = game.field.getEnemyPokemon().getBattlerIndex(); + const player = game.field.getPlayerPokemon(); game.move.select(MoveId.TACKLE); - await game.phaseInterceptor.to(TurnStartPhase, false); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const speedOrder = phase.getSpeedOrder(); - const commandOrder = phase.getCommandOrder(); + await game.phaseInterceptor.to("MoveEndPhase", false); // The opponent Pokemon (with Stall) goes first because its move is still within a higher priority bracket than its opponent. // The player Pokemon goes second because its move is in a lower priority bracket. - expect(speedOrder).toEqual([enemyIndex, playerIndex]); - expect(commandOrder).toEqual([enemyIndex, playerIndex]); + expect(player).not.toHaveFullHp(); }); it("If both Pokemon have stall and use the same move, speed is used to determine who goes first.", async () => { game.override.ability(AbilityId.STALL); await game.classicMode.startBattle([SpeciesId.SHUCKLE]); - const playerIndex = game.field.getPlayerPokemon().getBattlerIndex(); - const enemyIndex = game.field.getEnemyPokemon().getBattlerIndex(); + const player = game.field.getPlayerPokemon(); game.move.select(MoveId.TACKLE); - await game.phaseInterceptor.to(TurnStartPhase, false); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const speedOrder = phase.getSpeedOrder(); - const commandOrder = phase.getCommandOrder(); + await game.phaseInterceptor.to("MoveEndPhase", false); // The opponent Pokemon (with Stall) goes first because it has a higher speed. // The player Pokemon (with Stall) goes second because its speed is lower. - expect(speedOrder).toEqual([enemyIndex, playerIndex]); - expect(commandOrder).toEqual([enemyIndex, playerIndex]); + expect(player).not.toHaveFullHp(); }); }); diff --git a/test/abilities/storm-drain.test.ts b/test/abilities/storm-drain.test.ts index bc4d4f15cfa..5439459b1dd 100644 --- a/test/abilities/storm-drain.test.ts +++ b/test/abilities/storm-drain.test.ts @@ -37,9 +37,7 @@ describe("Abilities - Storm Drain", () => { it("should redirect water type moves", async () => { await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MAGIKARP]); - const enemy1 = game.scene.getEnemyField()[0]; - const enemy2 = game.scene.getEnemyField()[1]; - + const [enemy1, enemy2] = game.scene.getEnemyField(); game.field.mockAbility(enemy2, AbilityId.STORM_DRAIN); game.move.select(MoveId.WATER_GUN, BattlerIndex.PLAYER, BattlerIndex.ENEMY); @@ -53,8 +51,7 @@ describe("Abilities - Storm Drain", () => { game.override.moveset([MoveId.SPLASH, MoveId.AERIAL_ACE]); await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MAGIKARP]); - const enemy1 = game.scene.getEnemyField()[0]; - const enemy2 = game.scene.getEnemyField()[1]; + const [enemy1, enemy2] = game.scene.getEnemyField(); game.field.mockAbility(enemy2, AbilityId.STORM_DRAIN); @@ -83,8 +80,7 @@ describe("Abilities - Storm Drain", () => { game.override.ability(AbilityId.NORMALIZE); await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MAGIKARP]); - const enemy1 = game.scene.getEnemyField()[0]; - const enemy2 = game.scene.getEnemyField()[1]; + const [enemy1, enemy2] = game.scene.getEnemyField(); game.field.mockAbility(enemy2, AbilityId.STORM_DRAIN); game.move.select(MoveId.WATER_GUN, BattlerIndex.PLAYER, BattlerIndex.ENEMY); @@ -98,8 +94,7 @@ describe("Abilities - Storm Drain", () => { game.override.ability(AbilityId.LIQUID_VOICE); await game.classicMode.startBattle([SpeciesId.FEEBAS]); - const enemy1 = game.scene.getEnemyField()[0]; - const enemy2 = game.scene.getEnemyField()[1]; + const [enemy1, enemy2] = game.scene.getEnemyField(); game.field.mockAbility(enemy2, AbilityId.STORM_DRAIN); diff --git a/test/abilities/supreme-overlord.test.ts b/test/abilities/supreme-overlord.test.ts index a0f2d9050b3..d5470b70476 100644 --- a/test/abilities/supreme-overlord.test.ts +++ b/test/abilities/supreme-overlord.test.ts @@ -1,6 +1,7 @@ import { allMoves } from "#data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { BattlerIndex } from "#enums/battler-index"; +import { BattlerTagType } from "#enums/battler-tag-type"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import type { Move } from "#moves/move"; @@ -166,4 +167,41 @@ describe("Abilities - Supreme Overlord", () => { expect(move.calculateBattlePower).toHaveLastReturnedWith(basePower); }); + + it("should not increase in power if ally faints while on the field", async () => { + game.override.battleStyle("double"); + await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER, SpeciesId.SQUIRTLE]); + + game.move.select(MoveId.TACKLE, BattlerIndex.PLAYER, BattlerIndex.ENEMY); + game.move.select(MoveId.LUNAR_DANCE, BattlerIndex.PLAYER_2); + await game.setTurnOrder([BattlerIndex.PLAYER_2, BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]); + await game.toEndOfTurn(); + + expect(game.field.getPlayerPokemon()).not.toHaveBattlerTag(BattlerTagType.SUPREME_OVERLORD); + expect(move.calculateBattlePower).toHaveLastReturnedWith(basePower); + }); + + it("should persist fainted count through reload", async () => { + // Avoid learning moves + game.override.startingLevel(1000); + + await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER, SpeciesId.SQUIRTLE]); + + game.move.select(MoveId.LUNAR_DANCE); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + game.move.select(MoveId.TACKLE); + await game.toEndOfTurn(); + expect(move.calculateBattlePower).toHaveLastReturnedWith(basePower * 1.1); + + await game.toNextWave(); + await game.reload.reloadSession(); + + expect(game.field.getPlayerPokemon()).toHaveBattlerTag({ tagType: BattlerTagType.SUPREME_OVERLORD, faintCount: 1 }); + + game.move.select(MoveId.TACKLE); + await game.toEndOfTurn(); + expect(move.calculateBattlePower).toHaveLastReturnedWith(basePower * 1.1); + }); }); diff --git a/test/abilities/truant.test.ts b/test/abilities/truant.test.ts index 0d71cd393b0..31098fa1a85 100644 --- a/test/abilities/truant.test.ts +++ b/test/abilities/truant.test.ts @@ -54,7 +54,7 @@ describe("Ability - Truant", () => { expect(player.getLastXMoves(1)[0]).toEqual(expect.objectContaining({ move: MoveId.NONE, result: MoveResult.FAIL })); expect(enemy.hp).toBe(enemy.getMaxHp()); - expect(game.textInterceptor.logs).toContain( + expect(game).toHaveShownMessage( i18next.t("battlerTags:truantLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(player), }), diff --git a/test/abilities/unburden.test.ts b/test/abilities/unburden.test.ts index c10dd404ab9..285ea8af32c 100644 --- a/test/abilities/unburden.test.ts +++ b/test/abilities/unburden.test.ts @@ -362,7 +362,7 @@ describe("Abilities - Unburden", () => { .startingHeldItems([{ name: "WIDE_LENS" }]); await game.classicMode.startBattle([SpeciesId.TREECKO, SpeciesId.FEEBAS, SpeciesId.MILOTIC]); - const treecko = game.scene.getPlayerField()[0]; + const treecko = game.field.getPlayerPokemon(); const treeckoInitialHeldItems = getHeldItemCount(treecko); const initialSpeed = treecko.getStat(Stat.SPD); @@ -374,7 +374,7 @@ describe("Abilities - Unburden", () => { game.doSelectPartyPokemon(0, "RevivalBlessingPhase"); await game.toNextTurn(); - expect(game.scene.getPlayerField()[0]).toBe(treecko); + expect(game.field.getPlayerPokemon()).toBe(treecko); expect(getHeldItemCount(treecko)).toBeLessThan(treeckoInitialHeldItems); expect(treecko.getEffectiveStat(Stat.SPD)).toBe(initialSpeed); }); diff --git a/test/ai/ai-moveset-gen.test.ts b/test/ai/ai-moveset-gen.test.ts new file mode 100644 index 00000000000..6d927926131 --- /dev/null +++ b/test/ai/ai-moveset-gen.test.ts @@ -0,0 +1,285 @@ +import { __INTERNAL_TEST_EXPORTS } from "#app/ai/ai-moveset-gen"; +import { + COMMON_TIER_TM_LEVEL_REQUIREMENT, + GREAT_TIER_TM_LEVEL_REQUIREMENT, + ULTRA_TIER_TM_LEVEL_REQUIREMENT, +} from "#balance/moveset-generation"; +import { allMoves, allSpecies } from "#data/data-lists"; +import { MoveId } from "#enums/move-id"; +import { SpeciesId } from "#enums/species-id"; +import { TrainerSlot } from "#enums/trainer-slot"; +import { EnemyPokemon } from "#field/pokemon"; +import { GameManager } from "#test/test-utils/game-manager"; +import { NumberHolder } from "#utils/common"; +import { afterEach } from "node:test"; +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; + +/** + * Parameters for {@linkcode createTestablePokemon} + */ +interface MockPokemonParams { + /** The level to set the Pokémon to */ + level: number; + /** + * Whether the pokemon is a boss or not. + * @defaultValue `false` + */ + boss?: boolean; + /** + * The trainer slot to assign to the pokemon, if any. + * @defaultValue `TrainerSlot.NONE` + */ + trainerSlot?: TrainerSlot; + /** + * The form index to assign to the pokemon, if any. + * This *must* be one of the valid form indices for the species, or the test will break. + * @defaultValue `0` + */ + formIndex?: number; +} + +/** + * Construct an `EnemyPokemon` that can be used for testing + * @param species - The species ID of the pokemon to create + * @returns The newly created `EnemyPokemon`. + * @todo Move this to a dedicated unit test util folder if more tests come to rely on it + */ +function createTestablePokemon( + species: SpeciesId, + { level, trainerSlot = TrainerSlot.NONE, boss = false, formIndex = 0 }: MockPokemonParams, +): EnemyPokemon { + const pokemon = new EnemyPokemon(allSpecies[species], level, trainerSlot, boss); + if (formIndex !== 0) { + const formIndexLength = allSpecies[species]?.forms.length; + const name = allSpecies[species]?.name; + expect(formIndex, `${name} does not have a form with index ${formIndex}`).toBeLessThan(formIndexLength); + pokemon.formIndex = formIndex; + } + + return pokemon; +} + +describe("Unit Tests - ai-moveset-gen.ts", () => { + describe("filterPool", () => { + const { filterPool } = __INTERNAL_TEST_EXPORTS; + it("clones a pool when there are no predicates", () => { + const pool = new Map([ + [MoveId.TACKLE, 1], + [MoveId.FLAMETHROWER, 2], + ]); + + const filtered = filterPool(pool, () => true); + const expected = [ + [MoveId.TACKLE, 1], + [MoveId.FLAMETHROWER, 2], + ]; + expect(filtered).toEqual(expected); + }); + + it("does not modify the original pool", () => { + const pool = new Map([ + [MoveId.TACKLE, 1], + [MoveId.FLAMETHROWER, 2], + ]); + const original = new Map(pool); + + filterPool(pool, moveId => moveId !== MoveId.TACKLE); + expect(pool).toEqual(original); + }); + + it("filters out moves that do not match the predicate", () => { + const pool = new Map([ + [MoveId.TACKLE, 1], + [MoveId.FLAMETHROWER, 2], + [MoveId.SPLASH, 3], + ]); + const filtered = filterPool(pool, moveId => moveId !== MoveId.SPLASH); + expect(filtered).toEqual([ + [MoveId.TACKLE, 1], + [MoveId.FLAMETHROWER, 2], + ]); + }); + + it("returns an empty array if no moves match the predicate", () => { + const pool = new Map([ + [MoveId.TACKLE, 1], + [MoveId.FLAMETHROWER, 2], + ]); + const filtered = filterPool(pool, () => false); + expect(filtered).toEqual([]); + }); + + it("calculates totalWeight correctly when provided", () => { + const pool = new Map([ + [MoveId.TACKLE, 1], + [MoveId.FLAMETHROWER, 2], + [MoveId.SPLASH, 3], + ]); + const totalWeight = new NumberHolder(0); + const filtered = filterPool(pool, moveId => moveId !== MoveId.SPLASH, totalWeight); + expect(filtered).toEqual([ + [MoveId.TACKLE, 1], + [MoveId.FLAMETHROWER, 2], + ]); + expect(totalWeight.value).toBe(3); + }); + + it("Clears totalWeight when provided", () => { + const pool = new Map([ + [MoveId.TACKLE, 1], + [MoveId.FLAMETHROWER, 2], + ]); + const totalWeight = new NumberHolder(42); + const filtered = filterPool(pool, () => false, totalWeight); + expect(filtered).toEqual([]); + expect(totalWeight.value).toBe(0); + }); + }); + + describe("getAllowedTmTiers", () => { + const { getAllowedTmTiers } = __INTERNAL_TEST_EXPORTS; + + it.each([ + { tierName: "common", resIdx: 0, level: COMMON_TIER_TM_LEVEL_REQUIREMENT - 1 }, + { tierName: "great", resIdx: 1, level: GREAT_TIER_TM_LEVEL_REQUIREMENT - 1 }, + { tierName: "ultra", resIdx: 2, level: ULTRA_TIER_TM_LEVEL_REQUIREMENT - 1 }, + ])("should prevent $name TMs when below level $level", ({ level, resIdx }) => { + expect(getAllowedTmTiers(level)[resIdx]).toBe(false); + }); + + it.each([ + { tierName: "common", resIdx: 0, level: COMMON_TIER_TM_LEVEL_REQUIREMENT }, + { tierName: "great", resIdx: 1, level: GREAT_TIER_TM_LEVEL_REQUIREMENT }, + { tierName: "ultra", resIdx: 2, level: ULTRA_TIER_TM_LEVEL_REQUIREMENT }, + ])("should allow $name TMs when at level $level", ({ level, resIdx }) => { + expect(getAllowedTmTiers(level)[resIdx]).toBe(true); + }); + }); + + // Unit tests for methods that require a game context + describe("", () => { + //#region boilerplate + let phaserGame: Phaser.Game; + let game: GameManager; + /**A pokemon object that will be cleaned up after every test */ + let pokemon: EnemyPokemon | null = null; + + beforeAll(async () => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + // Game manager can be reused between tests as we are not really modifying the global state + // So there is no need to put this in a beforeEach with cleanup in afterEach. + game = new GameManager(phaserGame); + }); + + afterEach(() => { + pokemon?.destroy(); + }); + // Sanitize the interceptor after running the suite to ensure other tests are not affected + afterAll(() => { + game.phaseInterceptor.restoreOg(); + }); + //#endregion boilerplate + + function createCharmander(_ = pokemon): asserts _ is EnemyPokemon { + pokemon?.destroy(); + pokemon = createTestablePokemon(SpeciesId.CHARMANDER, { level: 10 }); + expect(pokemon).toBeInstanceOf(EnemyPokemon); + } + describe("getAndWeightLevelMoves", () => { + const { getAndWeightLevelMoves } = __INTERNAL_TEST_EXPORTS; + + it("returns an empty map if getLevelMoves throws", async () => { + createCharmander(pokemon); + vi.spyOn(pokemon, "getLevelMoves").mockImplementation(() => { + throw new Error("fail"); + }); + // Suppress the warning from the test output + const warnMock = vi.spyOn(console, "warn").mockImplementationOnce(() => {}); + + const result = getAndWeightLevelMoves(pokemon); + expect(warnMock).toHaveBeenCalled(); + expect(result.size).toBe(0); + }); + + it("skips unimplemented moves", () => { + createCharmander(pokemon); + vi.spyOn(pokemon, "getLevelMoves").mockReturnValue([ + [1, MoveId.TACKLE], + [5, MoveId.GROWL], + ]); + vi.spyOn(allMoves[MoveId.TACKLE], "name", "get").mockReturnValue("Tackle (N)"); + const result = getAndWeightLevelMoves(pokemon); + expect(result.has(MoveId.TACKLE)).toBe(false); + expect(result.has(MoveId.GROWL)).toBe(true); + }); + + it("skips moves already in the pool", () => { + createCharmander(pokemon); + vi.spyOn(pokemon, "getLevelMoves").mockReturnValue([ + [1, MoveId.TACKLE], + [5, MoveId.TACKLE], + ]); + + const result = getAndWeightLevelMoves(pokemon); + expect(result.get(MoveId.TACKLE)).toBe(21); + }); + + it("weights moves based on level", () => { + createCharmander(pokemon); + vi.spyOn(pokemon, "getLevelMoves").mockReturnValue([ + [1, MoveId.TACKLE], + [5, MoveId.GROWL], + [9, MoveId.EMBER], + ]); + + const result = getAndWeightLevelMoves(pokemon); + expect(result.get(MoveId.TACKLE)).toBe(21); + expect(result.get(MoveId.GROWL)).toBe(25); + expect(result.get(MoveId.EMBER)).toBe(29); + }); + }); + }); +}); + +describe("Regression Tests - ai-moveset-gen.ts", () => { + //#region boilerplate + let phaserGame: Phaser.Game; + let game: GameManager; + /**A pokemon object that will be cleaned up after every test */ + let pokemon: EnemyPokemon | null = null; + + beforeAll(async () => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + // Game manager can be reused between tests as we are not really modifying the global state + // So there is no need to put this in a beforeEach with cleanup in afterEach. + game = new GameManager(phaserGame); + }); + + afterEach(() => { + pokemon?.destroy(); + }); + + afterAll(() => { + game.phaseInterceptor.restoreOg(); + }); + //#endregion boilerplate + + describe("getTmPoolForSpecies", () => { + const { getTmPoolForSpecies } = __INTERNAL_TEST_EXPORTS; + + it("should not crash when generating a moveset for Pokemon without TM moves", () => { + pokemon = createTestablePokemon(SpeciesId.DITTO, { level: 50 }); + expect(() => + getTmPoolForSpecies(SpeciesId.DITTO, ULTRA_TIER_TM_LEVEL_REQUIREMENT, "", new Map(), new Map(), new Map(), [ + true, + true, + true, + ]), + ).not.toThrow(); + }); + }); +}); diff --git a/test/battle/battle-order.test.ts b/test/battle/battle-order.test.ts index 0b24fcbfa7d..de13b22df79 100644 --- a/test/battle/battle-order.test.ts +++ b/test/battle/battle-order.test.ts @@ -1,7 +1,8 @@ import { AbilityId } from "#enums/ability-id"; +import { BattlerIndex } from "#enums/battler-index"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; -import type { TurnStartPhase } from "#phases/turn-start-phase"; +import type { MovePhase } from "#phases/move-phase"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -34,38 +35,34 @@ describe("Battle order", () => { await game.classicMode.startBattle([SpeciesId.BULBASAUR]); const playerPokemon = game.field.getPlayerPokemon(); + const playerStartHp = playerPokemon.hp; const enemyPokemon = game.field.getEnemyPokemon(); + const enemyStartHp = enemyPokemon.hp; + vi.spyOn(playerPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 50]); // set playerPokemon's speed to 50 vi.spyOn(enemyPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set enemyPokemon's speed to 150 - game.move.select(MoveId.TACKLE); - await game.phaseInterceptor.to("TurnStartPhase", false); - const playerPokemonIndex = playerPokemon.getBattlerIndex(); - const enemyPokemonIndex = enemyPokemon.getBattlerIndex(); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const order = phase.getCommandOrder(); - expect(order[0]).toBe(enemyPokemonIndex); - expect(order[1]).toBe(playerPokemonIndex); + await game.phaseInterceptor.to("MoveEndPhase", false); + expect(playerPokemon.hp).not.toEqual(playerStartHp); + expect(enemyPokemon.hp).toEqual(enemyStartHp); }); it("Player faster than opponent 150 vs 50", async () => { await game.classicMode.startBattle([SpeciesId.BULBASAUR]); const playerPokemon = game.field.getPlayerPokemon(); + const playerStartHp = playerPokemon.hp; const enemyPokemon = game.field.getEnemyPokemon(); + const enemyStartHp = enemyPokemon.hp; vi.spyOn(playerPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set playerPokemon's speed to 150 vi.spyOn(enemyPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 50]); // set enemyPokemon's speed to 50 game.move.select(MoveId.TACKLE); - await game.phaseInterceptor.to("TurnStartPhase", false); - const playerPokemonIndex = playerPokemon.getBattlerIndex(); - const enemyPokemonIndex = enemyPokemon.getBattlerIndex(); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const order = phase.getCommandOrder(); - expect(order[0]).toBe(playerPokemonIndex); - expect(order[1]).toBe(enemyPokemonIndex); + await game.phaseInterceptor.to("MoveEndPhase", false); + expect(playerPokemon.hp).toEqual(playerStartHp); + expect(enemyPokemon.hp).not.toEqual(enemyStartHp); }); it("double - both opponents faster than player 50/50 vs 150/150", async () => { @@ -73,23 +70,24 @@ describe("Battle order", () => { await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.BLASTOISE]); const playerPokemon = game.scene.getPlayerField(); + const playerHps = playerPokemon.map(p => p.hp); const enemyPokemon = game.scene.getEnemyField(); + const enemyHps = enemyPokemon.map(p => p.hp); playerPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 50])); // set both playerPokemons' speed to 50 enemyPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150])); // set both enemyPokemons' speed to 150 - const playerIndices = playerPokemon.map(p => p?.getBattlerIndex()); - const enemyIndices = enemyPokemon.map(p => p?.getBattlerIndex()); game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE, 1); - await game.phaseInterceptor.to("TurnStartPhase", false); + await game.move.selectEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER); + await game.move.selectEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER_2); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const order = phase.getCommandOrder(); - expect(order.slice(0, 2).includes(enemyIndices[0])).toBe(true); - expect(order.slice(0, 2).includes(enemyIndices[1])).toBe(true); - expect(order.slice(2, 4).includes(playerIndices[0])).toBe(true); - expect(order.slice(2, 4).includes(playerIndices[1])).toBe(true); + await game.phaseInterceptor.to("MoveEndPhase", true); + await game.phaseInterceptor.to("MoveEndPhase", false); + for (let i = 0; i < 2; i++) { + expect(playerPokemon[i].hp).not.toEqual(playerHps[i]); + expect(enemyPokemon[i].hp).toEqual(enemyHps[i]); + } }); it("double - speed tie except 1 - 100/100 vs 100/150", async () => { @@ -101,18 +99,13 @@ describe("Battle order", () => { playerPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 100])); //set both playerPokemons' speed to 100 vi.spyOn(enemyPokemon[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 100]); // set enemyPokemon's speed to 100 vi.spyOn(enemyPokemon[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set enemyPokemon's speed to 150 - const playerIndices = playerPokemon.map(p => p?.getBattlerIndex()); - const enemyIndices = enemyPokemon.map(p => p?.getBattlerIndex()); game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE, 1); - await game.phaseInterceptor.to("TurnStartPhase", false); + await game.phaseInterceptor.to("MovePhase", false); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const order = phase.getCommandOrder(); - // enemy 2 should be first, followed by some other assortment of the other 3 pokemon - expect(order[0]).toBe(enemyIndices[1]); - expect(order.slice(1, 4)).toEqual(expect.arrayContaining([enemyIndices[0], ...playerIndices])); + const phase = game.scene.phaseManager.getCurrentPhase() as MovePhase; + expect(phase.pokemon).toEqual(enemyPokemon[1]); }); it("double - speed tie 100/150 vs 100/150", async () => { @@ -125,17 +118,13 @@ describe("Battle order", () => { vi.spyOn(playerPokemon[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set other playerPokemon's speed to 150 vi.spyOn(enemyPokemon[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 100]); // set one enemyPokemon's speed to 100 vi.spyOn(enemyPokemon[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set other enemyPokemon's speed to 150 - const playerIndices = playerPokemon.map(p => p?.getBattlerIndex()); - const enemyIndices = enemyPokemon.map(p => p?.getBattlerIndex()); game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE, 1); - await game.phaseInterceptor.to("TurnStartPhase", false); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const order = phase.getCommandOrder(); - // P2/E2 should be randomly first/second, then P1/E1 randomly 3rd/4th - expect(order.slice(0, 2)).toStrictEqual(expect.arrayContaining([playerIndices[1], enemyIndices[1]])); - expect(order.slice(2, 4)).toStrictEqual(expect.arrayContaining([playerIndices[0], enemyIndices[0]])); + await game.phaseInterceptor.to("MovePhase", false); + + const phase = game.scene.phaseManager.getCurrentPhase() as MovePhase; + expect(enemyPokemon[1] === phase.pokemon || playerPokemon[1] === phase.pokemon); }); }); diff --git a/test/battler-tags/substitute.test.ts b/test/battler-tags/substitute.test.ts index 7ae60ad1408..a2ff539d2a8 100644 --- a/test/battler-tags/substitute.test.ts +++ b/test/battler-tags/substitute.test.ts @@ -49,7 +49,7 @@ describe("BattlerTag - SubstituteTag", () => { vi.spyOn(messages, "getPokemonNameWithAffix").mockReturnValue(""); vi.spyOn(mockPokemon.scene as BattleScene, "getPokemonById").mockImplementation(pokemonId => - mockPokemon.id === pokemonId ? mockPokemon : null, + mockPokemon.id === pokemonId ? mockPokemon : undefined, ); }); diff --git a/test/challenges/hardcore.test.ts b/test/challenges/hardcore.test.ts index a52d7102868..0f4ab1b9f02 100644 --- a/test/challenges/hardcore.test.ts +++ b/test/challenges/hardcore.test.ts @@ -8,7 +8,7 @@ import { SpeciesId } from "#enums/species-id"; import { StatusEffect } from "#enums/status-effect"; import { UiMode } from "#enums/ui-mode"; import { GameManager } from "#test/test-utils/game-manager"; -import { ModifierSelectUiHandler } from "#ui/handlers/modifier-select-ui-handler"; +import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/test/challenges/limited-support.test.ts b/test/challenges/limited-support.test.ts index ba8930943dd..35413220550 100644 --- a/test/challenges/limited-support.test.ts +++ b/test/challenges/limited-support.test.ts @@ -5,7 +5,7 @@ import { SpeciesId } from "#enums/species-id"; import { UiMode } from "#enums/ui-mode"; import { ExpBoosterModifier } from "#modifiers/modifier"; import { GameManager } from "#test/test-utils/game-manager"; -import { ModifierSelectUiHandler } from "#ui/handlers/modifier-select-ui-handler"; +import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/test/daily-mode.test.ts b/test/daily-mode.test.ts index fae12a0b5d7..34a8da80478 100644 --- a/test/daily-mode.test.ts +++ b/test/daily-mode.test.ts @@ -5,7 +5,7 @@ import { SpeciesId } from "#enums/species-id"; import { UiMode } from "#enums/ui-mode"; import { MapModifier } from "#modifiers/modifier"; import { GameManager } from "#test/test-utils/game-manager"; -import { ModifierSelectUiHandler } from "#ui/handlers/modifier-select-ui-handler"; +import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; describe("Daily Mode", () => { diff --git a/test/eggs/egg.test.ts b/test/eggs/egg.test.ts index 8b47e68f402..001adf83b37 100644 --- a/test/eggs/egg.test.ts +++ b/test/eggs/egg.test.ts @@ -202,7 +202,7 @@ describe("Egg Generation Tests", () => { const scene = game.scene; const eggMoveIndex = new Egg({ scene }).eggMoveIndex; - const result = !Utils.isNullOrUndefined(eggMoveIndex) && eggMoveIndex >= 0 && eggMoveIndex <= 3; + const result = eggMoveIndex != null && eggMoveIndex >= 0 && eggMoveIndex <= 3; expect(result).toBe(true); }); diff --git a/test/escape-calculations.test.ts b/test/escape-calculations.test.ts index fb677e81a45..e1e521f4394 100644 --- a/test/escape-calculations.test.ts +++ b/test/escape-calculations.test.ts @@ -7,6 +7,7 @@ import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +// TODO: These tests are stupid and need to be redone describe("Escape chance calculations", () => { let phaserGame: Phaser.Game; let game: GameManager; diff --git a/test/imports.test.ts b/test/imports.test.ts deleted file mode 100644 index aeaa763c05e..00000000000 --- a/test/imports.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { initStatsKeys } from "#ui/handlers/game-stats-ui-handler"; -import { describe, expect, it } from "vitest"; - -async function importModule() { - try { - initStatsKeys(); - const { PokemonMove } = await import("#app/data/moves/pokemon-move"); - const { SpeciesId: Species } = await import("#enums/species-id"); - return { - PokemonMove, - Species, - }; - // Dynamically import the module - } catch (error) { - // Log the error stack trace - console.error("Error during import:", error.stack); - // Rethrow the error to ensure the test fails - throw error; - } -} - -describe("tests to debug the import, with trace", () => { - it("import PokemonMove module", async () => { - const module = await importModule(); - // Example assertion - expect(module.PokemonMove).toBeDefined(); - }); - - it("import Species module", async () => { - const module = await importModule(); - // Example assertion - expect(module.Species).toBeDefined(); - }); -}); diff --git a/test/items/dire-hit.test.ts b/test/items/dire-hit.test.ts index d704a94f3a8..6d4bc7524eb 100644 --- a/test/items/dire-hit.test.ts +++ b/test/items/dire-hit.test.ts @@ -10,7 +10,7 @@ import { NewBattlePhase } from "#phases/new-battle-phase"; import { TurnEndPhase } from "#phases/turn-end-phase"; import { TurnInitPhase } from "#phases/turn-init-phase"; import { GameManager } from "#test/test-utils/game-manager"; -import type { ModifierSelectUiHandler } from "#ui/handlers/modifier-select-ui-handler"; +import type { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; diff --git a/test/items/double-battle-chance-booster.test.ts b/test/items/double-battle-chance-booster.test.ts index 9985d4b3a55..ea3c400edb7 100644 --- a/test/items/double-battle-chance-booster.test.ts +++ b/test/items/double-battle-chance-booster.test.ts @@ -5,7 +5,7 @@ import { SpeciesId } from "#enums/species-id"; import { UiMode } from "#enums/ui-mode"; import { DoubleBattleChanceBoosterModifier } from "#modifiers/modifier"; import { GameManager } from "#test/test-utils/game-manager"; -import type { ModifierSelectUiHandler } from "#ui/handlers/modifier-select-ui-handler"; +import type { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -31,7 +31,7 @@ describe("Items - Double Battle Chance Boosters", () => { await game.classicMode.startBattle(); - expect(game.scene.getEnemyField().length).toBe(2); + expect(game.scene.getEnemyField()).toHaveLength(2); }); it("should guarantee double boss battle with 3 unique tiers", async () => { @@ -41,7 +41,7 @@ describe("Items - Double Battle Chance Boosters", () => { const enemyField = game.scene.getEnemyField(); - expect(enemyField.length).toBe(2); + expect(enemyField).toHaveLength(2); expect(enemyField[0].isBoss()).toBe(true); expect(enemyField[1].isBoss()).toBe(true); }); diff --git a/test/items/grip-claw.test.ts b/test/items/grip-claw.test.ts index 5ffebd76946..54a40942beb 100644 --- a/test/items/grip-claw.test.ts +++ b/test/items/grip-claw.test.ts @@ -44,7 +44,7 @@ describe("Items - Grip Claw", () => { it("should steal items on contact and only from the attack target", async () => { await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MILOTIC]); - const [playerPokemon] = game.scene.getPlayerField(); + const playerPokemon = game.field.getPlayerPokemon(); const gripClaw = playerPokemon.getHeldItems()[0] as ContactHeldItemTransferChanceModifier; vi.spyOn(gripClaw, "chance", "get").mockReturnValue(100); @@ -73,7 +73,7 @@ describe("Items - Grip Claw", () => { it("should not steal items when using a targetted, non attack move", async () => { await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MILOTIC]); - const [playerPokemon] = game.scene.getPlayerField(); + const playerPokemon = game.field.getPlayerPokemon(); const gripClaw = playerPokemon.getHeldItems()[0] as ContactHeldItemTransferChanceModifier; vi.spyOn(gripClaw, "chance", "get").mockReturnValue(100); diff --git a/test/items/multi-lens.test.ts b/test/items/multi-lens.test.ts index b69a07033c9..3686aff0fcf 100644 --- a/test/items/multi-lens.test.ts +++ b/test/items/multi-lens.test.ts @@ -103,7 +103,7 @@ describe("Items - Multi Lens", () => { await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.FEEBAS]); - const [magikarp] = game.scene.getPlayerField(); + const magikarp = game.field.getPlayerPokemon(); game.move.select(MoveId.SWIFT, 0); game.move.select(MoveId.SPLASH, 1); diff --git a/test/items/temp-stat-stage-booster.test.ts b/test/items/temp-stat-stage-booster.test.ts index 499f1d630b0..05ea5a03eae 100644 --- a/test/items/temp-stat-stage-booster.test.ts +++ b/test/items/temp-stat-stage-booster.test.ts @@ -7,7 +7,7 @@ import { BATTLE_STATS, Stat } from "#enums/stat"; import { UiMode } from "#enums/ui-mode"; import { TempStatStageBoosterModifier } from "#modifiers/modifier"; import { GameManager } from "#test/test-utils/game-manager"; -import type { ModifierSelectUiHandler } from "#ui/handlers/modifier-select-ui-handler"; +import type { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; diff --git a/test/moves/ability-ignore-moves.test.ts b/test/moves/ability-ignore-moves.test.ts index e3a7c7db12f..089af242f87 100644 --- a/test/moves/ability-ignore-moves.test.ts +++ b/test/moves/ability-ignore-moves.test.ts @@ -102,7 +102,7 @@ describe("Moves - Ability-Ignoring Moves", () => { // Both the initial and redirected instruct use ignored sturdy const [enemy1, enemy2] = game.scene.getEnemyField(); - expect(enemy1.isFainted()).toBe(true); - expect(enemy2.isFainted()).toBe(true); + expect(enemy1).toHaveFainted(); + expect(enemy2).toHaveFainted(); }); }); diff --git a/test/moves/baton-pass.test.ts b/test/moves/baton-pass.test.ts index f9bd92a63cd..caabcfa7158 100644 --- a/test/moves/baton-pass.test.ts +++ b/test/moves/baton-pass.test.ts @@ -76,12 +76,7 @@ describe("Moves - Baton Pass", () => { expect(game.field.getEnemyPokemon().getStatStage(Stat.SPATK)).toEqual(2); // confirm that a switch actually happened. can't use species because I // can't find a way to override trainer parties with more than 1 pokemon species - expect(game.phaseInterceptor.log.slice(-4)).toEqual([ - "MoveEffectPhase", - "SwitchSummonPhase", - "SummonPhase", - "PostSummonPhase", - ]); + expect(game.field.getEnemyPokemon().summonData.moveHistory).toHaveLength(0); }); it("doesn't transfer effects that aren't transferrable", async () => { diff --git a/test/moves/chilly-reception.test.ts b/test/moves/chilly-reception.test.ts index 096454132f3..f9caea3d560 100644 --- a/test/moves/chilly-reception.test.ts +++ b/test/moves/chilly-reception.test.ts @@ -47,7 +47,7 @@ describe("Moves - Chilly Reception", () => { expect(game.field.getPlayerPokemon()).toBe(meowth); expect(slowking.isOnField()).toBe(false); expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase"); - expect(game.textInterceptor.logs).toContain( + expect(game).toHaveShownMessage( i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(slowking) }), ); }); @@ -110,7 +110,7 @@ describe("Moves - Chilly Reception", () => { expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase"); expect(game.field.getPlayerPokemon()).toBe(slowking); expect(slowking.getLastXMoves()[0].result).toBe(MoveResult.FAIL); - expect(game.textInterceptor.logs).toContain( + expect(game).toHaveShownMessage( i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(slowking) }), ); }); @@ -129,7 +129,7 @@ describe("Moves - Chilly Reception", () => { expect(game.field.getPlayerPokemon()).toBe(meowth); expect(slowking.isOnField()).toBe(false); expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase"); - expect(game.textInterceptor.logs).not.toContain( + expect(game).not.toHaveShownMessage( i18next.t("moveTriggers:chillyReception", { pokemonName: getPokemonNameWithAffix(slowking) }), ); }); diff --git a/test/moves/defog.test.ts b/test/moves/defog.test.ts index 820dfaa6bcb..4ddb397ee71 100644 --- a/test/moves/defog.test.ts +++ b/test/moves/defog.test.ts @@ -1,4 +1,5 @@ import { AbilityId } from "#enums/ability-id"; +import { ArenaTagType } from "#enums/arena-tag-type"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; @@ -32,26 +33,21 @@ describe("Moves - Defog", () => { .enemyMoveset([MoveId.DEFOG, MoveId.GROWL]); }); + // TODO: Refactor these tests they suck ass it("should not allow Safeguard to be active", async () => { await game.classicMode.startBattle([SpeciesId.REGIELEKI]); - const playerPokemon = game.scene.getPlayerField(); - const enemyPokemon = game.scene.getEnemyField(); + game.scene.arena.addTag(ArenaTagType.SAFEGUARD, 0, 0, 0); - game.move.select(MoveId.SAFEGUARD); - await game.move.selectEnemyMove(MoveId.DEFOG); - await game.phaseInterceptor.to("BerryPhase"); + game.move.use(MoveId.DEFOG); + await game.toEndOfTurn(); - expect(playerPokemon[0].isSafeguarded(enemyPokemon[0])).toBe(false); - - expect(true).toBe(true); + expect(game).not.toHaveArenaTag(ArenaTagType.SAFEGUARD); }); it("should not allow Mist to be active", async () => { await game.classicMode.startBattle([SpeciesId.REGIELEKI]); - const playerPokemon = game.scene.getPlayerField(); - game.move.select(MoveId.MIST); await game.move.selectEnemyMove(MoveId.DEFOG); @@ -62,8 +58,6 @@ describe("Moves - Defog", () => { await game.phaseInterceptor.to("BerryPhase"); - expect(playerPokemon[0].getStatStage(Stat.ATK)).toBe(-1); - - expect(true).toBe(true); + expect(game.field.getPlayerPokemon()).toHaveStatStage(Stat.ATK, -1); }); }); diff --git a/test/moves/delayed-attack.test.ts b/test/moves/delayed-attack.test.ts index 420ef6d1f00..e31c7f28e48 100644 --- a/test/moves/delayed-attack.test.ts +++ b/test/moves/delayed-attack.test.ts @@ -99,7 +99,7 @@ describe("Moves - Delayed Attacks", () => { expectFutureSightActive(0); const enemy = game.field.getEnemyPokemon(); expect(enemy).not.toHaveFullHp(); - expect(game.textInterceptor.logs).toContain( + expect(game).toHaveShownMessage( i18next.t("moveTriggers:tookMoveAttack", { pokemonName: getPokemonNameWithAffix(enemy), moveName: allMoves[move].name, @@ -193,7 +193,7 @@ describe("Moves - Delayed Attacks", () => { // All attacks have concluded at this point, unshifting new `MoveEffectPhase`s to the queue. expectFutureSightActive(0); - const MEPs = game.scene.phaseManager.phaseQueue.filter(p => p.is("MoveEffectPhase")); + const MEPs = game.scene.phaseManager["phaseQueue"].findAll("MoveEffectPhase"); expect(MEPs).toHaveLength(4); expect(MEPs.map(mep => mep.getPokemon())).toEqual(oldOrder); }); @@ -227,7 +227,7 @@ describe("Moves - Delayed Attacks", () => { expect(karp).toHaveFullHp(); expect(feebas).toHaveFullHp(); - expect(game.textInterceptor.logs).not.toContain( + expect(game).not.toHaveShownMessage( i18next.t("moveTriggers:tookMoveAttack", { pokemonName: getPokemonNameWithAffix(karp), moveName: allMoves[MoveId.FUTURE_SIGHT].name, @@ -256,7 +256,7 @@ describe("Moves - Delayed Attacks", () => { await passTurns(2); expect(enemy1).not.toHaveFullHp(); - expect(game.textInterceptor.logs).toContain( + expect(game).toHaveShownMessage( i18next.t("moveTriggers:tookMoveAttack", { pokemonName: getPokemonNameWithAffix(enemy1), moveName: allMoves[MoveId.FUTURE_SIGHT].name, @@ -284,7 +284,7 @@ describe("Moves - Delayed Attacks", () => { expectFutureSightActive(0); expect(enemy1).toHaveFullHp(); - expect(game.textInterceptor.logs).not.toContain( + expect(game).not.toHaveShownMessage( i18next.t("moveTriggers:tookMoveAttack", { pokemonName: getPokemonNameWithAffix(enemy1), moveName: allMoves[MoveId.FUTURE_SIGHT].name, @@ -321,7 +321,7 @@ describe("Moves - Delayed Attacks", () => { expect(enemy1).toHaveFullHp(); expect(enemy2).not.toHaveFullHp(); - expect(game.textInterceptor.logs).toContain( + expect(game).toHaveShownMessage( i18next.t("moveTriggers:tookMoveAttack", { pokemonName: getPokemonNameWithAffix(enemy2), moveName: allMoves[MoveId.FUTURE_SIGHT].name, @@ -354,7 +354,7 @@ describe("Moves - Delayed Attacks", () => { // Player Normalize was not applied due to being off field const enemy = game.field.getEnemyPokemon(); expect(enemy).not.toHaveFullHp(); - expect(game.textInterceptor.logs).toContain( + expect(game).toHaveShownMessage( i18next.t("moveTriggers:tookMoveAttack", { pokemonName: getPokemonNameWithAffix(enemy), moveName: allMoves[MoveId.DOOM_DESIRE].name, diff --git a/test/moves/destiny-bond.test.ts b/test/moves/destiny-bond.test.ts index 118a45e7682..a5020b83944 100644 --- a/test/moves/destiny-bond.test.ts +++ b/test/moves/destiny-bond.test.ts @@ -160,11 +160,7 @@ describe("Moves - Destiny Bond", () => { game.override.moveset([MoveId.DESTINY_BOND, MoveId.CRUNCH]).battleStyle("double"); await game.classicMode.startBattle([SpeciesId.SHEDINJA, SpeciesId.BULBASAUR, SpeciesId.SQUIRTLE]); - const enemyPokemon0 = game.scene.getEnemyField()[0]; - const enemyPokemon1 = game.scene.getEnemyField()[1]; - const playerPokemon0 = game.scene.getPlayerField()[0]; - const playerPokemon1 = game.scene.getPlayerField()[1]; - + const [playerPokemon0, playerPokemon1, enemyPokemon0, enemyPokemon1] = game.scene.getField(); // Shedinja uses Destiny Bond, then ally Bulbasaur KO's Shedinja with Crunch game.move.select(MoveId.DESTINY_BOND, 0); game.move.select(MoveId.CRUNCH, 1, BattlerIndex.PLAYER); diff --git a/test/moves/dragon-tail.test.ts b/test/moves/dragon-tail.test.ts index e3a5bf459e8..28266465523 100644 --- a/test/moves/dragon-tail.test.ts +++ b/test/moves/dragon-tail.test.ts @@ -171,7 +171,7 @@ describe("Moves - Dragon Tail", () => { const enemy = game.field.getEnemyPokemon(); expect(enemy).toBeDefined(); expect(enemy.hp).toBe(Math.floor(enemy.getMaxHp() / 2)); - expect(game.scene.getEnemyField().length).toBe(1); + expect(game.scene.getEnemyField()).toHaveLength(1); }); it("should not cause a softlock when activating a player's reviver seed", async () => { diff --git a/test/moves/fell-stinger.test.ts b/test/moves/fell-stinger.test.ts index ede70b7af9b..4550cdffa12 100644 --- a/test/moves/fell-stinger.test.ts +++ b/test/moves/fell-stinger.test.ts @@ -107,7 +107,7 @@ describe("Moves - Fell Stinger", () => { await game.classicMode.startBattle([SpeciesId.LEAVANNY]); const leadPokemon = game.field.getPlayerPokemon(); - const leftEnemy = game.scene.getEnemyField()[0]!; + const leftEnemy = game.field.getEnemyPokemon(); // Turn 1: set Salt Cure, enemy splashes and does nothing game.move.select(MoveId.SALT_CURE, 0, leftEnemy.getBattlerIndex()); diff --git a/test/moves/flame-burst.test.ts b/test/moves/flame-burst.test.ts index ce82b46d0fc..e340936f94c 100644 --- a/test/moves/flame-burst.test.ts +++ b/test/moves/flame-burst.test.ts @@ -16,7 +16,7 @@ describe("Moves - Flame Burst", () => { * Calculates the effect damage of Flame Burst which is 1/16 of the target ally's max HP * See Flame Burst {@link https://bulbapedia.bulbagarden.net/wiki/Flame_Burst_(move)} * See Flame Burst's move attribute {@linkcode FlameBurstAttr} - * @param pokemon {@linkcode Pokemon} - The ally of the move's target + * @param pokemon - The ally of the move's target * @returns Effect damage of Flame Burst */ const getEffectDamage = (pokemon: Pokemon): number => { diff --git a/test/moves/focus-punch.test.ts b/test/moves/focus-punch.test.ts index d7b40569aaa..06594e85e27 100644 --- a/test/moves/focus-punch.test.ts +++ b/test/moves/focus-punch.test.ts @@ -3,7 +3,6 @@ import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { BerryPhase } from "#phases/berry-phase"; import { MessagePhase } from "#phases/message-phase"; -import { MoveHeaderPhase } from "#phases/move-header-phase"; import { SwitchSummonPhase } from "#phases/switch-summon-phase"; import { TurnStartPhase } from "#phases/turn-start-phase"; import { GameManager } from "#test/test-utils/game-manager"; @@ -116,7 +115,7 @@ describe("Moves - Focus Punch", () => { await game.phaseInterceptor.to(TurnStartPhase); expect(game.scene.phaseManager.getCurrentPhase() instanceof SwitchSummonPhase).toBeTruthy(); - expect(game.scene.phaseManager.phaseQueue.find(phase => phase instanceof MoveHeaderPhase)).toBeDefined(); + expect(game.scene.phaseManager.hasPhaseOfType("MoveHeaderPhase")).toBe(true); }); it("should replace the 'but it failed' text when the user gets hit", async () => { game.override.enemyMoveset([MoveId.TACKLE]); diff --git a/test/moves/healing-wish-lunar-dance.test.ts b/test/moves/healing-wish-lunar-dance.test.ts new file mode 100644 index 00000000000..0dcf993aeac --- /dev/null +++ b/test/moves/healing-wish-lunar-dance.test.ts @@ -0,0 +1,245 @@ +import { AbilityId } from "#enums/ability-id"; +import { ArenaTagType } from "#enums/arena-tag-type"; +import { Challenges } from "#enums/challenges"; +import { MoveId } from "#enums/move-id"; +import { MoveResult } from "#enums/move-result"; +import { PokemonType } from "#enums/pokemon-type"; +import { SpeciesId } from "#enums/species-id"; +import { StatusEffect } from "#enums/status-effect"; +import { GameManager } from "#test/test-utils/game-manager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Lunar Dance and Healing Wish", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override.battleStyle("double").enemyAbility(AbilityId.BALL_FETCH).enemyMoveset(MoveId.SPLASH); + }); + + describe.each([ + { moveName: "Healing Wish", moveId: MoveId.HEALING_WISH }, + { moveName: "Lunar Dance", moveId: MoveId.LUNAR_DANCE }, + ])("$moveName", ({ moveId }) => { + it("should sacrifice the user to restore the switched in Pokemon's HP", async () => { + await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER, SpeciesId.SQUIRTLE]); + + const [bulbasaur, charmander, squirtle] = game.scene.getPlayerParty(); + squirtle.hp = 1; + + game.move.use(MoveId.SPLASH, 0); + game.move.use(moveId, 1); + game.doSelectPartyPokemon(2); + + await game.toNextTurn(); + + expect(bulbasaur.isFullHp()).toBe(true); + expect(charmander.isFainted()).toBe(true); + expect(squirtle.isFullHp()).toBe(true); + }); + + it("should sacrifice the user to cure the switched in Pokemon's status", async () => { + game.override.statusEffect(StatusEffect.BURN); + + await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER, SpeciesId.SQUIRTLE]); + const [bulbasaur, charmander, squirtle] = game.scene.getPlayerParty(); + + game.move.use(MoveId.SPLASH, 0); + game.move.use(moveId, 1); + game.doSelectPartyPokemon(2); + + await game.toNextTurn(); + + expect(bulbasaur.status?.effect).toBe(StatusEffect.BURN); + expect(charmander.isFainted()).toBe(true); + expect(squirtle.status?.effect).toBeUndefined(); + }); + + it("should fail if the user has no non-fainted allies in their party", async () => { + game.override.battleStyle("single"); + + await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER]); + const [bulbasaur, charmander] = game.scene.getPlayerParty(); + + game.move.use(MoveId.MEMENTO); + game.doSelectPartyPokemon(1); + + await game.toNextTurn(); + + expect(bulbasaur.isFainted()).toBe(true); + expect(charmander.isActive(true)).toBe(true); + + game.move.use(moveId); + + await game.toEndOfTurn(); + + expect(charmander.isFullHp()); + expect(charmander.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + }); + + it("should fail if the user has no challenge-eligible allies", async () => { + game.override.battleStyle("single"); + // Mono normal challenge + game.challengeMode.addChallenge(Challenges.SINGLE_TYPE, PokemonType.NORMAL + 1, 0); + await game.challengeMode.startBattle([SpeciesId.RATICATE, SpeciesId.ODDISH]); + + const raticate = game.field.getPlayerPokemon(); + + game.move.use(moveId); + await game.toNextTurn(); + + expect(raticate.isFullHp()).toBe(true); + expect(raticate.getLastXMoves()[0].result).toEqual(MoveResult.FAIL); + }); + + it("should store its effect if the switched-in Pokemon would be unaffected", async () => { + game.override.battleStyle("single"); + + await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER, SpeciesId.SQUIRTLE]); + + const [bulbasaur, charmander, squirtle] = game.scene.getPlayerParty(); + squirtle.hp = 1; + + game.move.use(moveId); + game.doSelectPartyPokemon(1); + + await game.toNextTurn(); + + // Bulbasaur fainted and stored a healing effect + expect(bulbasaur.isFainted()).toBe(true); + expect(charmander.isFullHp()).toBe(true); + expect(game.phaseInterceptor.log).not.toContain("PokemonHealPhase"); + expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeDefined(); + + // Switch to damaged Squirtle. HW/LD's effect should activate + game.doSwitchPokemon(2); + + await game.toEndOfTurn(); + expect(squirtle.isFullHp()).toBe(true); + expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeUndefined(); + + // Set Charmander's HP to 1, then switch back to Charmander. + // HW/LD shouldn't activate again + charmander.hp = 1; + game.doSwitchPokemon(2); + + await game.toEndOfTurn(); + expect(charmander.hp).toBe(1); + }); + + it("should only store one charge of the effect at a time", async () => { + game.override.battleStyle("single"); + + await game.classicMode.startBattle([ + SpeciesId.BULBASAUR, + SpeciesId.CHARMANDER, + SpeciesId.SQUIRTLE, + SpeciesId.PIKACHU, + ]); + + const [bulbasaur, charmander, squirtle, pikachu] = game.scene.getPlayerParty(); + [squirtle, pikachu].forEach(p => (p.hp = 1)); + + // Use HW/LD and send in Charmander. HW/LD's effect should be stored + game.move.use(moveId); + game.doSelectPartyPokemon(1); + + await game.toNextTurn(); + expect(bulbasaur.isFainted()).toBe(true); + expect(charmander.isFullHp()).toBe(true); + expect(charmander.isFullHp()); + expect(game.phaseInterceptor.log).not.toContain("PokemonHealPhase"); + expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeDefined(); + + // Use HW/LD again, sending in Squirtle. HW/LD should activate and heal Squirtle + game.move.use(moveId); + game.doSelectPartyPokemon(2); + + await game.toNextTurn(); + expect(charmander.isFainted()).toBe(true); + expect(squirtle.isFullHp()).toBe(true); + expect(squirtle.isFullHp()); + + // Switch again to Pikachu. HW/LD's effect shouldn't be present + game.doSwitchPokemon(3); + + expect(pikachu.isFullHp()).toBe(false); + }); + }); + + it("Lunar Dance should sacrifice the user to restore the switched in Pokemon's PP", async () => { + game.override.battleStyle("single"); + + await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER]); + + const [bulbasaur, charmander] = game.scene.getPlayerParty(); + + game.move.use(MoveId.SPLASH); + await game.toNextTurn(); + + game.doSwitchPokemon(1); + await game.toNextTurn(); + + game.move.use(MoveId.LUNAR_DANCE); + game.doSelectPartyPokemon(1); + + await game.toNextTurn(); + expect(charmander.isFainted()).toBeTruthy(); + bulbasaur.getMoveset().forEach(mv => expect(mv.ppUsed).toBe(0)); + }); + + it("should stack with each other", async () => { + game.override.battleStyle("single"); + + await game.classicMode.startBattle([ + SpeciesId.BULBASAUR, + SpeciesId.CHARMANDER, + SpeciesId.SQUIRTLE, + SpeciesId.PIKACHU, + ]); + + const [bulbasaur, charmander, squirtle, pikachu] = game.scene.getPlayerParty(); + [squirtle, pikachu].forEach(p => { + p.hp = 1; + p.getMoveset().forEach(mv => (mv.ppUsed = 1)); + }); + + game.move.use(MoveId.LUNAR_DANCE); + game.doSelectPartyPokemon(1); + + await game.toNextTurn(); + expect(bulbasaur.isFainted()).toBe(true); + expect(charmander.isFullHp()).toBe(true); + expect(game.phaseInterceptor.log).not.toContain("PokemonHealPhase"); + expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeDefined(); + + game.move.use(MoveId.HEALING_WISH); + game.doSelectPartyPokemon(2); + + // Lunar Dance should apply first since it was used first, restoring Squirtle's HP and PP + await game.toNextTurn(); + expect(squirtle.isFullHp()).toBe(true); + squirtle.getMoveset().forEach(mv => expect(mv.ppUsed).toBe(0)); + expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeDefined(); + + game.doSwitchPokemon(3); + + // Healing Wish should apply on the next switch, restoring Pikachu's HP + await game.toEndOfTurn(); + expect(pikachu.isFullHp()).toBe(true); + pikachu.getMoveset().forEach(mv => expect(mv.ppUsed).toBe(1)); + expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeUndefined(); + }); +}); diff --git a/test/moves/instruct.test.ts b/test/moves/instruct.test.ts index 27318105783..eb3eccff400 100644 --- a/test/moves/instruct.test.ts +++ b/test/moves/instruct.test.ts @@ -498,7 +498,7 @@ describe("Moves - Instruct", () => { .enemyLevel(1); await game.classicMode.startBattle([SpeciesId.KORAIDON, SpeciesId.KLEFKI]); - const koraidon = game.scene.getPlayerField()[0]!; + const koraidon = game.field.getPlayerPokemon(); game.move.select(MoveId.BREAKING_SWIPE); await game.phaseInterceptor.to("TurnEndPhase", false); @@ -527,7 +527,7 @@ describe("Moves - Instruct", () => { .enemyLevel(1); await game.classicMode.startBattle([SpeciesId.KORAIDON, SpeciesId.KLEFKI]); - const koraidon = game.scene.getPlayerField()[0]!; + const koraidon = game.field.getPlayerPokemon(); game.move.select(MoveId.BRUTAL_SWING); await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); @@ -587,7 +587,7 @@ describe("Moves - Instruct", () => { .enemyLevel(5); await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.IVYSAUR]); - const [, ivysaur] = game.scene.getPlayerField(); + const ivysaur = game.scene.getPlayerField()[1]; game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER); game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER_2); diff --git a/test/moves/jaw-lock.test.ts b/test/moves/jaw-lock.test.ts index 441c74c7356..0ea2c0bd8bf 100644 --- a/test/moves/jaw-lock.test.ts +++ b/test/moves/jaw-lock.test.ts @@ -111,7 +111,8 @@ describe("Moves - Jaw Lock", () => { await game.classicMode.startBattle([SpeciesId.CHARMANDER, SpeciesId.BULBASAUR]); - const playerPokemon = game.scene.getPlayerField(); + const playerPokemon = game.field.getPlayerPokemon(); + const enemyPokemon = game.scene.getEnemyField(); game.move.select(MoveId.JAW_LOCK, 0, BattlerIndex.ENEMY); @@ -120,7 +121,7 @@ describe("Moves - Jaw Lock", () => { await game.phaseInterceptor.to(MoveEffectPhase); - expect(playerPokemon[0].getTag(BattlerTagType.TRAPPED)).toBeDefined(); + expect(playerPokemon.getTag(BattlerTagType.TRAPPED)).toBeDefined(); expect(enemyPokemon[0].getTag(BattlerTagType.TRAPPED)).toBeDefined(); await game.toNextTurn(); @@ -131,8 +132,8 @@ describe("Moves - Jaw Lock", () => { await game.phaseInterceptor.to(MoveEffectPhase); expect(enemyPokemon[1].getTag(BattlerTagType.TRAPPED)).toBeUndefined(); - expect(playerPokemon[0].getTag(BattlerTagType.TRAPPED)).toBeDefined(); - expect(playerPokemon[0].getTag(BattlerTagType.TRAPPED)?.sourceId).toBe(enemyPokemon[0].id); + expect(playerPokemon.getTag(BattlerTagType.TRAPPED)).toBeDefined(); + expect(playerPokemon.getTag(BattlerTagType.TRAPPED)?.sourceId).toBe(enemyPokemon[0].id); }); it("should not trap either pokemon if the target is protected", async () => { diff --git a/test/moves/laser-focus.test.ts b/test/moves/laser-focus.test.ts new file mode 100644 index 00000000000..7496e3ed87f --- /dev/null +++ b/test/moves/laser-focus.test.ts @@ -0,0 +1,104 @@ +import { getPokemonNameWithAffix } from "#app/messages"; +import { AbilityId } from "#enums/ability-id"; +import { BattlerIndex } from "#enums/battler-index"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { MoveId } from "#enums/move-id"; +import { SpeciesId } from "#enums/species-id"; +import { GameManager } from "#test/test-utils/game-manager"; +import i18next from "i18next"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Move - Laser Focus", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .ability(AbilityId.BALL_FETCH) + .battleStyle("single") + .criticalHits(false) + .enemySpecies(SpeciesId.MAGIKARP) + .enemyAbility(AbilityId.BALL_FETCH) + .enemyMoveset(MoveId.SPLASH) + .startingLevel(100) + .enemyLevel(100); + }); + + it("should make the user's next attack a guaranteed critical hit", async () => { + await game.classicMode.startBattle([SpeciesId.FEEBAS]); + + game.move.use(MoveId.LASER_FOCUS); + await game.toNextTurn(); + + const feebas = game.field.getPlayerPokemon(); + expect(feebas).toHaveBattlerTag(BattlerTagType.ALWAYS_CRIT); + expect(game).toHaveShownMessage( + i18next.t("battlerTags:laserFocusOnAdd", { + pokemonNameWithAffix: getPokemonNameWithAffix(feebas), + }), + ); + + const enemy = game.field.getEnemyPokemon(); + const critSpy = vi.spyOn(enemy, "getCriticalHitResult"); + + game.move.use(MoveId.TACKLE); + await game.toEndOfTurn(); + + expect(critSpy).toHaveLastReturnedWith(true); + }); + + it("should disappear at the end of the next turn", async () => { + await game.classicMode.startBattle([SpeciesId.FEEBAS]); + + const feebas = game.field.getPlayerPokemon(); + + game.move.use(MoveId.LASER_FOCUS); + await game.toNextTurn(); + + expect(feebas).toHaveBattlerTag(BattlerTagType.ALWAYS_CRIT); + + game.move.use(MoveId.SPLASH); + await game.toNextTurn(); + + expect(feebas).not.toHaveBattlerTag(BattlerTagType.ALWAYS_CRIT); + + const enemy = game.field.getEnemyPokemon(); + const critSpy = vi.spyOn(enemy, "getCriticalHitResult"); + + game.move.use(MoveId.TACKLE); + await game.toEndOfTurn(); + + expect(critSpy).toHaveLastReturnedWith(false); + }); + + it("should boost all attacks until the end of the next turn", async () => { + await game.classicMode.startBattle([SpeciesId.FEEBAS]); + + game.move.use(MoveId.LASER_FOCUS); + await game.toNextTurn(); + + const enemy = game.field.getEnemyPokemon(); + const critSpy = vi.spyOn(enemy, "getCriticalHitResult"); + + game.move.use(MoveId.TACKLE); + await game.move.forceEnemyMove(MoveId.INSTRUCT); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.toEndOfTurn(); + + expect(critSpy).toHaveReturnedTimes(2); + expect(critSpy).toHaveNthReturnedWith(1, true); + expect(critSpy).toHaveNthReturnedWith(2, true); + }); +}); diff --git a/test/moves/lunar-dance.test.ts b/test/moves/lunar-dance.test.ts deleted file mode 100644 index 7386d15079b..00000000000 --- a/test/moves/lunar-dance.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { AbilityId } from "#enums/ability-id"; -import { MoveId } from "#enums/move-id"; -import { SpeciesId } from "#enums/species-id"; -import { StatusEffect } from "#enums/status-effect"; -import { GameManager } from "#test/test-utils/game-manager"; -import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; - -describe("Moves - Lunar Dance", () => { - let phaserGame: Phaser.Game; - let game: GameManager; - - beforeAll(() => { - phaserGame = new Phaser.Game({ - type: Phaser.HEADLESS, - }); - }); - - afterEach(() => { - game.phaseInterceptor.restoreOg(); - }); - - beforeEach(() => { - game = new GameManager(phaserGame); - game.override - .statusEffect(StatusEffect.BURN) - .battleStyle("double") - .enemyAbility(AbilityId.BALL_FETCH) - .enemyMoveset(MoveId.SPLASH); - }); - - it("should full restore HP, PP and status of switched in pokemon, then fail second use because no remaining backup pokemon in party", async () => { - await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.ODDISH, SpeciesId.RATTATA]); - - const [bulbasaur, oddish, rattata] = game.scene.getPlayerParty(); - game.move.changeMoveset(bulbasaur, [MoveId.LUNAR_DANCE, MoveId.SPLASH]); - game.move.changeMoveset(oddish, [MoveId.LUNAR_DANCE, MoveId.SPLASH]); - game.move.changeMoveset(rattata, [MoveId.LUNAR_DANCE, MoveId.SPLASH]); - - game.move.select(MoveId.SPLASH, 0); - game.move.select(MoveId.SPLASH, 1); - await game.toNextTurn(); - - // Bulbasaur should still be burned and have used a PP for splash and not at max hp - expect(bulbasaur.status?.effect).toBe(StatusEffect.BURN); - expect(bulbasaur.moveset[1]?.ppUsed).toBe(1); - expect(bulbasaur.hp).toBeLessThan(bulbasaur.getMaxHp()); - - // Switch out Bulbasaur for Rattata so we can swtich bulbasaur back in with lunar dance - game.doSwitchPokemon(2); - game.move.select(MoveId.SPLASH, 1); - await game.toNextTurn(); - - game.move.select(MoveId.SPLASH, 0); - game.move.select(MoveId.LUNAR_DANCE); - game.doSelectPartyPokemon(2); - await game.phaseInterceptor.to("SwitchPhase", false); - await game.toNextTurn(); - - // Bulbasaur should NOT have any status and have full PP for splash and be at max hp - expect(bulbasaur.status?.effect).toBeUndefined(); - expect(bulbasaur.moveset[1]?.ppUsed).toBe(0); - expect(bulbasaur.isFullHp()).toBe(true); - - game.move.select(MoveId.SPLASH, 0); - game.move.select(MoveId.LUNAR_DANCE); - await game.toNextTurn(); - - // Using Lunar dance again should fail because nothing in party and rattata should be alive - expect(rattata.status?.effect).toBe(StatusEffect.BURN); - expect(rattata.hp).toBeLessThan(rattata.getMaxHp()); - }); -}); diff --git a/test/moves/rage-fist.test.ts b/test/moves/rage-fist.test.ts index 61164b5710c..c58d1296ac5 100644 --- a/test/moves/rage-fist.test.ts +++ b/test/moves/rage-fist.test.ts @@ -166,7 +166,6 @@ describe("Moves - Rage Fist", () => { // Charizard hit game.move.select(MoveId.SPLASH); - await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.toNextTurn(); expect(getPartyHitCount()).toEqual([1, 0]); diff --git a/test/moves/revival-blessing.test.ts b/test/moves/revival-blessing.test.ts index 4dc7cb97f2d..8c751458ff7 100644 --- a/test/moves/revival-blessing.test.ts +++ b/test/moves/revival-blessing.test.ts @@ -119,17 +119,16 @@ describe("Moves - Revival Blessing", () => { game.override .battleStyle("double") .enemyMoveset([MoveId.REVIVAL_BLESSING]) - .moveset([MoveId.SPLASH]) + .moveset([MoveId.SPLASH, MoveId.JUDGMENT]) + .startingLevel(100) .startingWave(25); // 2nd rival battle - must have 3+ pokemon await game.classicMode.startBattle([SpeciesId.ARCEUS, SpeciesId.GIRATINA]); const enemyFainting = game.scene.getEnemyField()[0]; - game.move.select(MoveId.SPLASH, 0); + game.move.use(MoveId.JUDGMENT, 0, BattlerIndex.ENEMY); game.move.select(MoveId.SPLASH, 1); - await game.killPokemon(enemyFainting); - await game.phaseInterceptor.to("BerryPhase"); await game.toNextTurn(); // If there are incorrectly two switch phases into this slot, the fainted pokemon will end up in slot 3 // Make sure it's still in slot 1 diff --git a/test/moves/shell-trap.test.ts b/test/moves/shell-trap.test.ts index 5ecad3116af..2a83f2c3266 100644 --- a/test/moves/shell-trap.test.ts +++ b/test/moves/shell-trap.test.ts @@ -48,7 +48,7 @@ describe("Moves - Shell Trap", () => { await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER, BattlerIndex.PLAYER_2]); - await game.phaseInterceptor.to(MoveEndPhase); + await game.phaseInterceptor.to("MoveEndPhase"); const movePhase = game.scene.phaseManager.getCurrentPhase(); expect(movePhase instanceof MovePhase).toBeTruthy(); diff --git a/test/moves/splash-celebrate.test.ts b/test/moves/splash-celebrate.test.ts new file mode 100644 index 00000000000..346ffedd12c --- /dev/null +++ b/test/moves/splash-celebrate.test.ts @@ -0,0 +1,52 @@ +import { loggedInUser } from "#app/account"; +import { AbilityId } from "#enums/ability-id"; +import { MoveId } from "#enums/move-id"; +import { SpeciesId } from "#enums/species-id"; +import { GameManager } from "#test/test-utils/game-manager"; +import i18next from "i18next"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe.each<{ name: string; move: MoveId; message: () => string }>([ + { name: "Splash", move: MoveId.SPLASH, message: () => i18next.t("moveTriggers:splash") }, + { + name: "Celebrate", + move: MoveId.CELEBRATE, + message: () => i18next.t("moveTriggers:celebrate", { playerName: loggedInUser?.username }), + }, +])("Move - $name", ({ move, message }) => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .ability(AbilityId.BALL_FETCH) + .battleStyle("single") + .criticalHits(false) + .enemySpecies(SpeciesId.MAGIKARP) + .enemyAbility(AbilityId.BALL_FETCH) + .enemyMoveset(MoveId.TACKLE) + .startingLevel(100) + .enemyLevel(100); + }); + + it("should show a message on use", async () => { + await game.classicMode.startBattle([SpeciesId.FEEBAS]); + + game.move.use(move); + await game.toEndOfTurn(); + + expect(game).toHaveShownMessage(message()); + }); +}); diff --git a/test/moves/tailwind.test.ts b/test/moves/tailwind.test.ts index 5c91a37f786..d9a0bdeb5f1 100644 --- a/test/moves/tailwind.test.ts +++ b/test/moves/tailwind.test.ts @@ -34,8 +34,7 @@ describe("Moves - Tailwind", () => { it("doubles the Speed stat of the Pokemons on its side", async () => { await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MEOWTH]); - const magikarp = game.scene.getPlayerField()[0]; - const meowth = game.scene.getPlayerField()[1]; + const [magikarp, meowth] = game.scene.getPlayerField(); const magikarpSpd = magikarp.getStat(Stat.SPD); const meowthSpd = meowth.getStat(Stat.SPD); diff --git a/test/moves/trick-room.test.ts b/test/moves/trick-room.test.ts index a1d81efb17e..d970dc9762d 100644 --- a/test/moves/trick-room.test.ts +++ b/test/moves/trick-room.test.ts @@ -5,10 +5,10 @@ import { BattlerIndex } from "#enums/battler-index"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; -import { TurnStartPhase } from "#phases/turn-start-phase"; +import { WeatherType } from "#enums/weather-type"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; describe("Move - Trick Room", () => { let phaserGame: Phaser.Game; @@ -56,13 +56,11 @@ describe("Move - Trick Room", () => { turnCount: 4, // The 5 turn limit _includes_ the current turn! }); - // Now, check that speed was indeed reduced - const turnOrderSpy = vi.spyOn(TurnStartPhase.prototype, "getSpeedOrder"); - - game.move.use(MoveId.SPLASH); + game.move.use(MoveId.SUNNY_DAY); + await game.move.forceEnemyMove(MoveId.RAIN_DANCE); await game.toEndOfTurn(); - expect(turnOrderSpy).toHaveLastReturnedWith([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + expect(game.scene.arena.getWeatherType()).toBe(WeatherType.SUNNY); }); it("should be removed when overlapped", async () => { diff --git a/test/moves/whirlwind.test.ts b/test/moves/whirlwind.test.ts index 61c05a30322..ac112f01ea3 100644 --- a/test/moves/whirlwind.test.ts +++ b/test/moves/whirlwind.test.ts @@ -10,6 +10,7 @@ import { PokemonType } from "#enums/pokemon-type"; import { SpeciesId } from "#enums/species-id"; import { StatusEffect } from "#enums/status-effect"; import { TrainerType } from "#enums/trainer-type"; +import { TrainerVariant } from "#enums/trainer-variant"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -193,7 +194,7 @@ describe("Moves - Whirlwind", () => { .battleType(BattleType.TRAINER) .randomTrainer({ trainerType: TrainerType.BREEDER, - alwaysDouble: true, + trainerVariant: TrainerVariant.DOUBLE, }) .enemyMoveset([MoveId.SPLASH, MoveId.LUNAR_DANCE]) .moveset([MoveId.WHIRLWIND, MoveId.SPLASH]); diff --git a/test/moves/wish.test.ts b/test/moves/wish.test.ts index 55877edbfd4..b64a15ac654 100644 --- a/test/moves/wish.test.ts +++ b/test/moves/wish.test.ts @@ -55,7 +55,7 @@ describe("Move - Wish", () => { await game.toEndOfTurn(); expect(game).toHavePositionalTag(PositionalTagType.WISH, 0); - expect(game.textInterceptor.logs).toContain( + expect(game).toHaveShownMessage( i18next.t("arenaTag:wishTagOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(alomomola), }), @@ -135,7 +135,7 @@ describe("Move - Wish", () => { // all wishes have activated and added healing phases expect(game).toHavePositionalTag(PositionalTagType.WISH, 0); - const healPhases = game.scene.phaseManager.phaseQueue.filter(p => p.is("PokemonHealPhase")); + const healPhases = game.scene.phaseManager["phaseQueue"].findAll("PokemonHealPhase"); expect(healPhases).toHaveLength(4); expect.soft(healPhases.map(php => php.getPokemon())).toEqual(oldOrder); @@ -165,7 +165,7 @@ describe("Move - Wish", () => { // Wish went away without doing anything expect(game).toHavePositionalTag(PositionalTagType.WISH, 0); - expect(game.textInterceptor.logs).not.toContain( + expect(game).not.toHaveShownMessage( i18next.t("arenaTag:wishTagOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(blissey), }), diff --git a/test/mystery-encounter/encounter-test-utils.ts b/test/mystery-encounter/encounter-test-utils.ts index fcf27b2c6fb..165678a88da 100644 --- a/test/mystery-encounter/encounter-test-utils.ts +++ b/test/mystery-encounter/encounter-test-utils.ts @@ -13,11 +13,10 @@ import { } from "#phases/mystery-encounter-phases"; import { VictoryPhase } from "#phases/victory-phase"; import type { GameManager } from "#test/test-utils/game-manager"; -import type { MessageUiHandler } from "#ui/handlers/message-ui-handler"; -import type { MysteryEncounterUiHandler } from "#ui/handlers/mystery-encounter-ui-handler"; -import type { PartyUiHandler } from "#ui/handlers/party-ui-handler"; +import type { MessageUiHandler } from "#ui/message-ui-handler"; +import type { MysteryEncounterUiHandler } from "#ui/mystery-encounter-ui-handler"; import type { OptionSelectUiHandler } from "#ui/option-select-ui-handler"; -import { isNullOrUndefined } from "#utils/common"; +import type { PartyUiHandler } from "#ui/party-ui-handler"; import { expect, vi } from "vitest"; /** @@ -71,7 +70,6 @@ export async function runMysteryEncounterToEnd( // If a battle is started, fast forward to end of the battle game.onNextPrompt("CommandPhase", UiMode.COMMAND, () => { game.scene.phaseManager.clearPhaseQueue(); - game.scene.phaseManager.clearPhaseQueueSplice(); game.scene.phaseManager.unshiftPhase(new VictoryPhase(0)); game.endPhase(); }); @@ -147,7 +145,7 @@ export async function runSelectMysteryEncounterOption( break; } - if (!isNullOrUndefined(secondaryOptionSelect?.pokemonNo)) { + if (secondaryOptionSelect?.pokemonNo != null) { await handleSecondaryOptionSelect(game, secondaryOptionSelect.pokemonNo, secondaryOptionSelect.optionNo); } else { uiHandler.processInput(Button.ACTION); @@ -174,7 +172,7 @@ async function handleSecondaryOptionSelect(game: GameManager, pokemonNo: number, partyUiHandler.processInput(Button.ACTION); // If there is a second choice to make after selecting a Pokemon - if (!isNullOrUndefined(optionNo)) { + if (optionNo != null) { // Wait for Summary menu to close and second options to spawn const secondOptionUiHandler = game.scene.ui.handlers[UiMode.OPTION_SELECT] as OptionSelectUiHandler; vi.spyOn(secondOptionUiHandler, "show"); @@ -197,7 +195,6 @@ async function handleSecondaryOptionSelect(game: GameManager, pokemonNo: number, */ export async function skipBattleRunMysteryEncounterRewardsPhase(game: GameManager, runRewardsPhase = true) { game.scene.phaseManager.clearPhaseQueue(); - game.scene.phaseManager.clearPhaseQueueSplice(); game.scene.getEnemyParty().forEach(p => { p.hp = 0; p.status = new Status(StatusEffect.FAINT); diff --git a/test/mystery-encounter/encounters/berries-abound-encounter.test.ts b/test/mystery-encounter/encounters/berries-abound-encounter.test.ts index 44585d4d795..5e9dffa1332 100644 --- a/test/mystery-encounter/encounters/berries-abound-encounter.test.ts +++ b/test/mystery-encounter/encounters/berries-abound-encounter.test.ts @@ -17,7 +17,7 @@ import { } from "#test/mystery-encounter/encounter-test-utils"; import { GameManager } from "#test/test-utils/game-manager"; import { initSceneWithoutEncounterPhase } from "#test/test-utils/game-manager-utils"; -import { ModifierSelectUiHandler } from "#ui/handlers/modifier-select-ui-handler"; +import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const namespace = "mysteryEncounters/berriesAbound"; diff --git a/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts b/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts index a3357b00b89..723516174fb 100644 --- a/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts +++ b/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts @@ -20,7 +20,7 @@ import { } from "#test/mystery-encounter/encounter-test-utils"; import { GameManager } from "#test/test-utils/game-manager"; import { initSceneWithoutEncounterPhase } from "#test/test-utils/game-manager-utils"; -import { ModifierSelectUiHandler } from "#ui/handlers/modifier-select-ui-handler"; +import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const namespace = "mysteryEncounters/bugTypeSuperfan"; diff --git a/test/mystery-encounter/encounters/clowning-around-encounter.test.ts b/test/mystery-encounter/encounters/clowning-around-encounter.test.ts index d199a331943..e7ec6e43392 100644 --- a/test/mystery-encounter/encounters/clowning-around-encounter.test.ts +++ b/test/mystery-encounter/encounters/clowning-around-encounter.test.ts @@ -29,8 +29,8 @@ import { } from "#test/mystery-encounter/encounter-test-utils"; import { GameManager } from "#test/test-utils/game-manager"; import { initSceneWithoutEncounterPhase } from "#test/test-utils/game-manager-utils"; -import type { PartyUiHandler } from "#ui/handlers/party-ui-handler"; import type { OptionSelectUiHandler } from "#ui/option-select-ui-handler"; +import type { PartyUiHandler } from "#ui/party-ui-handler"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; diff --git a/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts b/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts index bf42e6d4df3..81a2fc7463c 100644 --- a/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts +++ b/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts @@ -18,7 +18,7 @@ import { skipBattleRunMysteryEncounterRewardsPhase, } from "#test/mystery-encounter/encounter-test-utils"; import { GameManager } from "#test/test-utils/game-manager"; -import { ModifierSelectUiHandler } from "#ui/handlers/modifier-select-ui-handler"; +import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const namespace = "mysteryEncounters/dancingLessons"; diff --git a/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts b/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts index d6c566c4de6..c270429b394 100644 --- a/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts +++ b/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts @@ -11,7 +11,7 @@ import * as MysteryEncounters from "#mystery-encounters/mystery-encounters"; import { CIVILIZATION_ENCOUNTER_BIOMES } from "#mystery-encounters/mystery-encounters"; import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounter-test-utils"; import { GameManager } from "#test/test-utils/game-manager"; -import { ModifierSelectUiHandler } from "#ui/handlers/modifier-select-ui-handler"; +import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const namespace = "mysteryEncounters/departmentStoreSale"; diff --git a/test/mystery-encounter/encounters/field-trip-encounter.test.ts b/test/mystery-encounter/encounters/field-trip-encounter.test.ts index b8e6e36cf17..fd3e20012b1 100644 --- a/test/mystery-encounter/encounters/field-trip-encounter.test.ts +++ b/test/mystery-encounter/encounters/field-trip-encounter.test.ts @@ -12,7 +12,7 @@ import * as MysteryEncounters from "#mystery-encounters/mystery-encounters"; import { SelectModifierPhase } from "#phases/select-modifier-phase"; import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounter-test-utils"; import { GameManager } from "#test/test-utils/game-manager"; -import { ModifierSelectUiHandler } from "#ui/handlers/modifier-select-ui-handler"; +import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; import i18next from "i18next"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; diff --git a/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts b/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts index 34c0f635000..81dbad16e01 100644 --- a/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts +++ b/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts @@ -17,7 +17,7 @@ import { } from "#test/mystery-encounter/encounter-test-utils"; import { GameManager } from "#test/test-utils/game-manager"; import { initSceneWithoutEncounterPhase } from "#test/test-utils/game-manager-utils"; -import { ModifierSelectUiHandler } from "#ui/handlers/modifier-select-ui-handler"; +import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const namespace = "mysteryEncounters/fightOrFlight"; diff --git a/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts b/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts index 8a058bad5fe..bc1a2893627 100644 --- a/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts +++ b/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts @@ -22,7 +22,7 @@ import { } from "#test/mystery-encounter/encounter-test-utils"; import { GameManager } from "#test/test-utils/game-manager"; import { initSceneWithoutEncounterPhase } from "#test/test-utils/game-manager-utils"; -import { ModifierSelectUiHandler } from "#ui/handlers/modifier-select-ui-handler"; +import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const namespace = "mysteryEncounters/funAndGames"; diff --git a/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts b/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts index 90bc41fde2b..c8e934168bc 100644 --- a/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts +++ b/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts @@ -15,7 +15,7 @@ import * as MysteryEncounters from "#mystery-encounters/mystery-encounters"; import { CIVILIZATION_ENCOUNTER_BIOMES } from "#mystery-encounters/mystery-encounters"; import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounter-test-utils"; import { GameManager } from "#test/test-utils/game-manager"; -import { ModifierSelectUiHandler } from "#ui/handlers/modifier-select-ui-handler"; +import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; import * as Utils from "#utils/common"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; diff --git a/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts b/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts index ed8b6bffbe9..a8d91f8f101 100644 --- a/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts +++ b/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts @@ -20,7 +20,7 @@ import { GameManager } from "#test/test-utils/game-manager"; import { initSceneWithoutEncounterPhase } from "#test/test-utils/game-manager-utils"; import { TrainerConfig } from "#trainers/trainer-config"; import { TrainerPartyCompoundTemplate, TrainerPartyTemplate } from "#trainers/trainer-party-template"; -import { ModifierSelectUiHandler } from "#ui/handlers/modifier-select-ui-handler"; +import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const namespace = "mysteryEncounters/mysteriousChallengers"; diff --git a/test/mystery-encounter/encounters/part-timer-encounter.test.ts b/test/mystery-encounter/encounters/part-timer-encounter.test.ts index 1826c75381a..15d2664364c 100644 --- a/test/mystery-encounter/encounters/part-timer-encounter.test.ts +++ b/test/mystery-encounter/encounters/part-timer-encounter.test.ts @@ -168,6 +168,7 @@ describe("Part-Timer - Mystery Encounter", () => { // Override party levels to 50 so stats can be fully reflective scene.getPlayerParty().forEach(p => { p.level = 50; + p.ivs = [0, 0, 0, 0, 0, 0]; p.calculateStats(); }); await runMysteryEncounterToEnd(game, 2, { pokemonNo: 3 }); diff --git a/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts b/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts index a096ea5ff6e..c88d77a8cf5 100644 --- a/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts +++ b/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts @@ -16,7 +16,7 @@ import { } from "#test/mystery-encounter/encounter-test-utils"; import { GameManager } from "#test/test-utils/game-manager"; import { initSceneWithoutEncounterPhase } from "#test/test-utils/game-manager-utils"; -import { ModifierSelectUiHandler } from "#ui/handlers/modifier-select-ui-handler"; +import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; import i18next from "i18next"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; diff --git a/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts b/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts index 5df2d2ab358..06c4c3c1cee 100644 --- a/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts +++ b/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts @@ -24,7 +24,7 @@ import { } from "#test/mystery-encounter/encounter-test-utils"; import { GameManager } from "#test/test-utils/game-manager"; import { initSceneWithoutEncounterPhase } from "#test/test-utils/game-manager-utils"; -import { ModifierSelectUiHandler } from "#ui/handlers/modifier-select-ui-handler"; +import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; diff --git a/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts b/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts index 9b009879522..3bbb858a15d 100644 --- a/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts +++ b/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts @@ -20,7 +20,7 @@ import { VictoryPhase } from "#phases/victory-phase"; import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounter-test-utils"; import { GameManager } from "#test/test-utils/game-manager"; import { initSceneWithoutEncounterPhase } from "#test/test-utils/game-manager-utils"; -import { ModifierSelectUiHandler } from "#ui/handlers/modifier-select-ui-handler"; +import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -355,7 +355,6 @@ describe("The Winstrate Challenge - Mystery Encounter", () => { */ async function skipBattleToNextBattle(game: GameManager, isFinalBattle = false) { game.scene.phaseManager.clearPhaseQueue(); - game.scene.phaseManager.clearPhaseQueueSplice(); const commandUiHandler = game.scene.ui.handlers[UiMode.COMMAND]; commandUiHandler.clear(); game.scene.getEnemyParty().forEach(p => { diff --git a/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts b/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts index 6f6c01c7322..91a88712e9b 100644 --- a/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts +++ b/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts @@ -27,7 +27,7 @@ import { } from "#test/mystery-encounter/encounter-test-utils"; import { GameManager } from "#test/test-utils/game-manager"; import { initSceneWithoutEncounterPhase } from "#test/test-utils/game-manager-utils"; -import { ModifierSelectUiHandler } from "#ui/handlers/modifier-select-ui-handler"; +import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; import * as Utils from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; diff --git a/test/mystery-encounter/encounters/weird-dream-encounter.test.ts b/test/mystery-encounter/encounters/weird-dream-encounter.test.ts index 3640ed3809f..f00ef834624 100644 --- a/test/mystery-encounter/encounters/weird-dream-encounter.test.ts +++ b/test/mystery-encounter/encounters/weird-dream-encounter.test.ts @@ -16,7 +16,7 @@ import { } from "#test/mystery-encounter/encounter-test-utils"; import { GameManager } from "#test/test-utils/game-manager"; import { initSceneWithoutEncounterPhase } from "#test/test-utils/game-manager-utils"; -import { ModifierSelectUiHandler } from "#ui/handlers/modifier-select-ui-handler"; +import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const namespace = "mysteryEncounters/weirdDream"; diff --git a/test/phases/mystery-encounter-phase.test.ts b/test/phases/mystery-encounter-phase.test.ts index 8dfbd509a05..30ab977dbc6 100644 --- a/test/phases/mystery-encounter-phase.test.ts +++ b/test/phases/mystery-encounter-phase.test.ts @@ -5,8 +5,8 @@ import { SpeciesId } from "#enums/species-id"; import { UiMode } from "#enums/ui-mode"; import { MysteryEncounterOptionSelectedPhase } from "#phases/mystery-encounter-phases"; import { GameManager } from "#test/test-utils/game-manager"; -import type { MessageUiHandler } from "#ui/handlers/message-ui-handler"; -import type { MysteryEncounterUiHandler } from "#ui/handlers/mystery-encounter-ui-handler"; +import type { MessageUiHandler } from "#ui/message-ui-handler"; +import type { MysteryEncounterUiHandler } from "#ui/mystery-encounter-ui-handler"; import i18next from "i18next"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; diff --git a/test/phases/select-modifier-phase.test.ts b/test/phases/select-modifier-phase.test.ts index f6446c6e7be..c6f07088819 100644 --- a/test/phases/select-modifier-phase.test.ts +++ b/test/phases/select-modifier-phase.test.ts @@ -12,7 +12,7 @@ import { ModifierTypeOption } from "#modifiers/modifier-type"; import { SelectModifierPhase } from "#phases/select-modifier-phase"; import { GameManager } from "#test/test-utils/game-manager"; import { initSceneWithoutEncounterPhase } from "#test/test-utils/game-manager-utils"; -import { ModifierSelectUiHandler } from "#ui/handlers/modifier-select-ui-handler"; +import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; import { shiftCharCodes } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import Phaser from "phaser"; diff --git a/test/plugins/api/pokerogue-daily-api.test.ts b/test/plugins/api/pokerogue-daily-api.test.ts index b45896e6a2c..ef5dfddada5 100644 --- a/test/plugins/api/pokerogue-daily-api.test.ts +++ b/test/plugins/api/pokerogue-daily-api.test.ts @@ -2,7 +2,7 @@ import { PokerogueDailyApi } from "#api/pokerogue-daily-api"; import { initServerForApiTests } from "#test/test-utils/test-file-initialization"; import { getApiBaseUrl } from "#test/test-utils/test-utils"; import type { GetDailyRankingsPageCountRequest, GetDailyRankingsRequest } from "#types/api/pokerogue-daily-api"; -import { type RankingEntry, ScoreboardCategory } from "#ui/containers/daily-run-scoreboard"; +import { type RankingEntry, ScoreboardCategory } from "#ui/daily-run-scoreboard"; import { HttpResponse, http } from "msw"; import type { SetupServerApi } from "msw/node"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; diff --git a/test/plugins/api/pokerogue-session-savedata-api.test.ts b/test/plugins/api/pokerogue-session-savedata-api.test.ts index d7ee2703405..d91db4425cb 100644 --- a/test/plugins/api/pokerogue-session-savedata-api.test.ts +++ b/test/plugins/api/pokerogue-session-savedata-api.test.ts @@ -1,5 +1,4 @@ import { PokerogueSessionSavedataApi } from "#api/pokerogue-session-savedata-api"; -import type { SessionSaveData } from "#system/game-data"; import { initServerForApiTests } from "#test/test-utils/test-file-initialization"; import { getApiBaseUrl } from "#test/test-utils/test-utils"; import type { @@ -10,6 +9,7 @@ import type { NewClearSessionSavedataRequest, UpdateSessionSavedataRequest, } from "#types/api/pokerogue-session-save-data-api"; +import type { SessionSaveData } from "#types/save-data"; import { HttpResponse, http } from "msw"; import type { SetupServerApi } from "msw/node"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; diff --git a/test/plugins/api/pokerogue-system-savedata-api.test.ts b/test/plugins/api/pokerogue-system-savedata-api.test.ts index d6e4fe18eed..3480b00b206 100644 --- a/test/plugins/api/pokerogue-system-savedata-api.test.ts +++ b/test/plugins/api/pokerogue-system-savedata-api.test.ts @@ -1,5 +1,4 @@ import { PokerogueSystemSavedataApi } from "#api/pokerogue-system-savedata-api"; -import type { SystemSaveData } from "#system/game-data"; import { initServerForApiTests } from "#test/test-utils/test-file-initialization"; import { getApiBaseUrl } from "#test/test-utils/test-utils"; import type { @@ -8,6 +7,7 @@ import type { VerifySystemSavedataRequest, VerifySystemSavedataResponse, } from "#types/api/pokerogue-system-save-data-api"; +import type { SystemSaveData } from "#types/save-data"; import { HttpResponse, http } from "msw"; import type { SetupServerApi } from "msw/node"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; diff --git a/test/font-face.setup.ts b/test/setup/font-face.setup.ts similarity index 100% rename from test/font-face.setup.ts rename to test/setup/font-face.setup.ts diff --git a/test/matchers.setup.ts b/test/setup/matchers.setup.ts similarity index 82% rename from test/matchers.setup.ts rename to test/setup/matchers.setup.ts index fe2135f4db4..8ad14c8679a 100644 --- a/test/matchers.setup.ts +++ b/test/setup/matchers.setup.ts @@ -1,5 +1,11 @@ +/** + * Setup file for custom matchers. + * Make sure to define the call signatures in `#test/@types/vitest.d.ts` too! + * @module + */ + import { toBeAtPhase } from "#test/test-utils/matchers/to-be-at-phase"; -import { toEqualArrayUnsorted } from "#test/test-utils/matchers/to-equal-array-unsorted"; +import { toEqualUnsorted } from "#test/test-utils/matchers/to-equal-unsorted"; import { toHaveAbilityApplied } from "#test/test-utils/matchers/to-have-ability-applied"; import { toHaveArenaTag } from "#test/test-utils/matchers/to-have-arena-tag"; import { toHaveBattlerTag } from "#test/test-utils/matchers/to-have-battler-tag"; @@ -7,7 +13,9 @@ import { toHaveEffectiveStat } from "#test/test-utils/matchers/to-have-effective import { toHaveFainted } from "#test/test-utils/matchers/to-have-fainted"; import { toHaveFullHp } from "#test/test-utils/matchers/to-have-full-hp"; import { toHaveHp } from "#test/test-utils/matchers/to-have-hp"; +import { toHaveKey } from "#test/test-utils/matchers/to-have-key"; import { toHavePositionalTag } from "#test/test-utils/matchers/to-have-positional-tag"; +import { toHaveShownMessage } from "#test/test-utils/matchers/to-have-shown-message"; import { toHaveStatStage } from "#test/test-utils/matchers/to-have-stat-stage"; import { toHaveStatusEffect } from "#test/test-utils/matchers/to-have-status-effect"; import { toHaveTakenDamage } from "#test/test-utils/matchers/to-have-taken-damage"; @@ -18,13 +26,10 @@ import { toHaveUsedPP } from "#test/test-utils/matchers/to-have-used-pp"; import { toHaveWeather } from "#test/test-utils/matchers/to-have-weather"; import { expect } from "vitest"; -/* - * Setup file for custom matchers. - * Make sure to define the call signatures in `test/@types/vitest.d.ts` too! - */ - expect.extend({ - toEqualArrayUnsorted, + toEqualUnsorted, + toHaveKey, + toHaveShownMessage, toBeAtPhase, toHaveWeather, toHaveTerrain, diff --git a/test/vitest.setup.ts b/test/setup/vitest.setup.ts similarity index 93% rename from test/vitest.setup.ts rename to test/setup/vitest.setup.ts index 23adab01a05..3f506d73228 100644 --- a/test/vitest.setup.ts +++ b/test/setup/vitest.setup.ts @@ -35,8 +35,8 @@ vi.mock(import("i18next"), async importOriginal => { const filename = req.params[0]; try { - const localeFiles = import.meta.glob("../public/locales/en/**/*.json", { eager: true }); - const json = localeFiles[`../public/locales/en/${filename}`] || {}; + const localeFiles = import.meta.glob("../../public/locales/en/**/*.json", { eager: true }); + const json = localeFiles[`../../public/locales/en/${filename}`] || {}; if (import.meta.env.VITE_I18N_DEBUG === "1") { console.log("Loaded locale", filename); } diff --git a/test/system/game-data.test.ts b/test/system/game-data.test.ts index 18775f310b7..42a3ac339ce 100644 --- a/test/system/game-data.test.ts +++ b/test/system/game-data.test.ts @@ -3,8 +3,8 @@ import * as account from "#app/account"; import * as bypassLoginModule from "#app/global-vars/bypass-login"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; -import type { SessionSaveData } from "#system/game-data"; import { GameManager } from "#test/test-utils/game-manager"; +import type { SessionSaveData } from "#types/save-data"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; diff --git a/test/system/rename-run.test.ts b/test/system/rename-run.test.ts index 5031d84245f..038303c3254 100644 --- a/test/system/rename-run.test.ts +++ b/test/system/rename-run.test.ts @@ -1,10 +1,10 @@ import * as account from "#app/account"; import * as bypassLoginModule from "#app/global-vars/bypass-login"; import { pokerogueApi } from "#app/plugins/api/pokerogue-api"; -import type { SessionSaveData } from "#app/system/game-data"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { GameManager } from "#test/test-utils/game-manager"; +import type { SessionSaveData } from "#types/save-data"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; diff --git a/test/test-utils/game-manager-utils.ts b/test/test-utils/game-manager-utils.ts index e9c3e9809cc..4be05bf0ddb 100644 --- a/test/test-utils/game-manager-utils.ts +++ b/test/test-utils/game-manager-utils.ts @@ -8,8 +8,7 @@ import { GameModes } from "#enums/game-modes"; import type { MoveId } from "#enums/move-id"; import type { SpeciesId } from "#enums/species-id"; import { PlayerPokemon } from "#field/pokemon"; -import type { StarterMoveset } from "#system/game-data"; -import type { Starter } from "#ui/handlers/starter-select-ui-handler"; +import type { Starter, StarterMoveset } from "#types/save-data"; import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils"; /** Function to convert Blob to string */ @@ -33,24 +32,24 @@ export function holdOn(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)); } -export function generateStarter(scene: BattleScene, species?: SpeciesId[]): Starter[] { +export function generateStarters(scene: BattleScene, speciesIds?: SpeciesId[]): Starter[] { const seed = "test"; - const starters = getTestRunStarters(seed, species); + const starters = getTestRunStarters(seed, speciesIds); const startingLevel = scene.gameMode.getStartingLevel(); for (const starter of starters) { - const starterProps = scene.gameData.getSpeciesDexAttrProps(starter.species, starter.dexAttr); - const starterFormIndex = Math.min(starterProps.formIndex, Math.max(starter.species.forms.length - 1, 0)); + const species = getPokemonSpecies(starter.speciesId); + const starterFormIndex = starter.formIndex; const starterGender = - starter.species.malePercent !== null ? (!starterProps.female ? Gender.MALE : Gender.FEMALE) : Gender.GENDERLESS; + species.malePercent !== null ? (starter.female ? Gender.FEMALE : Gender.MALE) : Gender.GENDERLESS; const starterPokemon = scene.addPlayerPokemon( - starter.species, + species, startingLevel, starter.abilityIndex, starterFormIndex, starterGender, - starterProps.shiny, - starterProps.variant, - undefined, + starter.shiny, + starter.variant, + starter.ivs, starter.nature, ); const moveset: MoveId[] = []; @@ -62,20 +61,23 @@ export function generateStarter(scene: BattleScene, species?: SpeciesId[]): Star return starters; } -function getTestRunStarters(seed: string, species?: SpeciesId[]): Starter[] { - if (!species) { +function getTestRunStarters(seed: string, speciesIds?: SpeciesId[]): Starter[] { + if (!speciesIds || speciesIds.length === 0) { return getDailyRunStarters(seed); } const starters: Starter[] = []; const startingLevel = getGameMode(GameModes.CLASSIC).getStartingLevel(); - for (const specie of species) { - const starterSpeciesForm = getPokemonSpeciesForm(specie, 0); + for (const speciesId of speciesIds) { + const starterSpeciesForm = getPokemonSpeciesForm(speciesId, 0); const starterSpecies = getPokemonSpecies(starterSpeciesForm.speciesId); const pokemon = new PlayerPokemon(starterSpecies, startingLevel, undefined, 0); const starter: Starter = { - species: starterSpecies, - dexAttr: pokemon.getDexAttr(), + speciesId, + shiny: pokemon.shiny, + variant: pokemon.variant, + formIndex: pokemon.formIndex, + ivs: pokemon.ivs, abilityIndex: pokemon.abilityIndex, passive: false, nature: pokemon.getNature(), @@ -89,22 +91,20 @@ function getTestRunStarters(seed: string, species?: SpeciesId[]): Starter[] { /** * Useful for populating party, wave index, etc. without having to spin up and run through an entire EncounterPhase */ -export function initSceneWithoutEncounterPhase(scene: BattleScene, species?: SpeciesId[]): void { - const starters = generateStarter(scene, species); +export function initSceneWithoutEncounterPhase(scene: BattleScene, speciesIds?: SpeciesId[]): void { + const starters = generateStarters(scene, speciesIds); starters.forEach(starter => { - const starterProps = scene.gameData.getSpeciesDexAttrProps(starter.species, starter.dexAttr); - const starterFormIndex = Math.min(starterProps.formIndex, Math.max(starter.species.forms.length - 1, 0)); + const starterFormIndex = starter.formIndex; const starterGender = Gender.MALE; - const starterIvs = scene.gameData.dexData[starter.species.speciesId].ivs.slice(0); const starterPokemon = scene.addPlayerPokemon( - starter.species, + getPokemonSpecies(starter.speciesId), scene.gameMode.getStartingLevel(), starter.abilityIndex, starterFormIndex, starterGender, - starterProps.shiny, - starterProps.variant, - starterIvs, + starter.shiny, + starter.variant, + starter.ivs, starter.nature, ); starter.moveset && starterPokemon.tryPopulateMoveset(starter.moveset); diff --git a/test/test-utils/game-manager.ts b/test/test-utils/game-manager.ts index 6b7c805df74..abe0b8cfcf6 100644 --- a/test/test-utils/game-manager.ts +++ b/test/test-utils/game-manager.ts @@ -29,7 +29,7 @@ import { TurnEndPhase } from "#phases/turn-end-phase"; import { TurnInitPhase } from "#phases/turn-init-phase"; import { TurnStartPhase } from "#phases/turn-start-phase"; import { ErrorInterceptor } from "#test/test-utils/error-interceptor"; -import { generateStarter } from "#test/test-utils/game-manager-utils"; +import { generateStarters } from "#test/test-utils/game-manager-utils"; import { GameWrapper } from "#test/test-utils/game-wrapper"; import { ChallengeModeHelper } from "#test/test-utils/helpers/challenge-mode-helper"; import { ClassicModeHelper } from "#test/test-utils/helpers/classic-mode-helper"; @@ -45,14 +45,13 @@ import { MockFetch } from "#test/test-utils/mocks/mock-fetch"; import { PhaseInterceptor } from "#test/test-utils/phase-interceptor"; import { TextInterceptor } from "#test/test-utils/text-interceptor"; import type { PhaseClass, PhaseString } from "#types/phase-types"; -import type { BallUiHandler } from "#ui/handlers/ball-ui-handler"; -import type { BattleMessageUiHandler } from "#ui/handlers/battle-message-ui-handler"; -import type { CommandUiHandler } from "#ui/handlers/command-ui-handler"; -import type { ModifierSelectUiHandler } from "#ui/handlers/modifier-select-ui-handler"; -import type { PartyUiHandler } from "#ui/handlers/party-ui-handler"; -import type { StarterSelectUiHandler } from "#ui/handlers/starter-select-ui-handler"; -import type { TargetSelectUiHandler } from "#ui/handlers/target-select-ui-handler"; -import { isNullOrUndefined } from "#utils/common"; +import type { BallUiHandler } from "#ui/ball-ui-handler"; +import type { BattleMessageUiHandler } from "#ui/battle-message-ui-handler"; +import type { CommandUiHandler } from "#ui/command-ui-handler"; +import type { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; +import type { PartyUiHandler } from "#ui/party-ui-handler"; +import type { StarterSelectUiHandler } from "#ui/starter-select-ui-handler"; +import type { TargetSelectUiHandler } from "#ui/target-select-ui-handler"; import fs from "node:fs"; import { AES, enc } from "crypto-js"; import { expect, vi } from "vitest"; @@ -216,7 +215,7 @@ export class GameManager { this.onNextPrompt("TitlePhase", UiMode.TITLE, () => { this.scene.gameMode = getGameMode(mode); - const starters = generateStarter(this.scene, species); + const starters = generateStarters(this.scene, species); const selectStarterPhase = new SelectStarterPhase(); this.scene.phaseManager.pushPhase(new EncounterPhase(false)); selectStarterPhase.initBattle(starters); @@ -240,7 +239,7 @@ export class GameManager { * @returns A Promise that resolves when the EncounterPhase ends. */ async runToMysteryEncounter(encounterType?: MysteryEncounterType, species?: SpeciesId[]) { - if (!isNullOrUndefined(encounterType)) { + if (encounterType != null) { this.override.disableTrainerWaves(); this.override.mysteryEncounter(encounterType); } @@ -252,7 +251,7 @@ export class GameManager { UiMode.TITLE, () => { this.scene.gameMode = getGameMode(GameModes.CLASSIC); - const starters = generateStarter(this.scene, species); + const starters = generateStarters(this.scene, species); const selectStarterPhase = new SelectStarterPhase(); this.scene.phaseManager.pushPhase(new EncounterPhase(false)); selectStarterPhase.initBattle(starters); @@ -272,7 +271,7 @@ export class GameManager { ); await this.phaseInterceptor.to("EncounterPhase"); - if (!isNullOrUndefined(encounterType)) { + if (encounterType != null) { expect(this.scene.currentBattle?.mysteryEncounter?.encounterType).toBe(encounterType); } } @@ -465,6 +464,9 @@ export class GameManager { * Faint a player or enemy pokemon instantly by setting their HP to 0. * @param pokemon - The player/enemy pokemon being fainted * @returns A Promise that resolves once the fainted pokemon's FaintPhase finishes running. + * @remarks + * This method *pushes* a FaintPhase and runs until it's finished. This may cause a turn to play out unexpectedly + * @todo Consider whether running the faint phase immediately can be done */ async killPokemon(pokemon: PlayerPokemon | EnemyPokemon) { pokemon.hp = 0; @@ -534,7 +536,7 @@ export class GameManager { } /** - * Intercepts `TurnStartPhase` and mocks {@linkcode TurnStartPhase.getSpeedOrder}'s return value. + * Modifies the queue manager to return move phases in a particular order * Used to manually modify Pokemon turn order. * Note: This *DOES NOT* account for priority. * @param order - The turn order to set as an array of {@linkcode BattlerIndex}es. @@ -546,7 +548,7 @@ export class GameManager { async setTurnOrder(order: BattlerIndex[]): Promise { await this.phaseInterceptor.to("TurnStartPhase", false); - vi.spyOn(this.scene.phaseManager.getCurrentPhase() as TurnStartPhase, "getSpeedOrder").mockReturnValue(order); + this.scene.phaseManager.dynamicQueueManager.setMoveOrder(order); } /** diff --git a/test/test-utils/game-wrapper.ts b/test/test-utils/game-wrapper.ts index 166f4b8de7a..d18ea9301ea 100644 --- a/test/test-utils/game-wrapper.ts +++ b/test/test-utils/game-wrapper.ts @@ -12,7 +12,7 @@ import { MockLoader } from "#test/test-utils/mocks/mock-loader"; import { MockTextureManager } from "#test/test-utils/mocks/mock-texture-manager"; import { MockTimedEventManager } from "#test/test-utils/mocks/mock-timed-event-manager"; import { MockContainer } from "#test/test-utils/mocks/mocks-container/mock-container"; -import { PokedexMonContainer } from "#ui/containers/pokedex-mon-container"; +import { PokedexMonContainer } from "#ui/pokedex-mon-container"; import fs from "node:fs"; import Phaser from "phaser"; import { vi } from "vitest"; diff --git a/test/test-utils/helpers/challenge-mode-helper.ts b/test/test-utils/helpers/challenge-mode-helper.ts index 1b530bb2987..bee3be94dca 100644 --- a/test/test-utils/helpers/challenge-mode-helper.ts +++ b/test/test-utils/helpers/challenge-mode-helper.ts @@ -9,7 +9,7 @@ import { CommandPhase } from "#phases/command-phase"; import { EncounterPhase } from "#phases/encounter-phase"; import { SelectStarterPhase } from "#phases/select-starter-phase"; import { TurnInitPhase } from "#phases/turn-init-phase"; -import { generateStarter } from "#test/test-utils/game-manager-utils"; +import { generateStarters } from "#test/test-utils/game-manager-utils"; import { GameManagerHelper } from "#test/test-utils/helpers/game-manager-helper"; type challengeStub = { id: Challenges; value: number; severity: number }; @@ -33,11 +33,11 @@ export class ChallengeModeHelper extends GameManagerHelper { /** * Runs the challenge game to the summon phase. - * @param species - An array of {@linkcode Species} to summon. + * @param speciesIds - An array of {@linkcode Species} to summon. * @returns A promise that resolves when the summon phase is reached. * @todo This duplicates all but 1 line of code from the classic mode variant... */ - async runToSummon(species: SpeciesId[]): Promise; + async runToSummon(speciesIds: SpeciesId[]): Promise; /** * Runs the challenge game to the summon phase. * Selects 3 daily run starters with a fixed seed of "test" @@ -46,9 +46,10 @@ export class ChallengeModeHelper extends GameManagerHelper { * @deprecated - Specifying the starters helps prevent inconsistencies from internal RNG changes. * @todo This duplicates all but 1 line of code from the classic mode variant... */ + // biome-ignore lint/style/useUnifiedTypeSignatures: Marks for deprecation async runToSummon(): Promise; - async runToSummon(species?: SpeciesId[]): Promise; - async runToSummon(species?: SpeciesId[]): Promise { + async runToSummon(speciesIds?: SpeciesId[]): Promise; + async runToSummon(speciesIds?: SpeciesId[]): Promise { await this.game.runToTitle(); if (this.game.override.disableShinies) { @@ -57,7 +58,7 @@ export class ChallengeModeHelper extends GameManagerHelper { this.game.onNextPrompt("TitlePhase", UiMode.TITLE, () => { this.game.scene.gameMode.challenges = this.challenges; - const starters = generateStarter(this.game.scene, species); + const starters = generateStarters(this.game.scene, speciesIds); const selectStarterPhase = new SelectStarterPhase(); this.game.scene.phaseManager.pushPhase(new EncounterPhase(false)); selectStarterPhase.initBattle(starters); @@ -84,6 +85,7 @@ export class ChallengeModeHelper extends GameManagerHelper { * @deprecated - Specifying the starters helps prevent inconsistencies from internal RNG changes. * @todo This duplicates all its code with the classic mode variant... */ + // biome-ignore lint/style/useUnifiedTypeSignatures: Marks for deprecation async startBattle(): Promise; async startBattle(species?: SpeciesId[]) { await this.runToSummon(species); diff --git a/test/test-utils/helpers/classic-mode-helper.ts b/test/test-utils/helpers/classic-mode-helper.ts index f813a8f797e..896de7a8b6f 100644 --- a/test/test-utils/helpers/classic-mode-helper.ts +++ b/test/test-utils/helpers/classic-mode-helper.ts @@ -10,7 +10,7 @@ import { CommandPhase } from "#phases/command-phase"; import { EncounterPhase } from "#phases/encounter-phase"; import { SelectStarterPhase } from "#phases/select-starter-phase"; import { TurnInitPhase } from "#phases/turn-init-phase"; -import { generateStarter } from "#test/test-utils/game-manager-utils"; +import { generateStarters } from "#test/test-utils/game-manager-utils"; import { GameManagerHelper } from "#test/test-utils/helpers/game-manager-helper"; /** @@ -35,7 +35,7 @@ export class ClassicModeHelper extends GameManagerHelper { // biome-ignore lint/style/useUnifiedTypeSignatures: Marks the overload for deprecation async runToSummon(): Promise; async runToSummon(species: SpeciesId[] | undefined): Promise; - async runToSummon(species?: SpeciesId[]): Promise { + async runToSummon(speciesIds?: SpeciesId[]): Promise { await this.game.runToTitle(); if (this.game.override.disableShinies) { @@ -50,7 +50,7 @@ export class ClassicModeHelper extends GameManagerHelper { this.game.onNextPrompt("TitlePhase", UiMode.TITLE, () => { this.game.scene.gameMode = getGameMode(GameModes.CLASSIC); - const starters = generateStarter(this.game.scene, species); + const starters = generateStarters(this.game.scene, speciesIds); const selectStarterPhase = new SelectStarterPhase(); this.game.scene.phaseManager.pushPhase(new EncounterPhase(false)); selectStarterPhase.initBattle(starters); diff --git a/test/test-utils/helpers/daily-mode-helper.ts b/test/test-utils/helpers/daily-mode-helper.ts index 4b2ea8c5cf2..94b1e671b4b 100644 --- a/test/test-utils/helpers/daily-mode-helper.ts +++ b/test/test-utils/helpers/daily-mode-helper.ts @@ -7,7 +7,7 @@ import { EncounterPhase } from "#phases/encounter-phase"; import { TitlePhase } from "#phases/title-phase"; import { TurnInitPhase } from "#phases/turn-init-phase"; import { GameManagerHelper } from "#test/test-utils/helpers/game-manager-helper"; -import type { SaveSlotSelectUiHandler } from "#ui/handlers/save-slot-select-ui-handler"; +import type { SaveSlotSelectUiHandler } from "#ui/save-slot-select-ui-handler"; /** * Helper to handle daily mode specifics diff --git a/test/test-utils/helpers/modifiers-helper.ts b/test/test-utils/helpers/modifiers-helper.ts index bfda35427fa..7d3e29c420f 100644 --- a/test/test-utils/helpers/modifiers-helper.ts +++ b/test/test-utils/helpers/modifiers-helper.ts @@ -40,10 +40,7 @@ export class ModifierHelper extends GameManagerHelper { * @returns `this` */ testCheck(modifier: ModifierTypeKeys, expectToBePreset: boolean): this { - if (expectToBePreset) { - expect(itemPoolChecks.get(modifier)).toBeTruthy(); - } - expect(itemPoolChecks.get(modifier)).toBeFalsy(); + (expectToBePreset ? expect(itemPoolChecks) : expect(itemPoolChecks).not).toHaveKey(modifier); return this; } diff --git a/test/test-utils/helpers/overrides-helper.ts b/test/test-utils/helpers/overrides-helper.ts index 07ea1ea3d09..da0d75bf564 100644 --- a/test/test-utils/helpers/overrides-helper.ts +++ b/test/test-utils/helpers/overrides-helper.ts @@ -51,7 +51,8 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the starting biome - * @warning Any event listeners that are attached to {@linkcode NewArenaEvent} may need to be handled down the line + * + * ⚠️ Any event listeners that are attached to {@linkcode NewArenaEvent} may need to be handled down the line * @param biome - The biome to set */ public startingBiome(biome: BiomeId): this { @@ -341,7 +342,11 @@ export class OverridesHelper extends GameManagerHelper { /** * Force random critical hit rolls to always or never suceed. * @param crits - `true` to guarantee crits on eligible moves, `false` to force rolls to fail, `null` to disable override - * @remarks This does not bypass effects that guarantee or block critical hits; it merely mocks the chance-based rolls. + * @remarks + * This does not change any effects that guarantee or block critical hits; + * it merely mocks any chance-based rolls not already at 100%. \ + * For instance, a Pokemon at +3 crit stages will still critically hit with the override set to `false`, + * whereas one at +2 crit stages (a 50% chance) will not. * @returns `this` */ public criticalHits(crits: boolean | null): this { diff --git a/test/test-utils/helpers/reload-helper.ts b/test/test-utils/helpers/reload-helper.ts index fa627019483..e46096f3fab 100644 --- a/test/test-utils/helpers/reload-helper.ts +++ b/test/test-utils/helpers/reload-helper.ts @@ -3,9 +3,9 @@ import { UiMode } from "#enums/ui-mode"; import { CommandPhase } from "#phases/command-phase"; import { TitlePhase } from "#phases/title-phase"; import { TurnInitPhase } from "#phases/turn-init-phase"; -import type { SessionSaveData } from "#system/game-data"; import type { GameManager } from "#test/test-utils/game-manager"; import { GameManagerHelper } from "#test/test-utils/helpers/game-manager-helper"; +import type { SessionSaveData } from "#types/save-data"; import { vi } from "vitest"; /** diff --git a/test/test-utils/matchers/to-equal-array-unsorted.ts b/test/test-utils/matchers/to-equal-unsorted.ts similarity index 92% rename from test/test-utils/matchers/to-equal-array-unsorted.ts rename to test/test-utils/matchers/to-equal-unsorted.ts index 97398689032..c3d85288815 100644 --- a/test/test-utils/matchers/to-equal-array-unsorted.ts +++ b/test/test-utils/matchers/to-equal-unsorted.ts @@ -8,11 +8,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; * @param expected - The array to check equality with * @returns Whether the matcher passed */ -export function toEqualArrayUnsorted( - this: MatcherState, - received: unknown, - expected: unknown[], -): SyncExpectationResult { +export function toEqualUnsorted(this: MatcherState, received: unknown, expected: unknown[]): SyncExpectationResult { if (!Array.isArray(received)) { return { pass: this.isNot, diff --git a/test/test-utils/matchers/to-have-key.ts b/test/test-utils/matchers/to-have-key.ts new file mode 100644 index 00000000000..73d442fc979 --- /dev/null +++ b/test/test-utils/matchers/to-have-key.ts @@ -0,0 +1,47 @@ +import { getOnelineDiffStr } from "#test/test-utils/string-utils"; +import { receivedStr } from "#test/test-utils/test-utils"; +import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; + +/** + * Matcher that checks if a {@linkcode Map} contains the given key, regardless of its value. + * @param received - The received value. Should be a Map + * @param expectedKey - The key whose inclusion in the map is being checked + * @returns Whether the matcher passed + */ +export function toHaveKey(this: MatcherState, received: unknown, expectedKey: unknown): SyncExpectationResult { + if (!(received instanceof Map)) { + return { + pass: this.isNot, + message: () => `Expected to receive a Map, but got ${receivedStr(received)}!`, + }; + } + + if (received.size === 0) { + return { + pass: this.isNot, + message: () => "Expected to receive a non-empty Map, but received map was empty!", + expected: expectedKey, + actual: received, + }; + } + + const keys = [...received.keys()]; + const pass = this.equals(keys, expectedKey, [ + ...this.customTesters, + this.utils.iterableEquality, + this.utils.subsetEquality, + ]); + + const actualStr = getOnelineDiffStr.call(this, received); + const expectedStr = getOnelineDiffStr.call(this, expectedKey); + + return { + pass, + message: () => + pass + ? `Expected ${actualStr} to NOT have the key ${expectedStr}, but it did!` + : `Expected ${actualStr} to have the key ${expectedStr}, but it didn't!`, + expected: expectedKey, + actual: keys, + }; +} diff --git a/test/test-utils/matchers/to-have-shown-message.ts b/test/test-utils/matchers/to-have-shown-message.ts new file mode 100644 index 00000000000..bf5576ee630 --- /dev/null +++ b/test/test-utils/matchers/to-have-shown-message.ts @@ -0,0 +1,43 @@ +// biome-ignore lint/correctness/noUnusedImports: TSDoc +import type { GameManager } from "#test/test-utils/game-manager"; +import { isGameManagerInstance, receivedStr } from "#test/test-utils/test-utils"; +import { truncateString } from "#utils/common"; +import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; + +/** + * Matcher to check if the {@linkcode GameManager} has shown the given message at least once. + * @param received - The object to check. Should be the current {@linkcode GameManager}. + * @param expectedMessage - The expected message + * @returns The result of the matching + */ +export function toHaveShownMessage( + this: MatcherState, + received: unknown, + expectedMessage: string, +): SyncExpectationResult { + if (!isGameManagerInstance(received)) { + return { + pass: this.isNot, + message: () => `Expected to receive a GameManager, but got ${receivedStr(received)}!`, + }; + } + + if (!received.textInterceptor) { + return { + pass: this.isNot, + message: () => "Expected GameManager.TextInterceptor to be defined!", + }; + } + + // Pass if any of the matching tags meet our criteria + const pass = received.textInterceptor.logs.includes(expectedMessage); + return { + pass, + message: () => + pass + ? `Expected the GameManager to NOT have shown the message ${truncateString(expectedMessage, 30)}, but it did!` + : `Expected the GameManager to have shown the message ${truncateString(expectedMessage, 30)}, but it didn't!`, + expected: expectedMessage, + actual: received.textInterceptor.logs, + }; +} diff --git a/test/test-utils/matchers/to-have-terrain.ts b/test/test-utils/matchers/to-have-terrain.ts index f951abed0b3..9b6939168f0 100644 --- a/test/test-utils/matchers/to-have-terrain.ts +++ b/test/test-utils/matchers/to-have-terrain.ts @@ -8,8 +8,8 @@ import { isGameManagerInstance, receivedStr } from "#test/test-utils/test-utils" import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; /** - * Matcher that checks if the {@linkcode TerrainType} is as expected - * @param received - The object to check. Should be an instance of {@linkcode GameManager}. + * Matcher that checks if the current {@linkcode TerrainType} is as expected. + * @param received - The object to check. Should be the current {@linkcode GameManager}. * @param expectedTerrainType - The expected {@linkcode TerrainType}, or {@linkcode TerrainType.NONE} if no terrain should be active * @returns Whether the matcher passed */ diff --git a/test/test-utils/matchers/to-have-weather.ts b/test/test-utils/matchers/to-have-weather.ts index ffb1e0aad97..7604cd5f890 100644 --- a/test/test-utils/matchers/to-have-weather.ts +++ b/test/test-utils/matchers/to-have-weather.ts @@ -8,8 +8,8 @@ import { toTitleCase } from "#utils/strings"; import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; /** - * Matcher that checks if the {@linkcode WeatherType} is as expected - * @param received - The object to check. Expects an instance of {@linkcode GameManager}. + * Matcher that checks if the current {@linkcode WeatherType} is as expected. + * @param received - The object to check. Should be the current {@linkcode GameManager} * @param expectedWeatherType - The expected {@linkcode WeatherType} * @returns Whether the matcher passed */ diff --git a/test/test-utils/phase-interceptor.ts b/test/test-utils/phase-interceptor.ts index 4ac5e1150e5..6c7b5bf3033 100644 --- a/test/test-utils/phase-interceptor.ts +++ b/test/test-utils/phase-interceptor.ts @@ -67,7 +67,7 @@ import { UnlockPhase } from "#phases/unlock-phase"; import { VictoryPhase } from "#phases/victory-phase"; import { ErrorInterceptor } from "#test/test-utils/error-interceptor"; import type { PhaseClass, PhaseString } from "#types/phase-types"; -import type { AwaitableUiHandler } from "#ui/handlers/awaitable-ui-handler"; +import type { AwaitableUiHandler } from "#ui/awaitable-ui-handler"; import { UI } from "#ui/ui"; export interface PromptHandler { @@ -111,8 +111,8 @@ export class PhaseInterceptor { private intervalRun: NodeJS.Timeout; private prompts: PromptHandler[]; private inProgress?: InProgressStub; - private originalSetMode: UI["setMode"]; - private originalSuperEnd: Phase["end"]; + private originalSetMode: typeof UI.prototype.setMode; + private originalSuperEnd: typeof Phase.prototype.end; /** * List of phases with their corresponding start methods. diff --git a/test/test-utils/setup/test-end-log.ts b/test/test-utils/setup/test-end-log.ts index 9814ba8a45c..5be8299b124 100644 --- a/test/test-utils/setup/test-end-log.ts +++ b/test/test-utils/setup/test-end-log.ts @@ -1,16 +1,16 @@ +/** + * Code to add markers to the beginning and end of tests. + * Intended for use with {@linkcode CustomDefaultReporter}, and placed inside test hooks + * (rather than as part of the reporter) to ensure Vitest waits for the log messages to be printed. + * @module + */ + // biome-ignore lint/correctness/noUnusedImports: TSDoc import type CustomDefaultReporter from "#test/test-utils/reporters/custom-default-reporter"; import { basename, join, relative } from "path"; import chalk from "chalk"; import type { RunnerTask, RunnerTaskResult, RunnerTestCase } from "vitest"; -/** - * @module - * Code to add markers to the beginning and end of tests. - * Intended for use with {@linkcode CustomDefaultReporter}, and placed inside test hooks - * (rather than as part of the reporter) to ensure Vitest waits for the log messages to be printed. - */ - /** A long string of "="s to partition off each test from one another. */ const TEST_END_BARRIER = chalk.bold.hex("#ff7c7cff")("=================="); diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index b9e73c3e9da..fe8a8f3cf8a 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -68,7 +68,7 @@ function isObject(received: unknown): received is object { /** * Helper function to check if a given object is a {@linkcode Pokemon}. * @param received - The object to check - * @return Whether `received` is a {@linkcode Pokemon} instance. + * @returns Whether `received` is a {@linkcode Pokemon} instance. */ export function isPokemonInstance(received: unknown): received is Pokemon { return isObject(received) && received instanceof Pokemon; diff --git a/test/ui/item-manage-button.test.ts b/test/ui/item-manage-button.test.ts index b5c24776e7b..c28cd9e802e 100644 --- a/test/ui/item-manage-button.test.ts +++ b/test/ui/item-manage-button.test.ts @@ -5,8 +5,8 @@ import { SpeciesId } from "#enums/species-id"; import { UiMode } from "#enums/ui-mode"; import type { Pokemon } from "#field/pokemon"; import { GameManager } from "#test/test-utils/game-manager"; -import type { ModifierSelectUiHandler } from "#ui/handlers/modifier-select-ui-handler"; -import { type PartyUiHandler, PartyUiMode } from "#ui/handlers/party-ui-handler"; +import type { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; +import { type PartyUiHandler, PartyUiMode } from "#ui/party-ui-handler"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/test/ui/pokedex.test.ts b/test/ui/pokedex.test.ts index 47463fa1aab..6b84b253260 100644 --- a/test/ui/pokedex.test.ts +++ b/test/ui/pokedex.test.ts @@ -6,11 +6,11 @@ import { DropDownColumn } from "#enums/drop-down-column"; import { PokemonType } from "#enums/pokemon-type"; import { SpeciesId } from "#enums/species-id"; import { UiMode } from "#enums/ui-mode"; -import type { StarterAttributes } from "#system/game-data"; import { GameManager } from "#test/test-utils/game-manager"; -import { FilterTextRow } from "#ui/containers/filter-text"; -import { PokedexPageUiHandler } from "#ui/containers/pokedex-page-ui-handler"; -import { PokedexUiHandler } from "#ui/handlers/pokedex-ui-handler"; +import type { StarterAttributes } from "#types/save-data"; +import { FilterTextRow } from "#ui/filter-text"; +import { PokedexPageUiHandler } from "#ui/pokedex-page-ui-handler"; +import { PokedexUiHandler } from "#ui/pokedex-ui-handler"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; diff --git a/test/ui/starter-select.test.ts b/test/ui/starter-select.test.ts index 2f575b72a5c..397f3d6086f 100644 --- a/test/ui/starter-select.test.ts +++ b/test/ui/starter-select.test.ts @@ -8,10 +8,10 @@ import { SpeciesId } from "#enums/species-id"; import { UiMode } from "#enums/ui-mode"; import type { TitlePhase } from "#phases/title-phase"; import { GameManager } from "#test/test-utils/game-manager"; -import type { OptionSelectItem } from "#ui/handlers/abstract-option-select-ui-handler"; -import type { SaveSlotSelectUiHandler } from "#ui/handlers/save-slot-select-ui-handler"; -import type { StarterSelectUiHandler } from "#ui/handlers/starter-select-ui-handler"; +import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; import type { OptionSelectUiHandler } from "#ui/option-select-ui-handler"; +import type { SaveSlotSelectUiHandler } from "#ui/save-slot-select-ui-handler"; +import type { StarterSelectUiHandler } from "#ui/starter-select-ui-handler"; import i18next from "i18next"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/test/ui/transfer-item-options.test.ts b/test/ui/transfer-item-options.test.ts index 7e9c1b5e36b..901aa261f50 100644 --- a/test/ui/transfer-item-options.test.ts +++ b/test/ui/transfer-item-options.test.ts @@ -4,8 +4,8 @@ import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { UiMode } from "#enums/ui-mode"; import { GameManager } from "#test/test-utils/game-manager"; -import { type PartyUiHandler, PartyUiMode } from "#ui/handlers/party-ui-handler"; -import type { RenameFormUiHandler } from "#ui/handlers/rename-form-ui-handler"; +import { type PartyUiHandler, PartyUiMode } from "#ui/party-ui-handler"; +import type { RenameFormUiHandler } from "#ui/rename-form-ui-handler"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/test/ui/transfer-item.test.ts b/test/ui/transfer-item.test.ts index 67a21b0656d..c04c16623e3 100644 --- a/test/ui/transfer-item.test.ts +++ b/test/ui/transfer-item.test.ts @@ -4,8 +4,8 @@ import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { UiMode } from "#enums/ui-mode"; import { GameManager } from "#test/test-utils/game-manager"; -import { ModifierSelectUiHandler } from "#ui/handlers/modifier-select-ui-handler"; -import { PartyUiHandler, PartyUiMode } from "#ui/handlers/party-ui-handler"; +import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; +import { PartyUiHandler, PartyUiMode } from "#ui/party-ui-handler"; import Phaser from "phaser"; import type BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/test/ui/type-hints.test.ts b/test/ui/type-hints.test.ts index 56891e22c2a..b5fe0d9585a 100644 --- a/test/ui/type-hints.test.ts +++ b/test/ui/type-hints.test.ts @@ -4,7 +4,7 @@ import { SpeciesId } from "#enums/species-id"; import { UiMode } from "#enums/ui-mode"; import { GameManager } from "#test/test-utils/game-manager"; import type { MockText } from "#test/test-utils/mocks/mocks-container/mock-text"; -import { FightUiHandler } from "#ui/handlers/fight-ui-handler"; +import { FightUiHandler } from "#ui/fight-ui-handler"; import i18next from "i18next"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; diff --git a/test/utils/cookies.test.ts b/test/utils/cookies.test.ts new file mode 100644 index 00000000000..a5ea248c236 --- /dev/null +++ b/test/utils/cookies.test.ts @@ -0,0 +1,62 @@ +import { getCookie } from "#utils/cookies"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Unit Tests - cookies.ts", () => { + describe("getCookie", () => { + const cookieStart = document.cookie; + beforeEach(() => { + // clear cookie before each test + document.cookie = ""; + }); + + afterEach(() => { + // restore original cookie after each test + document.cookie = cookieStart; + }); + /** + * Spies on `document.cookie` and replaces its value with the provided string. + */ + function setDocumentCookie(value: string) { + vi.spyOn(document, "cookie", "get").mockReturnValue(value); + } + it("returns the value of a single cookie", () => { + setDocumentCookie("foo=bar"); + expect(getCookie("foo")).toBe("bar"); + }); + + it("returns empty string if cookie is not found", () => { + setDocumentCookie("foo=bar"); + expect(getCookie("baz")).toBe(""); + }); + + it("returns the value when multiple cookies exist", () => { + setDocumentCookie("foo=bar; baz=qux"); + expect(getCookie("baz")).toBe("qux"); + }); + + it("trims leading spaces in cookies", () => { + setDocumentCookie("foo=bar; baz=qux"); + expect(getCookie("baz")).toBe("qux"); + }); + + it("returns the value of the first matching cookie if only one exists", () => { + setDocumentCookie("foo=bar; test=val"); + expect(getCookie("foo")).toBe("bar"); + }); + + it("returns empty string if document.cookie is empty", () => { + setDocumentCookie(""); + expect(getCookie("foo")).toBe(""); + }); + + it("handles cookies that aren't separated with a space", () => { + setDocumentCookie("foo=bar;baz=qux;quux=corge;grault=garply"); + expect(getCookie("baz")).toBe("qux"); + }); + + it("handles cookies that may have leading tab characters", () => { + setDocumentCookie("foo=bar;\tbaz=qux"); + expect(getCookie("baz")).toBe("qux"); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 7bf82eaaca0..131a0503ee3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -50,7 +50,14 @@ ], "#trainers/*": ["./src/data/trainers/*.ts"], "#types/*": ["./src/@types/helpers/*.ts", "./src/@types/*.ts", "./src/typings/phaser/*.ts"], - "#ui/*": ["./src/ui/battle-info/*.ts", "./src/ui/settings/*.ts", "./src/ui/*.ts"], + "#ui/*": [ + "./src/ui/battle-info/*.ts", + "./src/ui/containers/*.ts", + "./src/ui/handlers/*.ts", + "./src/ui/settings/*.ts", + "./src/ui/utils/*.ts", + "./src/ui/*.ts" + ], "#utils/*": ["./src/utils/*.ts"], "#data/*": ["./src/data/pokemon-forms/*.ts", "./src/data/pokemon/*.ts", "./src/data/*.ts"], "#test/*": ["./test/*.ts"], diff --git a/tsdoc.json b/tsdoc.json index 689f7a96c5c..c17030cdbe7 100644 --- a/tsdoc.json +++ b/tsdoc.json @@ -13,6 +13,10 @@ { "tagName": "@module", "syntaxKind": "modifier" + }, + { + "tagName": "@interface", + "syntaxKind": "modifier" } ] } diff --git a/typedoc.config.js b/typedoc.config.js index ef932a5d077..d9e880743ca 100644 --- a/typedoc.config.js +++ b/typedoc.config.js @@ -3,7 +3,7 @@ import { globSync } from "node:fs"; const dryRun = !!process.env.DRY_RUN?.match(/true/gi); /** - * @type {Partial} + * */ const config = { entryPoints: ["./src", "./test/test-utils"], @@ -15,6 +15,7 @@ const config = { "test/test-utils/setup", "test/test-utils/reporters", ], + excludePrivate: false, // Private members are useful in the docs for contributors excludeReferences: true, // prevent documenting re-exports requiredToBeDocumented: [ "Enum", @@ -42,7 +43,7 @@ const config = { readme: "./README.md", coverageLabel: "Documented", coverageSvgWidth: 120, // Increased from 104 baseline due to adding 2 extra letters - favicon: "./public/images/logo.png", + favicon: "./favicon.ico", theme: "typedoc-github-theme", customFooterHtml: "

Copyright Pagefault Games 2025

", customFooterHtmlDisableWrapper: true, @@ -61,4 +62,5 @@ if (!dryRun && process.env.REF_NAME) { }; } +// biome-ignore lint/style/noDefaultExport: required by TypeDoc export default config; diff --git a/vitest.config.ts b/vitest.config.ts index 7fa2494bb4e..682b8052878 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -15,7 +15,7 @@ export default defineConfig(({ mode }) => ({ slowTestThreshold: 10_000, // TODO: Consider enabling // expect: {requireAssertions: true}, - setupFiles: ["./test/font-face.setup.ts", "./test/vitest.setup.ts", "./test/matchers.setup.ts"], + setupFiles: ["./test/setup/font-face.setup.ts", "./test/setup/vitest.setup.ts", "./test/setup/matchers.setup.ts"], sequence: { sequencer: MySequencer, },