Merge branch 'beta' into no-uncatchable-shinies

This commit is contained in:
Wlowscha 2025-03-29 02:48:30 +01:00 committed by GitHub
commit b30700e433
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
70 changed files with 8669 additions and 10812 deletions

View File

@ -29,4 +29,4 @@ jobs:
- 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' || '' }}
run: npx vitest --project ${{ inputs.project }} --no-isolate --shard=${{ inputs.shard }}/${{ inputs.totalShards }} ${{ !runner.debug && '--silent' || '' }}

View File

@ -15,29 +15,8 @@ on:
types: [checks_requested]
jobs:
pre-test:
name: Run Pre-test
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v4
with:
submodules: 'recursive'
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: Run Pre-test
working-directory: tests-action
run: npx vitest run --project pre ${{ !runner.debug && '--silent' || '' }}
run-tests:
name: Run Tests
needs: [pre-test]
strategy:
matrix:
shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

View File

@ -8,12 +8,6 @@ pre-commit:
skip:
- merge
- rebase
pre-push:
commands:
biome-lint:
glob: "*.{js,ts,jsx,tsx}"
run: npx @biomejs/biome check --write --reporter=summary {push_files} --no-errors-on-unmatched
post-merge:
commands:

174
package-lock.json generated
View File

@ -29,7 +29,7 @@
"@types/node": "^20.12.13",
"@typescript-eslint/eslint-plugin": "^8.0.0-alpha.54",
"@typescript-eslint/parser": "^8.0.0-alpha.54",
"@vitest/coverage-istanbul": "^2.1.9",
"@vitest/coverage-istanbul": "^3.0.9",
"dependency-cruiser": "^16.3.10",
"eslint": "^9.7.0",
"eslint-plugin-import-x": "^4.2.1",
@ -43,7 +43,7 @@
"typescript-eslint": "^8.0.0-alpha.54",
"vite": "^5.4.14",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^2.1.9",
"vitest": "^3.0.9",
"vitest-canvas-mock": "^0.3.3"
},
"engines": {
@ -2312,14 +2312,14 @@
}
},
"node_modules/@vitest/coverage-istanbul": {
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@vitest/coverage-istanbul/-/coverage-istanbul-2.1.9.tgz",
"integrity": "sha512-vdYE4FkC/y2lxcN3Dcj54Bw+ericmDwiex0B8LV5F/YNYEYP1mgVwhPnHwWGAXu38qizkjOuyczKbFTALfzFKw==",
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@vitest/coverage-istanbul/-/coverage-istanbul-3.0.9.tgz",
"integrity": "sha512-/TXh2qmOhclmVPjOnPTpIO4Xr6l2P5EwyXQygenwq4/ZQ/vPsrz+GCRZF9kBeQi6xrGcHv368Si9PGImWQawVg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@istanbuljs/schema": "^0.1.3",
"debug": "^4.3.7",
"debug": "^4.4.0",
"istanbul-lib-coverage": "^3.2.2",
"istanbul-lib-instrument": "^6.0.3",
"istanbul-lib-report": "^3.0.1",
@ -2327,48 +2327,48 @@
"istanbul-reports": "^3.1.7",
"magicast": "^0.3.5",
"test-exclude": "^7.0.1",
"tinyrainbow": "^1.2.0"
"tinyrainbow": "^2.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"vitest": "2.1.9"
"vitest": "3.0.9"
}
},
"node_modules/@vitest/expect": {
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz",
"integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==",
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.9.tgz",
"integrity": "sha512-5eCqRItYgIML7NNVgJj6TVCmdzE7ZVgJhruW0ziSQV4V7PvLkDL1bBkBdcTs/VuIz0IxPb5da1IDSqc1TR9eig==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "2.1.9",
"@vitest/utils": "2.1.9",
"chai": "^5.1.2",
"tinyrainbow": "^1.2.0"
"@vitest/spy": "3.0.9",
"@vitest/utils": "3.0.9",
"chai": "^5.2.0",
"tinyrainbow": "^2.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/mocker": {
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz",
"integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==",
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.9.tgz",
"integrity": "sha512-ryERPIBOnvevAkTq+L1lD+DTFBRcjueL9lOUfXsLfwP92h4e+Heb+PjiqS3/OURWPtywfafK0kj++yDFjWUmrA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "2.1.9",
"@vitest/spy": "3.0.9",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.12"
"magic-string": "^0.30.17"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"msw": "^2.4.9",
"vite": "^5.0.0"
"vite": "^5.0.0 || ^6.0.0"
},
"peerDependenciesMeta": {
"msw": {
@ -2380,51 +2380,51 @@
}
},
"node_modules/@vitest/pretty-format": {
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz",
"integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==",
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.9.tgz",
"integrity": "sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==",
"dev": true,
"license": "MIT",
"dependencies": {
"tinyrainbow": "^1.2.0"
"tinyrainbow": "^2.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/runner": {
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz",
"integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==",
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.9.tgz",
"integrity": "sha512-NX9oUXgF9HPfJSwl8tUZCMP1oGx2+Sf+ru6d05QjzQz4OwWg0psEzwY6VexP2tTHWdOkhKHUIZH+fS6nA7jfOw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/utils": "2.1.9",
"pathe": "^1.1.2"
"@vitest/utils": "3.0.9",
"pathe": "^2.0.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/snapshot": {
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz",
"integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==",
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.9.tgz",
"integrity": "sha512-AiLUiuZ0FuA+/8i19mTYd+re5jqjEc2jZbgJ2up0VY0Ddyyxg/uUtBDpIFAy4uzKaQxOW8gMgBdAJJ2ydhu39A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "2.1.9",
"magic-string": "^0.30.12",
"pathe": "^1.1.2"
"@vitest/pretty-format": "3.0.9",
"magic-string": "^0.30.17",
"pathe": "^2.0.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/spy": {
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz",
"integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==",
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.9.tgz",
"integrity": "sha512-/CcK2UDl0aQ2wtkp3YVWldrpLRNCfVcIOFGlVGKO4R5eajsH393Z1yiXLVQ7vWsj26JOEjeZI0x5sm5P4OGUNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -2435,15 +2435,15 @@
}
},
"node_modules/@vitest/utils": {
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz",
"integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==",
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.9.tgz",
"integrity": "sha512-ilHM5fHhZ89MCp5aAaM9uhfl1c2JdxVxl3McqsdVyVNN6JffnEen8UMCdRTzOhGXNQGo5GNL9QugHrz727Wnng==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "2.1.9",
"loupe": "^3.1.2",
"tinyrainbow": "^1.2.0"
"@vitest/pretty-format": "3.0.9",
"loupe": "^3.1.3",
"tinyrainbow": "^2.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
@ -2738,9 +2738,9 @@
"license": "CC-BY-4.0"
},
"node_modules/chai": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz",
"integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==",
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz",
"integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -5450,9 +5450,9 @@
}
},
"node_modules/pathe": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
"integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
"dev": true,
"license": "MIT"
},
@ -6308,9 +6308,9 @@
}
},
"node_modules/tinyrainbow": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz",
"integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
"integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
"dev": true,
"license": "MIT",
"engines": {
@ -6677,23 +6677,23 @@
}
},
"node_modules/vite-node": {
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz",
"integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==",
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.9.tgz",
"integrity": "sha512-w3Gdx7jDcuT9cNn9jExXgOyKmf5UOTb6WMHz8LGAm54eS1Elf5OuBhCxl6zJxGhEeIkgsE1WbHuoL0mj/UXqXg==",
"dev": true,
"license": "MIT",
"dependencies": {
"cac": "^6.7.14",
"debug": "^4.3.7",
"es-module-lexer": "^1.5.4",
"pathe": "^1.1.2",
"vite": "^5.0.0"
"debug": "^4.4.0",
"es-module-lexer": "^1.6.0",
"pathe": "^2.0.3",
"vite": "^5.0.0 || ^6.0.0"
},
"bin": {
"vite-node": "vite-node.mjs"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
@ -6720,47 +6720,48 @@
}
},
"node_modules/vitest": {
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz",
"integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==",
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.9.tgz",
"integrity": "sha512-BbcFDqNyBlfSpATmTtXOAOj71RNKDDvjBM/uPfnxxVGrG+FSH2RQIwgeEngTaTkuU/h0ScFvf+tRcKfYXzBybQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/expect": "2.1.9",
"@vitest/mocker": "2.1.9",
"@vitest/pretty-format": "^2.1.9",
"@vitest/runner": "2.1.9",
"@vitest/snapshot": "2.1.9",
"@vitest/spy": "2.1.9",
"@vitest/utils": "2.1.9",
"chai": "^5.1.2",
"debug": "^4.3.7",
"@vitest/expect": "3.0.9",
"@vitest/mocker": "3.0.9",
"@vitest/pretty-format": "^3.0.9",
"@vitest/runner": "3.0.9",
"@vitest/snapshot": "3.0.9",
"@vitest/spy": "3.0.9",
"@vitest/utils": "3.0.9",
"chai": "^5.2.0",
"debug": "^4.4.0",
"expect-type": "^1.1.0",
"magic-string": "^0.30.12",
"pathe": "^1.1.2",
"magic-string": "^0.30.17",
"pathe": "^2.0.3",
"std-env": "^3.8.0",
"tinybench": "^2.9.0",
"tinyexec": "^0.3.1",
"tinypool": "^1.0.1",
"tinyrainbow": "^1.2.0",
"vite": "^5.0.0",
"vite-node": "2.1.9",
"tinyexec": "^0.3.2",
"tinypool": "^1.0.2",
"tinyrainbow": "^2.0.0",
"vite": "^5.0.0 || ^6.0.0",
"vite-node": "3.0.9",
"why-is-node-running": "^2.3.0"
},
"bin": {
"vitest": "vitest.mjs"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"@edge-runtime/vm": "*",
"@types/node": "^18.0.0 || >=20.0.0",
"@vitest/browser": "2.1.9",
"@vitest/ui": "2.1.9",
"@types/debug": "^4.1.12",
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"@vitest/browser": "3.0.9",
"@vitest/ui": "3.0.9",
"happy-dom": "*",
"jsdom": "*"
},
@ -6768,6 +6769,9 @@
"@edge-runtime/vm": {
"optional": true
},
"@types/debug": {
"optional": true
},
"@types/node": {
"optional": true
},

View File

