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/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/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/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/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/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/modifier/modifier-type.ts b/src/modifier/modifier-type.ts
index 58501e330c7..b807dc8b7c6 100644
--- a/src/modifier/modifier-type.ts
+++ b/src/modifier/modifier-type.ts
@@ -1613,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;
}),
@@ -1683,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;
}),
@@ -1764,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;
}),
@@ -1773,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 => {
diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts
index 6508e450ad6..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 {
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/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/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/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/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/vitest.setup.ts b/src/test/vitest.setup.ts
index 7c778ade54b..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";
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/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/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/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",
]);