diff --git a/.github/workflows/test-shard-template.yml b/.github/workflows/test-shard-template.yml new file mode 100644 index 00000000000..ac89b503f0c --- /dev/null +++ b/.github/workflows/test-shard-template.yml @@ -0,0 +1,30 @@ +name: Test Template + +on: + workflow_call: + inputs: + project: + required: true + type: string + shard: + required: true + type: number + totalShards: + required: true + type: number + +jobs: + test: + name: Shard ${{ inputs.shard }} of ${{ inputs.totalShards }} + runs-on: ubuntu-latest + steps: + - name: Check out Git repository + uses: actions/checkout@v4 + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + - name: Install Node.js dependencies + run: npm ci + - name: Run tests + run: npx vitest --project ${{ inputs.project }} --shard=${{ inputs.shard }}/${{ inputs.totalShards }} ${{ !runner.debug && '--silent' || '' }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2a78ec252b8..66cc3ecc139 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,91 +15,33 @@ on: types: [checks_requested] jobs: - run-misc-tests: # Define a job named "run-tests" - name: Run misc tests # Human-readable name for the job - runs-on: ubuntu-latest # Specify the latest Ubuntu runner for the job - - steps: - - name: Check out Git repository # Step to check out the repository - uses: actions/checkout@v4 # Use the checkout action version 4 - - - name: Set up Node.js # Step to set up Node.js environment - uses: actions/setup-node@v4 # Use the setup-node action version 4 - with: - node-version: 20 # Specify Node.js version 20 - - - name: Install Node.js dependencies # Step to install Node.js dependencies - run: npm ci # Use 'npm ci' to install dependencies - - - name: pre-test # pre-test to check overrides - run: npx vitest run --project pre - - name: test misc - run: npx vitest --project misc - - run-abilities-tests: - name: Run abilities tests - runs-on: ubuntu-latest + pre-test: + name: Run Pre-test + runs-on: ubuntu-latest steps: - name: Check out Git repository uses: actions/checkout@v4 + with: + path: tests-action - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: 20 - name: Install Node.js dependencies + working-directory: tests-action run: npm ci - - name: pre-test - run: npx vitest run --project pre - - name: test abilities - run: npx vitest --project abilities + - name: Run Pre-test + working-directory: tests-action + run: npx vitest run --project pre ${{ !runner.debug && '--silent' || '' }} - run-items-tests: - name: Run items tests - runs-on: ubuntu-latest - steps: - - name: Check out Git repository - uses: actions/checkout@v4 - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - name: Install Node.js dependencies - run: npm ci - - name: pre-test - run: npx vitest run --project pre - - name: test items - run: npx vitest --project items - - run-moves-tests: - name: Run moves tests - runs-on: ubuntu-latest - steps: - - name: Check out Git repository - uses: actions/checkout@v4 - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - name: Install Node.js dependencies - run: npm ci - - name: pre-test - run: npx vitest run --project pre - - name: test moves - run: npx vitest --project moves - - run-battle-tests: - name: Run battle tests - runs-on: ubuntu-latest - steps: - - name: Check out Git repository - uses: actions/checkout@v4 - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - name: Install Node.js dependencies - run: npm ci - - name: pre-test - run: npx vitest run --project pre - - name: test battle - run: npx vitest --project battle \ No newline at end of file + run-tests: + name: Run Tests + needs: [pre-test] + strategy: + matrix: + shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + uses: ./.github/workflows/test-shard-template.yml + with: + project: main + shard: ${{ matrix.shard }} + totalShards: 10 \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js index eeea38e3178..80e9e67b525 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,7 +1,7 @@ import tseslint from '@typescript-eslint/eslint-plugin'; import stylisticTs from '@stylistic/eslint-plugin-ts' import parser from '@typescript-eslint/parser'; -// import imports from 'eslint-plugin-import'; // Disabled due to not being compatible with eslint v9 +import importX from 'eslint-plugin-import-x'; export default [ { @@ -11,7 +11,7 @@ export default [ parser: parser }, plugins: { - // imports: imports.configs.recommended // Disabled due to not being compatible with eslint v9 + "import-x": importX, '@stylistic/ts': stylisticTs, '@typescript-eslint': tseslint }, @@ -39,7 +39,8 @@ export default [ }], "space-before-blocks": ["error", "always"], // Enforces a space before blocks "keyword-spacing": ["error", { "before": true, "after": true }], // Enforces spacing before and after keywords - "comma-spacing": ["error", { "before": false, "after": true }] // Enforces spacing after comma + "comma-spacing": ["error", { "before": false, "after": true }], // Enforces spacing after comma + "import-x/extensions": ["error", "never", { "json": "always" }], // Enforces no extension for imports unless json } } ] diff --git a/package-lock.json b/package-lock.json index 0605b299dab..4a447554819 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "@vitest/coverage-istanbul": "^2.0.4", "dependency-cruiser": "^16.3.10", "eslint": "^9.7.0", + "eslint-plugin-import-x": "^4.2.1", "jsdom": "^24.0.0", "lefthook": "^1.6.12", "phaser3spectorjs": "^0.0.8", @@ -2505,6 +2506,19 @@ "node": ">=8" } }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2687,6 +2701,155 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import-x": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.2.1.tgz", + "integrity": "sha512-WWi2GedccIJa0zXxx3WDnTgouGQTtdYK1nhXMwywbqqAgB0Ov+p1pYBsWh3VaB0bvBOwLse6OfVII7jZD9xo5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.1.0", + "debug": "^4.3.4", + "doctrine": "^3.0.0", + "eslint-import-resolver-node": "^0.3.9", + "get-tsconfig": "^4.7.3", + "is-glob": "^4.0.3", + "minimatch": "^9.0.3", + "semver": "^7.6.3", + "stable-hash": "^0.0.4", + "tslib": "^2.6.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-import-x/node_modules/@typescript-eslint/scope-manager": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.5.0.tgz", + "integrity": "sha512-06JOQ9Qgj33yvBEx6tpC8ecP9o860rsR22hWMEd12WcTRrfaFgHr2RB/CA/B+7BMhHkXT4chg2MyboGdFGawYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.5.0", + "@typescript-eslint/visitor-keys": "8.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-import-x/node_modules/@typescript-eslint/types": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.5.0.tgz", + "integrity": "sha512-qjkormnQS5wF9pjSi6q60bKUHH44j2APxfh9TQRXK8wbYVeDYYdYJGIROL87LGZZ2gz3Rbmjc736qyL8deVtdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-import-x/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.5.0.tgz", + "integrity": "sha512-vEG2Sf9P8BPQ+d0pxdfndw3xIXaoSjliG0/Ejk7UggByZPKXmJmw3GW5jV2gHNQNawBUyfahoSiCFVov0Ruf7Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "8.5.0", + "@typescript-eslint/visitor-keys": "8.5.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-import-x/node_modules/@typescript-eslint/utils": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.5.0.tgz", + "integrity": "sha512-6yyGYVL0e+VzGYp60wvkBHiqDWOpT63pdMV2CVG4LVDd5uR6q1qQN/7LafBZtAtNIn/mqXjsSeS5ggv/P0iECw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.5.0", + "@typescript-eslint/types": "8.5.0", + "@typescript-eslint/typescript-estree": "8.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-import-x/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.5.0.tgz", + "integrity": "sha512-yTPqMnbAZJNy2Xq2XU8AdtOW9tJIr+UQb64aXB9f3B1498Zx9JorVgFJcZpEc9UBuCCrdzKID2RGAMkYcDtZOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.5.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/eslint-scope": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", @@ -3143,6 +3306,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-tsconfig": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.0.tgz", + "integrity": "sha512-Pgba6TExTZ0FJAn1qkJAjIeKoDJ3CsI2ChuLohJnZl/tTU8MVrq3b+2t5UOPfRa4RMsorClBjJALkJUMjG1PAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -4854,6 +5030,16 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -5069,6 +5255,13 @@ "node": ">=0.10.0" } }, + "node_modules/stable-hash": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz", + "integrity": "sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==", + "dev": true, + "license": "MIT" + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -5460,6 +5653,13 @@ "node": ">=6" } }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "dev": true, + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 83e82585d1e..dddf5aedebd 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@vitest/coverage-istanbul": "^2.0.4", "dependency-cruiser": "^16.3.10", "eslint": "^9.7.0", + "eslint-plugin-import-x": "^4.2.1", "jsdom": "^24.0.0", "lefthook": "^1.6.12", "phaser3spectorjs": "^0.0.8", diff --git a/public/images/events/egg-update_de.png b/public/images/events/egg-update_de.png new file mode 100644 index 00000000000..5de94877d5c Binary files /dev/null and b/public/images/events/egg-update_de.png differ diff --git a/public/images/events/egg-update_en.png b/public/images/events/egg-update_en.png new file mode 100644 index 00000000000..7104d340ca0 Binary files /dev/null and b/public/images/events/egg-update_en.png differ diff --git a/public/images/events/egg-update_es.png b/public/images/events/egg-update_es.png new file mode 100644 index 00000000000..ec5f5c46d17 Binary files /dev/null and b/public/images/events/egg-update_es.png differ diff --git a/public/images/events/egg-update_fr.png b/public/images/events/egg-update_fr.png new file mode 100644 index 00000000000..e0505fa96dd Binary files /dev/null and b/public/images/events/egg-update_fr.png differ diff --git a/public/images/events/egg-update_it.png b/public/images/events/egg-update_it.png new file mode 100644 index 00000000000..fc347bce9cf Binary files /dev/null and b/public/images/events/egg-update_it.png differ diff --git a/public/images/events/egg-update_ja.png b/public/images/events/egg-update_ja.png new file mode 100644 index 00000000000..2259cbb4d9a Binary files /dev/null and b/public/images/events/egg-update_ja.png differ diff --git a/public/images/events/egg-update_ko.png b/public/images/events/egg-update_ko.png new file mode 100644 index 00000000000..99dcc662402 Binary files /dev/null and b/public/images/events/egg-update_ko.png differ diff --git a/public/images/events/egg-update_pt-BR.png b/public/images/events/egg-update_pt-BR.png new file mode 100644 index 00000000000..ee347d35654 Binary files /dev/null and b/public/images/events/egg-update_pt-BR.png differ diff --git a/public/images/events/egg-update_zh-CN.png b/public/images/events/egg-update_zh-CN.png new file mode 100644 index 00000000000..02d780fab89 Binary files /dev/null and b/public/images/events/egg-update_zh-CN.png differ diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 8c91cae232a..17d2df63046 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -2287,8 +2287,14 @@ export default class BattleScene extends SceneBase { return true; } - findPhase(phaseFilter: (phase: Phase) => boolean): Phase | undefined { - return this.phaseQueue.find(phaseFilter); + /** + * Find a specific {@linkcode Phase} in the phase queue. + * + * @param phaseFilter filter function to use to find the wanted phase + * @returns the found phase or undefined if none found + */ + findPhase

(phaseFilter: (phase: P) => boolean): P | undefined { + return this.phaseQueue.find(phaseFilter) as P; } tryReplacePhase(phaseFilter: (phase: Phase) => boolean, phase: Phase): boolean { @@ -2875,20 +2881,20 @@ export default class BattleScene extends SceneBase { const keys: string[] = []; const playerParty = this.getParty(); playerParty.forEach(p => { - keys.push("pkmn__" + p.species.getSpriteId(p.gender === Gender.FEMALE, p.species.formIndex, p.shiny, p.variant)); - keys.push("pkmn__" + p.species.getSpriteId(p.gender === Gender.FEMALE, p.species.formIndex, p.shiny, p.variant, true)); - keys.push("cry/" + p.species.getCryKey(p.species.formIndex)); - if (p.fusionSpecies && p.getSpeciesForm() !== p.getFusionSpeciesForm()) { - keys.push("cry/"+p.getFusionSpeciesForm().getCryKey(p.fusionSpecies.formIndex)); + keys.push(p.getSpriteKey(true)); + keys.push(p.getBattleSpriteKey(true, true)); + keys.push("cry/" + p.species.getCryKey(p.formIndex)); + if (p.fusionSpecies) { + keys.push("cry/"+p.fusionSpecies.getCryKey(p.fusionFormIndex)); } }); // enemyParty has to be operated on separately from playerParty because playerPokemon =/= enemyPokemon const enemyParty = this.getEnemyParty(); enemyParty.forEach(p => { - keys.push(p.species.getSpriteKey(p.gender === Gender.FEMALE, p.species.formIndex, p.shiny, p.variant)); - keys.push("cry/" + p.species.getCryKey(p.species.formIndex)); - if (p.fusionSpecies && p.getSpeciesForm() !== p.getFusionSpeciesForm()) { - keys.push("cry/"+p.getFusionSpeciesForm().getCryKey(p.fusionSpecies.formIndex)); + keys.push(p.getSpriteKey(true)); + keys.push("cry/" + p.species.getCryKey(p.formIndex)); + if (p.fusionSpecies) { + keys.push("cry/"+p.fusionSpecies.getCryKey(p.fusionFormIndex)); } }); return keys; diff --git a/src/data/dialogue.ts b/src/data/dialogue.ts index 12f9af62c7c..6153dc439fa 100644 --- a/src/data/dialogue.ts +++ b/src/data/dialogue.ts @@ -1689,8 +1689,7 @@ export const trainerTypeDialogue: TrainerTypeDialogue = { "dialogue:roark.victory.1", "dialogue:roark.victory.2", "dialogue:roark.victory.3", - "dialogue:roark.victory.4", - "dialogue:roark.victory.5" + "dialogue:roark.victory.4" ], defeat: [ "dialogue:roark.defeat.1", diff --git a/src/data/egg.ts b/src/data/egg.ts index bf137610b3a..9816f971a2c 100644 --- a/src/data/egg.ts +++ b/src/data/egg.ts @@ -229,7 +229,7 @@ export class Egg { let pokemonSpecies = getPokemonSpecies(this._species); // Special condition to have Phione eggs also have a chance of generating Manaphy - if (this._species === Species.PHIONE) { + if (this._species === Species.PHIONE && this._sourceType === EggSourceType.SAME_SPECIES_EGG) { pokemonSpecies = getPokemonSpecies(Utils.randSeedInt(MANAPHY_EGG_MANAPHY_RATE) ? Species.PHIONE : Species.MANAPHY); } @@ -335,7 +335,8 @@ export class Egg { break; } - return Utils.randSeedInt(baseChance * Math.pow(2, 3 - this.tier)) ? Utils.randSeedInt(3) : 3; + const tierMultiplier = this.isManaphyEgg() ? 2 : Math.pow(2, 3 - this.tier); + return Utils.randSeedInt(baseChance * tierMultiplier) ? Utils.randSeedInt(3) : 3; } private getEggTierDefaultHatchWaves(eggTier?: EggTier): number { @@ -370,7 +371,12 @@ export class Egg { * the species that was the legendary focus at the time */ if (this.isManaphyEgg()) { - const rand = Utils.randSeedInt(MANAPHY_EGG_MANAPHY_RATE); + /** + * Adding a technicality to make unit tests easier: By making this check pass + * when Utils.randSeedInt(8) = 1, and by making the generatePlayerPokemon() species + * check pass when Utils.randSeedInt(8) = 0, we can tell them apart during tests. + */ + const rand = (Utils.randSeedInt(MANAPHY_EGG_MANAPHY_RATE) !== 1); return rand ? Species.PHIONE : Species.MANAPHY; } else if (this.tier === EggTier.MASTER && this._sourceType === EggSourceType.GACHA_LEGENDARY) { diff --git a/src/data/move.ts b/src/data/move.ts index d9e385fdd0e..7800d6df12a 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -6272,12 +6272,42 @@ export class VariableTargetAttr extends MoveAttr { } } +/** + * Attribute for {@linkcode Moves.AFTER_YOU} + * + * [After You - Move | Bulbapedia](https://bulbapedia.bulbagarden.net/wiki/After_You_(move)) + */ +export class AfterYouAttr extends MoveEffectAttr { + /** + * Allows the target of this move to act right after the user. + * + * @param user {@linkcode Pokemon} that is using the move. + * @param target {@linkcode Pokemon} that will move right after this move is used. + * @param move {@linkcode Move} {@linkcode Moves.AFTER_YOU} + * @param _args N/A + * @returns true + */ + override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean { + user.scene.queueMessage(i18next.t("moveTriggers:afterYou", {targetName: getPokemonNameWithAffix(target)})); + + //Will find next acting phase of the targeted pokémon, delete it and queue it next on successful delete. + const nextAttackPhase = target.scene.findPhase((phase) => phase.pokemon === target); + if (nextAttackPhase && target.scene.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) { + target.scene.prependToPhase(new MovePhase(target.scene, target, [...nextAttackPhase.targets], nextAttackPhase.move), MovePhase); + } + + return true; + } +} + const failOnGravityCondition: MoveConditionFunc = (user, target, move) => !user.scene.arena.getTag(ArenaTagType.GRAVITY); const failOnBossCondition: MoveConditionFunc = (user, target, move) => !target.isBossImmune(); const failOnMaxCondition: MoveConditionFunc = (user, target, move) => !target.isMax(); +const failIfSingleBattle: MoveConditionFunc = (user, target, move) => user.scene.currentBattle.double; + const failIfDampCondition: MoveConditionFunc = (user, target, move) => { const cancelled = new Utils.BooleanHolder(false); user.scene.getField(true).map(p=>applyAbAttrs(FieldPreventExplosiveMovesAbAttr, p, cancelled)); @@ -7925,7 +7955,10 @@ export function initMoves() { .attr(AbilityGiveAttr), new StatusMove(Moves.AFTER_YOU, Type.NORMAL, -1, 15, -1, 0, 5) .ignoresProtect() - .unimplemented(), + .target(MoveTarget.NEAR_OTHER) + .condition(failIfSingleBattle) + .condition((user, target, move) => !target.turnData.acted) + .attr(AfterYouAttr), new AttackMove(Moves.ROUND, Type.NORMAL, MoveCategory.SPECIAL, 60, 100, 15, -1, 0, 5) .soundBased() .partial(), diff --git a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts index c1e2efd4038..31e6cf14757 100644 --- a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts +++ b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts @@ -486,7 +486,7 @@ function doBerrySpritePile(scene: BattleScene, isEat: boolean = false) { }); } -function doBerryBounce(scene: BattleScene, berrySprites: Phaser.GameObjects.Sprite[], yd: number, baseBounceDuration: integer) { +function doBerryBounce(scene: BattleScene, berrySprites: Phaser.GameObjects.Sprite[], yd: number, baseBounceDuration: number) { let bouncePower = 1; let bounceYOffset = yd; 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 55fd8f3a6a9..5266ff4859f 100644 --- a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts +++ b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts @@ -582,7 +582,7 @@ function getTrainerConfigForWave(waveIndex: number) { } function getRandomPartyMemberFunc(speciesPool: Species[], trainerSlot: TrainerSlot = TrainerSlot.TRAINER, ignoreEvolution: boolean = false, postProcess?: (enemyPokemon: EnemyPokemon) => void) { - return (scene: BattleScene, level: integer, strength: PartyMemberStrength) => { + return (scene: BattleScene, level: number, strength: PartyMemberStrength) => { let species = Utils.randSeedItem(speciesPool); if (!ignoreEvolution) { species = getPokemonSpecies(species).getTrainerSpeciesForLevel(level, true, strength); diff --git a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts index b428ab5a8cb..4fb33a982ff 100644 --- a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts +++ b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts @@ -382,7 +382,7 @@ async function handleSwapAbility(scene: BattleScene) { } function displayYesNoOptions(scene: BattleScene, resolve) { - showEncounterText(scene, `${namespace}.option.1.ability_prompt`, 500, false); + showEncounterText(scene, `${namespace}.option.1.ability_prompt`, null, 500, false); const fullOptions = [ { label: i18next.t("menu:yes"), @@ -429,7 +429,7 @@ function onYesAbilitySwap(scene: BattleScene, resolve) { selectPokemonForOption(scene, onPokemonSelected, onPokemonNotSelected); } -function generateItemsOfTier(scene: BattleScene, pokemon: PlayerPokemon, numItems: integer, tier: ModifierTier | "Berries") { +function generateItemsOfTier(scene: BattleScene, pokemon: PlayerPokemon, numItems: number, tier: ModifierTier | "Berries") { // These pools have to be defined at runtime so that modifierTypes exist // Pools have instances of the modifier type equal to the max stacks that modifier can be applied to any one pokemon // This is to prevent "over-generating" a random item of a certain type during item swaps diff --git a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts index c85a994633d..f020c20550c 100644 --- a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts +++ b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts @@ -122,7 +122,7 @@ export const DelibirdyEncounter: MysteryEncounter = const shellBell = generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell); scene.playSound("item_fanfare"); - await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), undefined, true); + await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true); } else { scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.ABILITY_CHARM)); } @@ -197,7 +197,7 @@ export const DelibirdyEncounter: MysteryEncounter = const shellBell = generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell); scene.playSound("item_fanfare"); - await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), undefined, true); + await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true); } else { scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.CANDY_JAR)); } @@ -210,7 +210,7 @@ export const DelibirdyEncounter: MysteryEncounter = const shellBell = generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell); scene.playSound("item_fanfare"); - await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), undefined, true); + await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true); } else { scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.HEALING_CHARM)); } @@ -290,7 +290,7 @@ export const DelibirdyEncounter: MysteryEncounter = const shellBell = generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell); scene.playSound("item_fanfare"); - await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), undefined, true); + await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true); } else { scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.BERRY_POUCH)); } 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 80229933f74..6484cab184a 100644 --- a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts +++ b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts @@ -157,7 +157,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = const formName = tradePokemon.species.forms?.[pokemon.formIndex]?.formName; const line1 = i18next.t("pokemonInfoContainer:ability") + " " + tradePokemon.getAbility().name + (tradePokemon.getGender() !== Gender.GENDERLESS ? " | " + i18next.t("pokemonInfoContainer:gender") + " " + getGenderSymbol(tradePokemon.getGender()) : ""); const line2 = i18next.t("pokemonInfoContainer:nature") + " " + getNatureName(tradePokemon.getNature()) + (formName ? " | " + i18next.t("pokemonInfoContainer:form") + " " + formName : ""); - scene.ui.showText(`${line1}\n${line2}`, 0); + showEncounterText(scene, `${line1}\n${line2}`, 0); }, }; return option; @@ -195,7 +195,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = // Show the trade animation await showTradeBackground(scene); await doPokemonTradeSequence(scene, tradedPokemon, newPlayerPokemon); - await showEncounterText(scene, `${namespace}.trade_received`, 0, true, 4000); + await showEncounterText(scene, `${namespace}.trade_received`, null, 0, true, 4000); scene.playBgm("mystery_encounter_gts"); await hideTradeBackground(scene); tradedPokemon.destroy(); @@ -278,7 +278,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = // Show the trade animation await showTradeBackground(scene); await doPokemonTradeSequence(scene, tradedPokemon, newPlayerPokemon); - await showEncounterText(scene, `${namespace}.trade_received`, 0, true, 4000); + await showEncounterText(scene, `${namespace}.trade_received`, null, 0, true, 4000); scene.playBgm("mystery_encounter_gts"); await hideTradeBackground(scene); tradedPokemon.destroy(); diff --git a/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts b/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts index bdeef60fe91..2341bc993c1 100644 --- a/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts +++ b/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts @@ -1,6 +1,6 @@ -import { getPokemonSpecies } from "#app/data/pokemon-species.js"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; import { Moves } from "#app/enums/moves"; -import { Species } from "#app/enums/species.js"; +import { Species } from "#app/enums/species"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; diff --git a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts index c13f22a5624..32f45a5a6c4 100644 --- a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts +++ b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts @@ -2,7 +2,7 @@ import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-enco import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterRewards, transitionMysteryEncounterIntroVisuals } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { getHighestLevelPlayerPokemon, koPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { ModifierTier } from "#app/modifier/modifier-tier"; -import { randSeedInt } from "#app/utils.js"; +import { randSeedInt } from "#app/utils"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; diff --git a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts index e9eb7503c51..a26652c4e0d 100644 --- a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts +++ b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts @@ -175,9 +175,9 @@ const safariZoneGameOptions: MysteryEncounterOption[] = [ // 80% chance to increase flee stage +1 const fleeChangeResult = tryChangeFleeStage(scene, 1, 8); if (!fleeChangeResult) { - await showEncounterText(scene, getEncounterText(scene, `${namespace}.safari.busy_eating`) ?? "", 1000, false ); + await showEncounterText(scene, getEncounterText(scene, `${namespace}.safari.busy_eating`) ?? "", null, 1000, false ); } else { - await showEncounterText(scene, getEncounterText(scene, `${namespace}.safari.eating`) ?? "", 1000, false); + await showEncounterText(scene, getEncounterText(scene, `${namespace}.safari.eating`) ?? "", null, 1000, false); } await doEndTurn(scene, 1); @@ -204,9 +204,9 @@ const safariZoneGameOptions: MysteryEncounterOption[] = [ // 80% chance to decrease catch stage -1 const catchChangeResult = tryChangeCatchStage(scene, -1, 8); if (!catchChangeResult) { - await showEncounterText(scene, getEncounterText(scene, `${namespace}.safari.beside_itself_angry`) ?? "", 1000, false ); + await showEncounterText(scene, getEncounterText(scene, `${namespace}.safari.beside_itself_angry`) ?? "", null, 1000, false ); } else { - await showEncounterText(scene, getEncounterText(scene, `${namespace}.safari.angry`) ?? "", 1000, false ); + await showEncounterText(scene, getEncounterText(scene, `${namespace}.safari.angry`) ?? "", null, 1000, false ); } await doEndTurn(scene, 2); @@ -291,7 +291,7 @@ async function summonSafariPokemon(scene: BattleScene) { scene.unshiftPhase(new SummonPhase(scene, 0, false)); encounter.setDialogueToken("pokemonName", getPokemonNameWithAffix(pokemon)); - showEncounterText(scene, getEncounterText(scene, "battle:singleWildAppeared") ?? "", 1500, false) + showEncounterText(scene, getEncounterText(scene, "battle:singleWildAppeared") ?? "", null, 1500, false) .then(() => { const ivScannerModifier = scene.findModifier(m => m instanceof IvScannerModifier); if (ivScannerModifier) { diff --git a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts index 75dd838f05e..dd1fb285aa4 100644 --- a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts @@ -156,7 +156,7 @@ export const TheStrongStuffEncounter: MysteryEncounter = encounter.setDialogueToken("reductionValue", HIGH_BST_REDUCTION_VALUE.toString()); encounter.setDialogueToken("increaseValue", BST_INCREASE_VALUE.toString()); - await showEncounterText(scene, `${namespace}.option.1.selected_2`, undefined, true); + await showEncounterText(scene, `${namespace}.option.1.selected_2`, null, undefined, true); setEncounterRewards(scene, { fillRemaining: true }); leaveEncounterWithoutBattle(scene, true); diff --git a/src/data/mystery-encounters/encounters/training-session-encounter.ts b/src/data/mystery-encounters/encounters/training-session-encounter.ts index d9980e15cfa..92308594f79 100644 --- a/src/data/mystery-encounters/encounters/training-session-encounter.ts +++ b/src/data/mystery-encounters/encounters/training-session-encounter.ts @@ -13,7 +13,7 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; -import { getEncounterText, queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { getEncounterText, queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import HeldModifierConfig from "#app/interfaces/held-modifier-config"; @@ -286,27 +286,33 @@ export const TrainingSessionEncounter: MysteryEncounter = ? pokemon.getFusionSpeciesForm() : pokemon.getSpeciesForm(); const abilityCount = speciesForm.getAbilityCount(); - const abilities = new Array(abilityCount) + const abilities: Ability[] = new Array(abilityCount) .fill(null) .map((val, i) => allAbilities[speciesForm.getAbility(i)]); - return abilities.map((ability: Ability, index) => { - const option: OptionSelectItem = { - label: ability.name, - handler: () => { - // Pokemon and ability selected - encounter.setDialogueToken("ability", ability.name); - encounter.misc = { - playerPokemon: pokemon, - abilityIndex: index, - }; - return true; - }, - onHover: () => { - scene.ui.showText(ability.description); - }, - }; - return option; + + const optionSelectItems: OptionSelectItem[] = []; + abilities.forEach((ability: Ability, index) => { + if (!optionSelectItems.some(o => o.label === ability.name)) { + const option: OptionSelectItem = { + label: ability.name, + handler: () => { + // Pokemon and ability selected + encounter.setDialogueToken("ability", ability.name); + encounter.misc = { + playerPokemon: pokemon, + abilityIndex: index, + }; + return true; + }, + onHover: () => { + showEncounterText(scene, ability.description, 0); + }, + }; + optionSelectItems.push(option); + } }); + + return optionSelectItems; }; // Only Pokemon that are not KOed/legal can be trained diff --git a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts index e9bb6bb5d66..b3f9a37b9bd 100644 --- a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts +++ b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts @@ -179,7 +179,7 @@ async function tryApplyDigRewardItems(scene: BattleScene) { } scene.playSound("item_fanfare"); - await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: "2 " + leftovers.name }), undefined, true); + await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: "2 " + leftovers.name }), null, undefined, true); // First Shell bell for (const pokemon of party) { @@ -206,7 +206,7 @@ async function tryApplyDigRewardItems(scene: BattleScene) { } scene.playSound("item_fanfare"); - await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: "2 " + shellBell.name }), undefined, true); + await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: "2 " + shellBell.name }), null, undefined, true); } async function doGarbageDig(scene: BattleScene) { diff --git a/src/data/mystery-encounters/mystery-encounter-requirements.ts b/src/data/mystery-encounters/mystery-encounter-requirements.ts index 53eaa162dc2..9863cdc9cc1 100644 --- a/src/data/mystery-encounters/mystery-encounter-requirements.ts +++ b/src/data/mystery-encounters/mystery-encounter-requirements.ts @@ -22,7 +22,17 @@ export interface EncounterRequirement { } export abstract class EncounterSceneRequirement implements EncounterRequirement { + /** + * Returns whether the EncounterSceneRequirement's... requirements, are met by the given scene + * @param partyPokemon + */ abstract meetsRequirement(scene: BattleScene): boolean; + /** + * Returns a dialogue token key/value pair for a given Requirement. + * Should be overridden by child Requirement classes. + * @param scene + * @param pokemon + */ abstract getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string]; } @@ -34,7 +44,7 @@ export class CombinationSceneRequirement extends EncounterSceneRequirement { this.orRequirements = orRequirements; } - meetsRequirement(scene: BattleScene): boolean { + override meetsRequirement(scene: BattleScene): boolean { for (const req of this.orRequirements) { if (req.meetsRequirement(scene)) { return true; @@ -58,6 +68,10 @@ export abstract class EncounterPokemonRequirement implements EncounterRequiremen public minNumberOfPokemon: number; public invertQuery: boolean; + /** + * Returns whether the EncounterPokemonRequirement's... requirements, are met by the given scene + * @param partyPokemon + */ abstract meetsRequirement(scene: BattleScene): boolean; /** @@ -66,6 +80,12 @@ export abstract class EncounterPokemonRequirement implements EncounterRequiremen */ abstract queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[]; + /** + * Returns a dialogue token key/value pair for a given Requirement. + * Should be overridden by child Requirement classes. + * @param scene + * @param pokemon + */ abstract getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string]; } diff --git a/src/data/mystery-encounters/mystery-encounter.ts b/src/data/mystery-encounters/mystery-encounter.ts index 4d6fb4310ee..7871c459fae 100644 --- a/src/data/mystery-encounters/mystery-encounter.ts +++ b/src/data/mystery-encounters/mystery-encounter.ts @@ -281,11 +281,17 @@ export default class MysteryEncounter implements IMysteryEncounter { return sceneReq && secReqs && priReqs; } + /** + * Checks if a specific player pokemon meets all given primary EncounterPokemonRequirements + * Used automatically as part of {@linkcode meetsRequirements}, but can also be used to manually check certain Pokemon where needed + * @param scene + * @param pokemon + */ pokemonMeetsPrimaryRequirements(scene: BattleScene, pokemon: Pokemon) { return !this.primaryPokemonRequirements.some(req => !req.queryParty(scene.getParty()).map(p => p.id).includes(pokemon.id)); } - meetsPrimaryRequirementAndPrimaryPokemonSelected(scene: BattleScene): boolean { + private meetsPrimaryRequirementAndPrimaryPokemonSelected(scene: BattleScene): boolean { if (!this.primaryPokemonRequirements || this.primaryPokemonRequirements.length === 0) { const activeMon = scene.getParty().filter(p => p.isActive(true)); if (activeMon.length > 0) { @@ -342,7 +348,7 @@ export default class MysteryEncounter implements IMysteryEncounter { } } - meetsSecondaryRequirementAndSecondaryPokemonSelected(scene: BattleScene): boolean { + private meetsSecondaryRequirementAndSecondaryPokemonSelected(scene: BattleScene): boolean { if (!this.secondaryPokemonRequirements || this.secondaryPokemonRequirements.length === 0) { this.secondaryPokemon = []; return true; @@ -446,6 +452,14 @@ export default class MysteryEncounter implements IMysteryEncounter { } } + /** + * Used to cache a dialogue token for the encounter. + * Tokens will be auto-injected via the `{{key}}` pattern with `value`, + * when using the {@link showEncounterText} and {@link showEncounterDialogue} helper functions. + * + * @param key + * @param value + */ setDialogueToken(key: string, value: string): void { this.dialogueTokens[key] = value; } diff --git a/src/data/mystery-encounters/utils/encounter-dialogue-utils.ts b/src/data/mystery-encounters/utils/encounter-dialogue-utils.ts index 63030e4db4e..0f60c2cf13e 100644 --- a/src/data/mystery-encounters/utils/encounter-dialogue-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-dialogue-utils.ts @@ -4,6 +4,14 @@ import { UiTheme } from "#enums/ui-theme"; import { isNullOrUndefined } from "#app/utils"; import i18next from "i18next"; +/** + * Will inject all relevant dialogue tokens that exist in the {@link BattleScene.currentBattle.mysteryEncounter.dialogueTokens}, into i18n text. + * Also adds BBCodeText fragments for colored text, if applicable + * @param scene + * @param keyOrString + * @param primaryStyle - can define a text style to be applied to the entire string. Must be defined for BBCodeText styles to be applied correctly + * @param uiTheme + */ export function getEncounterText(scene: BattleScene, keyOrString?: string, primaryStyle?: TextStyle, uiTheme: UiTheme = UiTheme.DEFAULT): string | null { if (isNullOrUndefined(keyOrString)) { return null; @@ -20,6 +28,11 @@ export function getEncounterText(scene: BattleScene, keyOrString?: string, prima return textString; } +/** + * Helper function to inject {@link BattleScene.currentBattle.mysteryEncounter.dialogueTokens} into a given content string + * @param scene + * @param keyOrString + */ function getTextWithDialogueTokens(scene: BattleScene, keyOrString?: string): string | null { if (isNullOrUndefined(keyOrString)) { return null; @@ -51,14 +64,15 @@ export function queueEncounterMessage(scene: BattleScene, contentKey: string): v * Will display a message in UI with injected encounter data tokens * @param scene * @param contentKey + * @param delay * @param prompt * @param callbackDelay * @param promptDelay */ -export function showEncounterText(scene: BattleScene, contentKey: string, callbackDelay: number = 0, prompt: boolean = true, promptDelay: number | null = null): Promise { +export function showEncounterText(scene: BattleScene, contentKey: string, delay: number | null = null, callbackDelay: number = 0, prompt: boolean = true, promptDelay: number | null = null): Promise { return new Promise(resolve => { const text: string | null = getEncounterText(scene, contentKey); - scene.ui.showText(text ?? "", null, () => resolve(), callbackDelay, prompt, promptDelay); + scene.ui.showText(text ?? "", delay, () => resolve(), callbackDelay, prompt, promptDelay); }); } @@ -66,13 +80,14 @@ export function showEncounterText(scene: BattleScene, contentKey: string, callba * Will display a dialogue (with speaker title) in UI with injected encounter data tokens * @param scene * @param textContentKey + * @param delay * @param speakerContentKey * @param callbackDelay */ -export function showEncounterDialogue(scene: BattleScene, textContentKey: string, speakerContentKey: string, callbackDelay: number = 0): Promise { +export function showEncounterDialogue(scene: BattleScene, textContentKey: string, speakerContentKey: string, delay: number | null = null, callbackDelay: number = 0): Promise { return new Promise(resolve => { const text: string | null = getEncounterText(scene, textContentKey); const speaker: string | null = getEncounterText(scene, speakerContentKey); - scene.ui.showDialogue(text ?? "", speaker ?? "", null, () => resolve(), callbackDelay); + scene.ui.showDialogue(text ?? "", speaker ?? "", delay, () => resolve(), callbackDelay); }); } diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index 197a75d3b4b..f3997706f16 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -77,7 +77,7 @@ export interface EnemyPokemonConfig { passive?: boolean; moveSet?: Moves[]; nature?: Nature; - ivs?: [integer, integer, integer, integer, integer, integer]; + ivs?: [number, number, number, number, number, number]; shiny?: boolean; /** Can set just the status, or pass a timer on the status turns */ status?: StatusEffect | [StatusEffect, number]; @@ -422,7 +422,7 @@ export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (p const modeToSetOnExit = scene.ui.getMode(); // Open party screen to choose pokemon - scene.ui.setMode(Mode.PARTY, PartyUiMode.SELECT, -1, (slotIndex: integer, option: PartyOption) => { + scene.ui.setMode(Mode.PARTY, PartyUiMode.SELECT, -1, (slotIndex: number, option: PartyOption) => { if (slotIndex < scene.getParty().length) { scene.ui.setMode(modeToSetOnExit).then(() => { const pokemon = scene.getParty()[slotIndex]; @@ -456,7 +456,7 @@ export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (p return true; }, onHover: () => { - scene.ui.showText(i18next.t("mysteryEncounterMessages:cancel_option")); + showEncounterText(scene, i18next.t("mysteryEncounterMessages:cancel_option"), 0); } }); @@ -534,7 +534,7 @@ export function selectOptionThenPokemon(scene: BattleScene, options: OptionSelec const selectPokemonAfterOption = (selectedOptionIndex: number) => { // Open party screen to choose a Pokemon - scene.ui.setMode(Mode.PARTY, PartyUiMode.SELECT, -1, (slotIndex: integer, option: PartyOption) => { + scene.ui.setMode(Mode.PARTY, PartyUiMode.SELECT, -1, (slotIndex: number, option: PartyOption) => { if (slotIndex < scene.getParty().length) { // Pokemon and option selected scene.ui.setMode(modeToSetOnExit).then(() => { @@ -852,7 +852,7 @@ export function calculateMEAggregateStats(scene: BattleScene, baseSpawnWeight: n if (Array.isArray(biomeLinks[currentBiome])) { let biomes: Biome[]; scene.executeWithSeedOffset(() => { - biomes = (biomeLinks[currentBiome] as (Biome | [Biome, integer])[]) + biomes = (biomeLinks[currentBiome] as (Biome | [Biome, number])[]) .filter(b => { return !Array.isArray(b) || !Utils.randSeedInt(b[1]); }) diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index f73c25ddaf7..63c64c47de0 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -20,12 +20,24 @@ import { Gender } from "#app/data/gender"; import { PermanentStat } from "#enums/stat"; import { VictoryPhase } from "#app/phases/victory-phase"; -export function getSpriteKeysFromSpecies(species: Species, female?: boolean, formIndex?: integer, shiny?: boolean, variant?: integer): { spriteKey: string, fileRoot: string } { +/** + * Gets the sprite key and file root for a given PokemonSpecies (accounts for gender, shiny, variants, forms, and experimental) + * @param species + * @param female + * @param formIndex + * @param shiny + * @param variant + */ +export function getSpriteKeysFromSpecies(species: Species, female?: boolean, formIndex?: number, shiny?: boolean, variant?: number): { spriteKey: string, fileRoot: string } { const spriteKey = getPokemonSpecies(species).getSpriteKey(female ?? false, formIndex ?? 0, shiny ?? false, variant ?? 0); const fileRoot = getPokemonSpecies(species).getSpriteAtlasPath(female ?? false, formIndex ?? 0, shiny ?? false, variant ?? 0); return { spriteKey, fileRoot }; } +/** + * Gets the sprite key and file root for a given Pokemon (accounts for gender, shiny, variants, forms, and experimental) + * @param pokemon + */ export function getSpriteKeysFromPokemon(pokemon: Pokemon): { spriteKey: string, fileRoot: string } { const spriteKey = pokemon.getSpeciesForm().getSpriteKey(pokemon.getGender() === Gender.FEMALE, pokemon.formIndex, pokemon.shiny, pokemon.variant); const fileRoot = pokemon.getSpeciesForm().getSpriteAtlasPath(pokemon.getGender() === Gender.FEMALE, pokemon.formIndex, pokemon.shiny, pokemon.variant); @@ -442,6 +454,14 @@ export function trainerThrowPokeball(scene: BattleScene, pokemon: EnemyPokemon, }); } +/** + * Animates pokeball opening and messages when an attempted catch fails + * @param scene + * @param pokemon + * @param originalY + * @param pokeball + * @param pokeballType + */ function failCatch(scene: BattleScene, pokemon: EnemyPokemon, originalY: number, pokeball: Phaser.GameObjects.Sprite, pokeballType: PokeballType) { return new Promise(resolve => { scene.playSound("se/pb_rel"); @@ -541,7 +561,7 @@ export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, po scene.ui.showText(i18next.t("battle:partyFull", { pokemonName: pokemon.getNameToRender() }), null, () => { scene.pokemonInfoContainer.makeRoomForConfirmUi(); scene.ui.setMode(Mode.CONFIRM, () => { - scene.ui.setMode(Mode.PARTY, PartyUiMode.RELEASE, 0, (slotIndex: integer, _option: PartyOption) => { + scene.ui.setMode(Mode.PARTY, PartyUiMode.RELEASE, 0, (slotIndex: number, _option: PartyOption) => { scene.ui.setMode(Mode.MESSAGE).then(() => { if (slotIndex < 6) { addToParty(); @@ -573,6 +593,11 @@ export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, po }); } +/** + * Animates pokeball disappearing then destroys the object + * @param scene + * @param pokeball + */ function removePb(scene: BattleScene, pokeball: Phaser.GameObjects.Sprite) { if (pokeball) { scene.tweens.add({ @@ -588,6 +613,11 @@ function removePb(scene: BattleScene, pokeball: Phaser.GameObjects.Sprite) { } } +/** + * Animates a wild pokemon "fleeing", including sfx and messaging + * @param scene + * @param pokemon + */ export async function doPokemonFlee(scene: BattleScene, pokemon: EnemyPokemon): Promise { await new Promise(resolve => { scene.playSound("se/flee"); @@ -603,7 +633,7 @@ export async function doPokemonFlee(scene: BattleScene, pokemon: EnemyPokemon): onComplete: () => { pokemon.setVisible(false); scene.field.remove(pokemon, true); - showEncounterText(scene, i18next.t("battle:pokemonFled", { pokemonName: pokemon.getNameToRender() }), 600, false) + showEncounterText(scene, i18next.t("battle:pokemonFled", { pokemonName: pokemon.getNameToRender() }), null, 600, false) .then(() => { resolve(); }); @@ -612,6 +642,11 @@ export async function doPokemonFlee(scene: BattleScene, pokemon: EnemyPokemon): }); } +/** + * Handles the player fleeing from a wild pokemon, including sfx and messaging + * @param scene + * @param pokemon + */ export function doPlayerFlee(scene: BattleScene, pokemon: EnemyPokemon): Promise { return new Promise(resolve => { // Ease pokemon out @@ -626,7 +661,7 @@ export function doPlayerFlee(scene: BattleScene, pokemon: EnemyPokemon): Promise onComplete: () => { pokemon.setVisible(false); scene.field.remove(pokemon, true); - showEncounterText(scene, i18next.t("battle:playerFled", { pokemonName: pokemon.getNameToRender() }), 600, false) + showEncounterText(scene, i18next.t("battle:playerFled", { pokemonName: pokemon.getNameToRender() }), null, 600, false) .then(() => { resolve(); }); @@ -635,7 +670,9 @@ export function doPlayerFlee(scene: BattleScene, pokemon: EnemyPokemon): Promise }); } -// Bug Species and their corresponding weights +/** + * Bug Species and their corresponding weights + */ const GOLDEN_BUG_NET_SPECIES_POOL: [Species, number][] = [ [Species.SCYTHER, 40], [Species.SCIZOR, 40], @@ -666,7 +703,10 @@ const GOLDEN_BUG_NET_SPECIES_POOL: [Species, number][] = [ [Species.PHEROMOSA, 1], ]; -export function getGoldenBugNetSpecies(scene: BattleScene, waveIndex: integer, level: integer): PokemonSpecies { +/** + * Will randomly return one of the species from GOLDEN_BUG_NET_SPECIES_POOL, based on their weights + */ +export function getGoldenBugNetSpecies(): PokemonSpecies { const totalWeight = GOLDEN_BUG_NET_SPECIES_POOL.reduce((a, b) => a + b[1], 0); const roll = randSeedInt(totalWeight); diff --git a/src/data/mystery-encounters/utils/encounter-transformation-sequence.ts b/src/data/mystery-encounters/utils/encounter-transformation-sequence.ts index 9da36ee6846..f43688653cd 100644 --- a/src/data/mystery-encounters/utils/encounter-transformation-sequence.ts +++ b/src/data/mystery-encounters/utils/encounter-transformation-sequence.ts @@ -103,7 +103,7 @@ export function doPokemonTransformationSequence(scene: BattleScene, previousPoke scene.time.delayedCall(1000, () => { pokemonEvoTintSprite.setScale(0.25); pokemonEvoTintSprite.setVisible(true); - doCycle(scene, 2, 6, pokemonTintSprite, pokemonEvoTintSprite).then(success => { + doCycle(scene, 2, 6, pokemonTintSprite, pokemonEvoTintSprite).then(() => { pokemonEvoSprite.setVisible(true); doCircleInward(scene, transformationBaseBg, transformationContainer, xOffset, yOffset); @@ -143,7 +143,15 @@ export function doPokemonTransformationSequence(scene: BattleScene, previousPoke }); } -function doSpiralUpward(scene: BattleScene, transformationBaseBg, transformationContainer, xOffset: number, yOffset: number) { +/** + * Animates particles that "spiral" upwards at start of transform animation + * @param scene + * @param transformationBaseBg + * @param transformationContainer + * @param xOffset + * @param yOffset + */ +function doSpiralUpward(scene: BattleScene, transformationBaseBg: Phaser.GameObjects.Image, transformationContainer: Phaser.GameObjects.Container, xOffset: number, yOffset: number) { let f = 0; scene.tweens.addCounter({ @@ -162,7 +170,15 @@ function doSpiralUpward(scene: BattleScene, transformationBaseBg, transformation }); } -function doArcDownward(scene: BattleScene, transformationBaseBg, transformationContainer, xOffset: number, yOffset: number) { +/** + * Animates particles that arc downwards after the upwards spiral + * @param scene + * @param transformationBaseBg + * @param transformationContainer + * @param xOffset + * @param yOffset + */ +function doArcDownward(scene: BattleScene, transformationBaseBg: Phaser.GameObjects.Image, transformationContainer: Phaser.GameObjects.Container, xOffset: number, yOffset: number) { let f = 0; scene.tweens.addCounter({ @@ -181,7 +197,15 @@ function doArcDownward(scene: BattleScene, transformationBaseBg, transformationC }); } -function doCycle(scene: BattleScene, l: number, lastCycle: integer, pokemonTintSprite, pokemonEvoTintSprite): Promise { +/** + * Animates the transformation between the old pokemon form and new pokemon form + * @param scene + * @param l + * @param lastCycle + * @param pokemonTintSprite + * @param pokemonEvoTintSprite + */ +function doCycle(scene: BattleScene, l: number, lastCycle: number, pokemonTintSprite: Phaser.GameObjects.Sprite, pokemonEvoTintSprite: Phaser.GameObjects.Sprite): Promise { return new Promise(resolve => { const isLastCycle = l === lastCycle; scene.tweens.add({ @@ -209,7 +233,15 @@ function doCycle(scene: BattleScene, l: number, lastCycle: integer, pokemonTintS }); } -function doCircleInward(scene: BattleScene, transformationBaseBg, transformationContainer, xOffset: number, yOffset: number) { +/** + * Animates particles in a circle pattern + * @param scene + * @param transformationBaseBg + * @param transformationContainer + * @param xOffset + * @param yOffset + */ +function doCircleInward(scene: BattleScene, transformationBaseBg: Phaser.GameObjects.Image, transformationContainer: Phaser.GameObjects.Container, xOffset: number, yOffset: number) { let f = 0; scene.tweens.addCounter({ @@ -230,7 +262,16 @@ function doCircleInward(scene: BattleScene, transformationBaseBg, transformation }); } -function doSpiralUpwardParticle(scene: BattleScene, trigIndex: integer, transformationBaseBg, transformationContainer, xOffset: number, yOffset: number) { +/** + * Helper function for {@link doSpiralUpward}, handles a single particle + * @param scene + * @param trigIndex + * @param transformationBaseBg + * @param transformationContainer + * @param xOffset + * @param yOffset + */ +function doSpiralUpwardParticle(scene: BattleScene, trigIndex: number, transformationBaseBg: Phaser.GameObjects.Image, transformationContainer: Phaser.GameObjects.Container, xOffset: number, yOffset: number) { const initialX = transformationBaseBg.displayWidth / 2 + xOffset; const particle = scene.add.image(initialX, 0, "evo_sparkle"); transformationContainer.add(particle); @@ -266,7 +307,16 @@ function doSpiralUpwardParticle(scene: BattleScene, trigIndex: integer, transfor updateParticle(); } -function doArcDownParticle(scene: BattleScene, trigIndex: integer, transformationBaseBg, transformationContainer, xOffset: number, yOffset: number) { +/** + * Helper function for {@link doArcDownward}, handles a single particle + * @param scene + * @param trigIndex + * @param transformationBaseBg + * @param transformationContainer + * @param xOffset + * @param yOffset + */ +function doArcDownParticle(scene: BattleScene, trigIndex: number, transformationBaseBg: Phaser.GameObjects.Image, transformationContainer: Phaser.GameObjects.Container, xOffset: number, yOffset: number) { const initialX = transformationBaseBg.displayWidth / 2 + xOffset; const particle = scene.add.image(initialX, 0, "evo_sparkle"); particle.setScale(0.5); @@ -299,7 +349,17 @@ function doArcDownParticle(scene: BattleScene, trigIndex: integer, transformatio updateParticle(); } -function doCircleInwardParticle(scene: BattleScene, trigIndex: integer, speed: integer, transformationBaseBg, transformationContainer, xOffset: number, yOffset: number) { +/** + * Helper function for @{link doCircleInward}, handles a single particle + * @param scene + * @param trigIndex + * @param speed + * @param transformationBaseBg + * @param transformationContainer + * @param xOffset + * @param yOffset + */ +function doCircleInwardParticle(scene: BattleScene, trigIndex: number, speed: number, transformationBaseBg: Phaser.GameObjects.Image, transformationContainer: Phaser.GameObjects.Container, xOffset: number, yOffset: number) { const initialX = transformationBaseBg.displayWidth / 2 + xOffset; const initialY = transformationBaseBg.displayHeight / 2 + yOffset; const particle = scene.add.image(initialX, initialY, "evo_sparkle"); diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index bddb68998ad..d6b30522a7b 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -247,7 +247,7 @@ export abstract class PokemonSpeciesForm { * Gets the BST for the species * @returns The species' BST. */ - getBaseStatTotal(): integer { + getBaseStatTotal(): number { return this.baseStats.reduce((i, n) => n + i); } @@ -256,11 +256,11 @@ export abstract class PokemonSpeciesForm { * @param stat The desired stat. * @returns The species' base stat amount. */ - getBaseStat(stat: Stat): integer { + getBaseStat(stat: Stat): number { return this.baseStats[stat]; } - getBaseExp(): integer { + getBaseExp(): number { let ret = this.baseExp; switch (this.getFormSpriteKey()) { case SpeciesFormKey.MEGA: diff --git a/src/data/trainer-config.ts b/src/data/trainer-config.ts index 8ca1bdfbd0e..411a37ed4c8 100644 --- a/src/data/trainer-config.ts +++ b/src/data/trainer-config.ts @@ -994,6 +994,9 @@ export class TrainerConfig { }); } + /** + * Creates a copy of a trainer config so that it can be modified without affecting the {@link trainerConfigs} source map + */ copy(): TrainerConfig { let copy = new TrainerConfig(this.trainerType); copy = this.trainerTypeDouble ? copy.setDoubleTrainerType(this.trainerTypeDouble) : copy; diff --git a/src/data/weather.ts b/src/data/weather.ts index 2421f719e6e..afdd0a958cf 100644 --- a/src/data/weather.ts +++ b/src/data/weather.ts @@ -88,12 +88,14 @@ export class Weather { return 1; } - isMoveWeatherCancelled(move: Move): boolean { + isMoveWeatherCancelled(user: Pokemon, move: Move): boolean { + const moveType = user.getMoveType(move); + switch (this.weatherType) { case WeatherType.HARSH_SUN: - return move instanceof AttackMove && move.type === Type.WATER; + return move instanceof AttackMove && moveType === Type.WATER; case WeatherType.HEAVY_RAIN: - return move instanceof AttackMove && move.type === Type.FIRE; + return move instanceof AttackMove && moveType === Type.FIRE; } return false; diff --git a/src/enums/mystery-encounter-mode.ts b/src/enums/mystery-encounter-mode.ts index bba3ec3772e..3313b19e400 100644 --- a/src/enums/mystery-encounter-mode.ts +++ b/src/enums/mystery-encounter-mode.ts @@ -1,9 +1,12 @@ export enum MysteryEncounterMode { /** MysteryEncounter will always begin in this mode, but will always swap modes when an option is selected */ DEFAULT, + /** If the MysteryEncounter battle is a trainer type battle */ TRAINER_BATTLE, + /** If the MysteryEncounter battle is a wild type battle */ WILD_BATTLE, /** Enables special boss music during encounter */ BOSS_BATTLE, + /** If there is no battle in the MysteryEncounter or option selected */ NO_BATTLE } diff --git a/src/enums/mystery-encounter-type.ts b/src/enums/mystery-encounter-type.ts index 6f1a6bd7cd5..39a8087599c 100644 --- a/src/enums/mystery-encounter-type.ts +++ b/src/enums/mystery-encounter-type.ts @@ -9,7 +9,7 @@ export enum MysteryEncounterType { SHADY_VITAMIN_DEALER, FIELD_TRIP, SAFARI_ZONE, - LOST_AT_SEA, // Might be generalized later on + LOST_AT_SEA, FIERY_FALLOUT, THE_STRONG_STUFF, THE_POKEMON_SALESMAN, diff --git a/src/field/arena.ts b/src/field/arena.ts index fd6bf1a2b53..0466c01c82b 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -391,8 +391,8 @@ export class Arena { return true; } - isMoveWeatherCancelled(move: Move) { - return this.weather && !this.weather.isEffectSuppressed(this.scene) && this.weather.isMoveWeatherCancelled(move); + isMoveWeatherCancelled(user: Pokemon, move: Move) { + return this.weather && !this.weather.isEffectSuppressed(this.scene) && this.weather.isMoveWeatherCancelled(user, move); } isMoveTerrainCancelled(user: Pokemon, targets: BattlerIndex[], move: Move) { diff --git a/src/field/mystery-encounter-intro.ts b/src/field/mystery-encounter-intro.ts index 2adf26bb2b3..7c58a494699 100644 --- a/src/field/mystery-encounter-intro.ts +++ b/src/field/mystery-encounter-intro.ts @@ -175,6 +175,9 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con }); } + /** + * Loads the assets that were defined on construction (async) + */ loadAssets(): Promise { return new Promise(resolve => { if (!this.spriteConfigs) { @@ -226,6 +229,9 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con }); } + /** + * Sets the initial frames and tint of sprites after load + */ initSprite(): void { if (!this.spriteConfigs) { return; @@ -282,6 +288,9 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con return true; } + /** + * For sprites with animation and that do not have animation disabled, will begin frame animation + */ playAnim(): void { if (!this.spriteConfigs) { return; @@ -318,6 +327,9 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con return ret; } + /** + * Gets all non-tint sprites (these are the "real" unmodified sprites) + */ getSprites(): Phaser.GameObjects.Sprite[] { if (!this.spriteConfigs) { return []; @@ -330,6 +342,9 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con return ret; } + /** + * Gets all tint sprites (duplicate sprites that have different alpha and fill values) + */ getTintSprites(): Phaser.GameObjects.Sprite[] { if (!this.spriteConfigs) { return []; @@ -343,7 +358,15 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con return ret; } - tint(sprite, color: number, alpha?: number, duration?: integer, ease?: string): void { + /** + * Tints a single sprite + * @param sprite + * @param color + * @param alpha + * @param duration + * @param ease + */ + private tint(sprite, color: number, alpha?: number, duration?: integer, ease?: string): void { // const tintSprites = this.getTintSprites(); sprite.setTintFill(color); sprite.setVisible(true); @@ -362,6 +385,13 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con } } + /** + * Tints all sprites + * @param color + * @param alpha + * @param duration + * @param ease + */ tintAll(color: number, alpha?: number, duration?: integer, ease?: string): void { const tintSprites = this.getTintSprites(); tintSprites.map(tintSprite => { @@ -369,7 +399,13 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con }); } - untint(sprite, duration: integer, ease?: string): void { + /** + * Untints a single sprite over a duration + * @param sprite + * @param duration + * @param ease + */ + private untint(sprite, duration: integer, ease?: string): void { if (duration) { this.scene.tweens.add({ targets: sprite, @@ -387,6 +423,12 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con } } + /** + * Untints all sprites + * @param sprite + * @param duration + * @param ease + */ untintAll(duration: integer, ease?: string): void { const tintSprites = this.getTintSprites(); tintSprites.map(tintSprite => { @@ -406,6 +448,9 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con } } +/** + * Interface is required so as not to override {@link Phaser.GameObjects.Container.scene} + */ export default interface MysteryEncounterIntroVisuals { scene: BattleScene } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index b2cf70b445a..bbc234f44e1 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2875,7 +2875,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return this.fusionFaintCry(callback); } - const key = `cry/${this.getSpeciesForm().getCryKey(this.formIndex)}`; + const key = `cry/${this.species.getCryKey(this.formIndex)}`; //eslint-disable-next-line @typescript-eslint/no-unused-vars let i = 0; let rate = 0.85; @@ -2933,7 +2933,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } private fusionFaintCry(callback: Function): void { - const key = `cry/${this.getSpeciesForm().getCryKey(this.formIndex)}`; + const key = `cry/${this.species.getCryKey(this.formIndex)}`; let i = 0; let rate = 0.85; const cry = this.scene.playSound(key, { rate: rate }) as AnySound; @@ -2941,7 +2941,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const tintSprite = this.getTintSprite(); let duration = cry.totalDuration * 1000; - const fusionCryKey = `cry/${this.getFusionSpeciesForm().getCryKey(this.fusionFormIndex)}`; + const fusionCryKey = `cry/${this.fusionSpecies?.getCryKey(this.fusionFormIndex)}`; let fusionCry = this.scene.playSound(fusionCryKey, { rate: rate }) as AnySound; fusionCry.stop(); duration = Math.min(duration, fusionCry.totalDuration * 1000); @@ -3628,7 +3628,6 @@ export default interface Pokemon { export class PlayerPokemon extends Pokemon { public compatibleTms: Moves[]; - public usedTms: Moves[]; constructor(scene: BattleScene, species: PokemonSpecies, level: integer, abilityIndex?: integer, formIndex?: integer, gender?: Gender, shiny?: boolean, variant?: Variant, ivs?: integer[], nature?: Nature, dataSource?: Pokemon | PokemonData) { super(scene, 106, 148, species, level, abilityIndex, formIndex, gender, shiny, variant, ivs, nature, dataSource); @@ -3652,7 +3651,6 @@ export class PlayerPokemon extends Pokemon { } } this.generateCompatibleTms(); - this.usedTms = []; } initBattleInfo(): void { diff --git a/src/game-mode.ts b/src/game-mode.ts index 3f15f084cba..cadda4e7a21 100644 --- a/src/game-mode.ts +++ b/src/game-mode.ts @@ -9,7 +9,6 @@ import * as Utils from "./utils"; import { Biome } from "#enums/biome"; import { Species } from "#enums/species"; import { Challenges } from "./enums/challenges"; -import MAX_SAFE_INTEGER = Phaser.Math.MAX_SAFE_INTEGER; export enum GameModes { CLASSIC, @@ -62,7 +61,7 @@ export class GameMode implements GameModeConfig { } this.battleConfig = battleConfig || {}; this.minMysteryEncounterWave = this.minMysteryEncounterWave ?? 0; - this.maxMysteryEncounterWave = this.maxMysteryEncounterWave ?? MAX_SAFE_INTEGER; + this.maxMysteryEncounterWave = this.maxMysteryEncounterWave ?? Number.MAX_SAFE_INTEGER; } /** diff --git a/src/loading-scene.ts b/src/loading-scene.ts index 7b811eff8d7..d0818aa1e19 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -7,15 +7,15 @@ import { WindowVariant, getWindowVariantSuffix } from "./ui/ui-theme"; import { isMobile } from "./touch-controls"; import * as Utils from "./utils"; import { initI18n } from "./plugins/i18n"; -import {initPokemonPrevolutions} from "#app/data/pokemon-evolutions"; -import {initBiomes} from "#app/data/biomes"; -import {initEggMoves} from "#app/data/egg-moves"; -import {initPokemonForms} from "#app/data/pokemon-forms"; -import {initSpecies} from "#app/data/pokemon-species"; -import {initMoves} from "#app/data/move"; -import {initAbilities} from "#app/data/ability"; -import {initAchievements} from "#app/system/achv"; -import {initTrainerTypeDialogue} from "#app/data/dialogue"; +import { initPokemonPrevolutions } from "#app/data/pokemon-evolutions"; +import { initBiomes } from "#app/data/biomes"; +import { initEggMoves } from "#app/data/egg-moves"; +import { initPokemonForms } from "#app/data/pokemon-forms"; +import { initSpecies } from "#app/data/pokemon-species"; +import { initMoves } from "#app/data/move"; +import { initAbilities } from "#app/data/ability"; +import { initAchievements } from "#app/system/achv"; +import { initTrainerTypeDialogue } from "#app/data/dialogue"; import { initChallenges } from "./data/challenge"; import i18next from "i18next"; import { initStatsKeys } from "./ui/game-stats-ui-handler"; @@ -251,9 +251,9 @@ export class LoadingScene extends SceneBase { } const availableLangs = ["en", "de", "it", "fr", "ja", "ko", "es", "pt-BR", "zh-CN"]; if (lang && availableLangs.includes(lang)) { - this.loadImage("september-update-"+lang, "events"); + this.loadImage("egg-update_"+lang, "events"); } else { - this.loadImage("september-update-en", "events"); + this.loadImage("egg-update_en", "events"); } this.loadAtlas("statuses", ""); diff --git a/src/locales/de/move-trigger.json b/src/locales/de/move-trigger.json index 61283c9e62e..01b22429fb3 100644 --- a/src/locales/de/move-trigger.json +++ b/src/locales/de/move-trigger.json @@ -66,5 +66,6 @@ "revivalBlessing": "{{pokemonName}} ist wieder fit und kampfbereit!", "swapArenaTags": "{{pokemonName}} hat die Effekte, die auf den beiden Seiten des Kampffeldes wirken, miteinander getauscht!", "exposedMove": "{{pokemonName}} erkennt {{targetPokemonName}}!", - "safeguard": "{{targetName}} wird durch Bodyguard geschützt!" + "safeguard": "{{targetName}} wird durch Bodyguard geschützt!", + "afterYou": "{{targetName}} lässt sich auf Galanterie ein!" } diff --git a/src/locales/en/config.ts b/src/locales/en/config.ts index 94b7423f776..f83fec5be26 100644 --- a/src/locales/en/config.ts +++ b/src/locales/en/config.ts @@ -53,9 +53,48 @@ import terrain from "./terrain.json"; import modifierSelectUiHandler from "./modifier-select-ui-handler.json"; import moveTriggers from "./move-trigger.json"; import runHistory from "./run-history.json"; -import { mysteryEncounter } from "#app/locales/en/mystery-encounter"; import mysteryEncounterMessages from "./mystery-encounter-messages.json"; +import lostAtSea from "./mystery-encounters/lost-at-sea-dialogue.json"; +import mysteriousChest from "#app/locales/en/mystery-encounters/mysterious-chest-dialogue.json"; +import mysteriousChallengers from "#app/locales/en/mystery-encounters/mysterious-challengers-dialogue.json"; +import darkDeal from "#app/locales/en/mystery-encounters/dark-deal-dialogue.json"; +import departmentStoreSale from "#app/locales/en/mystery-encounters/department-store-sale-dialogue.json"; +import fieldTrip from "#app/locales/en/mystery-encounters/field-trip-dialogue.json"; +import fieryFallout from "#app/locales/en/mystery-encounters/fiery-fallout-dialogue.json"; +import fightOrFlight from "#app/locales/en/mystery-encounters/fight-or-flight-dialogue.json"; +import safariZone from "#app/locales/en/mystery-encounters/safari-zone-dialogue.json"; +import shadyVitaminDealer from "#app/locales/en/mystery-encounters/shady-vitamin-dealer-dialogue.json"; +import slumberingSnorlax from "#app/locales/en/mystery-encounters/slumbering-snorlax-dialogue.json"; +import trainingSession from "#app/locales/en/mystery-encounters/training-session-dialogue.json"; +import theStrongStuff from "#app/locales/en/mystery-encounters/the-strong-stuff-dialogue.json"; +import pokemonSalesman from "#app/locales/en/mystery-encounters/the-pokemon-salesman-dialogue.json"; +import offerYouCantRefuse from "#app/locales/en/mystery-encounters/an-offer-you-cant-refuse-dialogue.json"; +import delibirdy from "#app/locales/en/mystery-encounters/delibirdy-dialogue.json"; +import absoluteAvarice from "#app/locales/en/mystery-encounters/absolute-avarice-dialogue.json"; +import aTrainersTest from "#app/locales/en/mystery-encounters/a-trainers-test-dialogue.json"; +import trashToTreasure from "#app/locales/en/mystery-encounters/trash-to-treasure-dialogue.json"; +import berriesAbound from "#app/locales/en/mystery-encounters/berries-abound-dialogue.json"; +import clowningAround from "#app/locales/en/mystery-encounters/clowning-around-dialogue.json"; +import partTimer from "#app/locales/en/mystery-encounters/part-timer-dialogue.json"; +import dancingLessons from "#app/locales/en/mystery-encounters/dancing-lessons-dialogue.json"; +import weirdDream from "#app/locales/en/mystery-encounters/weird-dream-dialogue.json"; +import theWinstrateChallenge from "#app/locales/en/mystery-encounters/the-winstrate-challenge-dialogue.json"; +import teleportingHijinks from "#app/locales/en/mystery-encounters/teleporting-hijinks-dialogue.json"; +import bugTypeSuperfan from "#app/locales/en/mystery-encounters/bug-type-superfan-dialogue.json"; +import funAndGames from "#app/locales/en/mystery-encounters/fun-and-games-dialogue.json"; +import uncommonBreed from "#app/locales/en/mystery-encounters/uncommon-breed-dialogue.json"; +import globalTradeSystem from "#app/locales/en/mystery-encounters/global-trade-system-dialogue.json"; +/** + * Dialogue/Text token injection patterns that can be used: + * - `$` will be treated as a new line for Message and Dialogue strings. + * - `@d{}` will add a time delay to text animation for Message and Dialogue strings. + * - `@s{}` will play a specified sound effect for Message and Dialogue strings. + * - `@f{}` will fade the screen to black for the given duration, then fade back in for Message and Dialogue strings. + * - `{{}}` (MYSTERY ENCOUNTERS ONLY) will auto-inject the matching dialogue token value that is stored in {@link IMysteryEncounter.dialogueTokens}. + * - (see [i18next interpolations](https://www.i18next.com/translation-function/interpolation)) for more details. + * - `@[]{}` (STATIC TEXT ONLY, NOT USEABLE WITH {@link UI.showText()} OR {@link UI.showDialogue()}) will auto-color the given text to a specified {@link TextStyle} (e.g. `TextStyle.SUMMARY_GREEN`). + */ export const enConfig = { ability, abilityTriggers, @@ -112,6 +151,39 @@ export const enConfig = { modifierSelectUiHandler, moveTriggers, runHistory, - mysteryEncounter: mysteryEncounter, + mysteryEncounter: { + // DO NOT REMOVE + "unit_test_dialogue": "{{test}}{{test}} {{test{{test}}}} {{test1}} {{test\}} {{test\\}} {{test\\\}} {test}}", + mysteriousChallengers, + mysteriousChest, + darkDeal, + fightOrFlight, + slumberingSnorlax, + trainingSession, + departmentStoreSale, + shadyVitaminDealer, + fieldTrip, + safariZone, + lostAtSea, + fieryFallout, + theStrongStuff, + pokemonSalesman, + offerYouCantRefuse, + delibirdy, + absoluteAvarice, + aTrainersTest, + trashToTreasure, + berriesAbound, + clowningAround, + partTimer, + dancingLessons, + weirdDream, + theWinstrateChallenge, + teleportingHijinks, + bugTypeSuperfan, + funAndGames, + uncommonBreed, + globalTradeSystem + }, mysteryEncounterMessages }; diff --git a/src/locales/en/move-trigger.json b/src/locales/en/move-trigger.json index 867905c5a9f..375ea354d33 100644 --- a/src/locales/en/move-trigger.json +++ b/src/locales/en/move-trigger.json @@ -67,5 +67,6 @@ "revivalBlessing": "{{pokemonName}} was revived!", "swapArenaTags": "{{pokemonName}} swapped the battle effects affecting each side of the field!", "exposedMove": "{{pokemonName}} identified\n{{targetPokemonName}}!", - "safeguard": "{{targetName}} is protected by Safeguard!" -} \ No newline at end of file + "safeguard": "{{targetName}} is protected by Safeguard!", + "afterYou": "{{pokemonName}} took the kind offer!" +} diff --git a/src/locales/en/mystery-encounter.ts b/src/locales/en/mystery-encounter.ts deleted file mode 100644 index b4d9a3dbf3a..00000000000 --- a/src/locales/en/mystery-encounter.ts +++ /dev/null @@ -1,81 +0,0 @@ -import lostAtSea from "./mystery-encounters/lost-at-sea-dialogue.json"; -import mysteriousChest from "#app/locales/en/mystery-encounters/mysterious-chest-dialogue.json"; -import mysteriousChallengers from "#app/locales/en/mystery-encounters/mysterious-challengers-dialogue.json"; -import darkDeal from "#app/locales/en/mystery-encounters/dark-deal-dialogue.json"; -import departmentStoreSale from "#app/locales/en/mystery-encounters/department-store-sale-dialogue.json"; -import fieldTrip from "#app/locales/en/mystery-encounters/field-trip-dialogue.json"; -import fieryFallout from "#app/locales/en/mystery-encounters/fiery-fallout-dialogue.json"; -import fightOrFlight from "#app/locales/en/mystery-encounters/fight-or-flight-dialogue.json"; -import safariZone from "#app/locales/en/mystery-encounters/safari-zone-dialogue.json"; -import shadyVitaminDealer from "#app/locales/en/mystery-encounters/shady-vitamin-dealer-dialogue.json"; -import slumberingSnorlax from "#app/locales/en/mystery-encounters/slumbering-snorlax-dialogue.json"; -import trainingSession from "#app/locales/en/mystery-encounters/training-session-dialogue.json"; -import theStrongStuff from "#app/locales/en/mystery-encounters/the-strong-stuff-dialogue.json"; -import pokemonSalesman from "#app/locales/en/mystery-encounters/the-pokemon-salesman-dialogue.json"; -import offerYouCantRefuse from "#app/locales/en/mystery-encounters/an-offer-you-cant-refuse-dialogue.json"; -import delibirdy from "#app/locales/en/mystery-encounters/delibirdy-dialogue.json"; -import absoluteAvarice from "#app/locales/en/mystery-encounters/absolute-avarice-dialogue.json"; -import aTrainersTest from "#app/locales/en/mystery-encounters/a-trainers-test-dialogue.json"; -import trashToTreasure from "#app/locales/en/mystery-encounters/trash-to-treasure-dialogue.json"; -import berriesAbound from "#app/locales/en/mystery-encounters/berries-abound-dialogue.json"; -import clowningAround from "#app/locales/en/mystery-encounters/clowning-around-dialogue.json"; -import partTimer from "#app/locales/en/mystery-encounters/part-timer-dialogue.json"; -import dancingLessons from "#app/locales/en/mystery-encounters/dancing-lessons-dialogue.json"; -import weirdDream from "#app/locales/en/mystery-encounters/weird-dream-dialogue.json"; -import theWinstrateChallenge from "#app/locales/en/mystery-encounters/the-winstrate-challenge-dialogue.json"; -import teleportingHijinks from "#app/locales/en/mystery-encounters/teleporting-hijinks-dialogue.json"; -import bugTypeSuperfan from "#app/locales/en/mystery-encounters/bug-type-superfan-dialogue.json"; -import funAndGames from "#app/locales/en/mystery-encounters/fun-and-games-dialogue.json"; -import uncommonBreed from "#app/locales/en/mystery-encounters/uncommon-breed-dialogue.json"; -import globalTradeSystem from "#app/locales/en/mystery-encounters/global-trade-system-dialogue.json"; - -/** - * Injection patterns that can be used: - * - `$` will be treated as a new line for Message and Dialogue strings. - * - `@d{}` will add a time delay to text animation for Message and Dialogue strings. - * - `@s{}` will play a specified sound effect for Message and Dialogue strings. - * - `@f{}` will fade the screen to black for the given duration, then fade back in for Message and Dialogue strings. - * - `{{}}` will auto-inject the matching dialogue token value that is stored in {@link IMysteryEncounter.dialogueTokens}. - * - (see [i18next interpolations](https://www.i18next.com/translation-function/interpolation)) for more details. - * - `@[]{}` will auto-color the given text to a specified {@link TextStyle} (e.g. `TextStyle.SUMMARY_GREEN`). - * - * For Option tooltips ({@link OptionTextDisplay.buttonTooltip}): - * - Any tooltip that starts with `(+)` or `(-)` at the beginning of a newline will auto-color to green/blue respectively. - * - Note, this only occurs for option tooltips, nowhere else. - * - Other types of `(...)` tooltips will have to specify the text color manually by using the `@[SUMMARY_GREEN]{}` pattern. - */ -export const mysteryEncounter = { - // DO NOT REMOVE - "unit_test_dialogue": "{{test}}{{test}} {{test{{test}}}} {{test1}} {{test\}} {{test\\}} {{test\\\}} {test}}", - - mysteriousChallengers, - mysteriousChest, - darkDeal, - fightOrFlight, - slumberingSnorlax, - trainingSession, - departmentStoreSale, - shadyVitaminDealer, - fieldTrip, - safariZone, - lostAtSea, - fieryFallout, - theStrongStuff, - pokemonSalesman, - offerYouCantRefuse, - delibirdy, - absoluteAvarice, - aTrainersTest, - trashToTreasure, - berriesAbound, - clowningAround, - partTimer, - dancingLessons, - weirdDream, - theWinstrateChallenge, - teleportingHijinks, - bugTypeSuperfan, - funAndGames, - uncommonBreed, - globalTradeSystem -} as const; diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index adcaf0d7833..b807dc8b7c6 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -166,7 +166,7 @@ export interface GeneratedPersistentModifierType { getPregenArgs(): any[]; } -export class AddPokeballModifierType extends ModifierType { +class AddPokeballModifierType extends ModifierType { private pokeballType: PokeballType; private count: integer; @@ -647,6 +647,9 @@ export class BaseStatBoosterModifierType extends PokemonHeldItemModifierType imp } } +/** + * Shuckle Juice item + */ export class PokemonBaseStatTotalModifierType extends PokemonHeldItemModifierType implements GeneratedPersistentModifierType { private readonly statModifier: integer; @@ -655,7 +658,7 @@ export class PokemonBaseStatTotalModifierType extends PokemonHeldItemModifierTyp this.statModifier = statModifier; } - getDescription(scene: BattleScene): string { + override getDescription(scene: BattleScene): string { return i18next.t("modifierType:ModifierType.PokemonBaseStatTotalModifierType.description", { increaseDecrease: i18next.t(this.statModifier >= 0 ? "modifierType:ModifierType.PokemonBaseStatTotalModifierType.extra.increase" : "modifierType:ModifierType.PokemonBaseStatTotalModifierType.extra.decrease"), blessCurse: i18next.t(this.statModifier >= 0 ? "modifierType:ModifierType.PokemonBaseStatTotalModifierType.extra.blessed" : "modifierType:ModifierType.PokemonBaseStatTotalModifierType.extra.cursed"), @@ -663,11 +666,14 @@ export class PokemonBaseStatTotalModifierType extends PokemonHeldItemModifierTyp }); } - getPregenArgs(): any[] { + public getPregenArgs(): any[] { return [ this.statModifier ]; } } +/** + * Old Gateau item + */ export class PokemonBaseStatFlatModifierType extends PokemonHeldItemModifierType implements GeneratedPersistentModifierType { private readonly statModifier: integer; private readonly stats: Stat[]; @@ -678,14 +684,14 @@ export class PokemonBaseStatFlatModifierType extends PokemonHeldItemModifierType this.stats = stats; } - getDescription(scene: BattleScene): string { + override getDescription(scene: BattleScene): string { return i18next.t("modifierType:ModifierType.PokemonBaseStatFlatModifierType.description", { stats: this.stats.map(stat => i18next.t(getStatKey(stat))).join("/"), statValue: this.statModifier, }); } - getPregenArgs(): any[] { + public getPregenArgs(): any[] { return [ this.statModifier, this.stats ]; } } @@ -1607,6 +1613,7 @@ const modifierPool: ModifierPool = { new WeightedModifierType(modifierTypes.TEMP_STAT_STAGE_BOOSTER, 4), new WeightedModifierType(modifierTypes.BERRY, 2), new WeightedModifierType(modifierTypes.TM_COMMON, 2), + new WeightedModifierType(modifierTypes.VOUCHER, (party: Pokemon[], rerollCount: integer) => !party[0].scene.gameMode.isDaily ? Math.max(1 - rerollCount, 0) : 0, 1), ].map(m => { m.setTier(ModifierTier.COMMON); return m; }), @@ -1677,7 +1684,7 @@ const modifierPool: ModifierPool = { new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 3), new WeightedModifierType(modifierTypes.TERA_SHARD, 1), new WeightedModifierType(modifierTypes.DNA_SPLICERS, (party: Pokemon[]) => party[0].scene.gameMode.isSplicedOnly && party.filter(p => !p.fusionSpecies).length > 1 ? 4 : 0), - new WeightedModifierType(modifierTypes.VOUCHER, (party: Pokemon[], rerollCount: integer) => !party[0].scene.gameMode.isDaily ? Math.max(1 - rerollCount, 0) : 0, 1), + new WeightedModifierType(modifierTypes.VOUCHER, (party: Pokemon[], rerollCount: integer) => !party[0].scene.gameMode.isDaily ? Math.max(3 - rerollCount * 3, 0) : 0, 3), ].map(m => { m.setTier(ModifierTier.GREAT); return m; }), @@ -1758,7 +1765,7 @@ const modifierPool: ModifierPool = { new WeightedModifierType(modifierTypes.RARE_FORM_CHANGE_ITEM, (party: Pokemon[]) => Math.min(Math.ceil(party[0].scene.currentBattle.waveIndex / 50), 4) * 6, 24), new WeightedModifierType(modifierTypes.MEGA_BRACELET, (party: Pokemon[]) => Math.min(Math.ceil(party[0].scene.currentBattle.waveIndex / 50), 4) * 9, 36), new WeightedModifierType(modifierTypes.DYNAMAX_BAND, (party: Pokemon[]) => Math.min(Math.ceil(party[0].scene.currentBattle.waveIndex / 50), 4) * 9, 36), - new WeightedModifierType(modifierTypes.VOUCHER_PLUS, (party: Pokemon[], rerollCount: integer) => !party[0].scene.gameMode.isDaily ? Math.max(3 - rerollCount * 1, 0) : 0, 3), + new WeightedModifierType(modifierTypes.VOUCHER_PLUS, (party: Pokemon[], rerollCount: integer) => !party[0].scene.gameMode.isDaily ? Math.max(9 - rerollCount * 3, 0) : 0, 9), ].map(m => { m.setTier(ModifierTier.ROGUE); return m; }), @@ -1767,7 +1774,7 @@ const modifierPool: ModifierPool = { new WeightedModifierType(modifierTypes.SHINY_CHARM, 14), new WeightedModifierType(modifierTypes.HEALING_CHARM, 18), new WeightedModifierType(modifierTypes.MULTI_LENS, 18), - new WeightedModifierType(modifierTypes.VOUCHER_PREMIUM, (party: Pokemon[], rerollCount: integer) => !party[0].scene.gameMode.isDaily && !party[0].scene.gameMode.isEndless && !party[0].scene.gameMode.isSplicedOnly ? Math.max(5 - rerollCount * 2, 0) : 0, 5), + new WeightedModifierType(modifierTypes.VOUCHER_PREMIUM, (party: Pokemon[], rerollCount: integer) => !party[0].scene.gameMode.isDaily && !party[0].scene.gameMode.isEndless && !party[0].scene.gameMode.isSplicedOnly ? Math.max(15 - rerollCount * 5, 0) : 0, 15), new WeightedModifierType(modifierTypes.DNA_SPLICERS, (party: Pokemon[]) => !party[0].scene.gameMode.isSplicedOnly && party.filter(p => !p.fusionSpecies).length > 1 ? 24 : 0, 24), new WeightedModifierType(modifierTypes.MINI_BLACK_HOLE, (party: Pokemon[]) => (!party[0].scene.gameMode.isFreshStartChallenge() && party[0].scene.gameData.unlocks[Unlockables.MINI_BLACK_HOLE]) ? 1 : 0, 1), ].map(m => { @@ -2118,6 +2125,14 @@ export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemo return options; } +/** + * Will generate a ModifierType from the ModifierPoolType.PLAYER pool, attempting to retry duplicated items up to retryCount + * @param existingOptions - currently generated options + * @param retryCount - how many times to retry before allowing a dupe item + * @param party - current player party, used to calculate items in the pool + * @param tier - If specified will generate item of tier + * @param allowLuckUpgrades - allow items to upgrade tiers (the little animation that plays and is affected by luck) + */ function getModifierTypeOptionWithRetry(existingOptions: ModifierTypeOption[], retryCount: integer, party: PlayerPokemon[], tier?: ModifierTier, allowLuckUpgrades?: boolean): ModifierTypeOption { allowLuckUpgrades = allowLuckUpgrades ?? true; let candidate = getNewModifierTypeOption(party, ModifierPoolType.PLAYER, tier, undefined, 0, allowLuckUpgrades); @@ -2253,6 +2268,15 @@ export function getDailyRunStarterModifiers(party: PlayerPokemon[]): Modifiers.P return ret; } +/** + * Generates a ModifierType from the specified pool + * @param party - party of the trainer using the item + * @param poolType - PLAYER/WILD/TRAINER + * @param tier - If specified, will override the initial tier of an item (can still upgrade with luck) + * @param upgradeCount - If defined, means that this is a new ModifierType being generated to override another via luck upgrade. Used for recursive logic + * @param retryCount - Max allowed tries before the next tier down is checked for a valid ModifierType + * @param allowLuckUpgrades - Default true. If false, will not allow ModifierType to randomly upgrade to next tier + */ function getNewModifierTypeOption(party: Pokemon[], poolType: ModifierPoolType, tier?: ModifierTier, upgradeCount?: integer, retryCount: integer = 0, allowLuckUpgrades: boolean = true): ModifierTypeOption | null { const player = !poolType; const pool = getModifierPoolForType(poolType); diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index c3742c9ecbe..8b1be8bbf9c 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -367,6 +367,10 @@ export abstract class LapsingPersistentModifier extends PersistentModifier { return container; } + getIconStackText(_scene: BattleScene, _virtual?: boolean): Phaser.GameObjects.BitmapText | null { + return null; + } + getBattleCount(): number { return this.battleCount; } @@ -384,7 +388,8 @@ export abstract class LapsingPersistentModifier extends PersistentModifier { } getMaxStackCount(_scene: BattleScene, _forThreshold?: boolean): number { - return 1; + // Must be an abitrary number greater than 1 + return 2; } } @@ -787,7 +792,7 @@ export class TerastallizeModifier extends LapsingPokemonHeldItemModifier { /** * Modifier used for held items, specifically vitamins like Carbos, Hp Up, etc., that * increase the value of a given {@linkcode PermanentStat}. - * @extends LapsingPersistentModifier + * @extends PokemonHeldItemModifier * @see {@linkcode apply} */ export class BaseStatModifier extends PokemonHeldItemModifier { @@ -833,6 +838,9 @@ export class BaseStatModifier extends PokemonHeldItemModifier { } } +/** + * Currently used by Shuckle Juice item + */ export class PokemonBaseStatTotalModifier extends PokemonHeldItemModifier { private statModifier: integer; readonly isTransferrable: boolean = false; @@ -842,23 +850,23 @@ export class PokemonBaseStatTotalModifier extends PokemonHeldItemModifier { this.statModifier = statModifier; } - matchType(modifier: Modifier): boolean { + override matchType(modifier: Modifier): boolean { return modifier instanceof PokemonBaseStatTotalModifier; } - clone(): PersistentModifier { + override clone(): PersistentModifier { return new PokemonBaseStatTotalModifier(this.type as ModifierTypes.PokemonBaseStatTotalModifierType, this.pokemonId, this.statModifier, this.stackCount); } - getArgs(): any[] { + override getArgs(): any[] { return super.getArgs().concat(this.statModifier); } - shouldApply(args: any[]): boolean { + override shouldApply(args: any[]): boolean { return super.shouldApply(args) && args.length === 2 && args[1] instanceof Array; } - apply(args: any[]): boolean { + override apply(args: any[]): boolean { // Modifies the passed in baseStats[] array args[1].forEach((v, i) => { // HP is affected by half as much as other stats @@ -869,15 +877,18 @@ export class PokemonBaseStatTotalModifier extends PokemonHeldItemModifier { return true; } - getScoreMultiplier(): number { + override getScoreMultiplier(): number { return 1.2; } - getMaxHeldItemCount(pokemon: Pokemon): integer { + override getMaxHeldItemCount(pokemon: Pokemon): integer { return 2; } } +/** + * Currently used by Old Gateau item + */ export class PokemonBaseStatFlatModifier extends PokemonHeldItemModifier { private statModifier: integer; private stats: Stat[]; @@ -890,23 +901,23 @@ export class PokemonBaseStatFlatModifier extends PokemonHeldItemModifier { this.stats = stats; } - matchType(modifier: Modifier): boolean { + override matchType(modifier: Modifier): boolean { return modifier instanceof PokemonBaseStatFlatModifier; } - clone(): PersistentModifier { + override clone(): PersistentModifier { return new PokemonBaseStatFlatModifier(this.type, this.pokemonId, this.statModifier, this.stats, this.stackCount); } - getArgs(): any[] { + override getArgs(): any[] { return super.getArgs().concat(this.statModifier, this.stats); } - shouldApply(args: any[]): boolean { + override shouldApply(args: any[]): boolean { return super.shouldApply(args) && args.length === 2 && args[1] instanceof Array; } - apply(args: any[]): boolean { + override apply(args: any[]): boolean { // Modifies the passed in baseStats[] array by a flat value, only if the stat is specified in this.stats args[1].forEach((v, i) => { if (this.stats.includes(i)) { @@ -918,15 +929,18 @@ export class PokemonBaseStatFlatModifier extends PokemonHeldItemModifier { return true; } - getScoreMultiplier(): number { + override getScoreMultiplier(): number { return 1.1; } - getMaxHeldItemCount(pokemon: Pokemon): integer { + override getMaxHeldItemCount(pokemon: Pokemon): integer { return 1; } } +/** + * Currently used by Macho Brace item + */ export class PokemonIncrementingStatModifier extends PokemonHeldItemModifier { readonly isTransferrable: boolean = false; @@ -2500,6 +2514,9 @@ export class LockModifierTiersModifier extends PersistentModifier { } } +/** + * Black Sludge item + */ export class HealShopCostModifier extends PersistentModifier { constructor(type: ModifierType, stackCount?: integer) { super(type, stackCount); diff --git a/src/phases/egg-hatch-phase.ts b/src/phases/egg-hatch-phase.ts index 4b03aa62f02..90aceeb46bc 100644 --- a/src/phases/egg-hatch-phase.ts +++ b/src/phases/egg-hatch-phase.ts @@ -448,6 +448,7 @@ export class EggHatchPhase extends Phase { */ generatePokemon(): PlayerPokemon { this.eggHatchData = this.eggLapsePhase.generatePokemon(this.egg); + this.eggMoveIndex = this.eggHatchData.eggMoveIndex; return this.eggHatchData.pokemon; } } diff --git a/src/phases/egg-summary-phase.ts b/src/phases/egg-summary-phase.ts index 190af17c724..75c6939daf1 100644 --- a/src/phases/egg-summary-phase.ts +++ b/src/phases/egg-summary-phase.ts @@ -43,8 +43,9 @@ export class EggSummaryPhase extends Phase { } end() { - this.eggHatchHandler.clear(); - this.scene.ui.setModeForceTransition(Mode.MESSAGE).then(() => {}); - super.end(); + this.scene.time.delayedCall(250, () => this.scene.setModifiersVisible(true)); + this.scene.ui.setModeForceTransition(Mode.MESSAGE).then(() => { + super.end(); + }); } } diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index b6f1fa649e0..3ef9625ddee 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -99,7 +99,7 @@ export class EncounterPhase extends BattlePhase { let enemySpecies = this.scene.randomSpecies(battle.waveIndex, level, true); // If player has golden bug net, rolls 10% chance to replace with species from the golden bug net bug pool if (!!this.scene.findModifier(m => m instanceof BoostBugSpawnModifier) && randSeedInt(10) === 0) { - enemySpecies = getGoldenBugNetSpecies(this.scene, battle.waveIndex, level); + enemySpecies = getGoldenBugNetSpecies(); } battle.enemyParty[e] = this.scene.addEnemyPokemon(enemySpecies, level, TrainerSlot.NONE, !!this.scene.getEncounterBossSegments(battle.waveIndex, level, enemySpecies)); if (this.scene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) { @@ -433,7 +433,7 @@ export class EncounterPhase extends BattlePhase { } }); - if (this.scene.currentBattle.battleType !== BattleType.TRAINER && this.scene.currentBattle.battleType !== BattleType.MYSTERY_ENCOUNTER) { + if (![BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(this.scene.currentBattle.battleType)) { enemyField.map(p => this.scene.pushConditionalPhase(new PostSummonPhase(this.scene, p.getBattlerIndex()), () => { // if there is not a player party, we can't continue if (!this.scene.getParty()?.length) { diff --git a/src/phases/faint-phase.ts b/src/phases/faint-phase.ts index b64bcc1cb6d..c2a16393c1c 100644 --- a/src/phases/faint-phase.ts +++ b/src/phases/faint-phase.ts @@ -108,7 +108,7 @@ export class FaintPhase extends PokemonPhase { } } else { this.scene.unshiftPhase(new VictoryPhase(this.scene, this.battlerIndex)); - if (this.scene.currentBattle.battleType === BattleType.TRAINER || this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER) { + if ([BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(this.scene.currentBattle.battleType)) { const hasReservePartyMember = !!this.scene.getEnemyParty().filter(p => p.isActive() && !p.isOnField() && p.trainerSlot === (pokemon as EnemyPokemon).trainerSlot).length; if (hasReservePartyMember) { this.scene.pushPhase(new SwitchSummonPhase(this.scene, this.fieldIndex, -1, false, false, false)); diff --git a/src/phases/learn-move-phase.ts b/src/phases/learn-move-phase.ts index fad7eac9b68..5b4b16f3785 100644 --- a/src/phases/learn-move-phase.ts +++ b/src/phases/learn-move-phase.ts @@ -137,6 +137,9 @@ export class LearnMovePhase extends PlayerPartyMemberPokemonPhase { */ async learnMove(index: number, move: Move, pokemon: Pokemon, textMessage?: string) { if (this.fromTM) { + if (!pokemon.usedTMs) { + pokemon.usedTMs = []; + } pokemon.usedTMs.push(this.moveId); } pokemon.setMove(index, this.moveId); diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 8209bdd44d1..6089e7d3202 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -204,7 +204,7 @@ export class MovePhase extends BattlePhase { let success = this.move.getMove().applyConditions(this.pokemon, targets[0], this.move.getMove()); const cancelled = new Utils.BooleanHolder(false); let failedText = this.move.getMove().getFailedText(this.pokemon, targets[0], this.move.getMove(), cancelled); - if (success && this.scene.arena.isMoveWeatherCancelled(this.move.getMove())) { + if (success && this.scene.arena.isMoveWeatherCancelled(this.pokemon, this.move.getMove())) { success = false; } else if (success && this.scene.arena.isMoveTerrainCancelled(this.pokemon, this.targets, this.move.getMove())) { success = false; diff --git a/src/phases/mystery-encounter-phases.ts b/src/phases/mystery-encounter-phases.ts index ad3a5d150a8..5a6572aaae5 100644 --- a/src/phases/mystery-encounter-phases.ts +++ b/src/phases/mystery-encounter-phases.ts @@ -7,7 +7,6 @@ import MysteryEncounterOption, { OptionPhaseCallback } from "../data/mystery-enc import { getCharVariantFromDialogue } from "../data/dialogue"; import { TrainerSlot } from "../data/trainer-config"; import { BattleSpec } from "#enums/battle-spec"; -import { Tutorial, handleTutorial } from "../tutorial"; import { IvScannerModifier } from "../modifier/modifier"; import * as Utils from "../utils"; import { isNullOrUndefined } from "../utils"; @@ -50,6 +49,9 @@ export class MysteryEncounterPhase extends Phase { this.optionSelectSettings = optionSelectSettings; } + /** + * Updates seed offset, sets seen encounter session data, sets UI mode + */ start() { super.start(); @@ -70,6 +72,11 @@ export class MysteryEncounterPhase extends Phase { this.scene.ui.setMode(Mode.MYSTERY_ENCOUNTER, this.optionSelectSettings); } + /** + * Triggers after a player selects an option for the encounter + * @param option + * @param index + */ handleOptionSelect(option: MysteryEncounterOption, index: number): boolean { // Set option selected flag this.scene.currentBattle.mysteryEncounter!.selectedOption = option; @@ -106,6 +113,9 @@ export class MysteryEncounterPhase extends Phase { return true; } + /** + * Queues MysteryEncounterOptionSelectedPhase, displays option.selected dialogue and ends phase + */ continueEncounter() { const endDialogueAndContinueEncounter = () => { this.scene.pushPhase(new MysteryEncounterOptionSelectedPhase(this.scene)); @@ -141,10 +151,9 @@ export class MysteryEncounterPhase extends Phase { } } - cancel() { - this.end(); - } - + /** + * Ends phase + */ end() { this.scene.ui.setMode(Mode.MESSAGE).then(() => super.end()); } @@ -196,6 +205,9 @@ export class MysteryEncounterBattleStartCleanupPhase extends Phase { super(scene); } + /** + * Cleans up TURN_END tags, any PostTurnEffectPhases, checks for Pokemon switches, then continues + */ start() { super.start(); @@ -231,7 +243,7 @@ export class MysteryEncounterBattleStartCleanupPhase extends Phase { this.scene.unshiftPhase(new ToggleDoublePositionPhase(this.scene, true)); } - super.end(); + this.end(); } } @@ -250,13 +262,21 @@ export class MysteryEncounterBattlePhase extends Phase { this.disableSwitch = disableSwitch; } + /** + * Sets up a ME battle + */ start() { super.start(); this.doMysteryEncounterBattle(this.scene); } - getBattleMessage(scene: BattleScene): string { + /** + * Gets intro battle message for new battle + * @param scene + * @private + */ + private getBattleMessage(scene: BattleScene): string { const enemyField = scene.getEnemyField(); const encounterMode = scene.currentBattle.mysteryEncounter!.encounterMode; @@ -278,7 +298,12 @@ export class MysteryEncounterBattlePhase extends Phase { : i18next.t("battle:multiWildAppeared", { pokemonName1: enemyField[0].name, pokemonName2: enemyField[1].name }); } - doMysteryEncounterBattle(scene: BattleScene) { + /** + * Queues SummonPhases for the new battle, and handles trainer animations/dialogue if Trainer battle + * @param scene + * @private + */ + private doMysteryEncounterBattle(scene: BattleScene) { const encounterMode = scene.currentBattle.mysteryEncounter!.encounterMode; if (encounterMode === MysteryEncounterMode.WILD_BATTLE || encounterMode === MysteryEncounterMode.BOSS_BATTLE) { // Summons the wild/boss Pokemon @@ -342,7 +367,12 @@ export class MysteryEncounterBattlePhase extends Phase { } } - endBattleSetup(scene: BattleScene) { + /** + * Initiate SummonPhases, scanner phases, PostSummon phases, etc. + * @param scene + * @private + */ + private endBattleSetup(scene: BattleScene) { const enemyField = scene.getEnemyField(); const encounterMode = scene.currentBattle.mysteryEncounter!.encounterMode; @@ -385,11 +415,14 @@ export class MysteryEncounterBattlePhase extends Phase { } } - // TODO: remove? - handleTutorial(this.scene, Tutorial.Access_Menu).then(() => super.end()); + this.end(); } - showEnemyTrainer(): void { + /** + * Ease in enemy trainer + * @private + */ + private showEnemyTrainer(): void { // Show enemy trainer const trainer = this.scene.currentBattle.trainer; if (!trainer) { @@ -413,7 +446,7 @@ export class MysteryEncounterBattlePhase extends Phase { }); } - hideEnemyTrainer(): void { + private hideEnemyTrainer(): void { this.scene.tweens.add({ targets: this.scene.currentBattle.trainer, x: "+=16", @@ -444,6 +477,9 @@ export class MysteryEncounterRewardsPhase extends Phase { this.addHealPhase = addHealPhase; } + /** + * Runs {@link MysteryEncounter.doContinueEncounter} and ends phase, OR {@link MysteryEncounter.onRewards} then continues encounter + */ start() { super.start(); const encounter = this.scene.currentBattle.mysteryEncounter!; @@ -466,6 +502,9 @@ export class MysteryEncounterRewardsPhase extends Phase { } } + /** + * Queues encounter EXP and rewards phases, PostMysteryEncounterPhase, and ends phase + */ doEncounterRewardsAndContinue() { const encounter = this.scene.currentBattle.mysteryEncounter!; @@ -501,6 +540,9 @@ export class PostMysteryEncounterPhase extends Phase { this.onPostOptionSelect = this.scene.currentBattle.mysteryEncounter?.selectedOption?.onPostOptionPhase; } + /** + * Runs {@link MysteryEncounter.onPostOptionSelect} then continues encounter + */ start() { super.start(); @@ -518,6 +560,9 @@ export class PostMysteryEncounterPhase extends Phase { } } + /** + * Queues NewBattlePhase, plays outro dialogue and ends phase + */ continueEncounter() { const endPhase = () => { this.scene.pushPhase(new NewBattlePhase(this.scene)); diff --git a/src/phases/party-exp-phase.ts b/src/phases/party-exp-phase.ts index 9f7295ea825..8165d3e2b95 100644 --- a/src/phases/party-exp-phase.ts +++ b/src/phases/party-exp-phase.ts @@ -1,4 +1,4 @@ -import BattleScene from "#app/battle-scene.js"; +import BattleScene from "#app/battle-scene"; import { Phase } from "#app/phase"; export class PartyExpPhase extends Phase { diff --git a/src/phases/quiet-form-change-phase.ts b/src/phases/quiet-form-change-phase.ts index 6a1d31d137d..dde500e156a 100644 --- a/src/phases/quiet-form-change-phase.ts +++ b/src/phases/quiet-form-change-phase.ts @@ -65,7 +65,7 @@ export class QuietFormChangePhase extends BattlePhase { pokemonFormTintSprite.setVisible(false); pokemonFormTintSprite.setTintFill(0xFFFFFF); - this.scene.playSound("PRSFX- Transform"); + this.scene.playSound("battle_anims/PRSFX- Transform"); this.scene.tweens.add({ targets: pokemonTintSprite, diff --git a/src/phases/summon-phase.ts b/src/phases/summon-phase.ts index 3749add87aa..332ba81d195 100644 --- a/src/phases/summon-phase.ts +++ b/src/phases/summon-phase.ts @@ -232,7 +232,7 @@ export class SummonPhase extends PartyMemberPokemonPhase { pokemon.resetTurnData(); - if (!this.loaded || this.scene.currentBattle.battleType === BattleType.TRAINER || this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER || (this.scene.currentBattle.waveIndex % 10) === 1) { + if (!this.loaded || [BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(this.scene.currentBattle.battleType) || (this.scene.currentBattle.waveIndex % 10) === 1) { this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true); this.queuePostSummon(); } diff --git a/src/phases/trainer-victory-phase.ts b/src/phases/trainer-victory-phase.ts index e925f0c47d4..55b2a1608c0 100644 --- a/src/phases/trainer-victory-phase.ts +++ b/src/phases/trainer-victory-phase.ts @@ -30,7 +30,7 @@ export class TrainerVictoryPhase extends BattlePhase { const trainerType = this.scene.currentBattle.trainer?.config.trainerType!; // TODO: is this bang correct? if (vouchers.hasOwnProperty(TrainerType[trainerType])) { if (!this.scene.validateVoucher(vouchers[TrainerType[trainerType]]) && this.scene.currentBattle.trainer?.config.isBoss) { - this.scene.unshiftPhase(new ModifierRewardPhase(this.scene, [modifierTypes.VOUCHER, modifierTypes.VOUCHER, modifierTypes.VOUCHER_PLUS, modifierTypes.VOUCHER_PREMIUM][vouchers[TrainerType[trainerType]].voucherType])); + this.scene.unshiftPhase(new ModifierRewardPhase(this.scene, [modifierTypes.VOUCHER_PLUS, modifierTypes.VOUCHER_PLUS, modifierTypes.VOUCHER_PLUS, modifierTypes.VOUCHER_PREMIUM][vouchers[TrainerType[trainerType]].voucherType])); } } diff --git a/src/phases/weather-effect-phase.ts b/src/phases/weather-effect-phase.ts index e85ef0326f6..73de44389d0 100644 --- a/src/phases/weather-effect-phase.ts +++ b/src/phases/weather-effect-phase.ts @@ -1,5 +1,5 @@ import BattleScene from "#app/battle-scene"; -import { applyPreWeatherEffectAbAttrs, SuppressWeatherEffectAbAttr, PreWeatherDamageAbAttr, applyAbAttrs, BlockNonDirectDamageAbAttr, applyPostWeatherLapseAbAttrs, PostWeatherLapseAbAttr } from "#app/data/ability.js"; +import { applyPreWeatherEffectAbAttrs, SuppressWeatherEffectAbAttr, PreWeatherDamageAbAttr, applyAbAttrs, BlockNonDirectDamageAbAttr, applyPostWeatherLapseAbAttrs, PostWeatherLapseAbAttr } from "#app/data/ability"; import { CommonAnim } from "#app/data/battle-anims"; import { Weather, getWeatherDamageMessage, getWeatherLapseMessage } from "#app/data/weather"; import { BattlerTagType } from "#app/enums/battler-tag-type"; diff --git a/src/system/achv.ts b/src/system/achv.ts index 89e5493eb2e..6170fe23e1d 100644 --- a/src/system/achv.ts +++ b/src/system/achv.ts @@ -7,7 +7,7 @@ import * as Utils from "../utils"; import { PlayerGender } from "#enums/player-gender"; import { Challenge, FreshStartChallenge, SingleGenerationChallenge, SingleTypeChallenge, InverseBattleChallenge } from "#app/data/challenge"; import { ConditionFn } from "#app/@types/common"; -import { Stat, getShortenedStatKey } from "#app/enums/stat"; +import { Stat, getShortenedStatKey } from "#app/enums/stat"; import { Challenges } from "#app/enums/challenges"; export enum AchvTier { @@ -197,7 +197,7 @@ export function getAchievementDescription(localizationKey: string): string { case "100_RIBBONS": return i18next.t("achv:RibbonAchv.description", {context: genderStr, "ribbonAmount": achvs._100_RIBBONS.ribbonAmount.toLocaleString("en-US")}); case "TRANSFER_MAX_STAT_STAGE": - return i18next.t("achv:TRANSFER_MAX_BATTLE_STAT.description", { context: genderStr }); + return i18next.t("achv:TRANSFER_MAX_STAT_STAGE.description", { context: genderStr }); case "MAX_FRIENDSHIP": return i18next.t("achv:MAX_FRIENDSHIP.description", { context: genderStr }); case "MEGA_EVOLVE": diff --git a/src/system/version-converter.ts b/src/system/version-converter.ts index ed65fcd99b8..c297782ba66 100644 --- a/src/system/version-converter.ts +++ b/src/system/version-converter.ts @@ -1,4 +1,4 @@ -import { allSpecies } from "#app/data/pokemon-species.js"; +import { allSpecies } from "#app/data/pokemon-species"; import { AbilityAttr, defaultStarterSpecies, DexAttr, SessionSaveData, SystemSaveData } from "./game-data"; import { SettingKeys } from "./settings/settings"; @@ -31,7 +31,7 @@ export function applySessionDataPatches(data: SessionSaveData) { // From [ stat, battlesLeft ] to [ stat, maxBattles, battleCount ] m.args = [ newStat, 5, m.args[1] ]; - } else if (m.className === "DoubleBattleChanceBoosterModifier") { + } else if (m.className === "DoubleBattleChanceBoosterModifier" && m.args.length === 1) { let maxBattles: number; switch (m.typeId) { case "MAX_LURE": @@ -53,6 +53,8 @@ export function applySessionDataPatches(data: SessionSaveData) { data.enemyModifiers.forEach((m) => { if (m.className === "PokemonBaseStatModifier") { m.className = "BaseStatModifier"; + } else if (m.className === "PokemonResetNegativeStatStageModifier") { + m.className = "ResetNegativeStatStageModifier"; } }); } @@ -74,7 +76,7 @@ export function applySystemDataPatches(data: SystemSaveData) { if (data.starterData) { // Migrate ability starter data if empty for caught species Object.keys(data.starterData).forEach(sd => { - if (data.dexData[sd].caughtAttr && !data.starterData[sd].abilityAttr) { + if (data.dexData[sd]?.caughtAttr && (data.starterData[sd] && !data.starterData[sd].abilityAttr)) { data.starterData[sd].abilityAttr = 1; } }); @@ -102,9 +104,11 @@ export function applySystemDataPatches(data: SystemSaveData) { // --- PATCHES --- // Fix Starter Data - if (data.gameVersion) { - for (const starterId of defaultStarterSpecies) { + for (const starterId of defaultStarterSpecies) { + if (data.starterData[starterId]?.abilityAttr) { data.starterData[starterId].abilityAttr |= AbilityAttr.ABILITY_1; + } + if (data.dexData[starterId]?.caughtAttr) { data.dexData[starterId].caughtAttr |= DexAttr.FEMALE; } } diff --git a/src/test/account.spec.ts b/src/test/account.test.ts similarity index 100% rename from src/test/account.spec.ts rename to src/test/account.test.ts diff --git a/src/test/eggs/manaphy-egg.test.ts b/src/test/eggs/manaphy-egg.test.ts new file mode 100644 index 00000000000..257bf330bb8 --- /dev/null +++ b/src/test/eggs/manaphy-egg.test.ts @@ -0,0 +1,118 @@ +import { Egg } from "#app/data/egg"; +import { EggSourceType } from "#app/enums/egg-source-types"; +import { EggTier } from "#app/enums/egg-type"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Manaphy Eggs", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + const EGG_HATCH_COUNT: integer = 48; + let rngSweepProgress: number = 0; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + game = new GameManager(phaserGame); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.restoreAllMocks(); + }); + + beforeEach(async () => { + await game.importData("src/test/utils/saves/everything.prsv"); + + /** + * In our tests, we will perform an "RNG sweep" by letting rngSweepProgress + * increase uniformly from 0 to 1 in order to get a uniform sample of the + * possible RNG outcomes. This will let us quickly and consistently find + * the probability of each RNG outcome. + */ + vi.spyOn(Phaser.Math.RND, "realInRange").mockImplementation((min: number, max: number) => { + return rngSweepProgress * (max - min) + min; + }); + }); + + it("should have correct Manaphy rates and Rare Egg Move rates, from the egg gacha", () => { + const scene = game.scene; + + let manaphyCount = 0; + let phioneCount = 0; + let rareEggMoveCount = 0; + for (let i = 0; i < EGG_HATCH_COUNT; i++) { + rngSweepProgress = (2 * i + 1) / (2 * EGG_HATCH_COUNT); + + const newEgg = new Egg({ scene, tier: EggTier.COMMON, sourceType: EggSourceType.GACHA_SHINY, id: 204 }); + const newHatch = newEgg.generatePlayerPokemon(scene); + if (newHatch.species.speciesId === Species.MANAPHY) { + manaphyCount++; + } else if (newHatch.species.speciesId === Species.PHIONE) { + phioneCount++; + } + if (newEgg.eggMoveIndex === 3) { + rareEggMoveCount++; + } + } + + expect(manaphyCount + phioneCount).toBe(EGG_HATCH_COUNT); + expect(manaphyCount).toBe(1/8 * EGG_HATCH_COUNT); + expect(rareEggMoveCount).toBe(1/12 * EGG_HATCH_COUNT); + }); + + it("should have correct Manaphy rates and Rare Egg Move rates, from Phione species eggs", () => { + const scene = game.scene; + + let manaphyCount = 0; + let phioneCount = 0; + let rareEggMoveCount = 0; + for (let i = 0; i < EGG_HATCH_COUNT; i++) { + rngSweepProgress = (2 * i + 1) / (2 * EGG_HATCH_COUNT); + + const newEgg = new Egg({ scene, species: Species.PHIONE, sourceType: EggSourceType.SAME_SPECIES_EGG }); + const newHatch = newEgg.generatePlayerPokemon(scene); + if (newHatch.species.speciesId === Species.MANAPHY) { + manaphyCount++; + } else if (newHatch.species.speciesId === Species.PHIONE) { + phioneCount++; + } + if (newEgg.eggMoveIndex === 3) { + rareEggMoveCount++; + } + } + + expect(manaphyCount + phioneCount).toBe(EGG_HATCH_COUNT); + expect(manaphyCount).toBe(1/8 * EGG_HATCH_COUNT); + expect(rareEggMoveCount).toBe(1/6 * EGG_HATCH_COUNT); + }); + + it("should have correct Manaphy rates and Rare Egg Move rates, from Manaphy species eggs", () => { + const scene = game.scene; + + let manaphyCount = 0; + let phioneCount = 0; + let rareEggMoveCount = 0; + for (let i = 0; i < EGG_HATCH_COUNT; i++) { + rngSweepProgress = (2 * i + 1) / (2 * EGG_HATCH_COUNT); + + const newEgg = new Egg({ scene, species: Species.MANAPHY, sourceType: EggSourceType.SAME_SPECIES_EGG }); + const newHatch = newEgg.generatePlayerPokemon(scene); + if (newHatch.species.speciesId === Species.MANAPHY) { + manaphyCount++; + } else if (newHatch.species.speciesId === Species.PHIONE) { + phioneCount++; + } + if (newEgg.eggMoveIndex === 3) { + rareEggMoveCount++; + } + } + + expect(phioneCount).toBe(0); + expect(manaphyCount).toBe(EGG_HATCH_COUNT); + expect(rareEggMoveCount).toBe(1/6 * EGG_HATCH_COUNT); + }); +}); diff --git a/src/test/items/double_battle_chance_booster.test.ts b/src/test/items/double_battle_chance_booster.test.ts index f581af7afc5..1d5051fa9e9 100644 --- a/src/test/items/double_battle_chance_booster.test.ts +++ b/src/test/items/double_battle_chance_booster.test.ts @@ -1,13 +1,13 @@ -import { Moves } from "#app/enums/moves.js"; -import { Species } from "#app/enums/species.js"; +import { Moves } from "#app/enums/moves"; +import { Species } from "#app/enums/species"; import { DoubleBattleChanceBoosterModifier } from "#app/modifier/modifier"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -import { ShopCursorTarget } from "#app/enums/shop-cursor-target.js"; -import { Mode } from "#app/ui/ui.js"; -import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler.js"; -import { Button } from "#app/enums/buttons.js"; +import { ShopCursorTarget } from "#app/enums/shop-cursor-target"; +import { Mode } from "#app/ui/ui"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import { Button } from "#app/enums/buttons"; describe("Items - Double Battle Chance Boosters", () => { let phaserGame: Phaser.Game; diff --git a/src/test/moves/after_you.test.ts b/src/test/moves/after_you.test.ts new file mode 100644 index 00000000000..efce1b28a17 --- /dev/null +++ b/src/test/moves/after_you.test.ts @@ -0,0 +1,65 @@ +import { BattlerIndex } from "#app/battle"; +import { Abilities } from "#app/enums/abilities"; +import { MoveResult } from "#app/field/pokemon"; +import { MovePhase } from "#app/phases/move-phase"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +const TIMEOUT = 20 * 1000; + +describe("Moves - After You", () => { + 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 + .battleType("double") + .enemyLevel(5) + .enemySpecies(Species.PIKACHU) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH) + .ability(Abilities.BALL_FETCH) + .moveset([Moves.AFTER_YOU, Moves.SPLASH]); + }); + + it("makes the target move immediately after the user", async () => { + await game.classicMode.startBattle([Species.REGIELEKI, Species.SHUCKLE]); + + game.move.select(Moves.AFTER_YOU, 0, BattlerIndex.PLAYER_2); + game.move.select(Moves.SPLASH, 1); + + await game.phaseInterceptor.to("MoveEffectPhase"); + await game.phaseInterceptor.to(MovePhase, false); + const phase = game.scene.getCurrentPhase() as MovePhase; + expect(phase.pokemon).toBe(game.scene.getPlayerField()[1]); + await game.phaseInterceptor.to("MoveEndPhase"); + }, TIMEOUT); + + it("fails if target already moved", async () => { + game.override.enemySpecies(Species.SHUCKLE); + await game.classicMode.startBattle([Species.REGIELEKI, Species.PIKACHU]); + + game.move.select(Moves.SPLASH); + game.move.select(Moves.AFTER_YOU, 1, BattlerIndex.PLAYER); + + await game.phaseInterceptor.to("MoveEndPhase"); + await game.phaseInterceptor.to("MoveEndPhase"); + await game.phaseInterceptor.to(MovePhase); + + expect(game.scene.getPlayerField()[1].getLastXMoves(1)[0].result).toBe(MoveResult.FAIL); + }, TIMEOUT); +}); diff --git a/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts b/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts index 492299fe4da..5d43172f6c0 100644 --- a/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts @@ -1,7 +1,7 @@ import { LostAtSeaEncounter } from "#app/data/mystery-encounters/encounters/lost-at-sea-encounter"; import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import { getPokemonSpecies } from "#app/data/pokemon-species.js"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; import { Biome } from "#app/enums/biome"; import { Moves } from "#app/enums/moves"; import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; diff --git a/src/test/utils/gameManager.ts b/src/test/utils/gameManager.ts index 8df4bf0b12a..487d7de8e7c 100644 --- a/src/test/utils/gameManager.ts +++ b/src/test/utils/gameManager.ts @@ -94,6 +94,9 @@ export default class GameManager { this.challengeMode = new ChallengeModeHelper(this); this.settings = new SettingsHelper(this); this.reload = new ReloadHelper(this); + + // Disables Mystery Encounters on all tests (can be overridden at test level) + this.override.mysteryEncounterChance(0); } /** diff --git a/src/test/utils/gameManagerUtils.ts b/src/test/utils/gameManagerUtils.ts index 186f2efdc76..700d93082d8 100644 --- a/src/test/utils/gameManagerUtils.ts +++ b/src/test/utils/gameManagerUtils.ts @@ -96,7 +96,7 @@ export function getMovePosition(scene: BattleScene, pokemonIndex: 0 | 1, move: M * @param scene * @param species */ -export function initSceneWithoutEncounterPhase(scene, species?: Species[]) { +export function initSceneWithoutEncounterPhase(scene: BattleScene, species?: Species[]) { const starters = generateStarter(scene, species); starters.forEach((starter) => { const starterProps = scene.gameData.getSpeciesDexAttrProps(starter.species, starter.dexAttr); @@ -104,7 +104,7 @@ export function initSceneWithoutEncounterPhase(scene, species?: Species[]) { const starterGender = Gender.MALE; const starterIvs = scene.gameData.dexData[starter.species.speciesId].ivs.slice(0); const starterPokemon = scene.addPlayerPokemon(starter.species, scene.gameMode.getStartingLevel(), starter.abilityIndex, starterFormIndex, starterGender, starterProps.shiny, starterProps.variant, starterIvs, starter.nature); - starterPokemon.tryPopulateMoveset(starter.moveset); + starter.moveset && starterPokemon.tryPopulateMoveset(starter.moveset); scene.getParty().push(starterPokemon); }); diff --git a/src/test/utils/helpers/overridesHelper.ts b/src/test/utils/helpers/overridesHelper.ts index 00c489d950f..686de58e874 100644 --- a/src/test/utils/helpers/overridesHelper.ts +++ b/src/test/utils/helpers/overridesHelper.ts @@ -8,7 +8,7 @@ import * as GameMode from "#app/game-mode"; import { GameModes, getGameMode } from "#app/game-mode"; import { ModifierOverride } from "#app/modifier/modifier-type"; import Overrides from "#app/overrides"; -import { MockInstance, vi } from "vitest"; +import { vi } from "vitest"; import { GameManagerHelper } from "./gameManagerHelper"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; @@ -333,9 +333,9 @@ export class OverridesHelper extends GameManagerHelper { mysteryEncounterChance(percentage: number) { const maxRate: number = 256; // 100% const rate = maxRate * (percentage / 100); - const spy = vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_RATE_OVERRIDE", "get").mockReturnValue(rate); + vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_RATE_OVERRIDE", "get").mockReturnValue(rate); this.log(`Mystery encounter chance set to ${percentage}% (=${rate})!`); - return spy; + return this; } /** @@ -343,10 +343,10 @@ export class OverridesHelper extends GameManagerHelper { * @returns spy instance * @param tier */ - mysteryEncounterTier(tier: MysteryEncounterTier): MockInstance { - const spy = vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_TIER_OVERRIDE", "get").mockReturnValue(tier); + mysteryEncounterTier(tier: MysteryEncounterTier) { + vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_TIER_OVERRIDE", "get").mockReturnValue(tier); this.log(`Mystery encounter tier set to ${tier}!`); - return spy; + return this; } /** @@ -355,9 +355,9 @@ export class OverridesHelper extends GameManagerHelper { * @returns spy instance */ mysteryEncounter(encounterType: MysteryEncounterType) { - const spy = vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_OVERRIDE", "get").mockReturnValue(encounterType); + vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_OVERRIDE", "get").mockReturnValue(encounterType); this.log(`Mystery encounter override set to ${encounterType}!`); - return spy; + return this; } private log(...params: any[]) { diff --git a/src/test/utils/mocks/mocksContainer/mockContainer.ts b/src/test/utils/mocks/mocksContainer/mockContainer.ts index df660e15880..b1caf68d27d 100644 --- a/src/test/utils/mocks/mocksContainer/mockContainer.ts +++ b/src/test/utils/mocks/mocksContainer/mockContainer.ts @@ -14,7 +14,7 @@ export default class MockContainer implements MockGameObject { public frame; protected textureManager; public list: MockGameObject[] = []; - name: string; + public name: string; constructor(textureManager: MockTextureManager, x, y) { this.x = x; @@ -36,9 +36,7 @@ export default class MockContainer implements MockGameObject { // same as remove or destroy } - removeBetween(startIndex, endIndex, destroyChild) { - // Removes multiple children across an index range - } + removeBetween = vi.fn((startIndex, endIndex, destroyChild) => {}); addedToScene() { // This callback is invoked when this Game Object is added to a Scene. @@ -156,9 +154,7 @@ export default class MockContainer implements MockGameObject { // Sends this Game Object to the back of its parent's display list. } - moveTo(obj) { - // Moves this Game Object to the given index in the list. - } + moveTo = vi.fn((obj) => {}); moveAbove(obj) { // Moves this Game Object to be above the given Game Object in the display list. @@ -215,9 +211,9 @@ export default class MockContainer implements MockGameObject { return this.list; } - getByName(key: string) { + getByName = vi.fn((key: string) => { return this.list.find(v => v.name === key) ?? new MockContainer(this.textureManager, 0, 0); - } + }); disableInteractive = vi.fn(); } diff --git a/src/test/utils/mocks/mocksContainer/mockGraphics.ts b/src/test/utils/mocks/mocksContainer/mockGraphics.ts index 70a38c80aa0..b20faf4ed6a 100644 --- a/src/test/utils/mocks/mocksContainer/mockGraphics.ts +++ b/src/test/utils/mocks/mocksContainer/mockGraphics.ts @@ -3,7 +3,7 @@ import { MockGameObject } from "../mockGameObject"; export default class MockGraphics implements MockGameObject { private scene; public list: MockGameObject[] = []; - name: string; + public name: string; constructor(textureManager, config) { this.scene = textureManager.scene; } diff --git a/src/test/utils/mocks/mocksContainer/mockRectangle.ts b/src/test/utils/mocks/mocksContainer/mockRectangle.ts index 696427d10a3..48cd2cb1380 100644 --- a/src/test/utils/mocks/mocksContainer/mockRectangle.ts +++ b/src/test/utils/mocks/mocksContainer/mockRectangle.ts @@ -4,7 +4,7 @@ export default class MockRectangle implements MockGameObject { private fillColor; private scene; public list: MockGameObject[] = []; - name: string; + public name: string; constructor(textureManager, x, y, width, height, fillColor) { this.fillColor = fillColor; diff --git a/src/test/utils/mocks/mocksContainer/mockSprite.ts b/src/test/utils/mocks/mocksContainer/mockSprite.ts index b7edc4cb75d..955c7c9be74 100644 --- a/src/test/utils/mocks/mocksContainer/mockSprite.ts +++ b/src/test/utils/mocks/mocksContainer/mockSprite.ts @@ -15,7 +15,7 @@ export default class MockSprite implements MockGameObject { public scene; public anims; public list: MockGameObject[] = []; - name: string; + public name: string; constructor(textureManager, x, y, texture) { this.textureManager = textureManager; this.scene = textureManager.scene; diff --git a/src/test/utils/mocks/mocksContainer/mockText.ts b/src/test/utils/mocks/mocksContainer/mockText.ts index ec74d69e04d..b1463373ce1 100644 --- a/src/test/utils/mocks/mocksContainer/mockText.ts +++ b/src/test/utils/mocks/mocksContainer/mockText.ts @@ -11,7 +11,7 @@ export default class MockText implements MockGameObject { public list: MockGameObject[] = []; public style; public text = ""; - name: string; + public name: string; public color?: string; constructor(textureManager, x, y, content, styleOptions) { @@ -80,19 +80,19 @@ export default class MockText implements MockGameObject { return result; } - showText(text, delay, callback, callbackDelay, prompt, promptDelay) { + showText = vi.fn((text: string, delay?: integer | null, callback?: Function | null, callbackDelay?: integer | null, prompt?: boolean | null, promptDelay?: integer | null) => { this.scene.messageWrapper.showText(text, delay, callback, callbackDelay, prompt, promptDelay); if (callback) { callback(); } - } + }); - showDialogue(text, name, delay, callback, callbackDelay, promptDelay) { - this.scene.messageWrapper.showDialogue(text, name, delay, callback, callbackDelay, promptDelay); + showDialogue = vi.fn((keyOrText: string, name: string | undefined, delay: integer | null = 0, callback: Function, callbackDelay?: integer, promptDelay?: integer) => { + this.scene.messageWrapper.showDialogue(keyOrText, name, delay, callback, callbackDelay, promptDelay); if (callback) { callback(); } - } + }); setScale(scale) { // return this.phaserText.setScale(scale); @@ -257,13 +257,9 @@ export default class MockText implements MockGameObject { }; } - disableInteractive() { - // Disables interaction with this Game Object. - } + disableInteractive = vi.fn(); - clearTint() { - // Clears tint on this Game Object. - } + clearTint = vi.fn(); add(obj) { // Adds a child to this Game Object. diff --git a/src/test/utils/mocks/mocksContainer/mockTexture.ts b/src/test/utils/mocks/mocksContainer/mockTexture.ts index 7f7e7570655..bedd1d2c84a 100644 --- a/src/test/utils/mocks/mocksContainer/mockTexture.ts +++ b/src/test/utils/mocks/mocksContainer/mockTexture.ts @@ -12,7 +12,7 @@ export default class MockTexture implements MockGameObject { public source; public frames: object; public firstFrame: string; - name: string; + public name: string; constructor(manager, key: string, source) { this.manager = manager; diff --git a/src/test/utils/overridesHelper.ts b/src/test/utils/overridesHelper.ts deleted file mode 100644 index 5b190a9d6a9..00000000000 --- a/src/test/utils/overridesHelper.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { Weather, WeatherType } from "#app/data/weather"; -import { Biome } from "#app/enums/biome"; -import { MockInstance, vi } from "vitest"; -import GameManager from "#test/utils/gameManager"; -import { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import * as GameMode from "#app/game-mode"; -import { GameModes, getGameMode } from "#app/game-mode"; -import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; -import Overrides from "#app/overrides"; -import { ModifierOverride } from "#app/modifier/modifier-type"; - -/** - * Helper to handle overrides in tests - */ -export class OverridesHelper { - game: GameManager; - constructor(game: GameManager) { - this.game = game; - } - - /** - * Override the encounter chance for a mystery encounter. - * @param percentage the encounter chance in % - * @returns spy instance - */ - mysteryEncounterChance(percentage: number) { - const maxRate: number = 256; // 100% - const rate = maxRate * (percentage / 100); - const spy = vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_RATE_OVERRIDE", "get").mockReturnValue(rate); - this.log(`Mystery encounter chance set to ${percentage}% (=${rate})!`); - return spy; - } - - /** - * Override the encounter chance for a mystery encounter. - * @returns spy instance - * @param tier - */ - mysteryEncounterTier(tier: MysteryEncounterTier): MockInstance { - const spy = vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_TIER_OVERRIDE", "get").mockReturnValue(tier); - this.log(`Mystery encounter tier set to ${tier}!`); - return spy; - } - - /** - * Override the encounter that spawns for the scene - * @param encounterType - * @returns spy instance - */ - mysteryEncounter(encounterType: MysteryEncounterType) { - const spy = vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_OVERRIDE", "get").mockReturnValue(encounterType); - this.log(`Mystery encounter override set to ${encounterType}!`); - return spy; - } - - /** - * Override the starting biome - * @warning Any event listeners that are attached to [NewArenaEvent](events\battle-scene.ts) may need to be handled down the line - * @param biome the biome to set - */ - startingBiome(biome: Biome) { - this.game.scene.newArena(biome); - this.log(`Starting biome set to ${Biome[biome]} (=${biome})!`); - } - - /** - * Override the starting wave (index) - * @param wave the wave (index) to set. Classic: `1`-`200` - * @returns spy instance - */ - startingWave(wave: number) { - const spy = vi.spyOn(Overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(wave); - this.log(`Starting wave set to ${wave}!`); - return spy; - } - - /** - * Override each wave to have or not have standard trainer battles - * @returns spy instance - * @param disable - true - */ - disableTrainerWaves(disable: boolean): MockInstance { - const realFn = getGameMode; - const spy = vi.spyOn(GameMode, "getGameMode").mockImplementation((gameMode: GameModes) => { - const mode = realFn(gameMode); - mode.hasTrainers = !disable; - return mode; - }); - this.log(`Standard trainer waves are ${disable? "disabled" : "enabled"}!`); - return spy; - } - - /** - * Override the weather (type) - * @param type weather type to set - * @returns spy instance - */ - weather(type: WeatherType) { - const spy = vi.spyOn(Overrides, "WEATHER_OVERRIDE", "get").mockReturnValue(type); - this.log(`Weather set to ${Weather[type]} (=${type})!`); - return spy; - } - - /** - * Override the seed - * @param seed the seed to set - * @returns spy instance - */ - seed(seed: string) { - const spy = vi.spyOn(this.game.scene, "resetSeed").mockImplementation(() => { - this.game.scene.waveSeed = seed; - Phaser.Math.RND.sow([seed]); - this.game.scene.rngCounter = 0; - }); - this.game.scene.resetSeed(); - this.log(`Seed set to "${seed}"!`); - return spy; - } - - starterHeldItems(modifiers: ModifierOverride[]) { - const spy = vi.spyOn(Overrides, "STARTING_MODIFIER_OVERRIDE", "get").mockReturnValue(modifiers); - this.log(`Starting modifiers set to ${modifiers.map(m => JSON.stringify(m)).join(", ")}!`); - return spy; - } - - private log(...params: any[]) { - console.log("Overrides:", ...params); - } -} diff --git a/src/test/utils/phaseInterceptor.ts b/src/test/utils/phaseInterceptor.ts index c3d9d544272..b2e9845a32e 100644 --- a/src/test/utils/phaseInterceptor.ts +++ b/src/test/utils/phaseInterceptor.ts @@ -50,16 +50,15 @@ import { MysteryEncounterRewardsPhase, PostMysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; -import { LearnMovePhase } from "#app/phases/learn-move-phase"; import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase"; import { PartyExpPhase } from "#app/phases/party-exp-phase"; export interface PromptHandler { - phaseTarget?; - mode?; - callback?; - expireFn?; - awaitingActionInput?; + phaseTarget?: string; + mode?: Mode; + callback?: () => void; + expireFn?: () => void; + awaitingActionInput?: boolean; } export default class PhaseInterceptor { @@ -369,7 +368,10 @@ export default class PhaseInterceptor { if (expireFn) { this.prompts.shift(); } else if (currentMode === actionForNextPrompt.mode && currentPhase === actionForNextPrompt.phaseTarget && currentHandler.active && (!actionForNextPrompt.awaitingActionInput || (actionForNextPrompt.awaitingActionInput && currentHandler.awaitingActionInput))) { - this.prompts.shift()?.callback(); + const prompt = this.prompts.shift(); + if (prompt?.callback) { + prompt.callback(); + } } } }); diff --git a/src/test/vitest.setup.ts b/src/test/vitest.setup.ts index fe409321ff1..3bb5c240d94 100644 --- a/src/test/vitest.setup.ts +++ b/src/test/vitest.setup.ts @@ -1,4 +1,3 @@ -import "#test/fontFace.setup"; import "vitest-canvas-mock"; import { initLoggedInUser } from "#app/account"; @@ -13,8 +12,7 @@ import { initAchievements } from "#app/system/achv"; import { initVouchers } from "#app/system/voucher"; import { initStatsKeys } from "#app/ui/game-stats-ui-handler"; import { initMysteryEncounters } from "#app/data/mystery-encounters/mystery-encounters"; -import { beforeAll, beforeEach, vi } from "vitest"; -import Overrides from "#app/overrides"; +import { beforeAll, vi } from "vitest"; /** Mock the override import to always return default values, ignoring any custom overrides. */ vi.mock("#app/overrides", async (importOriginal) => { @@ -49,8 +47,3 @@ beforeAll(() => { } }); }); - -// Disables Mystery Encounters on all tests (can be overridden at test level) -beforeEach( () => { - vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_RATE_OVERRIDE", "get").mockReturnValue(0); -}); diff --git a/src/timed-event-manager.ts b/src/timed-event-manager.ts index 874bf6a8b46..9bfa3bdf54a 100644 --- a/src/timed-event-manager.ts +++ b/src/timed-event-manager.ts @@ -25,14 +25,14 @@ interface TimedEvent extends EventBanner { const timedEvents: TimedEvent[] = [ { - name: "September Update", + name: "Egg Skip Update", eventType: EventType.GENERIC, - startDate: new Date(Date.UTC(2024, 7, 28, 0)), - endDate: new Date(Date.UTC(2024, 8, 15, 0)), - bannerKey: "september-update", + startDate: new Date(Date.UTC(2024, 8, 8, 0)), + endDate: new Date(Date.UTC(2024, 8, 12, 0)), + bannerKey: "egg-update", xPosition: 19, - yPosition: 115, - scale: 0.30, + yPosition: 120, + scale: 0.21, availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es", "pt-BR", "zh-CN"] } ]; @@ -94,9 +94,9 @@ export class TimedEventDisplay extends Phaser.GameObjects.Container { let key = this.event.bannerKey; if (lang && this.event.availableLangs && this.event.availableLangs.length > 0) { if (this.event.availableLangs.includes(lang)) { - key += "-"+lang; + key += "_"+lang; } else { - key += "-en"; + key += "_en"; } } console.log(this.event.bannerKey); diff --git a/src/ui/egg-summary-ui-handler.ts b/src/ui/egg-summary-ui-handler.ts index af82ab33438..99fbccb4257 100644 --- a/src/ui/egg-summary-ui-handler.ts +++ b/src/ui/egg-summary-ui-handler.ts @@ -29,8 +29,10 @@ export default class EggSummaryUiHandler extends MessageUiHandler { private summaryContainer: Phaser.GameObjects.Container; /** container for the mini pokemon sprites */ private pokemonIconSpritesContainer: Phaser.GameObjects.Container; - /** container for the icons displayed alongside the mini icons (e.g. shiny, HA capsule) */ + /** container for the icons displayed on top of the mini pokemon sprites (e.g. shiny, HA capsule) */ private pokemonIconsContainer: Phaser.GameObjects.Container; + /** container for the elements displayed behind the mini pokemon sprites (e.g. egg rarity bg) */ + private pokemonBackgroundContainer: Phaser.GameObjects.Container; /** 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 */ @@ -71,15 +73,17 @@ export default class EggSummaryUiHandler extends MessageUiHandler { this.eggHatchBg.setOrigin(0, 0); this.eggHatchContainer.add(this.eggHatchBg); - this.pokemonIconsContainer = this.scene.add.container(iconContainerX, iconContainerY); - this.pokemonIconSpritesContainer = this.scene.add.container(iconContainerX, iconContainerY); - this.summaryContainer.add(this.pokemonIconsContainer); - this.summaryContainer.add(this.pokemonIconSpritesContainer); - this.cursorObj = this.scene.add.image(0, 0, "select_cursor"); this.cursorObj.setOrigin(0, 0); this.summaryContainer.add(this.cursorObj); + this.pokemonIconSpritesContainer = this.scene.add.container(iconContainerX, iconContainerY); + this.pokemonIconsContainer = this.scene.add.container(iconContainerX, iconContainerY); + this.pokemonBackgroundContainer = this.scene.add.container(iconContainerX, iconContainerY); + this.summaryContainer.add(this.pokemonBackgroundContainer); + this.summaryContainer.add(this.pokemonIconSpritesContainer); + this.summaryContainer.add(this.pokemonIconsContainer); + this.infoContainer = new PokemonHatchInfoContainer(this.scene, this.summaryContainer); this.infoContainer.setup(); this.infoContainer.changeToEggSummaryLayout(); @@ -95,8 +99,10 @@ export default class EggSummaryUiHandler extends MessageUiHandler { this.summaryContainer.setVisible(false); this.pokemonIconSpritesContainer.removeAll(true); this.pokemonIconsContainer.removeAll(true); + this.pokemonBackgroundContainer.removeAll(true); this.eggHatchBg.setVisible(false); this.getUi().hideTooltip(); + // Note: Questions on garbage collection go to @frutescens const activeKeys = this.scene.getActiveKeys(); // Removing unnecessary sprites from animation manager @@ -117,7 +123,6 @@ export default class EggSummaryUiHandler extends MessageUiHandler { this.eggHatchData.length = 0; // Removes Pokemon icons in EggSummaryUiHandler this.iconAnimHandler.removeAll(); - console.log("Egg Summary Handler cleared"); } /** @@ -164,25 +169,25 @@ export default class EggSummaryUiHandler extends MessageUiHandler { const offset = 2; const rightSideX = 12; - const bg = this.scene.add.image(x+2, y+5, "passive_bg"); - bg.setOrigin(0, 0); - bg.setScale(0.75); - bg.setVisible(true); - this.pokemonIconsContainer.add(bg); + const rarityBg = this.scene.add.image(x + 2, y + 5, "passive_bg"); + rarityBg.setOrigin(0, 0); + rarityBg.setScale(0.75); + rarityBg.setVisible(true); + this.pokemonBackgroundContainer.add(rarityBg); // set tint for passive bg switch (getEggTierForSpecies(displayPokemon.species)) { case EggTier.COMMON: - bg.setVisible(false); + rarityBg.setVisible(false); break; case EggTier.GREAT: - bg.setTint(0xabafff); + rarityBg.setTint(0xabafff); break; case EggTier.ULTRA: - bg.setTint(0xffffaa); + rarityBg.setTint(0xffffaa); break; case EggTier.MASTER: - bg.setTint(0xdfffaf); + rarityBg.setTint(0xdfffaf); break; } const species = displayPokemon.species; @@ -192,35 +197,31 @@ export default class EggSummaryUiHandler extends MessageUiHandler { const isShiny = displayPokemon.shiny; // set pokemon icon (and replace with base sprite if there is a mismatch) - const icon = this.scene.add.sprite(x - offset, y + offset, species.getIconAtlasKey(formIndex, isShiny, variant)); - icon.setScale(0.5); - icon.setOrigin(0, 0); - icon.setFrame(species.getIconId(female, formIndex, isShiny, variant)); + const pokemonIcon = this.scene.add.sprite(x - offset, y + offset, species.getIconAtlasKey(formIndex, isShiny, variant)); + pokemonIcon.setScale(0.5); + pokemonIcon.setOrigin(0, 0); + pokemonIcon.setFrame(species.getIconId(female, formIndex, isShiny, variant)); - if (icon.frame.name !== species.getIconId(female, formIndex, isShiny, variant)) { + if (pokemonIcon.frame.name !== species.getIconId(female, formIndex, isShiny, variant)) { console.log(`${species.name}'s variant icon does not exist. Replacing with default.`); - icon.setTexture(species.getIconAtlasKey(formIndex, false, variant)); - icon.setFrame(species.getIconId(female, formIndex, false, variant)); + pokemonIcon.setTexture(species.getIconAtlasKey(formIndex, false, variant)); + pokemonIcon.setFrame(species.getIconId(female, formIndex, false, variant)); } - this.pokemonIconSpritesContainer.add(icon); - this.iconAnimHandler.addOrUpdate(icon, PokemonIconAnimMode.NONE); + this.pokemonIconSpritesContainer.add(pokemonIcon); - const shiny = this.scene.add.image(x + rightSideX, y + offset * 2, "shiny_star_small"); - shiny.setScale(0.5); - shiny.setVisible(displayPokemon.shiny); - shiny.setTint(getVariantTint(displayPokemon.variant)); - this.pokemonIconsContainer.add(shiny); + const shinyIcon = this.scene.add.image(x + rightSideX, y + offset, "shiny_star_small"); + shinyIcon.setOrigin(0, 0); + shinyIcon.setScale(0.5); + shinyIcon.setVisible(displayPokemon.shiny); + shinyIcon.setTint(getVariantTint(displayPokemon.variant)); + this.pokemonIconsContainer.add(shinyIcon); - const ha = this.scene.add.image(x + rightSideX, y + 7, "ha_capsule"); - ha.setScale(0.5); - ha.setVisible((displayPokemon.hasAbility(displayPokemon.species.abilityHidden))); - this.pokemonIconsContainer.add(ha); + const haIcon = this.scene.add.image(x + rightSideX, y + offset * 4, "ha_capsule"); + haIcon.setOrigin(0, 0); + haIcon.setScale(0.5); + haIcon.setVisible(displayPokemon.abilityIndex === 2); + this.pokemonIconsContainer.add(haIcon); - const pb = this.scene.add.image(x + rightSideX, y + offset * 7, "icon_owned"); - pb.setOrigin(0, 0); - pb.setScale(0.5); - - // add animation for new unlocks (new catch or new shiny or new form) const dexEntry = value.dexEntryBeforeUpdate; const caughtAttr = dexEntry.caughtAttr; const newShiny = BigInt(1 << (displayPokemon.shiny ? 1 : 0)); @@ -228,17 +229,24 @@ export default class EggSummaryUiHandler extends MessageUiHandler { const newShinyOrVariant = ((newShiny & caughtAttr) === BigInt(0)) || ((newVariant & caughtAttr) === BigInt(0)); const newForm = (BigInt(1 << displayPokemon.formIndex) * DexAttr.DEFAULT_FORM & caughtAttr) === BigInt(0); - pb.setVisible(!caughtAttr || newForm); - if (!caughtAttr || newShinyOrVariant || newForm) { - this.iconAnimHandler.addOrUpdate(icon, PokemonIconAnimMode.PASSIVE); - } - this.pokemonIconsContainer.add(pb); + const pokeballIcon = this.scene.add.image(x + rightSideX, y + offset * 7, "icon_owned"); + pokeballIcon.setOrigin(0, 0); + pokeballIcon.setScale(0.5); + pokeballIcon.setVisible(!caughtAttr || newForm); + this.pokemonIconsContainer.add(pokeballIcon); - const em = this.scene.add.image(x, y + offset, "icon_egg_move"); - em.setOrigin(0, 0); - em.setScale(0.5); - em.setVisible(value.eggMoveUnlocked); - this.pokemonIconsContainer.add(em); + const eggMoveIcon = this.scene.add.image(x, y + offset, "icon_egg_move"); + eggMoveIcon.setOrigin(0, 0); + eggMoveIcon.setScale(0.5); + eggMoveIcon.setVisible(value.eggMoveUnlocked); + this.pokemonIconsContainer.add(eggMoveIcon); + + // add animation to the Pokemon sprite for new unlocks (new catch, new shiny or new form) + if (!caughtAttr || newShinyOrVariant || newForm) { + this.iconAnimHandler.addOrUpdate(pokemonIcon, PokemonIconAnimMode.PASSIVE); + } else { + this.iconAnimHandler.addOrUpdate(pokemonIcon, PokemonIconAnimMode.NONE); + } }); this.setCursor(0); @@ -256,7 +264,6 @@ export default class EggSummaryUiHandler extends MessageUiHandler { if (phase instanceof EggSummaryPhase) { phase.end(); } - ui.revertMode(); success = true; } else { const count = this.eggHatchData.length; diff --git a/src/ui/fight-ui-handler.ts b/src/ui/fight-ui-handler.ts index d275ceb1271..3bf55119335 100644 --- a/src/ui/fight-ui-handler.ts +++ b/src/ui/fight-ui-handler.ts @@ -122,7 +122,8 @@ export default class FightUiHandler extends UiHandler { } } else { // Cannot back out of fight menu if skipToFightInput is enabled - if (this.scene.currentBattle.battleType !== BattleType.MYSTERY_ENCOUNTER || !this.scene.currentBattle.mysteryEncounter?.skipToFightInput) { + const { battleType, mysteryEncounter } = this.scene.currentBattle; + if (battleType !== BattleType.MYSTERY_ENCOUNTER || !mysteryEncounter?.skipToFightInput) { ui.setMode(Mode.COMMAND, this.fieldIndex); success = true; } diff --git a/src/ui/form-modal-ui-handler.ts b/src/ui/form-modal-ui-handler.ts index 8c4ea5f6768..331154263ad 100644 --- a/src/ui/form-modal-ui-handler.ts +++ b/src/ui/form-modal-ui-handler.ts @@ -60,7 +60,7 @@ export abstract class FormModalUiHandler extends ModalUiHandler { const inputBg = addWindow(this.scene, 0, 0, 80, 16, false, false, 0, 0, WindowVariant.XTHIN); const isPassword = field.includes(i18next.t("menu:password")) || field.includes(i18next.t("menu:confirmPassword")); - const input = addTextInputObject(this.scene, 4, -2, 440, 116, TextStyle.TOOLTIP_CONTENT, { type: isPassword ? "password" : "text", maxLength: isPassword ? 64 : 18 }); + const input = addTextInputObject(this.scene, 4, -2, 440, 116, TextStyle.TOOLTIP_CONTENT, { type: isPassword ? "password" : "text", maxLength: isPassword ? 64 : 20 }); input.setOrigin(0, 0); inputContainer.add(inputBg); diff --git a/src/ui/message-ui-handler.ts b/src/ui/message-ui-handler.ts index 76b08f53f47..93e00cb6b70 100644 --- a/src/ui/message-ui-handler.ts +++ b/src/ui/message-ui-handler.ts @@ -29,6 +29,8 @@ export default abstract class MessageUiHandler extends AwaitableUiHandler { if (delay === null || delay === undefined) { delay = 20; } + + // Pattern matching regex that checks for @c{}, @f{}, @s{}, and @f{} patterns within message text and parses them to their respective behaviors. const charVarMap = new Map(); const delayMap = new Map(); const soundMap = new Map(); diff --git a/src/ui/mystery-encounter-ui-handler.ts b/src/ui/mystery-encounter-ui-handler.ts index 185bd548d19..c6c2ed7e68a 100644 --- a/src/ui/mystery-encounter-ui-handler.ts +++ b/src/ui/mystery-encounter-ui-handler.ts @@ -22,7 +22,8 @@ export default class MysteryEncounterUiHandler extends UiHandler { private cursorObj?: Phaser.GameObjects.Image; private optionsContainer: Phaser.GameObjects.Container; - private optionScrollTweens: (Phaser.Tweens.Tween | null)[] = [null, null, null, null]; + // Length = max number of allowable options (4) + private optionScrollTweens: (Phaser.Tweens.Tween | null)[] = new Array(4).fill(null); private tooltipWindow: Phaser.GameObjects.NineSlice; private tooltipContainer: Phaser.GameObjects.Container; @@ -49,7 +50,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { super(scene, Mode.MYSTERY_ENCOUNTER); } - setup() { + override setup() { const ui = this.getUi(); this.cursorContainer = this.scene.add.container(18, -38.7); @@ -89,7 +90,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { this.dexProgressContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, 24, 28), Phaser.Geom.Rectangle.Contains); } - show(args: any[]): boolean { + override show(args: any[]): boolean { super.show(args); this.overrideSettings = args[0] as OptionSelectSettings ?? {}; @@ -119,7 +120,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { return true; } - processInput(button: Button): boolean { + override processInput(button: Button): boolean { const ui = this.getUi(); let success = false; @@ -179,7 +180,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { return success; } - handleTwoOptionMoveInput(button: Button): boolean { + private handleTwoOptionMoveInput(button: Button): boolean { let success = false; const cursor = this.getCursor(); switch (button) { @@ -208,7 +209,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { return success; } - handleThreeOptionMoveInput(button: Button): boolean { + private handleThreeOptionMoveInput(button: Button): boolean { let success = false; const cursor = this.getCursor(); switch (button) { @@ -245,7 +246,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { return success; } - handleFourOptionMoveInput(button: Button): boolean { + private handleFourOptionMoveInput(button: Button): boolean { let success = false; const cursor = this.getCursor(); switch (button) { @@ -282,6 +283,10 @@ export default class MysteryEncounterUiHandler extends UiHandler { return success; } + /** + * When ME UI first displays, the option buttons will be disabled temporarily to prevent player accidentally clicking through hastily + * This method is automatically called after a short delay but can also be called manually + */ unblockInput() { if (this.blockInput) { this.blockInput = false; @@ -295,11 +300,11 @@ export default class MysteryEncounterUiHandler extends UiHandler { } } - getCursor(): integer { + override getCursor(): integer { return this.cursor ? this.cursor : 0; } - setCursor(cursor: integer): boolean { + override setCursor(cursor: integer): boolean { const prevCursor = this.getCursor(); const changed = prevCursor !== cursor; if (changed) { @@ -480,7 +485,11 @@ export default class MysteryEncounterUiHandler extends UiHandler { } } - displayOptionTooltip() { + /** + * Updates and displays the tooltip for a given option + * The tooltip will auto wrap and scroll if it is too long + */ + private displayOptionTooltip() { const cursor = this.getCursor(); // Clear tooltip box if (this.tooltipContainer.length > 1) { @@ -553,7 +562,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { } } - clear(): void { + override clear(): void { super.clear(); this.overrideSettings = undefined; this.optionsContainer.setVisible(false); @@ -567,7 +576,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { this.eraseCursor(); } - eraseCursor(): void { + private eraseCursor(): void { if (this.cursorObj) { this.cursorObj.destroy(); } @@ -575,10 +584,10 @@ export default class MysteryEncounterUiHandler extends UiHandler { } /** - * + * Will show or hide the Dex progress icon for an option that has dex progress * @param show - if true does show, if false does hide */ - showHideDexProgress(show: boolean) { + private showHideDexProgress(show: boolean) { if (show && !this.showDexProgress) { this.showDexProgress = true; this.scene.tweens.killTweensOf(this.dexProgressContainer); diff --git a/src/ui/party-ui-handler.ts b/src/ui/party-ui-handler.ts index ab8b353d6c7..bb86680d8bf 100644 --- a/src/ui/party-ui-handler.ts +++ b/src/ui/party-ui-handler.ts @@ -531,7 +531,6 @@ export default class PartyUiHandler extends MessageUiHandler { return this.processInput(Button.CANCEL); } else if (option === PartyOption.SELECT) { ui.playSelect(); - // ui.setModeWithoutClear(Mode.SUMMARY, pokemon).then(() => this.clearOptions()); return true; } } else if (button === Button.CANCEL) { diff --git a/src/ui/pokemon-info-container.ts b/src/ui/pokemon-info-container.ts index 49bfd4d7293..3c54e529d43 100644 --- a/src/ui/pokemon-info-container.ts +++ b/src/ui/pokemon-info-container.ts @@ -262,7 +262,7 @@ export default class PokemonInfoContainer extends Phaser.GameObjects.Container { this.pokemonFormText.disableInteractive(); } - const abilityTextStyle = pokemon.abilityIndex === (pokemon.species.ability2 ? 2 : 1) ? TextStyle.MONEY : TextStyle.WINDOW; + const abilityTextStyle = pokemon.abilityIndex === 2 ? TextStyle.MONEY : TextStyle.WINDOW; this.pokemonAbilityText.setText(pokemon.getAbility(true).name); this.pokemonAbilityText.setColor(getTextColor(abilityTextStyle, false, this.scene.uiTheme)); this.pokemonAbilityText.setShadowColor(getTextColor(abilityTextStyle, true, this.scene.uiTheme)); diff --git a/src/ui/run-info-ui-handler.ts b/src/ui/run-info-ui-handler.ts index d6bafb8599e..f398abed6f5 100644 --- a/src/ui/run-info-ui-handler.ts +++ b/src/ui/run-info-ui-handler.ts @@ -21,6 +21,7 @@ import { getVariantTint } from "#app/data/variant"; import * as Modifier from "../modifier/modifier"; import { Species } from "#enums/species"; import { PlayerGender } from "#enums/player-gender"; +import { SettingKeyboard } from "#app/system/settings/settings-keyboard"; /** * RunInfoUiMode indicates possible overlays of RunInfoUiHandler. @@ -151,7 +152,13 @@ export default class RunInfoUiHandler extends UiHandler { const headerBgCoords = headerBg.getTopRight(); const abilityButtonContainer = this.scene.add.container(0, 0); const abilityButtonText = addTextObject(this.scene, 8, 0, i18next.t("runHistory:viewHeldItems"), TextStyle.WINDOW, {fontSize:"34px"}); - const abilityButtonElement = new Phaser.GameObjects.Sprite(this.scene, 0, 2, "keyboard", "E.png"); + const gamepadType = this.getUi().getGamepadType(); + let abilityButtonElement: Phaser.GameObjects.Sprite; + if (gamepadType === "touch") { + abilityButtonElement = new Phaser.GameObjects.Sprite(this.scene, 0, 2, "keyboard", "E.png"); + } else { + abilityButtonElement = new Phaser.GameObjects.Sprite(this.scene, 0, 2, gamepadType, this.scene.inputController?.getIconForLatestInputRecorded(SettingKeyboard.Button_Cycle_Ability)); + } abilityButtonContainer.add([abilityButtonText, abilityButtonElement]); abilityButtonContainer.setPosition(headerBgCoords.x - abilityButtonText.displayWidth - abilityButtonElement.displayWidth - 8, 10); this.runContainer.add(abilityButtonContainer); @@ -180,11 +187,19 @@ export default class RunInfoUiHandler extends UiHandler { if (this.isVictory) { const hallofFameInstructionContainer = this.scene.add.container(0, 0); const shinyButtonText = addTextObject(this.scene, 8, 0, i18next.t("runHistory:viewHallOfFame"), TextStyle.WINDOW, {fontSize:"65px"}); - const shinyButtonElement = new Phaser.GameObjects.Sprite(this.scene, 0, 4, "keyboard", "R.png"); + const formButtonText = addTextObject(this.scene, 8, 12, i18next.t("runHistory:viewEndingSplash"), TextStyle.WINDOW, {fontSize:"65px"}); + const gamepadType = this.getUi().getGamepadType(); + let shinyButtonElement: Phaser.GameObjects.Sprite; + let formButtonElement: Phaser.GameObjects.Sprite; + if (gamepadType === "touch") { + shinyButtonElement = new Phaser.GameObjects.Sprite(this.scene, 0, 4, "keyboard", "R.png"); + formButtonElement = new Phaser.GameObjects.Sprite(this.scene, 0, 16, "keyboard", "F.png"); + } else { + shinyButtonElement = new Phaser.GameObjects.Sprite(this.scene, 0, 4, gamepadType, this.scene.inputController?.getIconForLatestInputRecorded(SettingKeyboard.Button_Cycle_Shiny)); + formButtonElement = new Phaser.GameObjects.Sprite(this.scene, 0, 16, gamepadType, this.scene.inputController?.getIconForLatestInputRecorded(SettingKeyboard.Button_Cycle_Form)); + } hallofFameInstructionContainer.add([shinyButtonText, shinyButtonElement]); - const formButtonText = addTextObject(this.scene, 8, 12, i18next.t("runHistory:viewEndingSplash"), TextStyle.WINDOW, {fontSize:"65px"}); - const formButtonElement = new Phaser.GameObjects.Sprite(this.scene, 0, 16, "keyboard", "F.png"); hallofFameInstructionContainer.add([formButtonText, formButtonElement]); hallofFameInstructionContainer.setPosition(12, 25); diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index e1269499b10..0c3d8de61b0 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -1220,6 +1220,19 @@ export default class StarterSelectUiHandler extends MessageUiHandler { } } + /** + * Update the display of candy upgrade icons or animations for the given StarterContainer + * @param starterContainer the container for the Pokemon to update + */ + updateCandyUpgradeDisplay(starterContainer: StarterContainer) { + if (this.isUpgradeIconEnabled() ) { + this.setUpgradeIcon(starterContainer); + } + if (this.isUpgradeAnimationEnabled()) { + this.setUpgradeAnimation(starterContainer.icon, this.lastSpecies, true); + } + } + /** * Processes an {@linkcode CandyUpgradeNotificationChangedEvent} sent when the corresponding setting changes * @param event {@linkcode Event} sent by the callback @@ -1624,7 +1637,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { } }); } - const candyCount = starterData.candyCount; + const passiveAttr = starterData.passiveAttr; if (passiveAttr & PassiveAttr.UNLOCKED) { // this is for enabling and disabling the passive if (!(passiveAttr & PassiveAttr.ENABLED)) { @@ -1705,8 +1718,13 @@ export default class StarterSelectUiHandler extends MessageUiHandler { return true; } }); - const showUseCandies = () => { // this lets you use your candies + + // Purchases with Candy + const candyCount = starterData.candyCount; + const showUseCandies = () => { const options: any[] = []; // TODO: add proper type + + // Unlock passive option if (!(passiveAttr & PassiveAttr.UNLOCKED)) { const passiveCost = getPassiveCandyCount(speciesStarters[this.lastSpecies.speciesId]); options.push({ @@ -1724,18 +1742,12 @@ export default class StarterSelectUiHandler extends MessageUiHandler { } }); ui.setMode(Mode.STARTER_SELECT); - this.setSpeciesDetails(this.lastSpecies, undefined, undefined, undefined, undefined, undefined, undefined); + this.setSpeciesDetails(this.lastSpecies); + this.scene.playSound("se/buy"); - // if starterContainer exists, update the passive background + // update the passive background and icon/animation for available upgrade if (starterContainer) { - // Update the candy upgrade display - if (this.isUpgradeIconEnabled() ) { - this.setUpgradeIcon(starterContainer); - } - if (this.isUpgradeAnimationEnabled()) { - this.setUpgradeAnimation(starterContainer.icon, this.lastSpecies, true); - } - + this.updateCandyUpgradeDisplay(starterContainer); starterContainer.starterPassiveBgs.setVisible(!!this.scene.gameData.starterData[this.lastSpecies.speciesId].passiveAttr); } return true; @@ -1746,6 +1758,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler { itemArgs: starterColors[this.lastSpecies.speciesId] }); } + + // Reduce cost option const valueReduction = starterData.valueReduction; if (valueReduction < valueReductionMax) { const reductionCost = getValueReductionCandyCounts(speciesStarters[this.lastSpecies.speciesId])[valueReduction]; @@ -1767,19 +1781,10 @@ export default class StarterSelectUiHandler extends MessageUiHandler { ui.setMode(Mode.STARTER_SELECT); this.scene.playSound("se/buy"); - // if starterContainer exists, update the value reduction background + // update the value label and icon/animation for available upgrade if (starterContainer) { this.updateStarterValueLabel(starterContainer); - - // If the notification setting is set to 'On', update the candy upgrade display - if (this.scene.candyUpgradeNotification === 2) { - if (this.isUpgradeIconEnabled() ) { - this.setUpgradeIcon(starterContainer); - } - if (this.isUpgradeAnimationEnabled()) { - this.setUpgradeAnimation(starterContainer.icon, this.lastSpecies, true); - } - } + this.updateCandyUpgradeDisplay(starterContainer); } return true; } @@ -1812,6 +1817,11 @@ export default class StarterSelectUiHandler extends MessageUiHandler { ui.setMode(Mode.STARTER_SELECT); this.scene.playSound("se/buy"); + // update the icon/animation for available upgrade + if (starterContainer) { + this.updateCandyUpgradeDisplay(starterContainer); + } + return true; } return false; diff --git a/src/ui/ui.ts b/src/ui/ui.ts index 6655550b0c1..0f4fa52e41e 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -52,6 +52,7 @@ import RunInfoUiHandler from "./run-info-ui-handler"; import EggSummaryUiHandler from "./egg-summary-ui-handler"; import TestDialogueUiHandler from "#app/ui/test-dialogue-ui-handler"; import AutoCompleteUiHandler from "./autocomplete-ui-handler"; +import { Device } from "#enums/devices"; import MysteryEncounterUiHandler from "./mystery-encounter-ui-handler"; export enum Mode { @@ -582,4 +583,20 @@ export default class UI extends Phaser.GameObjects.Container { public getModeChain(): Mode[] { return this.modeChain; } + + /** + * getGamepadType - returns the type of gamepad being used + * inputMethod could be "keyboard" or "touch" or "gamepad" + * if inputMethod is "keyboard" or "touch", then the inputMethod is returned + * if inputMethod is "gamepad", then the gamepad type is returned it could be "xbox" or "dualshock" + * @returns gamepad type + */ + public getGamepadType(): string { + const scene = this.scene as BattleScene; + if (scene.inputMethod === "gamepad") { + return scene.inputController.getConfig(scene.inputController.selectedDevice[Device.GAMEPAD]).padType; + } else { + return scene.inputMethod; + } + } } diff --git a/src/utils.ts b/src/utils.ts index 21be4df65cf..a8206bf4dcf 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -587,6 +587,10 @@ export function isNullOrUndefined(object: any): boolean { return null === object || undefined === object; } +/** + * Capitalizes the first letter of a string + * @param str + */ export function capitalizeFirstLetter(str: string) { return str.charAt(0).toUpperCase() + str.slice(1); } diff --git a/vitest.config.ts b/vitest.config.ts index 9a765a89ae7..54462675704 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,46 +1,42 @@ -import { defineProject, UserWorkspaceConfig } from 'vitest/config'; -import { defaultConfig } from './vite.config'; - -export const defaultProjectTestConfig: UserWorkspaceConfig["test"] = { - setupFiles: ['./src/test/vitest.setup.ts'], - server: { - deps: { - inline: ['vitest-canvas-mock'], - //@ts-ignore - optimizer: { - web: { - include: ['vitest-canvas-mock'], - } - } - } - }, - environment: 'jsdom' as const, - environmentOptions: { - jsdom: { - resources: 'usable', - }, - }, - threads: false, - trace: true, - restoreMocks: true, - watch: false, - coverage: { - provider: 'istanbul' as const, - reportsDirectory: 'coverage' as const, - reporters: ['text-summary', 'html'], - }, -} +import { defineProject } from "vitest/config"; +import { defaultConfig } from "./vite.config"; export default defineProject(({ mode }) => ({ - ...defaultConfig, - test: { - ...defaultProjectTestConfig, - name: "main", - include: ["./src/test/**/*.{test,spec}.ts"], - exclude: ["./src/test/pre.test.ts"], - }, - esbuild: { - pure: mode === 'production' ? [ 'console.log' ] : [], - keepNames: true, - }, -})) + ...defaultConfig, + test: { + setupFiles: ["./src/test/fontFace.setup.ts", "./src/test/vitest.setup.ts"], + server: { + deps: { + inline: ["vitest-canvas-mock"], + //@ts-ignore + optimizer: { + web: { + include: ["vitest-canvas-mock"], + }, + }, + }, + }, + environment: "jsdom" as const, + environmentOptions: { + jsdom: { + resources: "usable", + }, + }, + threads: false, + trace: true, + restoreMocks: true, + watch: false, + coverage: { + provider: "istanbul" as const, + reportsDirectory: "coverage" as const, + reporters: ["text-summary", "html"], + }, + name: "main", + include: ["./src/test/**/*.{test,spec}.ts"], + exclude: ["./src/test/pre.test.ts"], + }, + esbuild: { + pure: mode === "production" ? ["console.log"] : [], + keepNames: true, + }, +})); diff --git a/vitest.workspace.ts b/vitest.workspace.ts index a885b77dc9d..38121942004 100644 --- a/vitest.workspace.ts +++ b/vitest.workspace.ts @@ -1,6 +1,5 @@ import { defineWorkspace } from "vitest/config"; import { defaultConfig } from "./vite.config"; -import { defaultProjectTestConfig } from "./vitest.config"; export default defineWorkspace([ { @@ -11,58 +10,5 @@ export default defineWorkspace([ environment: "jsdom", }, }, - { - ...defaultConfig, - test: { - ...defaultProjectTestConfig, - name: "misc", - include: [ - "src/test/achievements/**/*.{test,spec}.ts", - "src/test/arena/**/*.{test,spec}.ts", - "src/test/battlerTags/**/*.{test,spec}.ts", - "src/test/eggs/**/*.{test,spec}.ts", - "src/test/field/**/*.{test,spec}.ts", - "src/test/inputs/**/*.{test,spec}.ts", - "src/test/localization/**/*.{test,spec}.ts", - "src/test/phases/**/*.{test,spec}.ts", - "src/test/settingMenu/**/*.{test,spec}.ts", - "src/test/sprites/**/*.{test,spec}.ts", - "src/test/ui/**/*.{test,spec}.ts", - "src/test/*.{test,spec}.ts", - ], - }, - }, - { - ...defaultConfig, - test: { - ...defaultProjectTestConfig, - name: "abilities", - include: ["src/test/abilities/**/*.{test,spec}.ts"], - }, - }, - { - ...defaultConfig, - test: { - ...defaultProjectTestConfig, - name: "battle", - include: ["src/test/battle/**/*.{test,spec}.ts"], - }, - }, - { - ...defaultConfig, - test: { - ...defaultProjectTestConfig, - name: "items", - include: ["src/test/items/**/*.{test,spec}.ts"], - }, - }, - { - ...defaultConfig, - test: { - ...defaultProjectTestConfig, - name: "moves", - include: ["src/test/moves/**/*.{test,spec}.ts"], - }, - }, "./vitest.config.ts", ]);