@ -9,10 +9,10 @@
"build": "vite build",
"build:beta": "vite build --mode beta",
"preview": "vite preview",
"test": "vitest run --project pre && vitest run --project main",
"test:cov": "vitest run --project pre && vitest run --project main --coverage",
"test:watch": "vitest run --project pre && vitest watch --project main --coverage",
"test:silent": "vitest run --project pre && vitest run --project main --silent",
"test": "vitest run",
"test:cov": "vitest run --coverage --no-isolate",
"test:watch": "vitest watch --coverage --no-isolate",
"test:silent": "vitest run --silent --no-isolate",
"typecheck": "tsc --noEmit",
"eslint": "eslint --fix .",
"eslint-ci": "eslint .",
@ -36,7 +36,7 @@
"@types/node": "^20.12.13",
"@typescript-eslint/eslint-plugin": "^8.0.0-alpha.54",
"@typescript-eslint/parser": "^8.0.0-alpha.54",
"@vitest/coverage-istanbul": "^2.1.9",
"@vitest/coverage-istanbul": "^3.0.9",
"dependency-cruiser": "^16.3.10",
"eslint": "^9.7.0",
"eslint-plugin-import-x": "^4.2.1",
@ -50,7 +50,7 @@
"typescript-eslint": "^8.0.0-alpha.54",
"vite": "^5.4.14",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^2.1.9",
"vitest": "^3.0.9",
"vitest-canvas-mock": "^0.3.3"
},
"dependencies": {

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 686 B

After

Width:  |  Height:  |  Size: 894 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 697 B

After

Width:  |  Height:  |  Size: 915 B

@ -1 +1 @@
Subproject commit cd4057af258b659ba2c1ed2778bb2793fa1f6141
Subproject commit cae82762eab25f928500b73e0e9a3e7df1ae6bbe

16
src/@types/DexData.ts Normal file
View File

@ -0,0 +1,16 @@
/**
* Dex entry for a single Pokemon Species
*/
export interface DexEntry {
seenAttr: bigint;
caughtAttr: bigint;
natureAttr: number;
seenCount: number;
caughtCount: number;
hatchedCount: number;
ivs: number[];
}
export interface DexData {
[key: number]: DexEntry;
}

View File

@ -1403,7 +1403,10 @@ export default class BattleScene extends SceneBase {
this.field.add(newTrainer);
}
} else {
if (!this.gameMode.hasTrainers) {
if (
!this.gameMode.hasTrainers ||
(Overrides.DISABLE_STANDARD_TRAINERS_OVERRIDE && isNullOrUndefined(trainerData))
) {
newBattleType = BattleType.WILD;
} else if (battleType === undefined) {
newBattleType = this.gameMode.isWaveTrainer(newWaveIndex, this.arena) ? BattleType.TRAINER : BattleType.WILD;
@ -2721,6 +2724,18 @@ export default class BattleScene extends SceneBase {
this.phaseQueue.splice(0, this.phaseQueue.length);
}
/**
* Clears all phase-related stuff, including all phase queues, the current and standby phases, and a splice index
*/
clearAllPhases(): void {
for (const queue of [this.phaseQueue, this.phaseQueuePrepend, this.conditionalQueue, this.nextCommandPhaseQueue]) {
queue.splice(0, queue.length);
}
this.currentPhase = null;
this.standbyPhase = null;
this.clearPhaseQueueSplice();
}
/**
* Used by function unshiftPhase(), sets index to start inserting at current length instead of the end of the array, useful if phaseQueuePrepend gets longer with Phases
*/

View File

@ -1233,7 +1233,7 @@ export class MoveEffectAttr extends MoveAttr {
getMoveChance(user: Pokemon, target: Pokemon, move: Move, selfEffect?: Boolean, showAbility?: Boolean): number {
const moveChance = new Utils.NumberHolder(this.effectChanceOverride ?? move.chance);
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, false, moveChance, move, target, selfEffect, showAbility);
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, !showAbility, moveChance, move);
if ((!move.hasAttr(FlinchAttr) || moveChance.value <= move.chance) && !move.hasAttr(SecretPowerAttr)) {
const userSide = user.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
@ -1241,7 +1241,7 @@ export class MoveEffectAttr extends MoveAttr {
}
if (!selfEffect) {
applyPreDefendAbAttrs(IgnoreMoveEffectsAbAttr, target, user, null, null, false, moveChance);
applyPreDefendAbAttrs(IgnoreMoveEffectsAbAttr, target, user, null, null, !showAbility, moveChance);
}
return moveChance.value;
}

View File

@ -23,7 +23,7 @@ import { allSpecies, getPokemonSpecies } from "#app/data/pokemon-species";
import { getTypeRgb } from "#app/data/type";
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { NumberHolder, isNullOrUndefined, randInt, randSeedInt, randSeedShuffle } from "#app/utils";
import { NumberHolder, isNullOrUndefined, randInt, randSeedInt, randSeedShuffle, randSeedItem } from "#app/utils";
import type { PlayerPokemon } from "#app/field/pokemon";
import type Pokemon from "#app/field/pokemon";
import { EnemyPokemon, PokemonMove } from "#app/field/pokemon";
@ -41,11 +41,11 @@ import { Gender, getGenderSymbol } from "#app/data/gender";
import { getNatureName } from "#app/data/nature";
import { getPokeballAtlasKey, getPokeballTintColor } from "#app/data/pokeball";
import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { trainerNamePools } from "#app/data/trainer-names";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { addPokemonDataToDexAndValidateAchievements } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import type { PokeballType } from "#enums/pokeball";
import { doShinySparkleAnim } from "#app/field/anims";
import { TrainerType } from "#enums/trainer-type";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/globalTradeSystem";
@ -982,15 +982,14 @@ function doTradeReceivedSequence(
}
function generateRandomTraderName() {
const length = Object.keys(trainerNamePools).length;
const length = TrainerType.YOUNGSTER - TrainerType.ACE_TRAINER + 1;
// +1 avoids TrainerType.UNKNOWN
let trainerTypePool = trainerNamePools[randInt(length) + 1];
while (!trainerTypePool) {
trainerTypePool = trainerNamePools[randInt(length) + 1];
}
const trainerTypePool = i18next.t("trainersCommon:" + TrainerType[randInt(length) + 1], { returnObjects: true });
// Some trainers have 2 gendered pools, some do not
const genderedPool = trainerTypePool[randInt(trainerTypePool.length)];
const trainerNameString = Array.isArray(genderedPool) ? genderedPool[randInt(genderedPool.length)] : genderedPool;
const gender = randInt(2) === 0 ? "MALE" : "FEMALE";
const trainerNameString = randSeedItem(
Object.values(trainerTypePool.hasOwnProperty(gender) ? trainerTypePool[gender] : trainerTypePool),
) as string;
// Some names have an '&' symbol and need to be trimmed to a single name instead of a double name
const trainerNames = trainerNameString.split(" & ");
return trainerNames[randInt(trainerNames.length)];

View File

@ -7,7 +7,7 @@ import i18next from "i18next";
import type { AnySound } from "#app/battle-scene";
import { globalScene } from "#app/global-scene";
import type { GameMode } from "#app/game-mode";
import { DexAttr, type StarterMoveset } from "#app/system/game-data";
import { DexAttr, DexEntry, type StarterMoveset } from "#app/system/game-data";
import * as Utils from "#app/utils";
import { uncatchableSpecies } from "#app/data/balance/biomes";
import { speciesEggMoves } from "#app/data/balance/egg-moves";

View File

@ -44,14 +44,17 @@ interface Season {
//#region Constants
/** The weight multiplier for the battles-won splash message */
const BATTLES_WON_WEIGHT_MULTIPLIER = 10;
const BATTLES_WON_WEIGHT_MULTIPLIER = 15;
/** The weight multiplier for the Pokémon names splash message */
const POKEMON_NAMES_WEIGHT_MULTIPLIER = 10;
/** The weight multiplier for the seasonal splash messages */
const SEASONAL_WEIGHT_MULTIPLIER = 10;
const SEASONAL_WEIGHT_MULTIPLIER = 15;
//#region Common Messages
const commonSplashMessages = [
...Array(BATTLES_WON_WEIGHT_MULTIPLIER).fill("battlesWon"),
...Array(POKEMON_NAMES_WEIGHT_MULTIPLIER).fill("underratedPokemon"),
"joinTheDiscord",
"infiniteLevels",
"everythingIsStackable",
@ -78,7 +81,7 @@ const commonSplashMessages = [
"mostlyConsistentSeeds",
"achievementPointsDontDoAnything",
"nothingBeatsAJellyFilledDonut",
"dontTalkAboutTheTinkatonIncident",
"dontTalkAboutThePokemonIncident",
"alsoTryPokengine",
"alsoTryEmeraldRogue",
"alsoTryRadicalRed",
@ -176,41 +179,146 @@ const commonSplashMessages = [
"timeForYourDeliDelivery",
"goodFirstImpression",
"iPreferRarerCandies",
"pocketRoguelite",
"porygonDidNothingWrong",
"critMattered",
"pickupNotRequired",
"stayHydrated",
"alsoTryCobblemon",
"alsoTryPokeDoku",
"mySleepStyleIsDoesnt",
"makeYourOwnWorldChampDifference",
"yoChampInTheMaking",
"notLiableForDecisionAnxiety",
"theAirIsTastyHere",
"continue",
"startANewRunToday",
"neverGiveUp",
"theresAlwaysNextTime",
"oneTwoThreeAndPoof",
"yourPokemonOnlyGoToLevelOneHundred",
"theBattlesWillBeLegendary",
"levelCurveBetterThanJohto",
"alsoTryShowering",
"wellStillBeHere",
"weHopeToSeeYouAgain",
"aHealthyTeamCanMeanGreaterRewards",
"aWildPokemonAppeared",
"isThisThingOn",
"needsMoreTesting",
"whoChecksStatChanges",
"whenTwoTrainersEyesMeet",
"notOfficiallyOnSteam",
"fiftyFifty",
"metaNotIncluded",
"bornToBeAWinner",
"onARollout",
"itsAlwaysNightDeepInTheAbyss",
"folksThisIsInsane"
];
//#region Seasonal Messages
const seasonalSplashMessages: Season[] = [
{
name: "New Year's",
start: "01-01",
end: "01-15",
messages: [
"newYears.happyNewYear",
"newYears.andAHappyNewYear"
],
},
{
name: "Valentines",
start: "02-07",
end: "02-21",
messages: [
"valentines.happyValentines",
"valentines.fullOfLove",
"valentines.applinForYou",
"valentines.thePowerOfLoveIsThreeThirtyBST",
"valentines.haveAHeartScale",
"valentines.i<3You"
],
},
{
name: "April Fools",
start: "04-01",
end: "04-03",
messages: [
"aprilFools.battlesOne",
"aprilFools.aprilFools",
"aprilFools.removedPokemon",
"aprilFools.helloKyleAmber",
"aprilFools.gotcha",
"aprilFools.alsoTryPokerogueTwo",
"aprilFools.nowWithSameScumCountermeasures",
"aprilFools.neverGonnaGiveYouGoodRolls",
"aprilFools.youBumblingBuffoon",
"aprilFools.doubleShinyOddsForTrainersOnly",
"aprilFools.nowWithZMoves",
"aprilFools.newLightType",
"aprilFools.removedMegas",
"aprilFools.nerfedYourFavorites",
"aprilFools.grrr",
"aprilFools.enabledEternatusPassiveGoodLuck",
"aprilFools.theDarkestDaySoundsLikeAFutureProblem",
"aprilFools.tmShopWhen",
"aprilFools.whoIsFinn",
"aprilFools.watchOutForShadowPokemon",
"aprilFools.nowWithDarkTypeLuxray",
"aprilFools.onlyOnPokerogueNetAGAIN",
"aprilFools.noFreeVouchers",
"aprilFools.altffourAchievementPoints",
"aprilFools.rokePogue",
"aprilFools.readMe",
"aprilFools.winningNotIncluded",
"aprilFools.timeForYourSoloUnownRun",
"aprilFools.nowARealTimeStrategyGame",
"aprilFools.nowWithQuickTimeEncounters",
"aprilFools.timeYourInputsForHigherCatchrate",
"aprilFools.certifiedButtonSimulator",
"aprilFools.iHopeYouGetSuckerPunched"
],
},
{
name: "Halloween",
start: "09-15",
start: "10-15",
end: "10-31",
messages: [
"halloween.happyHalloween",
"halloween.boo",
"halloween.pumpkabooAbout",
"halloween.mayContainSpiders",
"halloween.spookyScarySkeledirge",
"halloween.gourgeistUsedTrickOrTreat",
"halloween.letsSnuggleForever",
"halloween.letsSnuggleForever"
],
},
{
name: "XMAS",
name: "Winter Holiday",
start: "12-01",
end: "12-26",
end: "12-31",
messages: [
"xmas.happyHolidays",
"xmas.unaffilicatedWithDelibirdServices",
"xmas.delibirdSeason",
"xmas.diamondsFromTheSky",
"xmas.holidayStylePikachuNotIncluded",
"winterHoliday.happyHolidays",
"winterHoliday.unaffilicatedWithDelibirdServices",
"winterHoliday.delibirdSeason",
"winterHoliday.diamondsFromTheSky",
"winterHoliday.holidayStylePikachuNotIncluded",
"winterHoliday.delibirdDirectlyToYourHouse",
"winterHoliday.haveAnIceDay",
"winterHoliday.spinTheClaydol",
"winterHoliday.beGoodForGoodnessSake",
"winterHoliday.moomooMilkAndLavaCookies",
"winterHoliday.iNeedAYacheBerry",
"winterHoliday.getJolly",
"winterHoliday.tisTheSeasonToBeSpeSpa",
"winterHoliday.deckTheHalls",
"winterHoliday.saveScummingGetsYouOnTheNaughtyList",
"winterHoliday.badTrainersGetRolycoly"
],
},
{
name: "New Year's",
start: "01-01",
end: "01-31",
messages: ["newYears.happyNewYear"],
},
];
//#endregion

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,6 @@ import {
import type { EnemyPokemon } from "#app/field/pokemon";
import * as Utils from "#app/utils";
import type { PersistentModifier } from "#app/modifier/modifier";
import { trainerNamePools } from "#app/data/trainer-names";
import { ArenaTagSide, ArenaTrapTag } from "#app/data/arena-tag";
import { getIsInitialized, initI18n } from "#app/plugins/i18n";
import i18next from "i18next";
@ -61,11 +60,17 @@ export default class Trainer extends Phaser.GameObjects.Container {
: Utils.randSeedWeightedItem(this.config.partyTemplates.map((_, i) => i)),
this.config.partyTemplates.length - 1,
);
if (trainerNamePools.hasOwnProperty(trainerType)) {
const namePool = trainerNamePools[trainerType];
if (i18next.exists("trainersCommon:" + TrainerType[trainerType], { returnObjects: true })) {
const namePool = i18next.t("trainersCommon:" + TrainerType[trainerType], { returnObjects: true });
this.name =
name ||
Utils.randSeedItem(Array.isArray(namePool[0]) ? namePool[variant === TrainerVariant.FEMALE ? 1 : 0] : namePool);
Utils.randSeedItem(
Object.values(
namePool.hasOwnProperty("MALE")
? namePool[variant === TrainerVariant.FEMALE ? "FEMALE" : "MALE"]
: namePool,
),
);
if (variant === TrainerVariant.DOUBLE) {
if (this.config.doubleOnly) {
if (partnerName) {
@ -74,7 +79,9 @@ export default class Trainer extends Phaser.GameObjects.Container {
[this.name, this.partnerName] = this.name.split(" & ");
}
} else {
this.partnerName = partnerName || Utils.randSeedItem(Array.isArray(namePool[0]) ? namePool[1] : namePool);
this.partnerName =
partnerName ||
Utils.randSeedItem(Object.values(namePool.hasOwnProperty("FEMALE") ? namePool["FEMALE"] : namePool));
}
}
}

View File

@ -255,6 +255,11 @@ class DefaultOverrides {
* Note that, for all items in the array, `count` is not used.
*/
readonly ITEM_REWARD_OVERRIDE: ModifierOverride[] = [];
/**
* If `true`, disable all non-scripted opponent trainer encounters.
*/
readonly DISABLE_STANDARD_TRAINERS_OVERRIDE: boolean = false;
}
export const defaultOverrides = new DefaultOverrides();

View File

@ -237,6 +237,7 @@ export async function initI18n(): Promise<void> {
"terrain",
"titles",
"trainerClasses",
"trainersCommon",
"trainerNames",
"tutorial",
"voucher",

View File

@ -98,12 +98,13 @@ export function getDataTypeKey(dataType: GameDataType, slotId = 0): string {
switch (dataType) {
case GameDataType.SYSTEM:
return "data";
case GameDataType.SESSION:
case GameDataType.SESSION: {
let ret = "sessionData";
if (slotId) {
ret += slotId;
}
return ret;
}
case GameDataType.SETTINGS:
return "settings";
case GameDataType.TUTORIALS:
@ -201,39 +202,6 @@ export interface DexEntry {
ivs: number[];
}
export const DexAttr = {
NON_SHINY: 1n,
SHINY: 2n,
MALE: 4n,
FEMALE: 8n,
DEFAULT_VARIANT: 16n,
VARIANT_2: 32n,
VARIANT_3: 64n,
DEFAULT_FORM: 128n,
};
export interface DexAttrProps {
shiny: boolean;
female: boolean;
variant: Variant;
formIndex: number;
}
export const AbilityAttr = {
ABILITY_1: 1,
ABILITY_2: 2,
ABILITY_HIDDEN: 4,
};
export type RunHistoryData = Record<number, RunEntry>;
export interface RunEntry {
entry: SessionSaveData;
isVictory: boolean;
/*Automatically set to false at the moment - implementation TBD*/
isFavorite: boolean;
}
export type StarterMoveset = [Moves] | [Moves, Moves] | [Moves, Moves, Moves] | [Moves, Moves, Moves, Moves];
export interface StarterFormMoveData {
@ -260,6 +228,39 @@ export interface StarterPreferences {
[key: number]: StarterAttributes;
}
export interface DexAttrProps {
shiny: boolean;
female: boolean;
variant: Variant;
formIndex: number;
}
export type RunHistoryData = Record<number, RunEntry>;
export interface RunEntry {
entry: SessionSaveData;
isVictory: boolean;
/*Automatically set to false at the moment - implementation TBD*/
isFavorite: boolean;
}
export const DexAttr = {
NON_SHINY: 1n,
SHINY: 2n,
MALE: 4n,
FEMALE: 8n,
DEFAULT_VARIANT: 16n,
VARIANT_2: 32n,
VARIANT_3: 64n,
DEFAULT_FORM: 128n,
};
export const AbilityAttr = {
ABILITY_1: 1,
ABILITY_2: 2,
ABILITY_HIDDEN: 4,
};
// the latest data saved/loaded for the Starter Preferences. Required to reduce read/writes. Initialize as "{}", since this is the default value and no data needs to be stored if present.
// if they ever add private static variables, move this into StarterPrefs
const StarterPrefers_DEFAULT: string = "{}";
@ -1553,16 +1554,18 @@ export class GameData {
try {
dataName = GameDataType[dataType].toLowerCase();
switch (dataType) {
case GameDataType.SYSTEM:
case GameDataType.SYSTEM: {
dataStr = this.convertSystemDataStr(dataStr);
const systemData = this.parseSystemData(dataStr);
valid = !!systemData.dexData && !!systemData.timestamp;
break;
case GameDataType.SESSION:
}
case GameDataType.SESSION: {
const sessionData = this.parseSessionData(dataStr);
valid = !!sessionData.party && !!sessionData.enemyParty && !!sessionData.timestamp;
break;
case GameDataType.RUN_HISTORY:
}
case GameDataType.RUN_HISTORY: {
const data = JSON.parse(dataStr);
const keys = Object.keys(data);
dataName = i18next.t("menuUiHandler:RUN_HISTORY").toLowerCase();
@ -1572,6 +1575,7 @@ export class GameData {
["isFavorite", "isVictory", "entry"].every(v => entryKeys.includes(v)) && entryKeys.length === 3;
});
break;
}
case GameDataType.SETTINGS:
case GameDataType.TUTORIALS:
valid = true;

View File

@ -25,7 +25,7 @@ import { AbilityAttr, DexAttr } from "#app/system/game-data";
import type { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler";
import MessageUiHandler from "#app/ui/message-ui-handler";
import { StatsContainer } from "#app/ui/stats-container";
import { TextStyle, addTextObject, getTextStyleOptions } from "#app/ui/text";
import { TextStyle, addBBCodeTextObject, addTextObject, getTextColor, getTextStyleOptions } from "#app/ui/text";
import { Mode } from "#app/ui/ui";
import { addWindow } from "#app/ui/ui-theme";
import { Egg } from "#app/data/egg";
@ -63,6 +63,7 @@ import { TimeOfDay } from "#app/enums/time-of-day";
import type { Abilities } from "#app/enums/abilities";
import { BaseStatsOverlay } from "#app/ui/base-stats-overlay";
import { globalScene } from "#app/global-scene";
import type BBCodeText from "phaser3-rex-plugins/plugins/gameobjects/tagtext/bbcodetext/BBCodeText";
interface LanguageSetting {
starterInfoTextSize: string;
@ -250,7 +251,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
// Menu
private menuContainer: Phaser.GameObjects.Container;
private menuBg: Phaser.GameObjects.NineSlice;
protected optionSelectText: Phaser.GameObjects.Text;
protected optionSelectText: BBCodeText;
private menuOptions: MenuOptions[];
protected scale = 0.1666666667;
private menuDescriptions: string[];
@ -593,14 +594,13 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
this.menuOptions = Utils.getEnumKeys(MenuOptions).map(m => Number.parseInt(MenuOptions[m]) as MenuOptions);
this.optionSelectText = addTextObject(
this.optionSelectText = addBBCodeTextObject(
0,
0,
this.menuOptions.map(o => `${i18next.t(`pokedexUiHandler:${MenuOptions[o]}`)}`).join("\n"),
TextStyle.WINDOW,
{ maxLines: this.menuOptions.length },
{ maxLines: this.menuOptions.length, lineSpacing: 12 },
);
this.optionSelectText.setLineSpacing(12);
this.menuDescriptions = [
i18next.t("pokedexUiHandler:showBaseStats"),
@ -623,7 +623,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
);
this.menuBg.setOrigin(0, 0);
this.optionSelectText.setPositionRelative(this.menuBg, 10 + 24 * this.scale, 6);
this.optionSelectText.setPosition(this.menuBg.x + 10 + 24 * this.scale, this.menuBg.y + 6);
this.menuContainer.add(this.menuBg);
@ -704,11 +704,39 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
this.setSpecies();
this.updateInstructions();
this.optionSelectText.setText(this.getMenuText());
this.setCursor(0);
return true;
}
getMenuText(): string {
const isSeen = this.isSeen();
const isStarterCaught = !!this.isCaught(this.getStarterSpecies(this.species));
return this.menuOptions
.map(o => {
const label = `${i18next.t(`pokedexUiHandler:${MenuOptions[o]}`)}`;
const isDark =
!isSeen ||
(!isStarterCaught && (o === MenuOptions.TOGGLE_IVS || o === MenuOptions.NATURES)) ||
(this.tmMoves.length < 1 && o === MenuOptions.TM_MOVES);
const color = getTextColor(
isDark ? TextStyle.SHADOW_TEXT : TextStyle.SETTINGS_VALUE,
false,
globalScene.uiTheme,
);
const shadow = getTextColor(
isDark ? TextStyle.SHADOW_TEXT : TextStyle.SETTINGS_VALUE,
true,
globalScene.uiTheme,
);
return `[shadow=${shadow}][color=${color}]${label}[/color][/shadow]`;
})
.join("\n");
}
starterSetup(): void {
this.evolutions = [];
this.prevolutions = [];
@ -904,6 +932,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
return (dexEntry?.caughtAttr ?? 0n) & (starterDexEntry?.caughtAttr ?? 0n) & species.getFullUnlocksData();
}
/**
* Check whether a given form is caught for a given species.
* All forms that can be reached through a form change during battle are considered caught and show up in the dex as such.
@ -928,6 +957,14 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
return isFormCaught;
}
isSeen(): boolean {
if (this.speciesStarterDexEntry?.seenAttr) {
return true;
}
const starterCaughtAttr = this.isCaught(this.getStarterSpecies(this.species));
return !!starterCaughtAttr;
}
/**
* Get the starter attributes for the given PokemonSpecies, after sanitizing them.
* If somehow a preference is set for a form, variant, gender, ability or nature
@ -1076,6 +1113,8 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
const isCaught = this.isCaught();
const isFormCaught = this.isFormCaught();
const isSeen = this.isSeen();
const isStarterCaught = !!this.isCaught(this.getStarterSpecies(this.species));
if (this.blockInputOverlay) {
if (button === Button.CANCEL || button === Button.ACTION) {
@ -1130,7 +1169,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
if (button === Button.ACTION) {
switch (this.cursor) {
case MenuOptions.BASE_STATS:
if (!isCaught || !isFormCaught) {
if (!isSeen) {
error = true;
} else {
this.blockInput = true;
@ -1150,7 +1189,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
break;
case MenuOptions.LEVEL_MOVES:
if (!isCaught || !isFormCaught) {
if (!isSeen) {
error = true;
} else {
this.blockInput = true;
@ -1208,7 +1247,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
break;
case MenuOptions.EGG_MOVES:
if (!isCaught || !isFormCaught) {
if (!isSeen) {
error = true;
} else {
this.blockInput = true;
@ -1275,7 +1314,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
break;
case MenuOptions.TM_MOVES:
if (!isCaught || !isFormCaught) {
if (!isSeen) {
error = true;
} else if (this.tmMoves.length < 1) {
ui.showText(i18next.t("pokedexUiHandler:noTmMoves"));
@ -1326,7 +1365,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
break;
case MenuOptions.ABILITIES:
if (!isCaught || !isFormCaught) {
if (!isSeen) {
error = true;
} else {
this.blockInput = true;
@ -1414,7 +1453,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
break;
case MenuOptions.BIOMES:
if (!(isCaught || this.speciesStarterDexEntry?.seenAttr)) {
if (!isSeen) {
error = true;
} else {
this.blockInput = true;
@ -1493,7 +1532,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
break;
case MenuOptions.EVOLUTIONS:
if (!isCaught || !isFormCaught) {
if (!isSeen) {
error = true;
} else {
this.blockInput = true;
@ -1677,7 +1716,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
break;
case MenuOptions.TOGGLE_IVS:
if (!isCaught || !isFormCaught) {
if (!isStarterCaught) {
error = true;
} else {
this.toggleStatsMode();
@ -1687,7 +1726,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
break;
case MenuOptions.NATURES:
if (!isCaught || !isFormCaught) {
if (!isStarterCaught) {
error = true;
} else {
this.blockInput = true;
@ -2392,8 +2431,6 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
}
if (species) {
const dexEntry = globalScene.gameData.dexData[species.speciesId];
const caughtAttr = this.isCaught(species);
if (!caughtAttr) {
@ -2414,7 +2451,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler {
}
const isFormCaught = this.isFormCaught();
const isFormSeen = dexEntry ? (dexEntry.seenAttr & globalScene.gameData.getFormAttr(formIndex ?? 0)) > 0n : false;
const isFormSeen = this.isSeen();
this.shinyOverlay.setVisible(shiny ?? false); // TODO: is false the correct default?
this.pokemonNumberText.setColor(this.getTextColor(shiny ? TextStyle.SUMMARY_GOLD : TextStyle.SUMMARY, false));

View File

@ -781,6 +781,15 @@ export default class PokedexUiHandler extends MessageUiHandler {
this.starterSelectMessageBoxContainer.setVisible(!!text?.length);
}
isSeen(species: PokemonSpecies, dexEntry: DexEntry): boolean {
if (dexEntry?.seenAttr) {
return true;
}
const starterDexEntry = globalScene.gameData.dexData[this.getStarterSpeciesId(species.speciesId)];
return !!starterDexEntry?.caughtAttr;
}
/**
* Determines if 'Icon' based upgrade notifications should be shown
* @returns true if upgrade notifications are enabled and set to display an 'Icon'
@ -1364,7 +1373,7 @@ export default class PokedexUiHandler extends MessageUiHandler {
const levelMoves = pokemonSpeciesLevelMoves[species.speciesId].map(m => allMoves[m[1]].name);
// This always gets egg moves from the starter
const eggMoves = speciesEggMoves[starterId]?.map(m => allMoves[m].name) ?? [];
const tmMoves = speciesTmMoves[starterId]?.map(m => allMoves[Array.isArray(m) ? m[1] : m].name) ?? [];
const tmMoves = speciesTmMoves[species.speciesId]?.map(m => allMoves[Array.isArray(m) ? m[1] : m].name) ?? [];
const selectedMove1 = this.filterText.getValue(FilterTextRow.MOVE_1);
const selectedMove2 = this.filterText.getValue(FilterTextRow.MOVE_2);
@ -1740,7 +1749,7 @@ export default class PokedexUiHandler extends MessageUiHandler {
if (caughtAttr & data.species.getFullUnlocksData() || globalScene.dexForDevs) {
container.icon.clearTint();
} else if (dexEntry.seenAttr) {
} else if (this.isSeen(data.species, dexEntry)) {
container.icon.setTint(0x808080);
} else {
container.icon.setTint(0);
@ -1931,13 +1940,11 @@ export default class PokedexUiHandler extends MessageUiHandler {
const props = this.getSanitizedProps(globalScene.gameData.getSpeciesDexAttrProps(this.lastSpecies, dexAttr));
this.trayContainers = [];
const isFormSeen = this.isSeen(species, dexEntry);
this.trayForms.map((f, index) => {
const isFormCaught = dexEntry
? (dexEntry.caughtAttr & species.getFullUnlocksData() & globalScene.gameData.getFormAttr(f.formIndex ?? 0)) > 0n
: false;
const isFormSeen = dexEntry
? (dexEntry.seenAttr & globalScene.gameData.getFormAttr(f.formIndex ?? 0)) > 0n
: false;
const formContainer = new PokedexMonContainer(species, {
formIndex: f.formIndex,
female: props.female,
@ -2147,7 +2154,7 @@ export default class PokedexUiHandler extends MessageUiHandler {
}
const isFormCaught = dexEntry ? (caughtAttr & globalScene.gameData.getFormAttr(formIndex ?? 0)) > 0n : false;
const isFormSeen = dexEntry ? (dexEntry.seenAttr & globalScene.gameData.getFormAttr(formIndex ?? 0)) > 0n : false;
const isFormSeen = this.isSeen(species, dexEntry);
const assetLoadCancelled = new BooleanHolder(false);
this.assetLoadCancelled = assetLoadCancelled;

View File

@ -4553,4 +4553,13 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
icon.setFrame(species.getIconId(female, formIndex, false, variant));
}
}
/**
* Clears this UI's starter preferences.
*
* Designed to be used for unit tests that utilize this UI.
*/
clearStarterPreferences() {
this.starterPreferences = {};
}
}

View File

@ -8,10 +8,13 @@ import { TimedEventDisplay } from "#app/timed-event-manager";
import { version } from "../../package.json";
import { pokerogueApi } from "#app/plugins/api/pokerogue-api";
import { globalScene } from "#app/global-scene";
import type { Species } from "#enums/species";
import { getPokemonSpecies } from "#app/data/pokemon-species";
import { PlayerGender } from "#enums/player-gender";
export default class TitleUiHandler extends OptionSelectUiHandler {
/** If the stats can not be retrieved, use this fallback value */
private static readonly BATTLES_WON_FALLBACK: number = -99999999;
private static readonly BATTLES_WON_FALLBACK: number = -1;
private titleContainer: Phaser.GameObjects.Container;
private playerCountLabel: Phaser.GameObjects.Text;
@ -98,6 +101,29 @@ export default class TitleUiHandler extends OptionSelectUiHandler {
});
}
/** Used solely to display a random Pokémon name in a splash message. */
randomPokemon(): void {
const rand = Utils.randInt(1025, 1);
const pokemon = getPokemonSpecies(rand as Species);
if (
this.splashMessage === "splashMessages:underratedPokemon" ||
this.splashMessage === "splashMessages:dontTalkAboutThePokemonIncident" ||
this.splashMessage === "splashMessages:aWildPokemonAppeared" ||
this.splashMessage === "splashMessages:aprilFools.removedPokemon"
) {
this.splashMessageText.setText(i18next.t(this.splashMessage, { pokemonName: pokemon.name }));
}
}
/** Used for a specific April Fools splash message. */
genderSplash(): void {
if (this.splashMessage === "splashMessages:aprilFools.helloKyleAmber") {
globalScene.gameData.gender === PlayerGender.MALE
? this.splashMessageText.setText(i18next.t(this.splashMessage, { name: i18next.t("trainerNames:player_m") }))
: this.splashMessageText.setText(i18next.t(this.splashMessage, { name: i18next.t("trainerNames:player_f") }));
}
}
show(args: any[]): boolean {
const ret = super.show(args);
@ -121,6 +147,9 @@ export default class TitleUiHandler extends OptionSelectUiHandler {
this.eventDisplay.show();
}
this.randomPokemon();
this.genderSplash();
this.updateTitleStats();
this.titleStatsTimer = setInterval(() => {

View File

@ -12,7 +12,8 @@ describe("Abilities - Steely Spirit", () => {
let game: GameManager;
const steelySpiritMultiplier = 1.5;
const moveToCheck = Moves.IRON_HEAD;
const ironHeadPower = allMoves[moveToCheck].power;
let ironHeadPower: number;
beforeAll(() => {
phaserGame = new Phaser.Game({
@ -25,6 +26,7 @@ describe("Abilities - Steely Spirit", () => {
});
beforeEach(() => {
ironHeadPower = allMoves[moveToCheck].power;
game = new GameManager(phaserGame);
game.override.battleType("double");
game.override.enemySpecies(Species.SHUCKLE);

View File

@ -1,4 +1,5 @@
import { Moves } from "#app/enums/moves";
import type Move from "#app/data/moves/move";
import { Abilities } from "#enums/abilities";
import { Species } from "#enums/species";
import { BattlerIndex } from "#app/battle";
@ -12,8 +13,8 @@ describe("Abilities - Supreme Overlord", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const move = allMoves[Moves.TACKLE];
const basePower = move.power;
let move: Move;
let basePower: number;
beforeAll(() => {
phaserGame = new Phaser.Game({
@ -26,6 +27,8 @@ describe("Abilities - Supreme Overlord", () => {
});
beforeEach(() => {
move = allMoves[Moves.TACKLE];
basePower = move.power;
game = new GameManager(phaserGame);
game.override
.battleType("single")

View File

@ -32,22 +32,22 @@ describe("Abilities - Unseen Fist", () => {
game.override.enemyLevel(100);
});
it("should cause a contact move to ignore Protect", () =>
testUnseenFistHitResult(game, Moves.QUICK_ATTACK, Moves.PROTECT, true));
it("should cause a contact move to ignore Protect", async () =>
await testUnseenFistHitResult(game, Moves.QUICK_ATTACK, Moves.PROTECT, true));
it("should not cause a non-contact move to ignore Protect", () =>
testUnseenFistHitResult(game, Moves.ABSORB, Moves.PROTECT, false));
it("should not cause a non-contact move to ignore Protect", async () =>
await testUnseenFistHitResult(game, Moves.ABSORB, Moves.PROTECT, false));
it("should not apply if the source has Long Reach", async () => {
game.override.passiveAbility(Abilities.LONG_REACH);
await testUnseenFistHitResult(game, Moves.QUICK_ATTACK, Moves.PROTECT, false);
});
it("should cause a contact move to ignore Wide Guard", () =>
testUnseenFistHitResult(game, Moves.BREAKING_SWIPE, Moves.WIDE_GUARD, true));
it("should cause a contact move to ignore Wide Guard", async () =>
await testUnseenFistHitResult(game, Moves.BREAKING_SWIPE, Moves.WIDE_GUARD, true));
it("should not cause a non-contact move to ignore Wide Guard", () =>
testUnseenFistHitResult(game, Moves.BULLDOZE, Moves.WIDE_GUARD, false));
it("should not cause a non-contact move to ignore Wide Guard", async () =>
await testUnseenFistHitResult(game, Moves.BULLDOZE, Moves.WIDE_GUARD, false));
it("should cause a contact move to ignore Protect, but not Substitute", async () => {
game.override.enemyLevel(1);

View File

@ -56,16 +56,21 @@ describe("Abilities - Wonder Skin", () => {
expect(moveToCheck.calculateBattleAccuracy).toHaveReturnedWith(100);
});
const bypassAbilities = [Abilities.MOLD_BREAKER, Abilities.TERAVOLT, Abilities.TURBOBLAZE];
const bypassAbilities = [
[Abilities.MOLD_BREAKER, "Mold Breaker"],
[Abilities.TERAVOLT, "Teravolt"],
[Abilities.TURBOBLAZE, "Turboblaze"],
];
bypassAbilities.forEach(ability => {
it(`does not affect pokemon with ${allAbilities[ability].name}`, async () => {
it(`does not affect pokemon with ${ability[1]}`, async () => {
const moveToCheck = allMoves[Moves.CHARM];
game.override.ability(ability);
// @ts-ignore ts doesn't know that ability[0] is an ability and not a string...
game.override.ability(ability[0]);
vi.spyOn(moveToCheck, "calculateBattleAccuracy");
await game.startBattle([Species.PIKACHU]);
await game.classicMode.startBattle([Species.PIKACHU]);
game.move.select(Moves.CHARM);
await game.phaseInterceptor.to(MoveEffectPhase);

View File

@ -14,7 +14,7 @@ import { NumberHolder } from "#app/utils";
import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import BattleScene from "#app/battle-scene";
import type BattleScene from "#app/battle-scene";
describe("check some Achievement related stuff", () => {
it("should check Achievement creation", () => {
@ -77,6 +77,25 @@ describe("Achv", () => {
});
describe("MoneyAchv", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
let scene: BattleScene;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
scene = game.scene;
});
it("should create an instance of MoneyAchv", () => {
const moneyAchv = new MoneyAchv("", "Test Money Achievement", 10000, "money_icon", 10);
expect(moneyAchv).toBeInstanceOf(MoneyAchv);
@ -85,7 +104,6 @@ describe("MoneyAchv", () => {
it("should validate the achievement based on the money amount", () => {
const moneyAchv = new MoneyAchv("", "Test Money Achievement", 10000, "money_icon", 10);
const scene = new BattleScene();
scene.money = 5000;
expect(moneyAchv.validate([])).toBe(false);

View File

@ -1,22 +1,40 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { PokemonTurnData, TurnMove, PokemonMove } from "#app/field/pokemon";
import type Pokemon from "#app/field/pokemon";
import { MoveResult } from "#app/field/pokemon";
import BattleScene from "#app/battle-scene";
import type BattleScene from "#app/battle-scene";
import { BattlerTagLapseType, BindTag, SubstituteTag } from "#app/data/battler-tags";
import { Moves } from "#app/enums/moves";
import { PokemonAnimType } from "#app/enums/pokemon-anim-type";
import * as messages from "#app/messages";
import { allMoves } from "#app/data/moves/move";
import type { MoveEffectPhase } from "#app/phases/move-effect-phase";
import GameManager from "#test/testUtils/gameManager";
describe("BattlerTag - SubstituteTag", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
});
let mockPokemon: Pokemon;
describe("onAdd behavior", () => {
beforeEach(() => {
mockPokemon = {
scene: new BattleScene(),
scene: game.scene,
hp: 101,
id: 0,
getMaxHp: vi.fn().mockReturnValue(101) as Pokemon["getMaxHp"],
@ -77,7 +95,7 @@ describe("BattlerTag - SubstituteTag", () => {
describe("onRemove behavior", () => {
beforeEach(() => {
mockPokemon = {
scene: new BattleScene(),
scene: game.scene,
hp: 101,
id: 0,
isFainted: vi.fn().mockReturnValue(false) as Pokemon["isFainted"],
@ -109,7 +127,7 @@ describe("BattlerTag - SubstituteTag", () => {
describe("lapse behavior", () => {
beforeEach(() => {
mockPokemon = {
scene: new BattleScene(),
scene: game.scene,
hp: 101,
id: 0,
turnData: { acted: true } as PokemonTurnData,

View File

@ -7,10 +7,10 @@ describe("Data - Splash Messages", () => {
expect(getSplashMessages().length).toBeGreaterThanOrEqual(15);
});
// make sure to adjust this test if the weight it changed!
it("should add contain 10 `battlesWon` splash messages", () => {
const battlesWonMessages = getSplashMessages().filter(message => message === "splashMessages:battlesWon");
expect(battlesWonMessages).toHaveLength(10);
// Make sure to adjust this test if the weight is changed!
it("should add contain 15 `battlesWon` splash messages", () => {
const battlesWonMessages = getSplashMessages().filter((message) => message === "splashMessages:battlesWon");
expect(battlesWonMessages).toHaveLength(15);
});
describe("Seasonal", () => {
@ -22,16 +22,24 @@ describe("Data - Splash Messages", () => {
vi.useRealTimers(); // reset system time
});
it("should contain halloween messages from Sep 15 to Oct 31", () => {
testSeason(new Date("2024-09-15"), new Date("2024-10-31"), "halloween");
it("should contain new years messages from Jan 1 to Jan 15", () => {
testSeason(new Date("2025-01-01"), new Date("2025-01-15"), "newYears");
});
it("should contain xmas messages from Dec 1 to Dec 26", () => {
testSeason(new Date("2024-12-01"), new Date("2024-12-26"), "xmas");
it("should contain valentines messages from Feb 7 to Feb 21", () => {
testSeason(new Date("2025-02-07"), new Date("2025-02-21"), "valentines");
});
it("should contain new years messages frm Jan 1 to Jan 31", () => {
testSeason(new Date("2024-01-01"), new Date("2024-01-31"), "newYears");
it("should contain april fools messages from April 1 to April 3", () => {
testSeason(new Date("2025-04-01"), new Date("2025-04-03"), "aprilFools");
});
it("should contain halloween messages from Oct 15 to Oct 31", () => {
testSeason(new Date("2025-10-15"), new Date("2025-10-31"), "halloween");
});
it("should contain winter holiday messages from Dec 1 to Dec 31", () => {
testSeason(new Date("2025-12-01"), new Date("2025-12-31"), "winterHoliday");
});
});
});

View File

@ -13,17 +13,12 @@ import { Species } from "#enums/species";
import { StatusEffect } from "#enums/status-effect";
import GameManager from "#test/testUtils/gameManager";
import { mockI18next } from "#test/testUtils/testUtils";
import i18next from "i18next";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const pokemonName = "PKM";
const sourceText = "SOURCE";
describe("Status Effect Messages", () => {
beforeAll(async () => {
await i18next.init();
});
describe("NONE", () => {
const statusEffect = StatusEffect.NONE;
@ -31,7 +26,6 @@ describe("Status Effect Messages", () => {
mockI18next();
const text = getStatusEffectObtainText(statusEffect, pokemonName);
console.log("text:", text);
expect(text).toBe("");
const emptySourceText = getStatusEffectObtainText(statusEffect, pokemonName, "");

View File

@ -13,7 +13,7 @@ describe("Moves - Copycat", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const randomMoveAttr = allMoves[Moves.METRONOME].getAttrs(RandomMoveAttr)[0];
let randomMoveAttr: RandomMoveAttr;
beforeAll(() => {
phaserGame = new Phaser.Game({
@ -26,6 +26,7 @@ describe("Moves - Copycat", () => {
});
beforeEach(() => {
randomMoveAttr = allMoves[Moves.METRONOME].getAttrs(RandomMoveAttr)[0];
game = new GameManager(phaserGame);
game.override
.moveset([Moves.COPYCAT, Moves.SPIKY_SHIELD, Moves.SWORDS_DANCE, Moves.SPLASH])

View File

@ -3,6 +3,7 @@ import { allMoves } from "#app/data/moves/move";
import { DamageAnimPhase } from "#app/phases/damage-anim-phase";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import { Moves } from "#enums/moves";
import type Move from "#app/data/moves/move";
import { Species } from "#enums/species";
import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser";
@ -12,7 +13,7 @@ describe("Moves - Dynamax Cannon", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const dynamaxCannon = allMoves[Moves.DYNAMAX_CANNON];
let dynamaxCannon: Move;
beforeAll(() => {
phaserGame = new Phaser.Game({
@ -25,6 +26,7 @@ describe("Moves - Dynamax Cannon", () => {
});
beforeEach(() => {
dynamaxCannon = allMoves[Moves.DYNAMAX_CANNON];
game = new GameManager(phaserGame);
game.override.moveset([dynamaxCannon.id]);

View File

@ -1,6 +1,7 @@
import { Stat } from "#enums/stat";
import { BattlerIndex } from "#app/battle";
import { allMoves } from "#app/data/moves/move";
import type Move from "#app/data/moves/move";
import { DamageAnimPhase } from "#app/phases/damage-anim-phase";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import { MoveEndPhase } from "#app/phases/move-end-phase";
@ -15,8 +16,8 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const fusionFlare = allMoves[Moves.FUSION_FLARE];
const fusionBolt = allMoves[Moves.FUSION_BOLT];
let fusionFlare: Move;
let fusionBolt: Move;
beforeAll(() => {
phaserGame = new Phaser.Game({
@ -29,6 +30,8 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => {
});
beforeEach(() => {
fusionFlare = allMoves[Moves.FUSION_FLARE];
fusionBolt = allMoves[Moves.FUSION_BOLT];
game = new GameManager(phaserGame);
game.override.moveset([fusionFlare.id, fusionBolt.id]);
game.override.startingLevel(1);
@ -45,7 +48,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => {
});
it("FUSION_FLARE should double power of subsequent FUSION_BOLT", async () => {
await game.startBattle([Species.ZEKROM, Species.ZEKROM]);
await game.classicMode.startBattle([Species.ZEKROM, Species.ZEKROM]);
game.move.select(fusionFlare.id, 0, BattlerIndex.ENEMY);
game.move.select(fusionBolt.id, 1, BattlerIndex.ENEMY);
@ -65,7 +68,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => {
}, 20000);
it("FUSION_BOLT should double power of subsequent FUSION_FLARE", async () => {
await game.startBattle([Species.ZEKROM, Species.ZEKROM]);
await game.classicMode.startBattle([Species.ZEKROM, Species.ZEKROM]);
game.move.select(fusionBolt.id, 0, BattlerIndex.ENEMY);
game.move.select(fusionFlare.id, 1, BattlerIndex.ENEMY);
@ -85,7 +88,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => {
}, 20000);
it("FUSION_FLARE should double power of subsequent FUSION_BOLT if a move failed in between", async () => {
await game.startBattle([Species.ZEKROM, Species.ZEKROM]);
await game.classicMode.startBattle([Species.ZEKROM, Species.ZEKROM]);
game.move.select(fusionFlare.id, 0, BattlerIndex.PLAYER);
game.move.select(fusionBolt.id, 1, BattlerIndex.PLAYER);
@ -111,7 +114,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => {
it("FUSION_FLARE should not double power of subsequent FUSION_BOLT if a move succeeded in between", async () => {
game.override.enemyMoveset([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]);
await game.startBattle([Species.ZEKROM, Species.ZEKROM]);
await game.classicMode.startBattle([Species.ZEKROM, Species.ZEKROM]);
game.move.select(fusionFlare.id, 0, BattlerIndex.ENEMY);
game.move.select(fusionBolt.id, 1, BattlerIndex.ENEMY);
@ -156,7 +159,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => {
it("FUSION_FLARE and FUSION_BOLT alternating throughout turn should double power of subsequent moves", async () => {
game.override.enemyMoveset([fusionFlare.id, fusionFlare.id, fusionFlare.id, fusionFlare.id]);
await game.startBattle([Species.ZEKROM, Species.ZEKROM]);
await game.classicMode.startBattle([Species.ZEKROM, Species.ZEKROM]);
const party = game.scene.getPlayerParty();
const enemyParty = game.scene.getEnemyParty();
@ -210,7 +213,7 @@ describe("Moves - Fusion Flare and Fusion Bolt", () => {
it("FUSION_FLARE and FUSION_BOLT alternating throughout turn should double power of subsequent moves if moves are aimed at allies", async () => {
game.override.enemyMoveset([fusionFlare.id, fusionFlare.id, fusionFlare.id, fusionFlare.id]);
await game.startBattle([Species.ZEKROM, Species.ZEKROM]);
await game.classicMode.startBattle([Species.ZEKROM, Species.ZEKROM]);
const party = game.scene.getPlayerParty();
const enemyParty = game.scene.getEnemyParty();

View File

@ -6,12 +6,13 @@ import { Species } from "#enums/species";
import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type Move from "#app/data/moves/move";
describe("Moves - Hard Press", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const moveToCheck = allMoves[Moves.HARD_PRESS];
let moveToCheck: Move;
beforeAll(() => {
phaserGame = new Phaser.Game({
@ -24,6 +25,7 @@ describe("Moves - Hard Press", () => {
});
beforeEach(() => {
moveToCheck = allMoves[Moves.HARD_PRESS];
game = new GameManager(phaserGame);
game.override.battleType("single");
game.override.ability(Abilities.BALL_FETCH);

View File

@ -4,6 +4,7 @@ import { Species } from "#enums/species";
import { Abilities } from "#enums/abilities";
import GameManager from "#test/testUtils/gameManager";
import { allMoves } from "#app/data/moves/move";
import type Move from "#app/data/moves/move";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
@ -12,8 +13,8 @@ describe("Moves - Last Respects", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const move = allMoves[Moves.LAST_RESPECTS];
const basePower = move.power;
let move: Move;
let basePower: number;
beforeAll(() => {
phaserGame = new Phaser.Game({
@ -27,6 +28,8 @@ describe("Moves - Last Respects", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
move = allMoves[Moves.LAST_RESPECTS];
basePower = move.power;
game.override
.battleType("single")
.disableCrits()

View File

@ -13,7 +13,7 @@ describe("Moves - Metronome", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const randomMoveAttr = allMoves[Moves.METRONOME].getAttrs(RandomMoveAttr)[0];
let randomMoveAttr: RandomMoveAttr;
beforeAll(() => {
phaserGame = new Phaser.Game({
@ -26,6 +26,7 @@ describe("Moves - Metronome", () => {
});
beforeEach(() => {
randomMoveAttr = allMoves[Moves.METRONOME].getAttrs(RandomMoveAttr)[0];
game = new GameManager(phaserGame);
game.override
.moveset([Moves.METRONOME, Moves.SPLASH])

View File

@ -3,6 +3,7 @@ import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { allMoves } from "#app/data/moves/move";
import type Move from "#app/data/moves/move";
import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
@ -10,7 +11,7 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite
describe("Moves - Rage Fist", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const move = allMoves[Moves.RAGE_FIST];
let move: Move;
beforeAll(() => {
phaserGame = new Phaser.Game({
@ -23,6 +24,7 @@ describe("Moves - Rage Fist", () => {
});
beforeEach(() => {
move = allMoves[Moves.RAGE_FIST];
game = new GameManager(phaserGame);
game.override
.battleType("single")

View File

@ -4,12 +4,13 @@ import GameManager from "#test/testUtils/gameManager";
import { Species } from "#enums/species";
import { Moves } from "#enums/moves";
import { allMoves } from "#app/data/moves/move";
import type Move from "#app/data/moves/move";
describe("Moves - Retaliate", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const retaliate = allMoves[Moves.RETALIATE];
let retaliate: Move;
beforeAll(() => {
phaserGame = new Phaser.Game({
@ -22,6 +23,7 @@ describe("Moves - Retaliate", () => {
});
beforeEach(() => {
retaliate = allMoves[Moves.RETALIATE];
game = new GameManager(phaserGame);
game.override
.battleType("single")

View File

@ -1,5 +1,6 @@
import { BattlerIndex } from "#app/battle";
import { allMoves, ShellSideArmCategoryAttr } from "#app/data/moves/move";
import type Move from "#app/data/moves/move";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
@ -10,8 +11,8 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite
describe("Moves - Shell Side Arm", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const shellSideArm = allMoves[Moves.SHELL_SIDE_ARM];
const shellSideArmAttr = shellSideArm.getAttrs(ShellSideArmCategoryAttr)[0];
let shellSideArm: Move;
let shellSideArmAttr: ShellSideArmCategoryAttr;
beforeAll(() => {
phaserGame = new Phaser.Game({
@ -24,6 +25,8 @@ describe("Moves - Shell Side Arm", () => {
});
beforeEach(() => {
shellSideArm = allMoves[Moves.SHELL_SIDE_ARM];
shellSideArmAttr = shellSideArm.getAttrs(ShellSideArmCategoryAttr)[0];
game = new GameManager(phaserGame);
game.override
.moveset([Moves.SHELL_SIDE_ARM, Moves.SPLASH])

View File

@ -7,6 +7,7 @@ import { MoveResult } from "#app/field/pokemon";
import GameManager from "#test/testUtils/gameManager";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import type Move from "#app/data/moves/move";
import { Species } from "#enums/species";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
@ -17,7 +18,7 @@ describe("Moves - Spit Up", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const spitUp = allMoves[Moves.SPIT_UP];
let spitUp: Move;
beforeAll(() => {
phaserGame = new Phaser.Game({ type: Phaser.HEADLESS });
@ -28,6 +29,7 @@ describe("Moves - Spit Up", () => {
});
beforeEach(() => {
spitUp = allMoves[Moves.SPIT_UP];
game = new GameManager(phaserGame);
game.override.battleType("single");

View File

@ -1,6 +1,7 @@
import { BattlerIndex } from "#app/battle";
import { Stat } from "#enums/stat";
import { allMoves, TeraMoveCategoryAttr } from "#app/data/moves/move";
import type Move from "#app/data/moves/move";
import { PokemonType } from "#enums/pokemon-type";
import { Abilities } from "#app/enums/abilities";
import { HitResult } from "#app/field/pokemon";
@ -13,13 +14,16 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite
describe("Moves - Tera Blast", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const moveToCheck = allMoves[Moves.TERA_BLAST];
const teraBlastAttr = moveToCheck.getAttrs(TeraMoveCategoryAttr)[0];
let moveToCheck: Move;
let teraBlastAttr: TeraMoveCategoryAttr;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
moveToCheck = allMoves[Moves.TERA_BLAST];
teraBlastAttr = moveToCheck.getAttrs(TeraMoveCategoryAttr)[0];
});
afterEach(() => {

View File

@ -1,6 +1,7 @@
import { allMoves, FlinchAttr, StatStageChangeAttr } from "#app/data/moves/move";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import type Move from "#app/data/moves/move";
import { Species } from "#enums/species";
import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser";
@ -9,14 +10,17 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite
describe("Moves - Triple Arrows", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const tripleArrows = allMoves[Moves.TRIPLE_ARROWS];
const flinchAttr = tripleArrows.getAttrs(FlinchAttr)[0];
const defDropAttr = tripleArrows.getAttrs(StatStageChangeAttr)[0];
let tripleArrows: Move;
let flinchAttr: FlinchAttr;
let defDropAttr: StatStageChangeAttr;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
tripleArrows = allMoves[Moves.TRIPLE_ARROWS];
flinchAttr = tripleArrows.getAttrs(FlinchAttr)[0];
defDropAttr = tripleArrows.getAttrs(StatStageChangeAttr)[0];
});
afterEach(() => {

View File

@ -48,16 +48,6 @@ export async function runMysteryEncounterToEnd(
);
if (isBattle) {
game.onNextPrompt(
"DamageAnimPhase",
Mode.MESSAGE,
() => {
game.setMode(Mode.MESSAGE);
game.endPhase();
},
() => game.isCurrentPhase(CommandPhase),
);
game.onNextPrompt(
"CheckSwitchPhase",
Mode.CONFIRM,

View File

@ -31,7 +31,7 @@ describe("Phases", () => {
it("should start the login phase", async () => {
const loginPhase = new LoginPhase();
scene.unshiftPhase(loginPhase);
await game.phaseInterceptor.run(LoginPhase);
await game.phaseInterceptor.to(LoginPhase);
expect(scene.ui.getMode()).to.equal(Mode.MESSAGE);
});
});
@ -40,7 +40,7 @@ describe("Phases", () => {
it("should start the title phase", async () => {
const titlePhase = new TitlePhase();
scene.unshiftPhase(titlePhase);
await game.phaseInterceptor.run(TitlePhase);
await game.phaseInterceptor.to(TitlePhase);
expect(scene.ui.getMode()).to.equal(Mode.TITLE);
});
});
@ -49,7 +49,7 @@ describe("Phases", () => {
it("should start the unavailable phase", async () => {
const unavailablePhase = new UnavailablePhase();
scene.unshiftPhase(unavailablePhase);
await game.phaseInterceptor.run(UnavailablePhase);
await game.phaseInterceptor.to(UnavailablePhase);
expect(scene.ui.getMode()).to.equal(Mode.UNAVAILABLE);
}, 20000);
});

View File

@ -48,8 +48,8 @@ describe("SelectModifierPhase", () => {
it("should start a select modifier phase", async () => {
initSceneWithoutEncounterPhase(scene, [Species.ABRA, Species.VOLCARONA]);
const selectModifierPhase = new SelectModifierPhase();
scene.pushPhase(selectModifierPhase);
await game.phaseInterceptor.run(SelectModifierPhase);
scene.unshiftPhase(selectModifierPhase);
await game.phaseInterceptor.to(SelectModifierPhase);
expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT);
});

View File

@ -4,11 +4,17 @@ import { PokerogueAccountApi } from "#app/plugins/api/pokerogue-account-api";
import { getApiBaseUrl } from "#test/testUtils/testUtils";
import * as Utils from "#app/utils";
import { http, HttpResponse } from "msw";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { beforeAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { initServerForApiTests } from "#test/testUtils/testFileInitialization";
import type { SetupServerApi } from "msw/node";
const apiBase = getApiBaseUrl();
const accountApi = new PokerogueAccountApi(apiBase);
const { server } = global;
let server: SetupServerApi;
beforeAll(async () => {
server = await initServerForApiTests();
});
afterEach(() => {
server.resetHandlers();

View File

@ -9,11 +9,17 @@ import type {
import { PokerogueAdminApi } from "#app/plugins/api/pokerogue-admin-api";
import { getApiBaseUrl } from "#test/testUtils/testUtils";
import { http, HttpResponse } from "msw";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { beforeAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { initServerForApiTests } from "#test/testUtils/testFileInitialization";
import type { SetupServerApi } from "msw/node";
const apiBase = getApiBaseUrl();
const adminApi = new PokerogueAdminApi(apiBase);
const { server } = global;
let server: SetupServerApi;
beforeAll(async () => {
server = await initServerForApiTests();
});
afterEach(() => {
server.resetHandlers();

View File

@ -2,10 +2,16 @@ import type { TitleStatsResponse } from "#app/@types/PokerogueApi";
import { pokerogueApi } from "#app/plugins/api/pokerogue-api";
import { getApiBaseUrl } from "#test/testUtils/testUtils";
import { http, HttpResponse } from "msw";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { beforeAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { initServerForApiTests } from "#test/testUtils/testFileInitialization";
import type { SetupServerApi } from "msw/node";
const apiBase = getApiBaseUrl();
const { server } = global;
let server: SetupServerApi;
beforeAll(async () => {
server = await initServerForApiTests();
});
afterEach(() => {
server.resetHandlers();

View File

@ -3,11 +3,17 @@ import { PokerogueDailyApi } from "#app/plugins/api/pokerogue-daily-api";
import { getApiBaseUrl } from "#test/testUtils/testUtils";
import { ScoreboardCategory, type RankingEntry } from "#app/ui/daily-run-scoreboard";
import { http, HttpResponse } from "msw";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { beforeAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { initServerForApiTests } from "#test/testUtils/testFileInitialization";
import type { SetupServerApi } from "msw/node";
const apiBase = getApiBaseUrl();
const dailyApi = new PokerogueDailyApi(apiBase);
const { server } = global;
let server: SetupServerApi;
beforeAll(async () => {
server = await initServerForApiTests();
});
afterEach(() => {
server.resetHandlers();

View File

@ -2,11 +2,17 @@ import type { UpdateAllSavedataRequest } from "#app/@types/PokerogueSavedataApi"
import { PokerogueSavedataApi } from "#app/plugins/api/pokerogue-savedata-api";
import { getApiBaseUrl } from "#test/testUtils/testUtils";
import { http, HttpResponse } from "msw";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { beforeAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { initServerForApiTests } from "#test/testUtils/testFileInitialization";
import type { SetupServerApi } from "msw/node";
const apiBase = getApiBaseUrl();
const savedataApi = new PokerogueSavedataApi(apiBase);
const { server } = global;
let server: SetupServerApi;
beforeAll(async () => {
server = await initServerForApiTests();
});
afterEach(() => {
server.resetHandlers();

View File

@ -10,11 +10,17 @@ import { PokerogueSessionSavedataApi } from "#app/plugins/api/pokerogue-session-
import type { SessionSaveData } from "#app/system/game-data";
import { getApiBaseUrl } from "#test/testUtils/testUtils";
import { http, HttpResponse } from "msw";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { beforeAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { initServerForApiTests } from "#test/testUtils/testFileInitialization";
import type { SetupServerApi } from "msw/node";
const apiBase = getApiBaseUrl();
const sessionSavedataApi = new PokerogueSessionSavedataApi(apiBase);
const { server } = global;
let server: SetupServerApi;
beforeAll(async () => {
server = await initServerForApiTests();
});
afterEach(() => {
server.resetHandlers();

View File

@ -6,13 +6,20 @@ import type {
} from "#app/@types/PokerogueSystemSavedataApi";
import { PokerogueSystemSavedataApi } from "#app/plugins/api/pokerogue-system-savedata-api";
import type { SystemSaveData } from "#app/system/game-data";
import { initServerForApiTests } from "#test/testUtils/testFileInitialization";
import { getApiBaseUrl } from "#test/testUtils/testUtils";
import { http, HttpResponse } from "msw";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { SetupServerApi } from "msw/node";
const apiBase = getApiBaseUrl();
const systemSavedataApi = new PokerogueSystemSavedataApi(getApiBaseUrl());
const { server } = global;
let server: SetupServerApi;
beforeAll(async () => {
server = await initServerForApiTests();
});
afterEach(() => {
server.resetHandlers();

View File

@ -1,6 +0,0 @@
import Overrides, { defaultOverrides } from "#app/overrides";
import { expect, test } from "vitest";
test("Overrides are default values", () => {
expect(Overrides).toEqual(defaultOverrides);
});

View File

@ -55,6 +55,9 @@ import TextInterceptor from "#test/testUtils/TextInterceptor";
import { AES, enc } from "crypto-js";
import fs from "node:fs";
import { expect, vi } from "vitest";
import { globalScene } from "#app/global-scene";
import type StarterSelectUiHandler from "#app/ui/starter-select-ui-handler";
import { MockFetch } from "#test/testUtils/mocks/mockFetch";
/**
* Class to manage the game state and transitions between phases.
@ -84,10 +87,34 @@ export default class GameManager {
ErrorInterceptor.getInstance().clear();
BattleScene.prototype.randBattleSeedInt = (range, min = 0) => min + range - 1; // This simulates a max roll
this.gameWrapper = new GameWrapper(phaserGame, bypassLogin);
this.scene = new BattleScene();
let firstTimeScene = false;
if (globalScene) {
this.scene = globalScene;
} else {
this.scene = new BattleScene();
this.gameWrapper.setScene(this.scene);
firstTimeScene = true;
}
this.phaseInterceptor = new PhaseInterceptor(this.scene);
if (!firstTimeScene) {
this.scene.reset(false, true);
(this.scene.ui.handlers[Mode.STARTER_SELECT] as StarterSelectUiHandler).clearStarterPreferences();
this.scene.clearAllPhases();
// Must be run after phase interceptor has been initialized.
this.scene.pushPhase(new LoginPhase());
this.scene.pushPhase(new TitlePhase());
this.scene.shiftPhase();
this.gameWrapper.scene = this.scene;
}
this.textInterceptor = new TextInterceptor(this.scene);
this.gameWrapper.setScene(this.scene);
this.override = new OverridesHelper(this);
this.move = new MoveHelper(this);
this.classicMode = new ClassicModeHelper(this);
@ -96,9 +123,12 @@ export default class GameManager {
this.settings = new SettingsHelper(this);
this.reload = new ReloadHelper(this);
this.modifiers = new ModifierHelper(this);
this.override.sanitizeOverrides();
// Disables Mystery Encounters on all tests (can be overridden at test level)
this.override.mysteryEncounterChance(0);
global.fetch = vi.fn(MockFetch) as any;
}
/**

View File

@ -6,11 +6,11 @@ import Pokemon from "#app/field/pokemon";
import * as Utils from "#app/utils";
import { blobToString } from "#test/testUtils/gameManagerUtils";
import { MockClock } from "#test/testUtils/mocks/mockClock";
import mockConsoleLog from "#test/testUtils/mocks/mockConsoleLog";
import { MockConsoleLog } from "#test/testUtils/mocks/mockConsoleLog";
import { MockFetch } from "#test/testUtils/mocks/mockFetch";
import MockLoader from "#test/testUtils/mocks/mockLoader";
import mockLocalStorage from "#test/testUtils/mocks/mockLocalStorage";
import MockImage from "#test/testUtils/mocks/mocksContainer/mockImage";
import { mockLocalStorage } from "#test/testUtils/mocks/mockLocalStorage";
import { MockImage } from "#test/testUtils/mocks/mocksContainer/mockImage";
import MockTextureManager from "#test/testUtils/mocks/mockTextureManager";
import fs from "node:fs";
import Phaser from "phaser";
@ -27,18 +27,6 @@ import UpdateList = Phaser.GameObjects.UpdateList;
import { version } from "../../package.json";
import { MockTimedEventManager } from "./mocks/mockTimedEventManager";
Object.defineProperty(window, "localStorage", {
value: mockLocalStorage(),
});
Object.defineProperty(window, "console", {
value: mockConsoleLog(false),
});
BBCodeText.prototype.destroy = () => null;
BBCodeText.prototype.resize = () => null;
InputText.prototype.setElement = () => null;
InputText.prototype.resize = () => null;
Phaser.GameObjects.Image = MockImage;
window.URL.createObjectURL = (blob: Blob) => {
blobToString(blob).then((data: string) => {
localStorage.setItem("toExport", data);
@ -53,25 +41,6 @@ window.matchMedia = () => ({
matches: false,
});
/**
* Sets this object's position relative to another object with a given offset
* @param guideObject {@linkcode Phaser.GameObjects.GameObject} to base the position off of
* @param x The relative x position
* @param y The relative y position
*/
const setPositionRelative = function (guideObject: any, x: number, y: number) {
const offsetX = guideObject.width * (-0.5 + (0.5 - guideObject.originX));
const offsetY = guideObject.height * (-0.5 + (0.5 - guideObject.originY));
this.setPosition(guideObject.x + offsetX + x, guideObject.y + offsetY + y);
};
Phaser.GameObjects.Container.prototype.setPositionRelative = setPositionRelative;
Phaser.GameObjects.Sprite.prototype.setPositionRelative = setPositionRelative;
Phaser.GameObjects.Image.prototype.setPositionRelative = setPositionRelative;
Phaser.GameObjects.NineSlice.prototype.setPositionRelative = setPositionRelative;
Phaser.GameObjects.Text.prototype.setPositionRelative = setPositionRelative;
Phaser.GameObjects.Rectangle.prototype.setPositionRelative = setPositionRelative;
export default class GameWrapper {
public game: Phaser.Game;
public scene: BattleScene;

View File

@ -6,7 +6,7 @@ import type { GameModes } from "#app/game-mode";
import { getGameMode } from "#app/game-mode";
import type { ModifierOverride } from "#app/modifier/modifier-type";
import type { BattleStyle } from "#app/overrides";
import Overrides from "#app/overrides";
import Overrides, { defaultOverrides } from "#app/overrides";
import type { Unlockables } from "#app/system/unlockables";
import { Biome } from "#enums/biome";
import { Moves } from "#enums/moves";
@ -15,8 +15,9 @@ import type { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { Species } from "#enums/species";
import { StatusEffect } from "#enums/status-effect";
import type { WeatherType } from "#enums/weather-type";
import { vi } from "vitest";
import { expect, vi } from "vitest";
import { GameManagerHelper } from "./gameManagerHelper";
import { shiftCharCodes } from "#app/utils";
/**
* Helper to handle overrides in tests
@ -226,12 +227,7 @@ export class OverridesHelper extends GameManagerHelper {
* @returns `this`
*/
public disableTrainerWaves(): this {
const realFn = getGameMode;
vi.spyOn(GameMode, "getGameMode").mockImplementation((gameMode: GameModes) => {
const mode = realFn(gameMode);
mode.hasTrainers = false;
return mode;
});
vi.spyOn(Overrides, "DISABLE_STANDARD_TRAINERS_OVERRIDE", "get").mockReturnValue(true);
this.log("Standard trainer waves are disabled!");
return this;
}
@ -263,11 +259,8 @@ export class OverridesHelper extends GameManagerHelper {
* @returns `this`
*/
public seed(seed: string): this {
vi.spyOn(this.game.scene, "resetSeed").mockImplementation(() => {
this.game.scene.waveSeed = seed;
Phaser.Math.RND.sow([seed]);
this.game.scene.rngCounter = 0;
});
// Shift the seed here with a negative wave number, to compensate for `resetSeed()` shifting the seed itself.
this.game.scene.setSeed(shiftCharCodes(seed, (this.game.scene.currentBattle?.waveIndex ?? 0) * -1));
this.game.scene.resetSeed();
this.log(`Seed set to "${seed}"!`);
return this;
@ -539,4 +532,14 @@ export class OverridesHelper extends GameManagerHelper {
private log(...params: any[]) {
console.log("Overrides:", ...params);
}
public sanitizeOverrides(): void {
for (const key of Object.keys(defaultOverrides)) {
if (Overrides[key] !== defaultOverrides[key]) {
vi.spyOn(Overrides, key as any, "get").mockReturnValue(defaultOverrides[key]);
}
}
expect(Overrides).toEqual(defaultOverrides);
this.log("Sanitizing all overrides!");
}
}

View File

@ -0,0 +1,41 @@
import { expect } from "vitest";
/**
* Whether or not it is currently the first time running this manager.
*/
let firstTime = true;
/**
* The list of listeners that were present during the first time this manager is run.
* These initial listeners are needed throughout the entire test suite, so we never remove them.
*/
const initialListeners: NodeJS.MessageListener[] = [];
/**
* The current listener that is only needed for the current test file.
* We plan to delete it during the next test file, when it is no longer needed.
*/
let currentListener: NodeJS.MessageListener | null;
export function manageListeners() {
if (firstTime) {
initialListeners.push(...process.listeners("message"));
} else {
expect(process.listeners("message").length).toBeLessThan(7);
// Remove the listener that was used during the previous test file
if (currentListener) {
process.removeListener("message", currentListener);
currentListener = null;
}
// Find the new listener that is being used for the current test file
process.listeners("message").forEach(fn => {
if (!initialListeners.includes(fn)) {
currentListener = fn;
}
});
}
firstTime = false;
}

View File

@ -1,82 +1,80 @@
const MockConsoleLog = (_logDisabled = false, _phaseText = false) => {
let logs: any[] = [];
const logDisabled: boolean = _logDisabled;
const phaseText: boolean = _phaseText;
const originalLog = console.log;
const originalError = console.error;
const originalDebug = console.debug;
const originalWarn = console.warn;
const notified: any[] = [];
const originalLog = console.log;
const originalError = console.error;
const originalDebug = console.debug;
const originalWarn = console.warn;
const blacklist = ["Phaser", "variant icon does not exist", 'Texture "%s" not found'];
const whitelist = ["Phase"];
const blacklist = ["Phaser", "variant icon does not exist", 'Texture "%s" not found'];
const whitelist = ["Phase"];
return {
log(...args) {
const argsStr = this.getStr(args);
logs.push(argsStr);
if (logDisabled && !phaseText) {
return;
}
if ((phaseText && !whitelist.some(b => argsStr.includes(b))) || blacklist.some(b => argsStr.includes(b))) {
return;
}
originalLog(args);
},
error(...args) {
const argsStr = this.getStr(args);
logs.push(argsStr);
originalError(args); // Appelle le console.error originel
},
debug(...args) {
const argsStr = this.getStr(args);
logs.push(argsStr);
if (logDisabled && !phaseText) {
return;
}
if (!whitelist.some(b => argsStr.includes(b)) || blacklist.some(b => argsStr.includes(b))) {
return;
}
originalDebug(args);
},
warn(...args) {
const argsStr = this.getStr(args);
logs.push(args);
if (logDisabled && !phaseText) {
return;
}
if (!whitelist.some(b => argsStr.includes(b)) || blacklist.some(b => argsStr.includes(b))) {
return;
}
originalWarn(args);
},
notify(msg) {
originalLog(msg);
notified.push(msg);
},
getLogs() {
return logs;
},
clearLogs() {
logs = [];
},
getStr(...args) {
return args
.map(arg => {
if (typeof arg === "object" && arg !== null) {
// Handle objects including arrays
return JSON.stringify(arg, (_key, value) => (typeof value === "bigint" ? value.toString() : value));
}
if (typeof arg === "bigint") {
// Handle BigInt values
return arg.toString();
}
// Handle all other types
export class MockConsoleLog {
constructor(
private logDisabled = false,
private phaseText = false,
) {}
private logs: any[] = [];
private notified: any[] = [];
public log(...args) {
const argsStr = this.getStr(args);
this.logs.push(argsStr);
if (this.logDisabled && !this.phaseText) {
return;
}
if ((this.phaseText && !whitelist.some(b => argsStr.includes(b))) || blacklist.some(b => argsStr.includes(b))) {
return;
}
originalLog(args);
}
public error(...args) {
const argsStr = this.getStr(args);
this.logs.push(argsStr);
originalError(args); // Appelle le console.error originel
}
public debug(...args) {
const argsStr = this.getStr(args);
this.logs.push(argsStr);
if (this.logDisabled && !this.phaseText) {
return;
}
if (!whitelist.some(b => argsStr.includes(b)) || blacklist.some(b => argsStr.includes(b))) {
return;
}
originalDebug(args);
}
warn(...args) {
const argsStr = this.getStr(args);
this.logs.push(args);
if (this.logDisabled && !this.phaseText) {
return;
}
if (!whitelist.some(b => argsStr.includes(b)) || blacklist.some(b => argsStr.includes(b))) {
return;
}
originalWarn(args);
}
notify(msg) {
originalLog(msg);
this.notified.push(msg);
}
getLogs() {
return this.logs;
}
clearLogs() {
this.logs = [];
}
getStr(...args) {
return args
.map(arg => {
if (typeof arg === "object" && arg !== null) {
// Handle objects including arrays
return JSON.stringify(arg, (_key, value) => (typeof value === "bigint" ? value.toString() : value));
}
if (typeof arg === "bigint") {
// Handle BigInt values
return arg.toString();
})
.join(";");
},
};
};
export default MockConsoleLog;
}
return arg.toString();
})
.join(";");
}
}

View File

@ -0,0 +1,26 @@
/**
* A minimal stub object to mock HTMLCanvasElement
*/
export const mockCanvas: any = {
width: 0,
getContext() {
return mockContext;
},
};
/**
* A minimal stub object to mock CanvasRenderingContext2D
*/
export const mockContext: any = {
font: "",
measureText: () => {
return {};
},
save: () => {},
scale: () => {},
clearRect: () => {},
fillRect: () => {},
fillText: () => {},
getImageData: () => {},
canvas: mockCanvas,
restore: () => {},
};

View File

@ -1,4 +1,4 @@
const mockLocalStorage = () => {
export const mockLocalStorage = () => {
let store = {} as Storage;
return {
@ -23,5 +23,3 @@ const mockLocalStorage = () => {
},
};
};
export default mockLocalStorage;

View File

@ -1,5 +1,5 @@
import MockContainer from "#test/testUtils/mocks/mocksContainer/mockContainer";
import MockImage from "#test/testUtils/mocks/mocksContainer/mockImage";
import { MockImage } from "#test/testUtils/mocks/mocksContainer/mockImage";
import MockNineslice from "#test/testUtils/mocks/mocksContainer/mockNineslice";
import MockPolygon from "#test/testUtils/mocks/mocksContainer/mockPolygon";
import MockRectangle from "#test/testUtils/mocks/mocksContainer/mockRectangle";

View File

@ -215,4 +215,10 @@ export default class MockContainer implements MockGameObject {
}
disableInteractive = () => null;
each(method) {
for (const item of this.list) {
method(item);
}
}
}

View File

@ -1,6 +1,6 @@
import MockContainer from "#test/testUtils/mocks/mocksContainer/mockContainer";
export default class MockImage extends MockContainer {
export class MockImage extends MockContainer {
private texture;
constructor(textureManager, x, y, texture) {

View File

@ -1,3 +1,4 @@
import { off } from "process";
import type { MockGameObject } from "../mockGameObject";
export default class MockRectangle implements MockGameObject {
@ -72,4 +73,6 @@ export default class MockRectangle implements MockGameObject {
setScale(_scale) {
// return this.phaserText.setScale(scale);
}
off() {}
}

View File

@ -0,0 +1,117 @@
import { SESSION_ID_COOKIE_NAME } from "#app/constants";
import { initLoggedInUser } from "#app/account";
import { initAbilities } from "#app/data/ability";
import { initBiomes } from "#app/data/balance/biomes";
import { initEggMoves } from "#app/data/balance/egg-moves";
import { initPokemonPrevolutions } from "#app/data/balance/pokemon-evolutions";
import { initMoves } from "#app/data/moves/move";
import { initMysteryEncounters } from "#app/data/mystery-encounters/mystery-encounters";
import { initPokemonForms } from "#app/data/pokemon-forms";
import { initSpecies } from "#app/data/pokemon-species";
import { initAchievements } from "#app/system/achv";
import { initVouchers } from "#app/system/voucher";
import { initStatsKeys } from "#app/ui/game-stats-ui-handler";
import { setCookie } from "#app/utils";
import { blobToString } from "#test/testUtils/gameManagerUtils";
import { MockConsoleLog } from "#test/testUtils/mocks/mockConsoleLog";
import { mockContext } from "#test/testUtils/mocks/mockContextCanvas";
import { mockLocalStorage } from "#test/testUtils/mocks/mockLocalStorage";
import { MockImage } from "#test/testUtils/mocks/mocksContainer/mockImage";
import Phaser from "phaser";
import InputText from "phaser3-rex-plugins/plugins/inputtext";
import BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext";
import { manageListeners } from "./listenersManager";
let wasInitialized = false;
/**
* An initialization function that is run at the beginning of every test file (via `beforeAll()`).
*/
export function initTestFile() {
// Set the timezone to UTC for tests.
process.env.TZ = "UTC";
Object.defineProperty(window, "localStorage", {
value: mockLocalStorage(),
});
Object.defineProperty(window, "console", {
value: new MockConsoleLog(false),
});
Object.defineProperty(document, "fonts", {
writable: true,
value: {
add: () => {},
},
});
BBCodeText.prototype.destroy = () => null;
// @ts-ignore
BBCodeText.prototype.resize = () => null;
InputText.prototype.setElement = () => null as any;
InputText.prototype.resize = () => null as any;
Phaser.GameObjects.Image = MockImage as any;
window.URL.createObjectURL = (blob: Blob) => {
blobToString(blob).then((data: string) => {
localStorage.setItem("toExport", data);
});
return null as any;
};
navigator.getGamepads = () => [];
setCookie(SESSION_ID_COOKIE_NAME, "fake_token");
window.matchMedia = () =>
({
matches: false,
}) as any;
/**
* Sets this object's position relative to another object with a given offset
* @param guideObject {@linkcode Phaser.GameObjects.GameObject} to base the position off of
* @param x The relative x position
* @param y The relative y position
*/
const setPositionRelative = function (guideObject: any, x: number, y: number) {
const offsetX = guideObject.width * (-0.5 + (0.5 - guideObject.originX));
const offsetY = guideObject.height * (-0.5 + (0.5 - guideObject.originY));
this.setPosition(guideObject.x + offsetX + x, guideObject.y + offsetY + y);
};
Phaser.GameObjects.Container.prototype.setPositionRelative = setPositionRelative;
Phaser.GameObjects.Sprite.prototype.setPositionRelative = setPositionRelative;
Phaser.GameObjects.Image.prototype.setPositionRelative = setPositionRelative;
Phaser.GameObjects.NineSlice.prototype.setPositionRelative = setPositionRelative;
Phaser.GameObjects.Text.prototype.setPositionRelative = setPositionRelative;
Phaser.GameObjects.Rectangle.prototype.setPositionRelative = setPositionRelative;
HTMLCanvasElement.prototype.getContext = () => mockContext;
// Initialize all of these things if and only if they have not been initialized yet
// initSpecies();
if (!wasInitialized) {
wasInitialized = true;
initVouchers();
initAchievements();
initStatsKeys();
initPokemonPrevolutions();
initBiomes();
initEggMoves();
initPokemonForms();
initSpecies();
initMoves();
initAbilities();
initLoggedInUser();
initMysteryEncounters();
}
manageListeners();
}
/**
* Closes the current mock server and initializes a new mock server.
* This is run at the beginning of every API test file.
*/
export async function initServerForApiTests() {
global.server?.close();
const { setupServer } = await import("msw/node");
global.server = setupServer();
global.server.listen({ onUnhandledRequest: "error" });
return global.server;
}

View File

@ -14,8 +14,9 @@ import { initVouchers } from "#app/system/voucher";
import { initStatsKeys } from "#app/ui/game-stats-ui-handler";
import { afterAll, beforeAll, vi } from "vitest";
import { initTestFile } from "./testUtils/testFileInitialization";
/** Set the timezone to UTC for tests. */
process.env.TZ = "UTC";
/** Mock the override import to always return default values, ignoring any custom overrides. */
vi.mock("#app/overrides", async importOriginal => {
@ -63,28 +64,10 @@ vi.mock("i18next", async importOriginal => {
return await importOriginal();
});
initVouchers();
initAchievements();
initStatsKeys();
initPokemonPrevolutions();
initBiomes();
initEggMoves();
initPokemonForms();
initSpecies();
initMoves();
initAbilities();
initLoggedInUser();
initMysteryEncounters();
global.testFailed = false;
beforeAll(() => {
Object.defineProperty(document, "fonts", {
writable: true,
value: {
add: () => {},
},
});
initTestFile();
});
afterAll(() => {

View File

@ -1,20 +1,31 @@
import { defineProject } from "vitest/config";
import { defaultConfig } from "./vite.config";
import { BaseSequencer, type TestSpecification } from "vitest/node";
function getTestOrder(testName: string): number {
if (testName.includes("battle-scene.test.ts")) {
return 1;
}
if (testName.includes("inputs.test.ts")) {
return 2;
}
return 3;
}
export default defineProject(({ mode }) => ({
...defaultConfig,
test: {
testTimeout: 20000,
setupFiles: ["./test/fontFace.setup.ts", "./test/vitest.setup.ts"],
server: {
deps: {
inline: ["vitest-canvas-mock"],
//@ts-ignore
optimizer: {
web: {
include: ["vitest-canvas-mock"],
},
},
sequence: {
sequencer: class CustomSequencer extends BaseSequencer {
async sort(files: TestSpecification[]) {
// use default sorting at first.
files = await super.sort(files);
// Except, forcibly reorder
return files.sort((a, b) => getTestOrder(a.moduleId) - getTestOrder(b.moduleId));
}
},
},
environment: "jsdom" as const,
@ -34,7 +45,6 @@ export default defineProject(({ mode }) => ({
},
name: "main",
include: ["./test/**/*.{test,spec}.ts"],
exclude: ["./test/pre.test.ts"],
},
esbuild: {
pure: mode === "production" ? ["console.log"] : [],