Merge beta
1
.github/workflows/tests.yml
vendored
@ -11,6 +11,7 @@ on:
|
|||||||
- beta
|
- beta
|
||||||
merge_group:
|
merge_group:
|
||||||
types: [checks_requested]
|
types: [checks_requested]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-path-change-filter:
|
check-path-change-filter:
|
||||||
|
@ -11,14 +11,14 @@ _cfg: &cfg
|
|||||||
|
|
||||||
ls:
|
ls:
|
||||||
<<: *cfg
|
<<: *cfg
|
||||||
src:
|
src: &src
|
||||||
<<: *cfg
|
<<: *cfg
|
||||||
.dir: kebab-case | regex:@types
|
.dir: kebab-case | regex:@types
|
||||||
.js: exists:0
|
.js: exists:0
|
||||||
src/system/version-migration/versions:
|
src/system/version-migration/versions:
|
||||||
.ts: snake_case
|
.ts: snake_case
|
||||||
<<: *cfg
|
<<: *cfg
|
||||||
|
test: *src
|
||||||
ignore:
|
ignore:
|
||||||
- node_modules
|
- node_modules
|
||||||
- .vscode
|
- .vscode
|
||||||
|
@ -177,9 +177,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Overrides to prevent unused import removal inside `overrides.ts` and enums files (for TSDoc linkcodes)
|
// Overrides to prevent unused import removal inside `overrides.ts` and enums files (for TSDoc linkcodes),
|
||||||
|
// as well as in all TS files in `scripts/` (which are assumed to be boilerplate templates).
|
||||||
{
|
{
|
||||||
"includes": ["**/src/overrides.ts", "**/src/enums/**/*"],
|
"includes": ["**/src/overrides.ts", "**/src/enums/**/*", "**/scripts/**/*.ts"],
|
||||||
"linter": {
|
"linter": {
|
||||||
"rules": {
|
"rules": {
|
||||||
"correctness": {
|
"correctness": {
|
||||||
@ -189,7 +190,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"includes": ["**/src/overrides.ts"],
|
"includes": ["**/src/overrides.ts", "**/scripts/**/*.ts"],
|
||||||
"linter": {
|
"linter": {
|
||||||
"rules": {
|
"rules": {
|
||||||
"style": {
|
"style": {
|
||||||
|
11
package.json
@ -17,7 +17,7 @@
|
|||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"eslint": "eslint --fix .",
|
"eslint": "eslint --fix .",
|
||||||
"eslint-ci": "eslint .",
|
"eslint-ci": "eslint .",
|
||||||
"biome": "biome check --write --changed --no-errors-on-unmatched",
|
"biome": "biome check --write --changed --no-errors-on-unmatched --diagnostic-level=error",
|
||||||
"biome-ci": "biome ci --diagnostic-level=error --reporter=github --no-errors-on-unmatched",
|
"biome-ci": "biome ci --diagnostic-level=error --reporter=github --no-errors-on-unmatched",
|
||||||
"docs": "typedoc",
|
"docs": "typedoc",
|
||||||
"depcruise": "depcruise src test",
|
"depcruise": "depcruise src test",
|
||||||
@ -30,18 +30,19 @@
|
|||||||
"@biomejs/biome": "2.0.0",
|
"@biomejs/biome": "2.0.0",
|
||||||
"@ls-lint/ls-lint": "2.3.1",
|
"@ls-lint/ls-lint": "2.3.1",
|
||||||
"@types/jsdom": "^21.1.7",
|
"@types/jsdom": "^21.1.7",
|
||||||
"@types/node": "^22.16.3",
|
"@types/node": "^22.16.5",
|
||||||
"@vitest/coverage-istanbul": "^3.2.4",
|
"@vitest/coverage-istanbul": "^3.2.4",
|
||||||
|
"@vitest/expect": "^3.2.4",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
"dependency-cruiser": "^16.10.4",
|
"dependency-cruiser": "^16.10.4",
|
||||||
"inquirer": "^12.7.0",
|
"inquirer": "^12.8.2",
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"lefthook": "^1.12.2",
|
"lefthook": "^1.12.2",
|
||||||
"msw": "^2.10.4",
|
"msw": "^2.10.4",
|
||||||
"phaser3spectorjs": "^0.0.8",
|
"phaser3spectorjs": "^0.0.8",
|
||||||
"typedoc": "^0.28.7",
|
"typedoc": "^0.28.8",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vite": "^6.3.5",
|
"vite": "^7.0.6",
|
||||||
"vite-tsconfig-paths": "^5.1.4",
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
"vitest": "^3.2.4",
|
"vitest": "^3.2.4",
|
||||||
"vitest-canvas-mock": "^0.3.3"
|
"vitest-canvas-mock": "^0.3.3"
|
||||||
|
921
pnpm-lock.yaml
188
public/images/statuses_tl.json
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
{
|
||||||
|
"textures": [
|
||||||
|
{
|
||||||
|
"image": "statuses_tl.png",
|
||||||
|
"format": "RGBA8888",
|
||||||
|
"size": {
|
||||||
|
"w": 22,
|
||||||
|
"h": 64
|
||||||
|
},
|
||||||
|
"scale": 1,
|
||||||
|
"frames": [
|
||||||
|
{
|
||||||
|
"filename": "pokerus",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 22,
|
||||||
|
"h": 8
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 22,
|
||||||
|
"h": 8
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 22,
|
||||||
|
"h": 8
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "burn",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 20,
|
||||||
|
"h": 8
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 20,
|
||||||
|
"h": 8
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 8,
|
||||||
|
"w": 20,
|
||||||
|
"h": 8
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "faint",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 20,
|
||||||
|
"h": 8
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 20,
|
||||||
|
"h": 8
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 16,
|
||||||
|
"w": 20,
|
||||||
|
"h": 8
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "freeze",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 20,
|
||||||
|
"h": 8
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 20,
|
||||||
|
"h": 8
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 24,
|
||||||
|
"w": 20,
|
||||||
|
"h": 8
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "paralysis",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 20,
|
||||||
|
"h": 8
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 20,
|
||||||
|
"h": 8
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 32,
|
||||||
|
"w": 20,
|
||||||
|
"h": 8
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "poison",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 20,
|
||||||
|
"h": 8
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 20,
|
||||||
|
"h": 8
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 40,
|
||||||
|
"w": 20,
|
||||||
|
"h": 8
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "sleep",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 20,
|
||||||
|
"h": 8
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 20,
|
||||||
|
"h": 8
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 48,
|
||||||
|
"w": 20,
|
||||||
|
"h": 8
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "toxic",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 20,
|
||||||
|
"h": 8
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 20,
|
||||||
|
"h": 8
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 56,
|
||||||
|
"w": 20,
|
||||||
|
"h": 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"meta": {
|
||||||
|
"app": "https://www.codeandweb.com/texturepacker",
|
||||||
|
"version": "3.0",
|
||||||
|
"smartupdate": "$TexturePacker:SmartUpdate:37686e85605d17b806f22d43081c1139:70535ffee63ba61b3397d8470c2c8982:e6649238c018d3630e55681417c698ca$"
|
||||||
|
}
|
||||||
|
}
|
BIN
public/images/statuses_tl.png
Normal file
After Width: | Height: | Size: 419 B |
440
public/images/types_tl.json
Normal file
@ -0,0 +1,440 @@
|
|||||||
|
{
|
||||||
|
"textures": [
|
||||||
|
{
|
||||||
|
"image": "types_tl.png",
|
||||||
|
"format": "RGBA8888",
|
||||||
|
"size": {
|
||||||
|
"w": 32,
|
||||||
|
"h": 280
|
||||||
|
},
|
||||||
|
"scale": 1,
|
||||||
|
"frames": [
|
||||||
|
{
|
||||||
|
"filename": "unknown",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "bug",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 14,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "dark",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 28,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "dragon",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 42,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "electric",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 56,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "fairy",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 70,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "fighting",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 84,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "fire",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 98,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "flying",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 112,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "ghost",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 126,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "grass",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 140,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "ground",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 154,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "ice",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 168,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "normal",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 182,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "poison",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 196,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "psychic",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 210,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "rock",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 224,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "steel",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 238,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "water",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 252,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "stellar",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 266,
|
||||||
|
"w": 32,
|
||||||
|
"h": 14
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"meta": {
|
||||||
|
"app": "https://www.codeandweb.com/texturepacker",
|
||||||
|
"version": "3.0",
|
||||||
|
"smartupdate": "$TexturePacker:SmartUpdate:f14cf47d9a8f1d40c8e03aa6ba00fff3:6fc4227b57a95d429a1faad4280f7ec8:5961efbfbf4c56b8745347e7a663a32f$"
|
||||||
|
}
|
||||||
|
}
|
BIN
public/images/types_tl.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
public/images/ui/legacy/party_bg_double_manage.png
Normal file
After Width: | Height: | Size: 431 B |
62
public/images/ui/legacy/party_discard.json
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"textures": [
|
||||||
|
{
|
||||||
|
"image": "party_discard.png",
|
||||||
|
"format": "RGBA8888",
|
||||||
|
"size": {
|
||||||
|
"w": 75,
|
||||||
|
"h": 50
|
||||||
|
},
|
||||||
|
"scale": 1,
|
||||||
|
"frames": [
|
||||||
|
{
|
||||||
|
"filename": "normal",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 75,
|
||||||
|
"h": 25
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 75,
|
||||||
|
"h": 25
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 75,
|
||||||
|
"h": 25
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "selected",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 75,
|
||||||
|
"h": 25
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 75,
|
||||||
|
"h": 25
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 25,
|
||||||
|
"w": 75,
|
||||||
|
"h": 25
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"meta": {
|
||||||
|
"app": "https://www.codeandweb.com/texturepacker",
|
||||||
|
"version": "3.0",
|
||||||
|
"smartupdate": "$TexturePacker:SmartUpdate:17219773dfffd6b1204d988fea3f9462:1127ad21d64bc7ebb9df4fc28f3d2d39:7ad46e8fb4648c3d3d84a746ecb371ea$"
|
||||||
|
}
|
||||||
|
}
|
BIN
public/images/ui/legacy/party_discard.png
Normal file
After Width: | Height: | Size: 346 B |
62
public/images/ui/legacy/party_transfer.json
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"textures": [
|
||||||
|
{
|
||||||
|
"image": "party_transfer.png",
|
||||||
|
"format": "RGBA8888",
|
||||||
|
"size": {
|
||||||
|
"w": 75,
|
||||||
|
"h": 50
|
||||||
|
},
|
||||||
|
"scale": 1,
|
||||||
|
"frames": [
|
||||||
|
{
|
||||||
|
"filename": "normal",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 75,
|
||||||
|
"h": 25
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 75,
|
||||||
|
"h": 25
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 75,
|
||||||
|
"h": 25
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "selected",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 75,
|
||||||
|
"h": 25
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 75,
|
||||||
|
"h": 25
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 25,
|
||||||
|
"w": 75,
|
||||||
|
"h": 25
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"meta": {
|
||||||
|
"app": "https://www.codeandweb.com/texturepacker",
|
||||||
|
"version": "3.0",
|
||||||
|
"smartupdate": "$TexturePacker:SmartUpdate:17219773dfffd6b1204d988fea3f9462:1127ad21d64bc7ebb9df4fc28f3d2d39:7ad46e8fb4648c3d3d84a746ecb371ea$"
|
||||||
|
}
|
||||||
|
}
|
BIN
public/images/ui/legacy/party_transfer.png
Normal file
After Width: | Height: | Size: 366 B |
BIN
public/images/ui/party_bg_double_manage.png
Normal file
After Width: | Height: | Size: 837 B |
62
public/images/ui/party_discard.json
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"textures": [
|
||||||
|
{
|
||||||
|
"image": "party_discard.png",
|
||||||
|
"format": "RGBA8888",
|
||||||
|
"size": {
|
||||||
|
"w": 75,
|
||||||
|
"h": 50
|
||||||
|
},
|
||||||
|
"scale": 1,
|
||||||
|
"frames": [
|
||||||
|
{
|
||||||
|
"filename": "normal",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 75,
|
||||||
|
"h": 25
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 75,
|
||||||
|
"h": 25
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 75,
|
||||||
|
"h": 25
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "selected",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 75,
|
||||||
|
"h": 25
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 75,
|
||||||
|
"h": 25
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 25,
|
||||||
|
"w": 75,
|
||||||
|
"h": 25
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"meta": {
|
||||||
|
"app": "https://www.codeandweb.com/texturepacker",
|
||||||
|
"version": "3.0",
|
||||||
|
"smartupdate": "$TexturePacker:SmartUpdate:17219773dfffd6b1204d988fea3f9462:1127ad21d64bc7ebb9df4fc28f3d2d39:7ad46e8fb4648c3d3d84a746ecb371ea$"
|
||||||
|
}
|
||||||
|
}
|
BIN
public/images/ui/party_discard.png
Normal file
After Width: | Height: | Size: 386 B |
62
public/images/ui/party_transfer.json
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"textures": [
|
||||||
|
{
|
||||||
|
"image": "party_transfer.png",
|
||||||
|
"format": "RGBA8888",
|
||||||
|
"size": {
|
||||||
|
"w": 75,
|
||||||
|
"h": 50
|
||||||
|
},
|
||||||
|
"scale": 1,
|
||||||
|
"frames": [
|
||||||
|
{
|
||||||
|
"filename": "normal",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 75,
|
||||||
|
"h": 25
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 75,
|
||||||
|
"h": 25
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 75,
|
||||||
|
"h": 25
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "selected",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 75,
|
||||||
|
"h": 25
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 75,
|
||||||
|
"h": 25
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 25,
|
||||||
|
"w": 75,
|
||||||
|
"h": 25
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"meta": {
|
||||||
|
"app": "https://www.codeandweb.com/texturepacker",
|
||||||
|
"version": "3.0",
|
||||||
|
"smartupdate": "$TexturePacker:SmartUpdate:17219773dfffd6b1204d988fea3f9462:1127ad21d64bc7ebb9df4fc28f3d2d39:7ad46e8fb4648c3d3d84a746ecb371ea$"
|
||||||
|
}
|
||||||
|
}
|
BIN
public/images/ui/party_transfer.png
Normal file
After Width: | Height: | Size: 403 B |
@ -1 +1 @@
|
|||||||
Subproject commit e2fbba17ea7a96068970ea98a8a84ed3e25b6f07
|
Subproject commit 7898c0018a70601a6ead76c9dd497ff966cc2e2a
|
@ -17,15 +17,20 @@ const version = "2.0.1";
|
|||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
const projectRoot = path.join(__dirname, "..", "..");
|
const projectRoot = path.join(__dirname, "..", "..");
|
||||||
const boilerplateFilePath = path.join(__dirname, "test-boilerplate.ts");
|
|
||||||
const choices = [
|
const choices = /** @type {const} */ (["Move", "Ability", "Item", "Reward", "Mystery Encounter", "Utils", "UI"]);
|
||||||
{ label: "Move", dir: "moves" },
|
/** @typedef {choices[number]} choiceType */
|
||||||
{ label: "Ability", dir: "abilities" },
|
|
||||||
{ label: "Item", dir: "items" },
|
/** @satisfies {{[k in choiceType]: string}} */
|
||||||
{ label: "Mystery Encounter", dir: "mystery-encounter/encounters" },
|
const choicesToDirs = /** @type {const} */ ({
|
||||||
{ label: "Utils", dir: "utils" },
|
Move: "moves",
|
||||||
{ label: "UI", dir: "ui" },
|
Ability: "abilities",
|
||||||
];
|
Item: "items",
|
||||||
|
Reward: "rewards",
|
||||||
|
"Mystery Encounter": "mystery-encounter/encounters",
|
||||||
|
Utils: "utils",
|
||||||
|
UI: "ui",
|
||||||
|
});
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
//#region Functions
|
//#region Functions
|
||||||
@ -41,46 +46,47 @@ function getTestFolderPath(...folders) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Prompts the user to select a type via list.
|
* Prompts the user to select a type via list.
|
||||||
* @returns {Promise<{selectedOption: {label: string, dir: string}}>} the selected type
|
* @returns {Promise<choiceType>} the selected type
|
||||||
*/
|
*/
|
||||||
async function promptTestType() {
|
async function promptTestType() {
|
||||||
const typeAnswer = await inquirer.prompt([
|
/** @type {choiceType | "EXIT"} */
|
||||||
{
|
const choice = await inquirer
|
||||||
type: "list",
|
.prompt([
|
||||||
name: "selectedOption",
|
{
|
||||||
message: "What type of test would you like to create?",
|
type: "list",
|
||||||
choices: [...choices.map(choice => ({ name: choice.label, value: choice })), "EXIT"],
|
name: "selectedOption",
|
||||||
},
|
message: "What type of test would you like to create?",
|
||||||
]);
|
choices: [...choices, "EXIT"],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.then(ta => ta.selectedOption);
|
||||||
|
|
||||||
if (typeAnswer.selectedOption === "EXIT") {
|
if (choice === "EXIT") {
|
||||||
console.log("Exiting...");
|
console.log("Exiting...");
|
||||||
return process.exit();
|
return process.exit(0);
|
||||||
}
|
|
||||||
if (!choices.some(choice => choice.dir === typeAnswer.selectedOption.dir)) {
|
|
||||||
console.error(`Please provide a valid type: (${choices.map(choice => choice.label).join(", ")})!`);
|
|
||||||
return await promptTestType();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return typeAnswer;
|
return choice;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prompts the user to provide a file name.
|
* Prompts the user to provide a file name.
|
||||||
* @param {string} selectedType
|
* @param {choiceType} selectedType The chosen string (used to display console logs)
|
||||||
* @returns {Promise<{userInput: string}>} the selected file name
|
* @returns {Promise<string>} the selected file name
|
||||||
*/
|
*/
|
||||||
async function promptFileName(selectedType) {
|
async function promptFileName(selectedType) {
|
||||||
/** @type {{userInput: string}} */
|
/** @type {string} */
|
||||||
const fileNameAnswer = await inquirer.prompt([
|
const fileNameAnswer = await inquirer
|
||||||
{
|
.prompt([
|
||||||
type: "input",
|
{
|
||||||
name: "userInput",
|
type: "input",
|
||||||
message: `Please provide the name of the ${selectedType}:`,
|
name: "userInput",
|
||||||
},
|
message: `Please provide the name of the ${selectedType}.`,
|
||||||
]);
|
},
|
||||||
|
])
|
||||||
|
.then(fa => fa.userInput);
|
||||||
|
|
||||||
if (!fileNameAnswer.userInput || fileNameAnswer.userInput.trim().length === 0) {
|
if (fileNameAnswer.trim().length === 0) {
|
||||||
console.error("Please provide a valid file name!");
|
console.error("Please provide a valid file name!");
|
||||||
return await promptFileName(selectedType);
|
return await promptFileName(selectedType);
|
||||||
}
|
}
|
||||||
@ -88,51 +94,66 @@ async function promptFileName(selectedType) {
|
|||||||
return fileNameAnswer;
|
return fileNameAnswer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtain the path to the boilerplate file based on the current option.
|
||||||
|
* @param {choiceType} choiceType The choice selected
|
||||||
|
* @returns {string} The path to the boilerplate file
|
||||||
|
*/
|
||||||
|
function getBoilerplatePath(choiceType) {
|
||||||
|
switch (choiceType) {
|
||||||
|
// case "Reward":
|
||||||
|
// return path.join(__dirname, "boilerplates/reward.ts");
|
||||||
|
default:
|
||||||
|
return path.join(__dirname, "boilerplates/default.ts");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs the interactive test:create "CLI"
|
* Runs the interactive test:create "CLI"
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async function runInteractive() {
|
async function runInteractive() {
|
||||||
console.group(chalk.grey(`Create Test - v${version}\n`));
|
console.group(chalk.grey(`🧪 Create Test - v${version}\n`));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const typeAnswer = await promptTestType();
|
const choice = await promptTestType();
|
||||||
const fileNameAnswer = await promptFileName(typeAnswer.selectedOption.label);
|
const fileNameAnswer = await promptFileName(choice);
|
||||||
|
|
||||||
const type = typeAnswer.selectedOption;
|
|
||||||
// Convert fileName from snake_case or camelCase to kebab-case
|
// Convert fileName from snake_case or camelCase to kebab-case
|
||||||
const fileName = fileNameAnswer.userInput
|
const fileName = fileNameAnswer
|
||||||
.replace(/_+/g, "-") // Convert snake_case (underscore) to kebab-case (dashes)
|
.replace(/_+/g, "-") // Convert snake_case (underscore) to kebab-case (dashes)
|
||||||
.replace(/([a-z])([A-Z])/g, "$1-$2") // Convert camelCase to kebab-case
|
.replace(/([a-z])([A-Z])/g, "$1-$2") // Convert camelCase to kebab-case
|
||||||
.replace(/\s+/g, "-") // Replace spaces with dashes
|
.replace(/\s+/g, "-") // Replace spaces with dashes
|
||||||
.toLowerCase(); // Ensure all lowercase
|
.toLowerCase(); // Ensure all lowercase
|
||||||
// Format the description for the test case
|
|
||||||
|
|
||||||
|
// Format the description for the test case in Title Case
|
||||||
const formattedName = fileName.replace(/-/g, " ").replace(/\b\w/g, char => char.toUpperCase());
|
const formattedName = fileName.replace(/-/g, " ").replace(/\b\w/g, char => char.toUpperCase());
|
||||||
|
const description = `${choice} - ${formattedName}`;
|
||||||
|
|
||||||
// Determine the directory based on the type
|
// Determine the directory based on the type
|
||||||
const dir = getTestFolderPath(type.dir);
|
const localDir = choicesToDirs[choice];
|
||||||
const description = `${type.label} - ${formattedName}`;
|
const absoluteDir = getTestFolderPath(localDir);
|
||||||
|
|
||||||
// Define the content template
|
// Define the content template
|
||||||
const content = fs.readFileSync(boilerplateFilePath, "utf8").replace("{{description}}", description);
|
const content = fs.readFileSync(getBoilerplatePath(choice), "utf8").replace("{{description}}", description);
|
||||||
|
|
||||||
// Ensure the directory exists
|
// Ensure the directory exists
|
||||||
if (!fs.existsSync(dir)) {
|
if (!fs.existsSync(absoluteDir)) {
|
||||||
fs.mkdirSync(dir, { recursive: true });
|
fs.mkdirSync(absoluteDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the file with the given name
|
// Create the file with the given name
|
||||||
const filePath = path.join(dir, `${fileName}.test.ts`);
|
const filePath = path.join(absoluteDir, `${fileName}.test.ts`);
|
||||||
|
|
||||||
if (fs.existsSync(filePath)) {
|
if (fs.existsSync(filePath)) {
|
||||||
console.error(chalk.red.bold(`\n✗ File "${fileName}.test.ts" already exists!\n`));
|
console.error(chalk.red.bold(`✗ File "${fileName}.test.ts" already exists!\n`));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the template content to the file
|
// Write the template content to the file
|
||||||
fs.writeFileSync(filePath, content, "utf8");
|
fs.writeFileSync(filePath, content, "utf8");
|
||||||
|
|
||||||
console.log(chalk.green.bold(`\n✔ File created at: test/${type.dir}/${fileName}.test.ts\n`));
|
console.log(chalk.green.bold(`✔ File created at: test/${localDir}/${fileName}.test.ts\n`));
|
||||||
console.groupEnd();
|
console.groupEnd();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(chalk.red("✗ Error: ", err.message));
|
console.error(chalk.red("✗ Error: ", err.message));
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import type { ArenaTagTypeMap } from "#data/arena-tag";
|
import type { ArenaTagTypeMap } from "#data/arena-tag";
|
||||||
import type { ArenaTagType } from "#enums/arena-tag-type";
|
import type { ArenaTagType } from "#enums/arena-tag-type";
|
||||||
import type { NonFunctionProperties } from "./type-helpers";
|
|
||||||
|
|
||||||
/** Subset of {@linkcode ArenaTagType}s that apply some negative effect to pokemon that switch in ({@link https://bulbapedia.bulbagarden.net/wiki/List_of_moves_that_cause_entry_hazards#List_of_traps | entry hazards} and Imprison. */
|
/** Subset of {@linkcode ArenaTagType}s that apply some negative effect to pokemon that switch in ({@link https://bulbapedia.bulbagarden.net/wiki/List_of_moves_that_cause_entry_hazards#List_of_traps | entry hazards} and Imprison. */
|
||||||
export type ArenaTrapTagType =
|
export type ArenaTrapTagType =
|
||||||
@ -10,9 +9,6 @@ export type ArenaTrapTagType =
|
|||||||
| ArenaTagType.STEALTH_ROCK
|
| ArenaTagType.STEALTH_ROCK
|
||||||
| ArenaTagType.IMPRISON;
|
| ArenaTagType.IMPRISON;
|
||||||
|
|
||||||
/** Subset of {@linkcode ArenaTagType}s that are considered delayed attacks */
|
|
||||||
export type ArenaDelayedAttackTagType = ArenaTagType.FUTURE_SIGHT | ArenaTagType.DOOM_DESIRE;
|
|
||||||
|
|
||||||
/** Subset of {@linkcode ArenaTagType}s that create {@link https://bulbapedia.bulbagarden.net/wiki/Category:Screen-creating_moves | screens}. */
|
/** Subset of {@linkcode ArenaTagType}s that create {@link https://bulbapedia.bulbagarden.net/wiki/Category:Screen-creating_moves | screens}. */
|
||||||
export type ArenaScreenTagType = ArenaTagType.REFLECT | ArenaTagType.LIGHT_SCREEN | ArenaTagType.AURORA_VEIL;
|
export type ArenaScreenTagType = ArenaTagType.REFLECT | ArenaTagType.LIGHT_SCREEN | ArenaTagType.AURORA_VEIL;
|
||||||
|
|
||||||
@ -30,13 +26,13 @@ export type NonSerializableArenaTagType = ArenaTagType.NONE | TurnProtectArenaTa
|
|||||||
export type SerializableArenaTagType = Exclude<ArenaTagType, NonSerializableArenaTagType>;
|
export type SerializableArenaTagType = Exclude<ArenaTagType, NonSerializableArenaTagType>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type-safe representation of the serializable data of an ArenaTag
|
* Type-safe representation of an arbitrary, serialized Arena Tag
|
||||||
*/
|
*/
|
||||||
export type ArenaTagTypeData = NonFunctionProperties<
|
export type ArenaTagTypeData = Parameters<
|
||||||
ArenaTagTypeMap[keyof {
|
ArenaTagTypeMap[keyof {
|
||||||
[K in keyof ArenaTagTypeMap as K extends SerializableArenaTagType ? K : never]: ArenaTagTypeMap[K];
|
[K in keyof ArenaTagTypeMap as K extends SerializableArenaTagType ? K : never]: ArenaTagTypeMap[K];
|
||||||
}]
|
}]["loadTag"]
|
||||||
>;
|
>[0];
|
||||||
|
|
||||||
/** Dummy, typescript-only declaration to ensure that
|
/** Dummy, typescript-only declaration to ensure that
|
||||||
* {@linkcode ArenaTagTypeMap} has a map for all ArenaTagTypes.
|
* {@linkcode ArenaTagTypeMap} has a map for all ArenaTagTypes.
|
||||||
|
128
src/@types/battler-tags.ts
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
// biome-ignore-start lint/correctness/noUnusedImports: Used in a TSDoc comment
|
||||||
|
import type { AbilityBattlerTag, BattlerTagTypeMap, SerializableBattlerTag, TypeBoostTag } from "#data/battler-tags";
|
||||||
|
import type { AbilityId } from "#enums/ability-id";
|
||||||
|
// biome-ignore-end lint/correctness/noUnusedImports: end
|
||||||
|
import type { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subset of {@linkcode BattlerTagType}s that restrict the use of moves.
|
||||||
|
*/
|
||||||
|
export type MoveRestrictionBattlerTagType =
|
||||||
|
| BattlerTagType.THROAT_CHOPPED
|
||||||
|
| BattlerTagType.TORMENT
|
||||||
|
| BattlerTagType.TAUNT
|
||||||
|
| BattlerTagType.IMPRISON
|
||||||
|
| BattlerTagType.HEAL_BLOCK
|
||||||
|
| BattlerTagType.ENCORE
|
||||||
|
| BattlerTagType.DISABLED
|
||||||
|
| BattlerTagType.GORILLA_TACTICS;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subset of {@linkcode BattlerTagType}s that block damage from moves.
|
||||||
|
*/
|
||||||
|
export type FormBlockDamageBattlerTagType = BattlerTagType.ICE_FACE | BattlerTagType.DISGUISE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subset of {@linkcode BattlerTagType}s that are related to trapping effects.
|
||||||
|
*/
|
||||||
|
export type TrappingBattlerTagType =
|
||||||
|
| BattlerTagType.BIND
|
||||||
|
| BattlerTagType.WRAP
|
||||||
|
| BattlerTagType.FIRE_SPIN
|
||||||
|
| BattlerTagType.WHIRLPOOL
|
||||||
|
| BattlerTagType.CLAMP
|
||||||
|
| BattlerTagType.SAND_TOMB
|
||||||
|
| BattlerTagType.MAGMA_STORM
|
||||||
|
| BattlerTagType.SNAP_TRAP
|
||||||
|
| BattlerTagType.THUNDER_CAGE
|
||||||
|
| BattlerTagType.INFESTATION
|
||||||
|
| BattlerTagType.INGRAIN
|
||||||
|
| BattlerTagType.OCTOLOCK
|
||||||
|
| BattlerTagType.NO_RETREAT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subset of {@linkcode BattlerTagType}s that are related to protection effects.
|
||||||
|
*/
|
||||||
|
export type ProtectionBattlerTagType = BattlerTagType.PROTECTED | BattlerTagType.SPIKY_SHIELD | DamageProtectedTagType;
|
||||||
|
/**
|
||||||
|
* Subset of {@linkcode BattlerTagType}s related to protection effects that block damage but not status moves.
|
||||||
|
*/
|
||||||
|
export type DamageProtectedTagType = ContactSetStatusProtectedTagType | ContactStatStageChangeProtectedTagType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subset of {@linkcode BattlerTagType}s related to protection effects that set a status effect on the attacker.
|
||||||
|
*/
|
||||||
|
export type ContactSetStatusProtectedTagType = BattlerTagType.BANEFUL_BUNKER | BattlerTagType.BURNING_BULWARK;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subset of {@linkcode BattlerTagType}s related to protection effects that change stat stages of the attacker.
|
||||||
|
*/
|
||||||
|
export type ContactStatStageChangeProtectedTagType =
|
||||||
|
| BattlerTagType.KINGS_SHIELD
|
||||||
|
| BattlerTagType.SILK_TRAP
|
||||||
|
| BattlerTagType.OBSTRUCT;
|
||||||
|
|
||||||
|
/** Subset of {@linkcode BattlerTagType}s that provide the Endure effect */
|
||||||
|
export type EndureTagType = BattlerTagType.ENDURE_TOKEN | BattlerTagType.ENDURING;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subset of {@linkcode BattlerTagType}s that are related to semi-invulnerable states.
|
||||||
|
*/
|
||||||
|
export type SemiInvulnerableTagType =
|
||||||
|
| BattlerTagType.FLYING
|
||||||
|
| BattlerTagType.UNDERGROUND
|
||||||
|
| BattlerTagType.UNDERWATER
|
||||||
|
| BattlerTagType.HIDDEN;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subset of {@linkcode BattlerTagType}s corresponding to {@linkcode AbilityBattlerTag}s
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* ⚠️ {@linkcode AbilityId.FLASH_FIRE | Flash Fire}'s {@linkcode BattlerTagType.FIRE_BOOST} is not included as it
|
||||||
|
* subclasses {@linkcode TypeBoostTag} and not `AbilityBattlerTag`.
|
||||||
|
*/
|
||||||
|
export type AbilityBattlerTagType =
|
||||||
|
| BattlerTagType.PROTOSYNTHESIS
|
||||||
|
| BattlerTagType.QUARK_DRIVE
|
||||||
|
| BattlerTagType.UNBURDEN
|
||||||
|
| BattlerTagType.SLOW_START
|
||||||
|
| BattlerTagType.TRUANT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subset of {@linkcode BattlerTagType}s related to abilities that boost the highest stat.
|
||||||
|
*/
|
||||||
|
export type HighestStatBoostTagType =
|
||||||
|
| BattlerTagType.QUARK_DRIVE // formatting
|
||||||
|
| BattlerTagType.PROTOSYNTHESIS;
|
||||||
|
/**
|
||||||
|
* Subset of {@linkcode BattlerTagType}s that are able to persist between turns and should therefore be serialized
|
||||||
|
*/
|
||||||
|
export type SerializableBattlerTagType = keyof {
|
||||||
|
[K in keyof BattlerTagTypeMap as BattlerTagTypeMap[K] extends SerializableBattlerTag
|
||||||
|
? K
|
||||||
|
: never]: BattlerTagTypeMap[K];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subset of {@linkcode BattlerTagType}s that are not able to persist across waves and should therefore not be serialized
|
||||||
|
*/
|
||||||
|
export type NonSerializableBattlerTagType = Exclude<BattlerTagType, SerializableBattlerTagType>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type-safe representation of an arbitrary, serialized Battler Tag
|
||||||
|
*/
|
||||||
|
export type BattlerTagTypeData = Parameters<
|
||||||
|
BattlerTagTypeMap[keyof {
|
||||||
|
[K in keyof BattlerTagTypeMap as K extends SerializableBattlerTagType ? K : never]: BattlerTagTypeMap[K];
|
||||||
|
}]["loadTag"]
|
||||||
|
>[0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dummy, typescript-only declaration to ensure that
|
||||||
|
* {@linkcode BattlerTagTypeMap} has an entry for all `BattlerTagType`s.
|
||||||
|
*
|
||||||
|
* If a battler tag is missing from the map, Typescript will throw an error on this statement.
|
||||||
|
*
|
||||||
|
* ⚠️ Does not actually exist at runtime, so it must not be used!
|
||||||
|
*/
|
||||||
|
declare const EnsureAllBattlerTagTypesAreMapped: BattlerTagTypeMap[BattlerTagType] & never;
|
@ -1,18 +1,14 @@
|
|||||||
|
import type { ObjectValues } from "#types/type-helpers";
|
||||||
|
|
||||||
/** Union type accepting any TS Enum or `const object`, with or without reverse mapping. */
|
/** Union type accepting any TS Enum or `const object`, with or without reverse mapping. */
|
||||||
export type EnumOrObject = Record<string | number, string | number>;
|
export type EnumOrObject = Record<string | number, string | number>;
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility type to extract the enum values from a `const object`,
|
|
||||||
* or convert an `enum` interface produced by `typeof Enum` into the union type representing its values.
|
|
||||||
*/
|
|
||||||
export type EnumValues<E> = E[keyof E];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic type constraint representing a TS numeric enum with reverse mappings.
|
* Generic type constraint representing a TS numeric enum with reverse mappings.
|
||||||
* @example
|
* @example
|
||||||
* TSNumericEnum<typeof WeatherType>
|
* TSNumericEnum<typeof WeatherType>
|
||||||
*/
|
*/
|
||||||
export type TSNumericEnum<T extends EnumOrObject> = number extends EnumValues<T> ? T : never;
|
export type TSNumericEnum<T extends EnumOrObject> = number extends ObjectValues<T> ? T : never;
|
||||||
|
|
||||||
/** Generic type constraint representing a non reverse-mapped TS enum or `const object`. */
|
/** Generic type constraint representing a non reverse-mapped TS enum or `const object`. */
|
||||||
export type NormalEnum<T extends EnumOrObject> = Exclude<T, TSNumericEnum<T>>;
|
export type NormalEnum<T extends EnumOrObject> = Exclude<T, TSNumericEnum<T>>;
|
@ -6,8 +6,6 @@
|
|||||||
import type { AbAttr } from "#abilities/ability";
|
import type { AbAttr } from "#abilities/ability";
|
||||||
// biome-ignore-end lint/correctness/noUnusedImports: Used in a tsdoc comment
|
// biome-ignore-end lint/correctness/noUnusedImports: Used in a tsdoc comment
|
||||||
|
|
||||||
import type { EnumValues } from "#types/enum-types";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exactly matches the type of the argument, preventing adding additional properties.
|
* Exactly matches the type of the argument, preventing adding additional properties.
|
||||||
*
|
*
|
||||||
@ -37,16 +35,25 @@ export type Mutable<T> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type helper to obtain the keys associated with a given value inside a `const object`.
|
* Type helper to obtain the keys associated with a given value inside an object.
|
||||||
* @typeParam O - The type of the object
|
* @typeParam O - The type of the object
|
||||||
* @typeParam V - The type of one of O's values
|
* @typeParam V - The type of one of O's values
|
||||||
*/
|
*/
|
||||||
export type InferKeys<O extends Record<keyof any, unknown>, V extends EnumValues<O>> = {
|
export type InferKeys<O extends object, V extends ObjectValues<O>> = {
|
||||||
[K in keyof O]: O[K] extends V ? K : never;
|
[K in keyof O]: O[K] extends V ? K : never;
|
||||||
}[keyof O];
|
}[keyof O];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type helper that matches any `Function` type. Equivalent to `Function`, but will not raise a warning from Biome.
|
* Utility type to obtain the values of a given object. \
|
||||||
|
* Functions similar to `keyof E`, except producing the values instead of the keys.
|
||||||
|
* @remarks
|
||||||
|
* This can be used to convert an `enum` interface produced by `typeof Enum` into the union type representing its members.
|
||||||
|
*/
|
||||||
|
export type ObjectValues<E extends object> = E[keyof E];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type helper that matches any `Function` type.
|
||||||
|
* Equivalent to `Function`, but will not raise a warning from Biome.
|
||||||
*/
|
*/
|
||||||
export type AnyFn = (...args: any[]) => any;
|
export type AnyFn = (...args: any[]) => any;
|
||||||
|
|
||||||
@ -65,6 +72,7 @@ export type NonFunctionProperties<T> = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Type helper to extract out non-function properties from a type, recursively applying to nested properties.
|
* Type helper to extract out non-function properties from a type, recursively applying to nested properties.
|
||||||
|
* This can be used to mimic the effects of JSON serialization and de-serialization on a given type.
|
||||||
*/
|
*/
|
||||||
export type NonFunctionPropertiesRecursive<Class> = {
|
export type NonFunctionPropertiesRecursive<Class> = {
|
||||||
[K in keyof Class as Class[K] extends AnyFn ? never : K]: Class[K] extends Array<infer U>
|
[K in keyof Class as Class[K] extends AnyFn ? never : K]: Class[K] extends Array<infer U>
|
||||||
@ -75,3 +83,14 @@ export type NonFunctionPropertiesRecursive<Class> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type AbstractConstructor<T> = abstract new (...args: any[]) => T;
|
export type AbstractConstructor<T> = abstract new (...args: any[]) => T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type helper that iterates through the fields of the type and coerces any `null` properties to `undefined` (including in union types).
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* This is primarily useful when an object with nullable properties wants to be serialized and have its `null`
|
||||||
|
* properties coerced to `undefined`.
|
||||||
|
*/
|
||||||
|
export type CoerceNullPropertiesToUndefined<T extends object> = {
|
||||||
|
[K in keyof T]: null extends T[K] ? Exclude<T[K], null> | undefined : T[K];
|
||||||
|
};
|
@ -11,7 +11,7 @@ export interface IllusionData {
|
|||||||
/** The name of pokemon featured in the illusion */
|
/** The name of pokemon featured in the illusion */
|
||||||
name: string;
|
name: string;
|
||||||
/** The nickname of the pokemon featured in the illusion */
|
/** The nickname of the pokemon featured in the illusion */
|
||||||
nickname: string;
|
nickname?: string;
|
||||||
/** Whether the pokemon featured in the illusion is shiny or not */
|
/** Whether the pokemon featured in the illusion is shiny or not */
|
||||||
shiny: boolean;
|
shiny: boolean;
|
||||||
/** The variant of the pokemon featured in the illusion */
|
/** The variant of the pokemon featured in the illusion */
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
import type { Pokemon } from "#field/pokemon";
|
import type { Pokemon } from "#field/pokemon";
|
||||||
import type { ModifierConstructorMap } from "#modifiers/modifier";
|
import type { ModifierConstructorMap } from "#modifiers/modifier";
|
||||||
import type { ModifierType, WeightedModifierType } from "#modifiers/modifier-type";
|
import type { ModifierType, WeightedModifierType } from "#modifiers/modifier-type";
|
||||||
|
import type { ObjectValues } from "#types/type-helpers";
|
||||||
|
|
||||||
export type ModifierTypeFunc = () => ModifierType;
|
export type ModifierTypeFunc = () => ModifierType;
|
||||||
export type WeightedModifierTypeWeightFunc = (party: Pokemon[], rerollCount?: number) => number;
|
export type WeightedModifierTypeWeightFunc = (party: Pokemon[], rerollCount?: number) => number;
|
||||||
@ -19,7 +20,7 @@ export type ModifierInstanceMap = {
|
|||||||
/**
|
/**
|
||||||
* Union type of all modifier constructors.
|
* Union type of all modifier constructors.
|
||||||
*/
|
*/
|
||||||
export type ModifierClass = ModifierConstructorMap[keyof ModifierConstructorMap];
|
export type ModifierClass = ObjectValues<ModifierConstructorMap>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Union type of all modifier names as strings.
|
* Union type of all modifier names as strings.
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import type { PhaseConstructorMap } from "#app/phase-manager";
|
import type { PhaseConstructorMap } from "#app/phase-manager";
|
||||||
|
import type { ObjectValues } from "#types/type-helpers";
|
||||||
|
|
||||||
// Intentionally export the types of everything in phase-manager, as this file is meant to be
|
// Intentionally export the types of everything in phase-manager, as this file is meant to be
|
||||||
// the centralized place for type definitions for the phase system.
|
// the centralized place for type definitions for the phase system.
|
||||||
@ -17,7 +18,7 @@ export type PhaseMap = {
|
|||||||
/**
|
/**
|
||||||
* Union type of all phase constructors.
|
* Union type of all phase constructors.
|
||||||
*/
|
*/
|
||||||
export type PhaseClass = PhaseConstructorMap[keyof PhaseConstructorMap];
|
export type PhaseClass = ObjectValues<PhaseConstructorMap>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Union type of all phase names as strings.
|
* Union type of all phase names as strings.
|
||||||
|
@ -657,9 +657,7 @@ export class BattleScene extends SceneBase {
|
|||||||
).then(() => loadMoveAnimAssets(defaultMoves, true)),
|
).then(() => loadMoveAnimAssets(defaultMoves, true)),
|
||||||
this.initStarterColors(),
|
this.initStarterColors(),
|
||||||
]).then(() => {
|
]).then(() => {
|
||||||
this.phaseManager.pushNew("LoginPhase");
|
this.phaseManager.toTitleScreen(true);
|
||||||
this.phaseManager.pushNew("TitlePhase");
|
|
||||||
|
|
||||||
this.phaseManager.shiftPhase();
|
this.phaseManager.shiftPhase();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1269,13 +1267,12 @@ export class BattleScene extends SceneBase {
|
|||||||
duration: 250,
|
duration: 250,
|
||||||
ease: "Sine.easeInOut",
|
ease: "Sine.easeInOut",
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
this.phaseManager.clearPhaseQueue();
|
|
||||||
|
|
||||||
this.ui.freeUIData();
|
this.ui.freeUIData();
|
||||||
this.uiContainer.remove(this.ui, true);
|
this.uiContainer.remove(this.ui, true);
|
||||||
this.uiContainer.destroy();
|
this.uiContainer.destroy();
|
||||||
this.children.removeAll(true);
|
this.children.removeAll(true);
|
||||||
this.game.domContainer.innerHTML = "";
|
this.game.domContainer.innerHTML = "";
|
||||||
|
// TODO: `launchBattle` calls `reset(false, false, true)`
|
||||||
this.launchBattle();
|
this.launchBattle();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -2848,6 +2845,23 @@ export class BattleScene extends SceneBase {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Attempt to discard one or more copies of a held item.
|
||||||
|
* @param itemModifier - The {@linkcode PokemonHeldItemModifier} being discarded
|
||||||
|
* @param discardQuantity - The number of copies to remove (up to the amount currently held); default `1`
|
||||||
|
* @returns Whether the item was successfully discarded.
|
||||||
|
* Removing fewer items than requested is still considered a success.
|
||||||
|
*/
|
||||||
|
tryDiscardHeldItemModifier(itemModifier: PokemonHeldItemModifier, discardQuantity = 1): boolean {
|
||||||
|
const countTaken = Math.min(discardQuantity, itemModifier.stackCount);
|
||||||
|
itemModifier.stackCount -= countTaken;
|
||||||
|
|
||||||
|
if (itemModifier.stackCount > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.removeModifier(itemModifier);
|
||||||
|
}
|
||||||
|
|
||||||
canTransferHeldItemModifier(itemModifier: PokemonHeldItemModifier, target: Pokemon, transferQuantity = 1): boolean {
|
canTransferHeldItemModifier(itemModifier: PokemonHeldItemModifier, target: Pokemon, transferQuantity = 1): boolean {
|
||||||
const mod = itemModifier.clone() as PokemonHeldItemModifier;
|
const mod = itemModifier.clone() as PokemonHeldItemModifier;
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
/** biome-ignore-start lint/correctness/noUnusedImports: TSDoc imports */
|
||||||
|
import type { BattlerTag } from "#app/data/battler-tags";
|
||||||
|
/** biome-ignore-end lint/correctness/noUnusedImports: TSDoc imports */
|
||||||
|
|
||||||
import { applyAbAttrs, applyOnGainAbAttrs, applyOnLoseAbAttrs } from "#abilities/apply-ab-attrs";
|
import { applyAbAttrs, applyOnGainAbAttrs, applyOnLoseAbAttrs } from "#abilities/apply-ab-attrs";
|
||||||
import { globalScene } from "#app/global-scene";
|
import { globalScene } from "#app/global-scene";
|
||||||
import { getPokemonNameWithAffix } from "#app/messages";
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
@ -6,58 +10,72 @@ import { allMoves } from "#data/data-lists";
|
|||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
import { ArenaTagSide } from "#enums/arena-tag-side";
|
import { ArenaTagSide } from "#enums/arena-tag-side";
|
||||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
import { ArenaTagType } from "#enums/arena-tag-type";
|
||||||
import type { BattlerIndex } from "#enums/battler-index";
|
|
||||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
import { HitResult } from "#enums/hit-result";
|
import { HitResult } from "#enums/hit-result";
|
||||||
import { CommonAnim } from "#enums/move-anims-common";
|
import { CommonAnim } from "#enums/move-anims-common";
|
||||||
import { MoveCategory } from "#enums/move-category";
|
import { MoveCategory } from "#enums/move-category";
|
||||||
import { MoveId } from "#enums/move-id";
|
import { MoveId } from "#enums/move-id";
|
||||||
import { MoveTarget } from "#enums/move-target";
|
import { MoveTarget } from "#enums/move-target";
|
||||||
import { MoveUseMode } from "#enums/move-use-mode";
|
|
||||||
import { PokemonType } from "#enums/pokemon-type";
|
import { PokemonType } from "#enums/pokemon-type";
|
||||||
import { Stat } from "#enums/stat";
|
import { Stat } from "#enums/stat";
|
||||||
import { StatusEffect } from "#enums/status-effect";
|
import { StatusEffect } from "#enums/status-effect";
|
||||||
import type { Arena } from "#field/arena";
|
import type { Arena } from "#field/arena";
|
||||||
import type { Pokemon } from "#field/pokemon";
|
import type { Pokemon } from "#field/pokemon";
|
||||||
import type {
|
import type {
|
||||||
ArenaDelayedAttackTagType,
|
|
||||||
ArenaScreenTagType,
|
ArenaScreenTagType,
|
||||||
ArenaTagTypeData,
|
ArenaTagTypeData,
|
||||||
ArenaTrapTagType,
|
ArenaTrapTagType,
|
||||||
SerializableArenaTagType,
|
SerializableArenaTagType,
|
||||||
} from "#types/arena-tags";
|
} from "#types/arena-tags";
|
||||||
import type { Mutable, NonFunctionProperties } from "#types/type-helpers";
|
import type { Mutable } from "#types/type-helpers";
|
||||||
import { BooleanHolder, isNullOrUndefined, NumberHolder, toDmgValue } from "#utils/common";
|
import { BooleanHolder, NumberHolder, toDmgValue } from "#utils/common";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
/*
|
/**
|
||||||
ArenaTags are are meant for effects that are tied to the arena (as opposed to a specific pokemon).
|
* @module
|
||||||
Examples include (but are not limited to)
|
* ArenaTags are are meant for effects that are tied to the arena (as opposed to a specific pokemon).
|
||||||
- Cross-turn effects that persist even if the user/target switches out, such as Wish, Future Sight, and Happy Hour
|
* Examples include (but are not limited to)
|
||||||
- Effects that are applied to a specific side of the field, such as Crafty Shield, Reflect, and Spikes
|
* - Cross-turn effects that persist even if the user/target switches out, such as Happy Hour
|
||||||
- Field-Effects, like Gravity and Trick Room
|
* - Effects that are applied to a specific side of the field, such as Crafty Shield, Reflect, and Spikes
|
||||||
|
* - Field-Effects, like Gravity and Trick Room
|
||||||
Any arena tag that persists across turns *must* extend from `SerializableArenaTag` in the class definition signature.
|
*
|
||||||
|
* Any arena tag that persists across turns *must* extend from `SerializableArenaTag` in the class definition signature.
|
||||||
Serializable ArenaTags have strict rules for their fields.
|
*
|
||||||
These rules ensure that only the data necessary to reconstruct the tag is serialized, and that the
|
* Serializable ArenaTags have strict rules for their fields.
|
||||||
session loader is able to deserialize saved tags correctly.
|
* These rules ensure that only the data necessary to reconstruct the tag is serialized, and that the
|
||||||
|
* session loader is able to deserialize saved tags correctly.
|
||||||
If the data is static (i.e. it is always the same for all instances of the class, such as the
|
*
|
||||||
type that is weakened by Mud Sport/Water Sport), then it must not be defined as a field, and must
|
* If the data is static (i.e. it is always the same for all instances of the class, such as the
|
||||||
instead be defined as a getter.
|
* type that is weakened by Mud Sport/Water Sport), then it must not be defined as a field, and must
|
||||||
A static property is also acceptable, though static properties are less ergonomic with inheritance.
|
* instead be defined as a getter.
|
||||||
|
* A static property is also acceptable, though static properties are less ergonomic with inheritance.
|
||||||
If the data is mutable (i.e. it can change over the course of the tag's lifetime), then it *must*
|
*
|
||||||
be defined as a field, and it must be set in the `loadTag` method.
|
* If the data is mutable (i.e. it can change over the course of the tag's lifetime), then it *must*
|
||||||
Such fields cannot be marked as `private/protected`, as if they were, typescript would omit them from
|
* be defined as a field, and it must be set in the `loadTag` method.
|
||||||
types that are based off of the class, namely, `ArenaTagTypeData`. It is preferrable to trade the
|
* Such fields cannot be marked as `private`/`protected`; if they were, Typescript would omit them from
|
||||||
type-safety of private/protected fields for the type safety when deserializing arena tags from save data.
|
* types that are based off of the class, namely, `ArenaTagTypeData`. It is preferrable to trade the
|
||||||
|
* type-safety of private/protected fields for the type safety when deserializing arena tags from save data.
|
||||||
For data that is mutable only within a turn (e.g. SuppressAbilitiesTag's beingRemoved field),
|
*
|
||||||
where it does not make sense to be serialized, the field should use ES2020's private field syntax (a `#` prepended to the field name).
|
* For data that is mutable only within a turn (e.g. SuppressAbilitiesTag's beingRemoved field),
|
||||||
If the field should be accessible outside of the class, then a public getter should be used.
|
* where it does not make sense to be serialized, the field should use ES2020's
|
||||||
*/
|
* [private field syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_elements#private_fields).
|
||||||
|
* If the field should be accessible outside of the class, then a public getter should be used.
|
||||||
|
*
|
||||||
|
* If any new serializable fields *are* added, then the class *must* override the
|
||||||
|
* `loadTag` method to set the new fields. Its signature *must* match the example below,
|
||||||
|
* ```
|
||||||
|
* class ExampleTag extends SerializableArenaTag {
|
||||||
|
* // Example, if we add 2 new fields that should be serialized:
|
||||||
|
* public a: string;
|
||||||
|
* public b: number;
|
||||||
|
* // Then we must also define a loadTag method with one of the following signatures
|
||||||
|
* public override loadTag(source: BaseArenaTag & Pick<ExampleTag, "tagType" | "a" | "b"): void;
|
||||||
|
* public override loadTag<const T extends this>(source: BaseArenaTag & Pick<T, "tagType" | "a" | "b">): void;
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
* Notes
|
||||||
|
* - If the class has any subclasses, then the second form of `loadTag` *must* be used.
|
||||||
|
*/
|
||||||
|
|
||||||
/** Interface containing the serializable fields of ArenaTagData. */
|
/** Interface containing the serializable fields of ArenaTagData. */
|
||||||
interface BaseArenaTag {
|
interface BaseArenaTag {
|
||||||
@ -141,9 +159,9 @@ export abstract class ArenaTag implements BaseArenaTag {
|
|||||||
/**
|
/**
|
||||||
* When given a arena tag or json representing one, load the data for it.
|
* When given a arena tag or json representing one, load the data for it.
|
||||||
* This is meant to be inherited from by any arena tag with custom attributes
|
* This is meant to be inherited from by any arena tag with custom attributes
|
||||||
* @param source - The {@linkcode BaseArenaTag} being loaded
|
* @param source - The arena tag being loaded
|
||||||
*/
|
*/
|
||||||
loadTag(source: BaseArenaTag): void {
|
loadTag<const T extends this>(source: BaseArenaTag & Pick<T, "tagType">): void {
|
||||||
this.turnCount = source.turnCount;
|
this.turnCount = source.turnCount;
|
||||||
this.sourceMove = source.sourceMove;
|
this.sourceMove = source.sourceMove;
|
||||||
this.sourceId = source.sourceId;
|
this.sourceId = source.sourceId;
|
||||||
@ -604,56 +622,6 @@ export class NoCritTag extends SerializableArenaTag {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Wish_(move) | Wish}.
|
|
||||||
* Heals the Pokémon in the user's position the turn after Wish is used.
|
|
||||||
*/
|
|
||||||
class WishTag extends SerializableArenaTag {
|
|
||||||
// The following fields are meant to be inwardly mutable, but outwardly immutable.
|
|
||||||
readonly battlerIndex: BattlerIndex;
|
|
||||||
readonly healHp: number;
|
|
||||||
readonly sourceName: string;
|
|
||||||
// End inwardly mutable fields
|
|
||||||
|
|
||||||
public readonly tagType = ArenaTagType.WISH;
|
|
||||||
|
|
||||||
constructor(turnCount: number, sourceId: number | undefined, side: ArenaTagSide) {
|
|
||||||
super(turnCount, MoveId.WISH, sourceId, side);
|
|
||||||
}
|
|
||||||
|
|
||||||
onAdd(_arena: Arena): void {
|
|
||||||
const source = this.getSourcePokemon();
|
|
||||||
if (!source) {
|
|
||||||
console.warn(`Failed to get source Pokemon for WishTag on add message; id: ${this.sourceId}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
(this as Mutable<this>).sourceName = getPokemonNameWithAffix(source);
|
|
||||||
(this as Mutable<this>).healHp = toDmgValue(source.getMaxHp() / 2);
|
|
||||||
(this as Mutable<this>).battlerIndex = source.getBattlerIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
onRemove(_arena: Arena): void {
|
|
||||||
const target = globalScene.getField()[this.battlerIndex];
|
|
||||||
if (target?.isActive(true)) {
|
|
||||||
globalScene.phaseManager.queueMessage(
|
|
||||||
// TODO: Rename key as it triggers on activation
|
|
||||||
i18next.t("arenaTag:wishTagOnAdd", {
|
|
||||||
pokemonNameWithAffix: this.sourceName,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
globalScene.phaseManager.unshiftNew("PokemonHealPhase", target.getBattlerIndex(), this.healHp, null, true, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override loadTag(source: NonFunctionProperties<WishTag>): void {
|
|
||||||
super.loadTag(source);
|
|
||||||
(this as Mutable<this>).battlerIndex = source.battlerIndex;
|
|
||||||
(this as Mutable<this>).healHp = source.healHp;
|
|
||||||
(this as Mutable<this>).sourceName = source.sourceName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract class to implement weakened moves of a specific type.
|
* Abstract class to implement weakened moves of a specific type.
|
||||||
*/
|
*/
|
||||||
@ -813,7 +781,7 @@ export abstract class ArenaTrapTag extends SerializableArenaTag {
|
|||||||
: Phaser.Math.Linear(0, 1 / Math.pow(2, this.layers), Math.min(pokemon.getHpRatio(), 0.5) * 2);
|
: Phaser.Math.Linear(0, 1 / Math.pow(2, this.layers), Math.min(pokemon.getHpRatio(), 0.5) * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadTag(source: NonFunctionProperties<ArenaTrapTag>): void {
|
public loadTag<T extends this>(source: BaseArenaTag & Pick<T, "tagType" | "layers" | "maxLayers">): void {
|
||||||
super.loadTag(source);
|
super.loadTag(source);
|
||||||
this.layers = source.layers;
|
this.layers = source.layers;
|
||||||
this.maxLayers = source.maxLayers;
|
this.maxLayers = source.maxLayers;
|
||||||
@ -1126,48 +1094,6 @@ class StickyWebTag extends ArenaTrapTag {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Arena Tag class for delayed attacks, such as {@linkcode MoveId.FUTURE_SIGHT} or {@linkcode MoveId.DOOM_DESIRE}.
|
|
||||||
* Delays the attack's effect by a set amount of turns, usually 3 (including the turn the move is used),
|
|
||||||
* and deals damage after the turn count is reached.
|
|
||||||
*/
|
|
||||||
export class DelayedAttackTag extends SerializableArenaTag {
|
|
||||||
public targetIndex: BattlerIndex;
|
|
||||||
public readonly tagType: ArenaDelayedAttackTagType;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
tagType: ArenaTagType.DOOM_DESIRE | ArenaTagType.FUTURE_SIGHT,
|
|
||||||
sourceMove: MoveId | undefined,
|
|
||||||
sourceId: number | undefined,
|
|
||||||
targetIndex: BattlerIndex,
|
|
||||||
side: ArenaTagSide = ArenaTagSide.BOTH,
|
|
||||||
) {
|
|
||||||
super(3, sourceMove, sourceId, side);
|
|
||||||
this.tagType = tagType;
|
|
||||||
this.targetIndex = targetIndex;
|
|
||||||
this.side = side;
|
|
||||||
}
|
|
||||||
|
|
||||||
lapse(arena: Arena): boolean {
|
|
||||||
const ret = super.lapse(arena);
|
|
||||||
|
|
||||||
if (!ret) {
|
|
||||||
// TODO: This should not add to move history (for Spite)
|
|
||||||
globalScene.phaseManager.unshiftNew(
|
|
||||||
"MoveEffectPhase",
|
|
||||||
this.sourceId!,
|
|
||||||
[this.targetIndex],
|
|
||||||
allMoves[this.sourceMove!],
|
|
||||||
MoveUseMode.FOLLOW_UP,
|
|
||||||
); // TODO: are those bangs correct?
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
onRemove(_arena: Arena): void {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Trick_Room_(move) Trick Room}.
|
* Arena Tag class for {@link https://bulbapedia.bulbagarden.net/wiki/Trick_Room_(move) Trick Room}.
|
||||||
* Reverses the Speed stats for all Pokémon on the field as long as this arena tag is up,
|
* Reverses the Speed stats for all Pokémon on the field as long as this arena tag is up,
|
||||||
@ -1581,7 +1507,7 @@ export class SuppressAbilitiesTag extends SerializableArenaTag {
|
|||||||
this.#beingRemoved = false;
|
this.#beingRemoved = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override loadTag(source: NonFunctionProperties<SuppressAbilitiesTag>): void {
|
public override loadTag(source: BaseArenaTag & Pick<SuppressAbilitiesTag, "tagType" | "sourceCount">): void {
|
||||||
super.loadTag(source);
|
super.loadTag(source);
|
||||||
(this as Mutable<this>).sourceCount = source.sourceCount;
|
(this as Mutable<this>).sourceCount = source.sourceCount;
|
||||||
}
|
}
|
||||||
@ -1663,7 +1589,6 @@ export function getArenaTag(
|
|||||||
turnCount: number,
|
turnCount: number,
|
||||||
sourceMove: MoveId | undefined,
|
sourceMove: MoveId | undefined,
|
||||||
sourceId: number | undefined,
|
sourceId: number | undefined,
|
||||||
targetIndex?: BattlerIndex,
|
|
||||||
side: ArenaTagSide = ArenaTagSide.BOTH,
|
side: ArenaTagSide = ArenaTagSide.BOTH,
|
||||||
): ArenaTag | null {
|
): ArenaTag | null {
|
||||||
switch (tagType) {
|
switch (tagType) {
|
||||||
@ -1689,14 +1614,6 @@ export function getArenaTag(
|
|||||||
return new SpikesTag(sourceId, side);
|
return new SpikesTag(sourceId, side);
|
||||||
case ArenaTagType.TOXIC_SPIKES:
|
case ArenaTagType.TOXIC_SPIKES:
|
||||||
return new ToxicSpikesTag(sourceId, side);
|
return new ToxicSpikesTag(sourceId, side);
|
||||||
case ArenaTagType.FUTURE_SIGHT:
|
|
||||||
case ArenaTagType.DOOM_DESIRE:
|
|
||||||
if (isNullOrUndefined(targetIndex)) {
|
|
||||||
return null; // If missing target index, no tag is created
|
|
||||||
}
|
|
||||||
return new DelayedAttackTag(tagType, sourceMove, sourceId, targetIndex, side);
|
|
||||||
case ArenaTagType.WISH:
|
|
||||||
return new WishTag(turnCount, sourceId, side);
|
|
||||||
case ArenaTagType.STEALTH_ROCK:
|
case ArenaTagType.STEALTH_ROCK:
|
||||||
return new StealthRockTag(sourceId, side);
|
return new StealthRockTag(sourceId, side);
|
||||||
case ArenaTagType.STICKY_WEB:
|
case ArenaTagType.STICKY_WEB:
|
||||||
@ -1739,16 +1656,12 @@ export function getArenaTag(
|
|||||||
* @param source - An arena tag
|
* @param source - An arena tag
|
||||||
* @returns The valid arena tag
|
* @returns The valid arena tag
|
||||||
*/
|
*/
|
||||||
export function loadArenaTag(source: (ArenaTag | ArenaTagTypeData) & { targetIndex?: BattlerIndex }): ArenaTag {
|
export function loadArenaTag(source: ArenaTag | ArenaTagTypeData | { tagType: ArenaTagType.NONE }): ArenaTag {
|
||||||
|
if (source.tagType === ArenaTagType.NONE) {
|
||||||
|
return new NoneTag();
|
||||||
|
}
|
||||||
const tag =
|
const tag =
|
||||||
getArenaTag(
|
getArenaTag(source.tagType, source.turnCount, source.sourceMove, source.sourceId, source.side) ?? new NoneTag();
|
||||||
source.tagType,
|
|
||||||
source.turnCount,
|
|
||||||
source.sourceMove,
|
|
||||||
source.sourceId,
|
|
||||||
source.targetIndex,
|
|
||||||
source.side,
|
|
||||||
) ?? new NoneTag();
|
|
||||||
tag.loadTag(source);
|
tag.loadTag(source);
|
||||||
return tag;
|
return tag;
|
||||||
}
|
}
|
||||||
@ -1765,9 +1678,6 @@ export type ArenaTagTypeMap = {
|
|||||||
[ArenaTagType.CRAFTY_SHIELD]: CraftyShieldTag;
|
[ArenaTagType.CRAFTY_SHIELD]: CraftyShieldTag;
|
||||||
[ArenaTagType.NO_CRIT]: NoCritTag;
|
[ArenaTagType.NO_CRIT]: NoCritTag;
|
||||||
[ArenaTagType.TOXIC_SPIKES]: ToxicSpikesTag;
|
[ArenaTagType.TOXIC_SPIKES]: ToxicSpikesTag;
|
||||||
[ArenaTagType.FUTURE_SIGHT]: DelayedAttackTag;
|
|
||||||
[ArenaTagType.DOOM_DESIRE]: DelayedAttackTag;
|
|
||||||
[ArenaTagType.WISH]: WishTag;
|
|
||||||
[ArenaTagType.STEALTH_ROCK]: StealthRockTag;
|
[ArenaTagType.STEALTH_ROCK]: StealthRockTag;
|
||||||
[ArenaTagType.STICKY_WEB]: StickyWebTag;
|
[ArenaTagType.STICKY_WEB]: StickyWebTag;
|
||||||
[ArenaTagType.TRICK_ROOM]: TrickRoomTag;
|
[ArenaTagType.TRICK_ROOM]: TrickRoomTag;
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { allMoves } from "#data/data-lists";
|
import { allMoves } from "#data/data-lists";
|
||||||
import { MoveId } from "#enums/move-id";
|
import { MoveId } from "#enums/move-id";
|
||||||
import { SpeciesId } from "#enums/species-id";
|
import { SpeciesId } from "#enums/species-id";
|
||||||
import { toReadableString } from "#utils/common";
|
|
||||||
import { getEnumKeys, getEnumValues } from "#utils/enums";
|
import { getEnumKeys, getEnumValues } from "#utils/enums";
|
||||||
|
import { toTitleCase } from "#utils/strings";
|
||||||
|
|
||||||
export const speciesEggMoves = {
|
export const speciesEggMoves = {
|
||||||
[SpeciesId.BULBASAUR]: [ MoveId.SAPPY_SEED, MoveId.MALIGNANT_CHAIN, MoveId.EARTH_POWER, MoveId.MATCHA_GOTCHA ],
|
[SpeciesId.BULBASAUR]: [ MoveId.SAPPY_SEED, MoveId.MALIGNANT_CHAIN, MoveId.EARTH_POWER, MoveId.MATCHA_GOTCHA ],
|
||||||
@ -617,7 +617,7 @@ function parseEggMoves(content: string): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (eggMoves.every(m => m === MoveId.NONE)) {
|
if (eggMoves.every(m => m === MoveId.NONE)) {
|
||||||
console.warn(`Species ${toReadableString(SpeciesId[species])} could not be parsed, excluding from output...`)
|
console.warn(`Species ${toTitleCase(SpeciesId[species])} could not be parsed, excluding from output...`)
|
||||||
} else {
|
} else {
|
||||||
output += `[SpeciesId.${SpeciesId[species]}]: [ ${eggMoves.map(m => `MoveId.${MoveId[m]}`).join(", ")} ],\n`;
|
output += `[SpeciesId.${SpeciesId[species]}]: [ ${eggMoves.map(m => `MoveId.${MoveId[m]}`).join(", ")} ],\n`;
|
||||||
}
|
}
|
||||||
|
@ -7,8 +7,9 @@ import { AnimBlendType, AnimFocus, AnimFrameTarget, ChargeAnim, CommonAnim } fro
|
|||||||
import { MoveFlags } from "#enums/move-flags";
|
import { MoveFlags } from "#enums/move-flags";
|
||||||
import { MoveId } from "#enums/move-id";
|
import { MoveId } from "#enums/move-id";
|
||||||
import type { Pokemon } from "#field/pokemon";
|
import type { Pokemon } from "#field/pokemon";
|
||||||
import { animationFileName, coerceArray, getFrameMs, isNullOrUndefined, type nil } from "#utils/common";
|
import { coerceArray, getFrameMs, isNullOrUndefined, type nil } from "#utils/common";
|
||||||
import { getEnumKeys, getEnumValues } from "#utils/enums";
|
import { getEnumKeys, getEnumValues } from "#utils/enums";
|
||||||
|
import { toKebabCase } from "#utils/strings";
|
||||||
import Phaser from "phaser";
|
import Phaser from "phaser";
|
||||||
|
|
||||||
export class AnimConfig {
|
export class AnimConfig {
|
||||||
@ -412,7 +413,7 @@ export function initCommonAnims(): Promise<void> {
|
|||||||
const commonAnimId = commonAnimIds[ca];
|
const commonAnimId = commonAnimIds[ca];
|
||||||
commonAnimFetches.push(
|
commonAnimFetches.push(
|
||||||
globalScene
|
globalScene
|
||||||
.cachedFetch(`./battle-anims/common-${commonAnimNames[ca].toLowerCase().replace(/_/g, "-")}.json`)
|
.cachedFetch(`./battle-anims/common-${toKebabCase(commonAnimNames[ca])}.json`)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(cas => commonAnims.set(commonAnimId, new AnimConfig(cas))),
|
.then(cas => commonAnims.set(commonAnimId, new AnimConfig(cas))),
|
||||||
);
|
);
|
||||||
@ -450,7 +451,7 @@ export function initMoveAnim(move: MoveId): Promise<void> {
|
|||||||
|
|
||||||
const fetchAnimAndResolve = (move: MoveId) => {
|
const fetchAnimAndResolve = (move: MoveId) => {
|
||||||
globalScene
|
globalScene
|
||||||
.cachedFetch(`./battle-anims/${animationFileName(move)}.json`)
|
.cachedFetch(`./battle-anims/${toKebabCase(MoveId[move])}.json`)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
const contentType = response.headers.get("content-type");
|
const contentType = response.headers.get("content-type");
|
||||||
if (!response.ok || contentType?.indexOf("application/json") === -1) {
|
if (!response.ok || contentType?.indexOf("application/json") === -1) {
|
||||||
@ -506,7 +507,7 @@ function useDefaultAnim(move: MoveId, defaultMoveAnim: MoveId) {
|
|||||||
* @remarks use {@linkcode useDefaultAnim} to use a default animation
|
* @remarks use {@linkcode useDefaultAnim} to use a default animation
|
||||||
*/
|
*/
|
||||||
function logMissingMoveAnim(move: MoveId, ...optionalParams: any[]) {
|
function logMissingMoveAnim(move: MoveId, ...optionalParams: any[]) {
|
||||||
const moveName = animationFileName(move);
|
const moveName = toKebabCase(MoveId[move]);
|
||||||
console.warn(`Could not load animation file for move '${moveName}'`, ...optionalParams);
|
console.warn(`Could not load animation file for move '${moveName}'`, ...optionalParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -524,7 +525,7 @@ export async function initEncounterAnims(encounterAnim: EncounterAnim | Encounte
|
|||||||
}
|
}
|
||||||
encounterAnimFetches.push(
|
encounterAnimFetches.push(
|
||||||
globalScene
|
globalScene
|
||||||
.cachedFetch(`./battle-anims/encounter-${encounterAnimNames[anim].toLowerCase().replace(/_/g, "-")}.json`)
|
.cachedFetch(`./battle-anims/encounter-${toKebabCase(encounterAnimNames[anim])}.json`)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(cas => encounterAnims.set(anim, new AnimConfig(cas))),
|
.then(cas => encounterAnims.set(anim, new AnimConfig(cas))),
|
||||||
);
|
);
|
||||||
@ -548,7 +549,7 @@ export function initMoveChargeAnim(chargeAnim: ChargeAnim): Promise<void> {
|
|||||||
} else {
|
} else {
|
||||||
chargeAnims.set(chargeAnim, null);
|
chargeAnims.set(chargeAnim, null);
|
||||||
globalScene
|
globalScene
|
||||||
.cachedFetch(`./battle-anims/${ChargeAnim[chargeAnim].toLowerCase().replace(/_/g, "-")}.json`)
|
.cachedFetch(`./battle-anims/${toKebabCase(ChargeAnim[chargeAnim])}.json`)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(ca => {
|
.then(ca => {
|
||||||
if (Array.isArray(ca)) {
|
if (Array.isArray(ca)) {
|
||||||
@ -1405,7 +1406,9 @@ export async function populateAnims() {
|
|||||||
const chargeAnimIds = getEnumValues(ChargeAnim);
|
const chargeAnimIds = getEnumValues(ChargeAnim);
|
||||||
const commonNamePattern = /name: (?:Common:)?(Opp )?(.*)/;
|
const commonNamePattern = /name: (?:Common:)?(Opp )?(.*)/;
|
||||||
const moveNameToId = {};
|
const moveNameToId = {};
|
||||||
|
// Exclude MoveId.NONE;
|
||||||
for (const move of getEnumValues(MoveId).slice(1)) {
|
for (const move of getEnumValues(MoveId).slice(1)) {
|
||||||
|
// KARATE_CHOP => KARATECHOP
|
||||||
const moveName = MoveId[move].toUpperCase().replace(/_/g, "");
|
const moveName = MoveId[move].toUpperCase().replace(/_/g, "");
|
||||||
moveNameToId[moveName] = move;
|
moveNameToId[moveName] = move;
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import { defaultStarterSpecies } from "#app/constants";
|
|||||||
import { globalScene } from "#app/global-scene";
|
import { globalScene } from "#app/global-scene";
|
||||||
import { pokemonEvolutions } from "#balance/pokemon-evolutions";
|
import { pokemonEvolutions } from "#balance/pokemon-evolutions";
|
||||||
import { speciesStarterCosts } from "#balance/starters";
|
import { speciesStarterCosts } from "#balance/starters";
|
||||||
|
import { getEggTierForSpecies } from "#data/egg";
|
||||||
import { pokemonFormChanges } from "#data/pokemon-forms";
|
import { pokemonFormChanges } from "#data/pokemon-forms";
|
||||||
import type { PokemonSpecies } from "#data/pokemon-species";
|
import type { PokemonSpecies } from "#data/pokemon-species";
|
||||||
import { getPokemonSpeciesForm } from "#data/pokemon-species";
|
import { getPokemonSpeciesForm } from "#data/pokemon-species";
|
||||||
@ -11,6 +12,7 @@ import { BattleType } from "#enums/battle-type";
|
|||||||
import { ChallengeType } from "#enums/challenge-type";
|
import { ChallengeType } from "#enums/challenge-type";
|
||||||
import { Challenges } from "#enums/challenges";
|
import { Challenges } from "#enums/challenges";
|
||||||
import { TypeColor, TypeShadow } from "#enums/color";
|
import { TypeColor, TypeShadow } from "#enums/color";
|
||||||
|
import { EggTier } from "#enums/egg-type";
|
||||||
import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves";
|
import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves";
|
||||||
import { ModifierTier } from "#enums/modifier-tier";
|
import { ModifierTier } from "#enums/modifier-tier";
|
||||||
import { MoveId } from "#enums/move-id";
|
import { MoveId } from "#enums/move-id";
|
||||||
@ -28,6 +30,7 @@ import type { DexAttrProps, GameData } from "#system/game-data";
|
|||||||
import { BooleanHolder, type NumberHolder, randSeedItem } from "#utils/common";
|
import { BooleanHolder, type NumberHolder, randSeedItem } from "#utils/common";
|
||||||
import { deepCopy } from "#utils/data";
|
import { deepCopy } from "#utils/data";
|
||||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
||||||
|
import { toCamelCase, toSnakeCase } from "#utils/strings";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
/** A constant for the default max cost of the starting party before a run */
|
/** A constant for the default max cost of the starting party before a run */
|
||||||
@ -68,14 +71,11 @@ export abstract class Challenge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the localisation key for the challenge
|
* Gets the localization key for the challenge
|
||||||
* @returns {@link string} The i18n key for this challenge
|
* @returns The i18n key for this challenge as camel case.
|
||||||
*/
|
*/
|
||||||
geti18nKey(): string {
|
geti18nKey(): string {
|
||||||
return Challenges[this.id]
|
return toCamelCase(Challenges[this.id]);
|
||||||
.split("_")
|
|
||||||
.map((f, i) => (i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()))
|
|
||||||
.join("");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -106,23 +106,22 @@ export abstract class Challenge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the textual representation of a challenge's current value.
|
* Return the textual representation of a challenge's current value.
|
||||||
* @param overrideValue {@link number} The value to check for. If undefined, gets the current value.
|
* @param overrideValue - The value to check for; default {@linkcode this.value}
|
||||||
* @returns {@link string} The localised name for the current value.
|
* @returns The localised text for the current value.
|
||||||
*/
|
*/
|
||||||
getValue(overrideValue?: number): string {
|
getValue(overrideValue: number = this.value): string {
|
||||||
const value = overrideValue ?? this.value;
|
return i18next.t(`challenges:${this.geti18nKey()}.value.${overrideValue}`);
|
||||||
return i18next.t(`challenges:${this.geti18nKey()}.value.${value}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the description of a challenge's current value.
|
* Return the description of a challenge's current value.
|
||||||
* @param overrideValue {@link number} The value to check for. If undefined, gets the current value.
|
* @param overrideValue - The value to check for; default {@linkcode this.value}
|
||||||
* @returns {@link string} The localised description for the current value.
|
* @returns The localised description for the current value.
|
||||||
*/
|
*/
|
||||||
getDescription(overrideValue?: number): string {
|
// TODO: Do we need an override value here? it's currently unused
|
||||||
const value = overrideValue ?? this.value;
|
getDescription(overrideValue: number = this.value): string {
|
||||||
return `${i18next.t([`challenges:${this.geti18nKey()}.desc.${value}`, `challenges:${this.geti18nKey()}.desc`])}`;
|
return `${i18next.t([`challenges:${this.geti18nKey()}.desc.${overrideValue}`, `challenges:${this.geti18nKey()}.desc`])}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -649,31 +648,19 @@ export class SingleGenerationChallenge extends Challenge {
|
|||||||
return this.value > 0 ? 1 : 0;
|
return this.value > 0 ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
getValue(overrideValue: number = this.value): string {
|
||||||
* Returns the textual representation of a challenge's current value.
|
if (overrideValue === 0) {
|
||||||
* @param {value} overrideValue The value to check for. If undefined, gets the current value.
|
|
||||||
* @returns {string} The localised name for the current value.
|
|
||||||
*/
|
|
||||||
getValue(overrideValue?: number): string {
|
|
||||||
const value = overrideValue ?? this.value;
|
|
||||||
if (value === 0) {
|
|
||||||
return i18next.t("settings:off");
|
return i18next.t("settings:off");
|
||||||
}
|
}
|
||||||
return i18next.t(`starterSelectUiHandler:gen${value}`);
|
return i18next.t(`starterSelectUiHandler:gen${overrideValue}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
getDescription(overrideValue: number = this.value): string {
|
||||||
* Returns the description of a challenge's current value.
|
if (overrideValue === 0) {
|
||||||
* @param {value} overrideValue The value to check for. If undefined, gets the current value.
|
|
||||||
* @returns {string} The localised description for the current value.
|
|
||||||
*/
|
|
||||||
getDescription(overrideValue?: number): string {
|
|
||||||
const value = overrideValue ?? this.value;
|
|
||||||
if (value === 0) {
|
|
||||||
return i18next.t("challenges:singleGeneration.desc_default");
|
return i18next.t("challenges:singleGeneration.desc_default");
|
||||||
}
|
}
|
||||||
return i18next.t("challenges:singleGeneration.desc", {
|
return i18next.t("challenges:singleGeneration.desc", {
|
||||||
gen: i18next.t(`challenges:singleGeneration.gen_${value}`),
|
gen: i18next.t(`challenges:singleGeneration.gen_${overrideValue}`),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -741,29 +728,13 @@ export class SingleTypeChallenge extends Challenge {
|
|||||||
return this.value > 0 ? 1 : 0;
|
return this.value > 0 ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
getValue(overrideValue: number = this.value): string {
|
||||||
* Returns the textual representation of a challenge's current value.
|
return toSnakeCase(PokemonType[overrideValue - 1]);
|
||||||
* @param {value} overrideValue The value to check for. If undefined, gets the current value.
|
|
||||||
* @returns {string} The localised name for the current value.
|
|
||||||
*/
|
|
||||||
getValue(overrideValue?: number): string {
|
|
||||||
if (overrideValue === undefined) {
|
|
||||||
overrideValue = this.value;
|
|
||||||
}
|
|
||||||
return PokemonType[this.value - 1].toLowerCase();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
getDescription(overrideValue: number = this.value): string {
|
||||||
* Returns the description of a challenge's current value.
|
const type = i18next.t(`pokemonInfo:Type.${PokemonType[overrideValue - 1]}`);
|
||||||
* @param {value} overrideValue The value to check for. If undefined, gets the current value.
|
const typeColor = `[color=${TypeColor[PokemonType[overrideValue - 1]]}][shadow=${TypeShadow[PokemonType[this.value - 1]]}]${type}[/shadow][/color]`;
|
||||||
* @returns {string} The localised description for the current value.
|
|
||||||
*/
|
|
||||||
getDescription(overrideValue?: number): string {
|
|
||||||
if (overrideValue === undefined) {
|
|
||||||
overrideValue = this.value;
|
|
||||||
}
|
|
||||||
const type = i18next.t(`pokemonInfo:Type.${PokemonType[this.value - 1]}`);
|
|
||||||
const typeColor = `[color=${TypeColor[PokemonType[this.value - 1]]}][shadow=${TypeShadow[PokemonType[this.value - 1]]}]${type}[/shadow][/color]`;
|
|
||||||
const defaultDesc = i18next.t("challenges:singleType.desc_default");
|
const defaultDesc = i18next.t("challenges:singleType.desc_default");
|
||||||
const typeDesc = i18next.t("challenges:singleType.desc", {
|
const typeDesc = i18next.t("challenges:singleType.desc", {
|
||||||
type: typeColor,
|
type: typeColor,
|
||||||
@ -784,11 +755,14 @@ export class SingleTypeChallenge extends Challenge {
|
|||||||
*/
|
*/
|
||||||
export class FreshStartChallenge extends Challenge {
|
export class FreshStartChallenge extends Challenge {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(Challenges.FRESH_START, 1);
|
super(Challenges.FRESH_START, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
applyStarterChoice(pokemon: PokemonSpecies, valid: BooleanHolder): boolean {
|
applyStarterChoice(pokemon: PokemonSpecies, valid: BooleanHolder): boolean {
|
||||||
if (!defaultStarterSpecies.includes(pokemon.speciesId)) {
|
if (
|
||||||
|
(this.value === 1 && !defaultStarterSpecies.includes(pokemon.speciesId)) ||
|
||||||
|
(this.value === 2 && getEggTierForSpecies(pokemon) >= EggTier.EPIC)
|
||||||
|
) {
|
||||||
valid.value = false;
|
valid.value = false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -796,15 +770,12 @@ export class FreshStartChallenge extends Challenge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
applyStarterCost(species: SpeciesId, cost: NumberHolder): boolean {
|
applyStarterCost(species: SpeciesId, cost: NumberHolder): boolean {
|
||||||
if (defaultStarterSpecies.includes(species)) {
|
cost.value = speciesStarterCosts[species];
|
||||||
cost.value = speciesStarterCosts[species];
|
return true;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
applyStarterModify(pokemon: Pokemon): boolean {
|
applyStarterModify(pokemon: Pokemon): boolean {
|
||||||
pokemon.abilityIndex = 0; // Always base ability, not hidden ability
|
pokemon.abilityIndex = pokemon.abilityIndex % 2; // Always base ability, if you set it to hidden it wraps to first ability
|
||||||
pokemon.passive = false; // Passive isn't unlocked
|
pokemon.passive = false; // Passive isn't unlocked
|
||||||
pokemon.nature = Nature.HARDY; // Neutral nature
|
pokemon.nature = Nature.HARDY; // Neutral nature
|
||||||
pokemon.moveset = pokemon.species
|
pokemon.moveset = pokemon.species
|
||||||
@ -816,7 +787,22 @@ export class FreshStartChallenge extends Challenge {
|
|||||||
pokemon.luck = 0; // No luck
|
pokemon.luck = 0; // No luck
|
||||||
pokemon.shiny = false; // Not shiny
|
pokemon.shiny = false; // Not shiny
|
||||||
pokemon.variant = 0; // Not shiny
|
pokemon.variant = 0; // Not shiny
|
||||||
pokemon.formIndex = 0; // Froakie should be base form
|
if (pokemon.species.speciesId === SpeciesId.ZYGARDE && pokemon.formIndex >= 2) {
|
||||||
|
pokemon.formIndex -= 2; // Sets 10%-PC to 10%-AB and 50%-PC to 50%-AB
|
||||||
|
} else if (
|
||||||
|
pokemon.formIndex > 0 &&
|
||||||
|
[
|
||||||
|
SpeciesId.PIKACHU,
|
||||||
|
SpeciesId.EEVEE,
|
||||||
|
SpeciesId.PICHU,
|
||||||
|
SpeciesId.ROTOM,
|
||||||
|
SpeciesId.MELOETTA,
|
||||||
|
SpeciesId.FROAKIE,
|
||||||
|
SpeciesId.ROCKRUFF,
|
||||||
|
].includes(pokemon.species.speciesId)
|
||||||
|
) {
|
||||||
|
pokemon.formIndex = 0; // These mons are set to form 0 because they're meant to be unlocks or mid-run form changes
|
||||||
|
}
|
||||||
pokemon.ivs = [15, 15, 15, 15, 15, 15]; // Default IVs of 15 for all stats (Updated to 15 from 10 in 1.2.0)
|
pokemon.ivs = [15, 15, 15, 15, 15, 15]; // Default IVs of 15 for all stats (Updated to 15 from 10 in 1.2.0)
|
||||||
pokemon.teraType = pokemon.species.type1; // Always primary tera type
|
pokemon.teraType = pokemon.species.type1; // Always primary tera type
|
||||||
return true;
|
return true;
|
||||||
@ -902,13 +888,7 @@ export class LowerStarterMaxCostChallenge extends Challenge {
|
|||||||
super(Challenges.LOWER_MAX_STARTER_COST, 9);
|
super(Challenges.LOWER_MAX_STARTER_COST, 9);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
getValue(overrideValue: number = this.value): string {
|
||||||
* @override
|
|
||||||
*/
|
|
||||||
getValue(overrideValue?: number): string {
|
|
||||||
if (overrideValue === undefined) {
|
|
||||||
overrideValue = this.value;
|
|
||||||
}
|
|
||||||
return (DEFAULT_PARTY_MAX_COST - overrideValue).toString();
|
return (DEFAULT_PARTY_MAX_COST - overrideValue).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -936,13 +916,7 @@ export class LowerStarterPointsChallenge extends Challenge {
|
|||||||
super(Challenges.LOWER_STARTER_POINTS, 9);
|
super(Challenges.LOWER_STARTER_POINTS, 9);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
getValue(overrideValue: number = this.value): string {
|
||||||
* @override
|
|
||||||
*/
|
|
||||||
getValue(overrideValue?: number): string {
|
|
||||||
if (overrideValue === undefined) {
|
|
||||||
overrideValue = this.value;
|
|
||||||
}
|
|
||||||
return (DEFAULT_PARTY_MAX_COST - overrideValue).toString();
|
return (DEFAULT_PARTY_MAX_COST - overrideValue).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { BattleSpec } from "#enums/battle-spec";
|
import { BattleSpec } from "#enums/battle-spec";
|
||||||
import { TrainerType } from "#enums/trainer-type";
|
import { TrainerType } from "#enums/trainer-type";
|
||||||
import { trainerConfigs } from "#trainers/trainer-config";
|
import { trainerConfigs } from "#trainers/trainer-config";
|
||||||
|
import { capitalizeFirstLetter } from "#utils/strings";
|
||||||
|
|
||||||
export interface TrainerTypeMessages {
|
export interface TrainerTypeMessages {
|
||||||
encounter?: string | string[];
|
encounter?: string | string[];
|
||||||
@ -1755,8 +1756,7 @@ export function initTrainerTypeDialogue(): void {
|
|||||||
trainerConfigs[trainerType][`${messageType}Messages`] = messages[0][messageType];
|
trainerConfigs[trainerType][`${messageType}Messages`] = messages[0][messageType];
|
||||||
}
|
}
|
||||||
if (messages.length > 1) {
|
if (messages.length > 1) {
|
||||||
trainerConfigs[trainerType][`female${messageType.slice(0, 1).toUpperCase()}${messageType.slice(1)}Messages`] =
|
trainerConfigs[trainerType][`female${capitalizeFirstLetter(messageType)}Messages`] = messages[1][messageType];
|
||||||
messages[1][messageType];
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
trainerConfigs[trainerType][`${messageType}Messages`] = messages[messageType];
|
trainerConfigs[trainerType][`${messageType}Messages`] = messages[messageType];
|
||||||
|
@ -25,6 +25,7 @@ import { getBerryEffectFunc } from "#data/berry";
|
|||||||
import { applyChallenges } from "#data/challenge";
|
import { applyChallenges } from "#data/challenge";
|
||||||
import { allAbilities, allMoves } from "#data/data-lists";
|
import { allAbilities, allMoves } from "#data/data-lists";
|
||||||
import { SpeciesFormChangeRevertWeatherFormTrigger } from "#data/form-change-triggers";
|
import { SpeciesFormChangeRevertWeatherFormTrigger } from "#data/form-change-triggers";
|
||||||
|
import { DelayedAttackTag } from "#data/positional-tags/positional-tag";
|
||||||
import {
|
import {
|
||||||
getNonVolatileStatusEffects,
|
getNonVolatileStatusEffects,
|
||||||
getStatusEffectHealText,
|
getStatusEffectHealText,
|
||||||
@ -54,6 +55,7 @@ import { MoveFlags } from "#enums/move-flags";
|
|||||||
import { MoveTarget } from "#enums/move-target";
|
import { MoveTarget } from "#enums/move-target";
|
||||||
import { MultiHitType } from "#enums/multi-hit-type";
|
import { MultiHitType } from "#enums/multi-hit-type";
|
||||||
import { PokemonType } from "#enums/pokemon-type";
|
import { PokemonType } from "#enums/pokemon-type";
|
||||||
|
import { PositionalTagType } from "#enums/positional-tag-type";
|
||||||
import { SpeciesId } from "#enums/species-id";
|
import { SpeciesId } from "#enums/species-id";
|
||||||
import {
|
import {
|
||||||
BATTLE_STATS,
|
BATTLE_STATS,
|
||||||
@ -87,8 +89,9 @@ import type { AttackMoveResult } from "#types/attack-move-result";
|
|||||||
import type { Localizable } from "#types/locales";
|
import type { Localizable } from "#types/locales";
|
||||||
import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString } from "#types/move-types";
|
import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString } from "#types/move-types";
|
||||||
import type { TurnMove } from "#types/turn-move";
|
import type { TurnMove } from "#types/turn-move";
|
||||||
import { BooleanHolder, type Constructor, isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue, toReadableString } from "#utils/common";
|
import { BooleanHolder, type Constructor, isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common";
|
||||||
import { getEnumValues } from "#utils/enums";
|
import { getEnumValues } from "#utils/enums";
|
||||||
|
import { toTitleCase } from "#utils/strings";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -422,9 +425,8 @@ export abstract class Move implements Localizable {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the {@linkcode MoveFlags.MAKES_CONTACT} flag for the calling Move
|
* Sets the {@linkcode MoveFlags.MAKES_CONTACT} flag for the calling Move
|
||||||
* @param setFlag Default `true`, set to `false` if the move doesn't make contact
|
* @param setFlag - Whether the move should make contact; default `true`
|
||||||
* @see {@linkcode AbilityId.STATIC}
|
* @returns `this`
|
||||||
* @returns The {@linkcode Move} that called this function
|
|
||||||
*/
|
*/
|
||||||
makesContact(setFlag: boolean = true): this {
|
makesContact(setFlag: boolean = true): this {
|
||||||
this.setFlag(MoveFlags.MAKES_CONTACT, setFlag);
|
this.setFlag(MoveFlags.MAKES_CONTACT, setFlag);
|
||||||
@ -3122,54 +3124,110 @@ export class OverrideMoveEffectAttr extends MoveAttr {
|
|||||||
* Its sole purpose is to ensure that typescript is able to properly narrow when the `is` method is called.
|
* Its sole purpose is to ensure that typescript is able to properly narrow when the `is` method is called.
|
||||||
*/
|
*/
|
||||||
declare private _: never;
|
declare private _: never;
|
||||||
override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
/**
|
||||||
|
* Apply the move attribute to override other effects of this move.
|
||||||
|
* @param user - The {@linkcode Pokemon} using the move
|
||||||
|
* @param target - The {@linkcode Pokemon} targeted by the move
|
||||||
|
* @param move - The {@linkcode Move} being used
|
||||||
|
* @param args -
|
||||||
|
* `[0]`: A {@linkcode BooleanHolder} containing whether move effects were successfully overridden; should be set to `true` on success \
|
||||||
|
* `[1]`: The {@linkcode MoveUseMode} dictating how this move was used.
|
||||||
|
* @returns `true` if the move effect was successfully overridden.
|
||||||
|
*/
|
||||||
|
public override apply(_user: Pokemon, _target: Pokemon, _move: Move, _args: [overridden: BooleanHolder, useMode: MoveUseMode]): boolean {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Abstract class for moves that add {@linkcode PositionalTag}s to the field. */
|
||||||
|
abstract class AddPositionalTagAttr extends OverrideMoveEffectAttr {
|
||||||
|
protected abstract readonly tagType: PositionalTagType;
|
||||||
|
|
||||||
|
public override getCondition(): MoveConditionFunc {
|
||||||
|
// Check the arena if another similar positional tag is active and affecting the same slot
|
||||||
|
return (_user, target, move) => globalScene.arena.positionalTagManager.canAddTag(this.tagType, target.getBattlerIndex())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attack Move that doesn't hit the turn it is played and doesn't allow for multiple
|
* Attribute to implement delayed attacks, such as {@linkcode MoveId.FUTURE_SIGHT} or {@linkcode MoveId.DOOM_DESIRE}.
|
||||||
* uses on the same target. Examples are Future Sight or Doom Desire.
|
* Delays the attack's effect with a {@linkcode DelayedAttackTag},
|
||||||
* @extends OverrideMoveEffectAttr
|
* activating against the given slot after the given turn count has elapsed.
|
||||||
* @param tagType The {@linkcode ArenaTagType} that will be placed on the field when the move is used
|
|
||||||
* @param chargeAnim The {@linkcode ChargeAnim | Charging Animation} used for the move
|
|
||||||
* @param chargeText The text to display when the move is used
|
|
||||||
*/
|
*/
|
||||||
export class DelayedAttackAttr extends OverrideMoveEffectAttr {
|
export class DelayedAttackAttr extends OverrideMoveEffectAttr {
|
||||||
public tagType: ArenaTagType;
|
|
||||||
public chargeAnim: ChargeAnim;
|
public chargeAnim: ChargeAnim;
|
||||||
private chargeText: string;
|
private chargeText: string;
|
||||||
|
|
||||||
constructor(tagType: ArenaTagType, chargeAnim: ChargeAnim, chargeText: string) {
|
/**
|
||||||
|
* @param chargeAnim - The {@linkcode ChargeAnim | charging animation} used for the move's charging phase.
|
||||||
|
* @param chargeKey - The `i18next` locales **key** to show when the delayed attack is used.
|
||||||
|
* In the displayed text, `{{pokemonName}}` will be populated with the user's name.
|
||||||
|
*/
|
||||||
|
constructor(chargeAnim: ChargeAnim, chargeKey: string) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.tagType = tagType;
|
|
||||||
this.chargeAnim = chargeAnim;
|
this.chargeAnim = chargeAnim;
|
||||||
this.chargeText = chargeText;
|
this.chargeText = chargeKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
public override apply(user: Pokemon, target: Pokemon, move: Move, args: [overridden: BooleanHolder, useMode: MoveUseMode]): boolean {
|
||||||
// Edge case for the move applied on a pokemon that has fainted
|
const useMode = args[1];
|
||||||
if (!target) {
|
if (useMode === MoveUseMode.DELAYED_ATTACK) {
|
||||||
return true;
|
// don't trigger if already queueing an indirect attack
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const overridden = args[0] as BooleanHolder;
|
const overridden = args[0];
|
||||||
const virtual = args[1] as boolean;
|
overridden.value = true;
|
||||||
|
|
||||||
if (!virtual) {
|
// Display the move animation to foresee an attack
|
||||||
overridden.value = true;
|
globalScene.phaseManager.unshiftNew("MoveAnimPhase", new MoveChargeAnim(this.chargeAnim, move.id, user));
|
||||||
globalScene.phaseManager.unshiftNew("MoveAnimPhase", new MoveChargeAnim(this.chargeAnim, move.id, user));
|
globalScene.phaseManager.queueMessage(
|
||||||
globalScene.phaseManager.queueMessage(this.chargeText.replace("{TARGET}", getPokemonNameWithAffix(target)).replace("{USER}", getPokemonNameWithAffix(user)));
|
i18next.t(
|
||||||
user.pushMoveHistory({ move: move.id, targets: [ target.getBattlerIndex() ], result: MoveResult.OTHER, useMode: MoveUseMode.NORMAL });
|
this.chargeText,
|
||||||
const side = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
|
{ pokemonName: getPokemonNameWithAffix(user) }
|
||||||
globalScene.arena.addTag(this.tagType, 3, move.id, user.id, side, false, target.getBattlerIndex());
|
)
|
||||||
} else {
|
)
|
||||||
globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:tookMoveAttack", { pokemonName: getPokemonNameWithAffix(globalScene.getPokemonById(target.id) ?? undefined), moveName: move.name }));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
user.pushMoveHistory({move: move.id, targets: [target.getBattlerIndex()], result: MoveResult.OTHER, useMode, turn: globalScene.currentBattle.turn})
|
||||||
|
user.pushMoveHistory({move: move.id, targets: [target.getBattlerIndex()], result: MoveResult.OTHER, useMode, turn: globalScene.currentBattle.turn})
|
||||||
|
// Queue up an attack on the given slot.
|
||||||
|
globalScene.arena.positionalTagManager.addTag<PositionalTagType.DELAYED_ATTACK>({
|
||||||
|
tagType: PositionalTagType.DELAYED_ATTACK,
|
||||||
|
sourceId: user.id,
|
||||||
|
targetIndex: target.getBattlerIndex(),
|
||||||
|
sourceMove: move.id,
|
||||||
|
turnCount: 3
|
||||||
|
})
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override getCondition(): MoveConditionFunc {
|
||||||
|
// Check the arena if another similar attack is active and affecting the same slot
|
||||||
|
return (_user, target) => globalScene.arena.positionalTagManager.canAddTag(PositionalTagType.DELAYED_ATTACK, target.getBattlerIndex())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attribute to queue a {@linkcode WishTag} to activate in 2 turns.
|
||||||
|
* The tag whill heal
|
||||||
|
*/
|
||||||
|
export class WishAttr extends MoveEffectAttr {
|
||||||
|
public override apply(user: Pokemon, target: Pokemon, _move: Move): boolean {
|
||||||
|
globalScene.arena.positionalTagManager.addTag<PositionalTagType.WISH>({
|
||||||
|
tagType: PositionalTagType.WISH,
|
||||||
|
healHp: toDmgValue(user.getMaxHp() / 2),
|
||||||
|
targetIndex: target.getBattlerIndex(),
|
||||||
|
turnCount: 2,
|
||||||
|
pokemonName: getPokemonNameWithAffix(user),
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override getCondition(): MoveConditionFunc {
|
||||||
|
// Check the arena if another wish is active and affecting the same slot
|
||||||
|
return (_user, target) => globalScene.arena.positionalTagManager.canAddTag(PositionalTagType.WISH, target.getBattlerIndex())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -3187,8 +3245,8 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr {
|
|||||||
* @param user the {@linkcode Pokemon} using this move
|
* @param user the {@linkcode Pokemon} using this move
|
||||||
* @param target n/a
|
* @param target n/a
|
||||||
* @param move the {@linkcode Move} being used
|
* @param move the {@linkcode Move} being used
|
||||||
* @param args
|
* @param args -
|
||||||
* - [0] a {@linkcode BooleanHolder} indicating whether the move's base
|
* `[0]`: A {@linkcode BooleanHolder} indicating whether the move's base
|
||||||
* effects should be overridden this turn.
|
* effects should be overridden this turn.
|
||||||
* @returns `true` if base move effects were overridden; `false` otherwise
|
* @returns `true` if base move effects were overridden; `false` otherwise
|
||||||
*/
|
*/
|
||||||
@ -3575,8 +3633,7 @@ export class CutHpStatStageBoostAttr extends StatStageChangeAttr {
|
|||||||
/**
|
/**
|
||||||
* Attribute implementing the stat boosting effect of {@link https://bulbapedia.bulbagarden.net/wiki/Order_Up_(move) | Order Up}.
|
* Attribute implementing the stat boosting effect of {@link https://bulbapedia.bulbagarden.net/wiki/Order_Up_(move) | Order Up}.
|
||||||
* If the user has a Pokemon with {@link https://bulbapedia.bulbagarden.net/wiki/Commander_(Ability) | Commander} in their mouth,
|
* If the user has a Pokemon with {@link https://bulbapedia.bulbagarden.net/wiki/Commander_(Ability) | Commander} in their mouth,
|
||||||
* one of the user's stats are increased by 1 stage, depending on the "commanding" Pokemon's form. This effect does not respect
|
* one of the user's stats are increased by 1 stage, depending on the "commanding" Pokemon's form.
|
||||||
* effect chance, but Order Up itself may be boosted by Sheer Force.
|
|
||||||
*/
|
*/
|
||||||
export class OrderUpStatBoostAttr extends MoveEffectAttr {
|
export class OrderUpStatBoostAttr extends MoveEffectAttr {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -8137,7 +8194,7 @@ export class ResistLastMoveTypeAttr extends MoveEffectAttr {
|
|||||||
}
|
}
|
||||||
const type = validTypes[user.randBattleSeedInt(validTypes.length)];
|
const type = validTypes[user.randBattleSeedInt(validTypes.length)];
|
||||||
user.summonData.types = [ type ];
|
user.summonData.types = [ type ];
|
||||||
globalScene.phaseManager.queueMessage(i18next.t("battle:transformedIntoType", { pokemonName: getPokemonNameWithAffix(user), type: toReadableString(PokemonType[type]) }));
|
globalScene.phaseManager.queueMessage(i18next.t("battle:transformedIntoType", { pokemonName: getPokemonNameWithAffix(user), type: toTitleCase(PokemonType[type]) }));
|
||||||
user.updateInfo();
|
user.updateInfo();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -9204,9 +9261,12 @@ export function initMoves() {
|
|||||||
.attr(StatStageChangeAttr, [ Stat.SPDEF ], -1)
|
.attr(StatStageChangeAttr, [ Stat.SPDEF ], -1)
|
||||||
.ballBombMove(),
|
.ballBombMove(),
|
||||||
new AttackMove(MoveId.FUTURE_SIGHT, PokemonType.PSYCHIC, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 2)
|
new AttackMove(MoveId.FUTURE_SIGHT, PokemonType.PSYCHIC, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 2)
|
||||||
.partial() // cannot be used on multiple Pokemon on the same side in a double battle, hits immediately when called by Metronome/etc, should not apply abilities or held items if user is off the field
|
.attr(DelayedAttackAttr, ChargeAnim.FUTURE_SIGHT_CHARGING, "moveTriggers:foresawAnAttack")
|
||||||
.ignoresProtect()
|
.ignoresProtect()
|
||||||
.attr(DelayedAttackAttr, ArenaTagType.FUTURE_SIGHT, ChargeAnim.FUTURE_SIGHT_CHARGING, i18next.t("moveTriggers:foresawAnAttack", { pokemonName: "{USER}" })),
|
/*
|
||||||
|
* Should not apply abilities or held items if user is off the field
|
||||||
|
*/
|
||||||
|
.edgeCase(),
|
||||||
new AttackMove(MoveId.ROCK_SMASH, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 40, 100, 15, 50, 0, 2)
|
new AttackMove(MoveId.ROCK_SMASH, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 40, 100, 15, 50, 0, 2)
|
||||||
.attr(StatStageChangeAttr, [ Stat.DEF ], -1),
|
.attr(StatStageChangeAttr, [ Stat.DEF ], -1),
|
||||||
new AttackMove(MoveId.WHIRLPOOL, PokemonType.WATER, MoveCategory.SPECIAL, 35, 85, 15, -1, 0, 2)
|
new AttackMove(MoveId.WHIRLPOOL, PokemonType.WATER, MoveCategory.SPECIAL, 35, 85, 15, -1, 0, 2)
|
||||||
@ -9227,7 +9287,7 @@ export function initMoves() {
|
|||||||
new SelfStatusMove(MoveId.STOCKPILE, PokemonType.NORMAL, -1, 20, -1, 0, 3)
|
new SelfStatusMove(MoveId.STOCKPILE, PokemonType.NORMAL, -1, 20, -1, 0, 3)
|
||||||
.condition(user => (user.getTag(StockpilingTag)?.stockpiledCount ?? 0) < 3)
|
.condition(user => (user.getTag(StockpilingTag)?.stockpiledCount ?? 0) < 3)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.STOCKPILING, true),
|
.attr(AddBattlerTagAttr, BattlerTagType.STOCKPILING, true),
|
||||||
new AttackMove(MoveId.SPIT_UP, PokemonType.NORMAL, MoveCategory.SPECIAL, -1, -1, 10, -1, 0, 3)
|
new AttackMove(MoveId.SPIT_UP, PokemonType.NORMAL, MoveCategory.SPECIAL, -1, 100, 10, -1, 0, 3)
|
||||||
.condition(hasStockpileStacksCondition)
|
.condition(hasStockpileStacksCondition)
|
||||||
.attr(SpitUpPowerAttr, 100)
|
.attr(SpitUpPowerAttr, 100)
|
||||||
.attr(RemoveBattlerTagAttr, [ BattlerTagType.STOCKPILING ], true),
|
.attr(RemoveBattlerTagAttr, [ BattlerTagType.STOCKPILING ], true),
|
||||||
@ -9292,8 +9352,8 @@ export function initMoves() {
|
|||||||
.ignoresSubstitute()
|
.ignoresSubstitute()
|
||||||
.attr(AbilityCopyAttr),
|
.attr(AbilityCopyAttr),
|
||||||
new SelfStatusMove(MoveId.WISH, PokemonType.NORMAL, -1, 10, -1, 0, 3)
|
new SelfStatusMove(MoveId.WISH, PokemonType.NORMAL, -1, 10, -1, 0, 3)
|
||||||
.triageMove()
|
.attr(WishAttr)
|
||||||
.attr(AddArenaTagAttr, ArenaTagType.WISH, 2, true),
|
.triageMove(),
|
||||||
new SelfStatusMove(MoveId.ASSIST, PokemonType.NORMAL, -1, 20, -1, 0, 3)
|
new SelfStatusMove(MoveId.ASSIST, PokemonType.NORMAL, -1, 20, -1, 0, 3)
|
||||||
.attr(RandomMovesetMoveAttr, invalidAssistMoves, true),
|
.attr(RandomMovesetMoveAttr, invalidAssistMoves, true),
|
||||||
new SelfStatusMove(MoveId.INGRAIN, PokemonType.GRASS, -1, 20, -1, 0, 3)
|
new SelfStatusMove(MoveId.INGRAIN, PokemonType.GRASS, -1, 20, -1, 0, 3)
|
||||||
@ -9470,7 +9530,7 @@ export function initMoves() {
|
|||||||
new AttackMove(MoveId.SAND_TOMB, PokemonType.GROUND, MoveCategory.PHYSICAL, 35, 85, 15, -1, 0, 3)
|
new AttackMove(MoveId.SAND_TOMB, PokemonType.GROUND, MoveCategory.PHYSICAL, 35, 85, 15, -1, 0, 3)
|
||||||
.attr(TrapAttr, BattlerTagType.SAND_TOMB)
|
.attr(TrapAttr, BattlerTagType.SAND_TOMB)
|
||||||
.makesContact(false),
|
.makesContact(false),
|
||||||
new AttackMove(MoveId.SHEER_COLD, PokemonType.ICE, MoveCategory.SPECIAL, 200, 20, 5, -1, 0, 3)
|
new AttackMove(MoveId.SHEER_COLD, PokemonType.ICE, MoveCategory.SPECIAL, 200, 30, 5, -1, 0, 3)
|
||||||
.attr(IceNoEffectTypeAttr)
|
.attr(IceNoEffectTypeAttr)
|
||||||
.attr(OneHitKOAttr)
|
.attr(OneHitKOAttr)
|
||||||
.attr(SheerColdAccuracyAttr),
|
.attr(SheerColdAccuracyAttr),
|
||||||
@ -9542,9 +9602,12 @@ export function initMoves() {
|
|||||||
.attr(ConfuseAttr)
|
.attr(ConfuseAttr)
|
||||||
.pulseMove(),
|
.pulseMove(),
|
||||||
new AttackMove(MoveId.DOOM_DESIRE, PokemonType.STEEL, MoveCategory.SPECIAL, 140, 100, 5, -1, 0, 3)
|
new AttackMove(MoveId.DOOM_DESIRE, PokemonType.STEEL, MoveCategory.SPECIAL, 140, 100, 5, -1, 0, 3)
|
||||||
.partial() // cannot be used on multiple Pokemon on the same side in a double battle, hits immediately when called by Metronome/etc, should not apply abilities or held items if user is off the field
|
.attr(DelayedAttackAttr, ChargeAnim.DOOM_DESIRE_CHARGING, "moveTriggers:choseDoomDesireAsDestiny")
|
||||||
.ignoresProtect()
|
.ignoresProtect()
|
||||||
.attr(DelayedAttackAttr, ArenaTagType.DOOM_DESIRE, ChargeAnim.DOOM_DESIRE_CHARGING, i18next.t("moveTriggers:choseDoomDesireAsDestiny", { pokemonName: "{USER}" })),
|
/*
|
||||||
|
* Should not apply abilities or held items if user is off the field
|
||||||
|
*/
|
||||||
|
.edgeCase(),
|
||||||
new AttackMove(MoveId.PSYCHO_BOOST, PokemonType.PSYCHIC, MoveCategory.SPECIAL, 140, 90, 5, -1, 0, 3)
|
new AttackMove(MoveId.PSYCHO_BOOST, PokemonType.PSYCHIC, MoveCategory.SPECIAL, 140, 90, 5, -1, 0, 3)
|
||||||
.attr(StatStageChangeAttr, [ Stat.SPATK ], -2, true),
|
.attr(StatStageChangeAttr, [ Stat.SPATK ], -2, true),
|
||||||
new SelfStatusMove(MoveId.ROOST, PokemonType.FLYING, -1, 5, -1, 0, 4)
|
new SelfStatusMove(MoveId.ROOST, PokemonType.FLYING, -1, 5, -1, 0, 4)
|
||||||
@ -10392,7 +10455,7 @@ export function initMoves() {
|
|||||||
.attr(RemoveBattlerTagAttr, [ BattlerTagType.FLYING, BattlerTagType.FLOATING, BattlerTagType.TELEKINESIS ])
|
.attr(RemoveBattlerTagAttr, [ BattlerTagType.FLYING, BattlerTagType.FLOATING, BattlerTagType.TELEKINESIS ])
|
||||||
.makesContact(false)
|
.makesContact(false)
|
||||||
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
||||||
new AttackMove(MoveId.THOUSAND_WAVES, PokemonType.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6)
|
new AttackMove(MoveId.THOUSAND_WAVES, PokemonType.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, 100, 0, 6)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, false, 1, 1, true)
|
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, false, 1, 1, true)
|
||||||
.makesContact(false)
|
.makesContact(false)
|
||||||
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
.target(MoveTarget.ALL_NEAR_ENEMIES),
|
||||||
@ -10799,7 +10862,7 @@ export function initMoves() {
|
|||||||
new SelfStatusMove(MoveId.NO_RETREAT, PokemonType.FIGHTING, -1, 5, -1, 0, 8)
|
new SelfStatusMove(MoveId.NO_RETREAT, PokemonType.FIGHTING, -1, 5, -1, 0, 8)
|
||||||
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true)
|
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.NO_RETREAT, true, false)
|
.attr(AddBattlerTagAttr, BattlerTagType.NO_RETREAT, true, false)
|
||||||
.condition((user, target, move) => user.getTag(TrappedTag)?.sourceMove !== MoveId.NO_RETREAT), // fails if the user is currently trapped by No Retreat
|
.condition((user, target, move) => user.getTag(TrappedTag)?.tagType !== BattlerTagType.NO_RETREAT), // fails if the user is currently trapped by No Retreat
|
||||||
new StatusMove(MoveId.TAR_SHOT, PokemonType.ROCK, 100, 15, -1, 0, 8)
|
new StatusMove(MoveId.TAR_SHOT, PokemonType.ROCK, 100, 15, -1, 0, 8)
|
||||||
.attr(StatStageChangeAttr, [ Stat.SPD ], -1)
|
.attr(StatStageChangeAttr, [ Stat.SPD ], -1)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.TAR_SHOT, false)
|
.attr(AddBattlerTagAttr, BattlerTagType.TAR_SHOT, false)
|
||||||
@ -10927,7 +10990,8 @@ export function initMoves() {
|
|||||||
new StatusMove(MoveId.LIFE_DEW, PokemonType.WATER, -1, 10, -1, 0, 8)
|
new StatusMove(MoveId.LIFE_DEW, PokemonType.WATER, -1, 10, -1, 0, 8)
|
||||||
.attr(HealAttr, 0.25, true, false)
|
.attr(HealAttr, 0.25, true, false)
|
||||||
.target(MoveTarget.USER_AND_ALLIES)
|
.target(MoveTarget.USER_AND_ALLIES)
|
||||||
.ignoresProtect(),
|
.ignoresProtect()
|
||||||
|
.triageMove(),
|
||||||
new SelfStatusMove(MoveId.OBSTRUCT, PokemonType.DARK, 100, 10, -1, 4, 8)
|
new SelfStatusMove(MoveId.OBSTRUCT, PokemonType.DARK, 100, 10, -1, 4, 8)
|
||||||
.attr(ProtectAttr, BattlerTagType.OBSTRUCT)
|
.attr(ProtectAttr, BattlerTagType.OBSTRUCT)
|
||||||
.condition(failIfLastCondition),
|
.condition(failIfLastCondition),
|
||||||
@ -11005,7 +11069,8 @@ export function initMoves() {
|
|||||||
new StatusMove(MoveId.JUNGLE_HEALING, PokemonType.GRASS, -1, 10, -1, 0, 8)
|
new StatusMove(MoveId.JUNGLE_HEALING, PokemonType.GRASS, -1, 10, -1, 0, 8)
|
||||||
.attr(HealAttr, 0.25, true, false)
|
.attr(HealAttr, 0.25, true, false)
|
||||||
.attr(HealStatusEffectAttr, false, getNonVolatileStatusEffects())
|
.attr(HealStatusEffectAttr, false, getNonVolatileStatusEffects())
|
||||||
.target(MoveTarget.USER_AND_ALLIES),
|
.target(MoveTarget.USER_AND_ALLIES)
|
||||||
|
.triageMove(),
|
||||||
new AttackMove(MoveId.WICKED_BLOW, PokemonType.DARK, MoveCategory.PHYSICAL, 75, 100, 5, -1, 0, 8)
|
new AttackMove(MoveId.WICKED_BLOW, PokemonType.DARK, MoveCategory.PHYSICAL, 75, 100, 5, -1, 0, 8)
|
||||||
.attr(CritOnlyAttr)
|
.attr(CritOnlyAttr)
|
||||||
.punchingMove(),
|
.punchingMove(),
|
||||||
@ -11233,7 +11298,7 @@ export function initMoves() {
|
|||||||
.makesContact(false),
|
.makesContact(false),
|
||||||
new AttackMove(MoveId.LUMINA_CRASH, PokemonType.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 10, 100, 0, 9)
|
new AttackMove(MoveId.LUMINA_CRASH, PokemonType.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 10, 100, 0, 9)
|
||||||
.attr(StatStageChangeAttr, [ Stat.SPDEF ], -2),
|
.attr(StatStageChangeAttr, [ Stat.SPDEF ], -2),
|
||||||
new AttackMove(MoveId.ORDER_UP, PokemonType.DRAGON, MoveCategory.PHYSICAL, 80, 100, 10, 100, 0, 9)
|
new AttackMove(MoveId.ORDER_UP, PokemonType.DRAGON, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 9)
|
||||||
.attr(OrderUpStatBoostAttr)
|
.attr(OrderUpStatBoostAttr)
|
||||||
.makesContact(false),
|
.makesContact(false),
|
||||||
new AttackMove(MoveId.JET_PUNCH, PokemonType.WATER, MoveCategory.PHYSICAL, 60, 100, 15, -1, 1, 9)
|
new AttackMove(MoveId.JET_PUNCH, PokemonType.WATER, MoveCategory.PHYSICAL, 60, 100, 15, -1, 1, 9)
|
||||||
@ -11416,7 +11481,7 @@ export function initMoves() {
|
|||||||
.attr(IvyCudgelTypeAttr)
|
.attr(IvyCudgelTypeAttr)
|
||||||
.attr(HighCritAttr)
|
.attr(HighCritAttr)
|
||||||
.makesContact(false),
|
.makesContact(false),
|
||||||
new ChargingAttackMove(MoveId.ELECTRO_SHOT, PokemonType.ELECTRIC, MoveCategory.SPECIAL, 130, 100, 10, 100, 0, 9)
|
new ChargingAttackMove(MoveId.ELECTRO_SHOT, PokemonType.ELECTRIC, MoveCategory.SPECIAL, 130, 100, 10, -1, 0, 9)
|
||||||
.chargeText(i18next.t("moveTriggers:absorbedElectricity", { pokemonName: "{USER}" }))
|
.chargeText(i18next.t("moveTriggers:absorbedElectricity", { pokemonName: "{USER}" }))
|
||||||
.chargeAttr(StatStageChangeAttr, [ Stat.SPATK ], 1, true)
|
.chargeAttr(StatStageChangeAttr, [ Stat.SPATK ], 1, true)
|
||||||
.chargeAttr(WeatherInstantChargeAttr, [ WeatherType.RAIN, WeatherType.HEAVY_RAIN ]),
|
.chargeAttr(WeatherInstantChargeAttr, [ WeatherType.RAIN, WeatherType.HEAVY_RAIN ]),
|
||||||
|
@ -39,6 +39,7 @@ import { addPokemonDataToDexAndValidateAchievements } from "#mystery-encounters/
|
|||||||
import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
|
import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
|
||||||
import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter";
|
import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter";
|
||||||
import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option";
|
import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option";
|
||||||
|
import { PartySizeRequirement } from "#mystery-encounters/mystery-encounter-requirements";
|
||||||
import { PokemonData } from "#system/pokemon-data";
|
import { PokemonData } from "#system/pokemon-data";
|
||||||
import { MusicPreference } from "#system/settings";
|
import { MusicPreference } from "#system/settings";
|
||||||
import type { OptionSelectItem } from "#ui/abstact-option-select-ui-handler";
|
import type { OptionSelectItem } from "#ui/abstact-option-select-ui-handler";
|
||||||
@ -151,7 +152,8 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil
|
|||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
.withOption(
|
.withOption(
|
||||||
MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DEFAULT)
|
MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT)
|
||||||
|
.withSceneRequirement(new PartySizeRequirement([2, 6], true)) // Requires 2 valid party members
|
||||||
.withHasDexProgress(true)
|
.withHasDexProgress(true)
|
||||||
.withDialogue({
|
.withDialogue({
|
||||||
buttonLabel: `${namespace}:option.1.label`,
|
buttonLabel: `${namespace}:option.1.label`,
|
||||||
@ -257,7 +259,8 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil
|
|||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
.withOption(
|
.withOption(
|
||||||
MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DEFAULT)
|
MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT)
|
||||||
|
.withSceneRequirement(new PartySizeRequirement([2, 6], true)) // Requires 2 valid party members
|
||||||
.withHasDexProgress(true)
|
.withHasDexProgress(true)
|
||||||
.withDialogue({
|
.withDialogue({
|
||||||
buttonLabel: `${namespace}:option.2.label`,
|
buttonLabel: `${namespace}:option.2.label`,
|
||||||
|
@ -25,7 +25,8 @@ import {
|
|||||||
StatusEffectRequirement,
|
StatusEffectRequirement,
|
||||||
WaveRangeRequirement,
|
WaveRangeRequirement,
|
||||||
} from "#mystery-encounters/mystery-encounter-requirements";
|
} from "#mystery-encounters/mystery-encounter-requirements";
|
||||||
import { capitalizeFirstLetter, coerceArray, isNullOrUndefined, randSeedInt } from "#utils/common";
|
import { coerceArray, isNullOrUndefined, randSeedInt } from "#utils/common";
|
||||||
|
import { capitalizeFirstLetter } from "#utils/strings";
|
||||||
|
|
||||||
export interface EncounterStartOfBattleEffect {
|
export interface EncounterStartOfBattleEffect {
|
||||||
sourcePokemon?: Pokemon;
|
sourcePokemon?: Pokemon;
|
||||||
|
@ -3,7 +3,7 @@ import { EFFECTIVE_STATS, getShortenedStatKey, Stat } from "#enums/stat";
|
|||||||
import { TextStyle } from "#enums/text-style";
|
import { TextStyle } from "#enums/text-style";
|
||||||
import { UiTheme } from "#enums/ui-theme";
|
import { UiTheme } from "#enums/ui-theme";
|
||||||
import { getBBCodeFrag } from "#ui/text";
|
import { getBBCodeFrag } from "#ui/text";
|
||||||
import { toReadableString } from "#utils/common";
|
import { toTitleCase } from "#utils/strings";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
export function getNatureName(
|
export function getNatureName(
|
||||||
@ -13,7 +13,7 @@ export function getNatureName(
|
|||||||
ignoreBBCode = false,
|
ignoreBBCode = false,
|
||||||
uiTheme: UiTheme = UiTheme.DEFAULT,
|
uiTheme: UiTheme = UiTheme.DEFAULT,
|
||||||
): string {
|
): string {
|
||||||
let ret = toReadableString(Nature[nature]);
|
let ret = toTitleCase(Nature[nature]);
|
||||||
//Translating nature
|
//Translating nature
|
||||||
if (i18next.exists(`nature:${ret}`)) {
|
if (i18next.exists(`nature:${ret}`)) {
|
||||||
ret = i18next.t(`nature:${ret}` as any);
|
ret = i18next.t(`nature:${ret}` as any);
|
||||||
|
@ -29,15 +29,9 @@ import type { Variant, VariantSet } from "#sprites/variant";
|
|||||||
import { populateVariantColorCache, variantColorCache, variantData } from "#sprites/variant";
|
import { populateVariantColorCache, variantColorCache, variantData } from "#sprites/variant";
|
||||||
import type { StarterMoveset } from "#system/game-data";
|
import type { StarterMoveset } from "#system/game-data";
|
||||||
import type { Localizable } from "#types/locales";
|
import type { Localizable } from "#types/locales";
|
||||||
import {
|
import { isNullOrUndefined, randSeedFloat, randSeedGauss, randSeedInt, randSeedItem } from "#utils/common";
|
||||||
capitalizeString,
|
|
||||||
isNullOrUndefined,
|
|
||||||
randSeedFloat,
|
|
||||||
randSeedGauss,
|
|
||||||
randSeedInt,
|
|
||||||
randSeedItem,
|
|
||||||
} from "#utils/common";
|
|
||||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
||||||
|
import { toCamelCase, toPascalCase } from "#utils/strings";
|
||||||
import { argbFromRgba, QuantizerCelebi, rgbaFromArgb } from "@material/material-color-utilities";
|
import { argbFromRgba, QuantizerCelebi, rgbaFromArgb } from "@material/material-color-utilities";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
@ -91,6 +85,7 @@ export function getPokemonSpeciesForm(species: SpeciesId, formIndex: number): Po
|
|||||||
return retSpecies;
|
return retSpecies;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Clean this up and seriously review alternate means of fusion naming
|
||||||
export function getFusedSpeciesName(speciesAName: string, speciesBName: string): string {
|
export function getFusedSpeciesName(speciesAName: string, speciesBName: string): string {
|
||||||
const fragAPattern = /([a-z]{2}.*?[aeiou(?:y$)\-']+)(.*?)$/i;
|
const fragAPattern = /([a-z]{2}.*?[aeiou(?:y$)\-']+)(.*?)$/i;
|
||||||
const fragBPattern = /([a-z]{2}.*?[aeiou(?:y$)\-'])(.*?)$/i;
|
const fragBPattern = /([a-z]{2}.*?[aeiou(?:y$)\-'])(.*?)$/i;
|
||||||
@ -904,14 +899,14 @@ export class PokemonSpecies extends PokemonSpeciesForm implements Localizable {
|
|||||||
* @returns the pokemon-form locale key for the single form name ("Alolan Form", "Eternal Flower" etc)
|
* @returns the pokemon-form locale key for the single form name ("Alolan Form", "Eternal Flower" etc)
|
||||||
*/
|
*/
|
||||||
getFormNameToDisplay(formIndex = 0, append = false): string {
|
getFormNameToDisplay(formIndex = 0, append = false): string {
|
||||||
const formKey = this.forms?.[formIndex!]?.formKey;
|
const formKey = this.forms[formIndex]?.formKey ?? "";
|
||||||
const formText = capitalizeString(formKey, "-", false, false) || "";
|
const formText = toPascalCase(formKey);
|
||||||
const speciesName = capitalizeString(SpeciesId[this.speciesId], "_", true, false);
|
const speciesName = toCamelCase(SpeciesId[this.speciesId]);
|
||||||
let ret = "";
|
let ret = "";
|
||||||
|
|
||||||
const region = this.getRegion();
|
const region = this.getRegion();
|
||||||
if (this.speciesId === SpeciesId.ARCEUS) {
|
if (this.speciesId === SpeciesId.ARCEUS) {
|
||||||
ret = i18next.t(`pokemonInfo:Type.${formText?.toUpperCase()}`);
|
ret = i18next.t(`pokemonInfo:Type.${formText.toUpperCase()}`);
|
||||||
} else if (
|
} else if (
|
||||||
[
|
[
|
||||||
SpeciesFormKey.MEGA,
|
SpeciesFormKey.MEGA,
|
||||||
@ -937,7 +932,7 @@ export class PokemonSpecies extends PokemonSpeciesForm implements Localizable {
|
|||||||
if (i18next.exists(i18key)) {
|
if (i18next.exists(i18key)) {
|
||||||
ret = i18next.t(i18key);
|
ret = i18next.t(i18key);
|
||||||
} else {
|
} else {
|
||||||
const rootSpeciesName = capitalizeString(SpeciesId[this.getRootSpeciesId()], "_", true, false);
|
const rootSpeciesName = toCamelCase(SpeciesId[this.getRootSpeciesId()]);
|
||||||
const i18RootKey = `pokemonForm:${rootSpeciesName}${formText}`;
|
const i18RootKey = `pokemonForm:${rootSpeciesName}${formText}`;
|
||||||
ret = i18next.exists(i18RootKey) ? i18next.t(i18RootKey) : formText;
|
ret = i18next.exists(i18RootKey) ? i18next.t(i18RootKey) : formText;
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,30 @@
|
|||||||
import { type BattlerTag, loadBattlerTag } from "#data/battler-tags";
|
import type { BattlerTag } from "#data/battler-tags";
|
||||||
|
import { loadBattlerTag, SerializableBattlerTag } from "#data/battler-tags";
|
||||||
|
import { allSpecies } from "#data/data-lists";
|
||||||
import type { Gender } from "#data/gender";
|
import type { Gender } from "#data/gender";
|
||||||
import { PokemonMove } from "#data/moves/pokemon-move";
|
import { PokemonMove } from "#data/moves/pokemon-move";
|
||||||
import type { PokemonSpeciesForm } from "#data/pokemon-species";
|
import { getPokemonSpeciesForm, type PokemonSpeciesForm } from "#data/pokemon-species";
|
||||||
import type { TypeDamageMultiplier } from "#data/type";
|
import type { TypeDamageMultiplier } from "#data/type";
|
||||||
import type { AbilityId } from "#enums/ability-id";
|
import type { AbilityId } from "#enums/ability-id";
|
||||||
import type { BerryType } from "#enums/berry-type";
|
import type { BerryType } from "#enums/berry-type";
|
||||||
import type { MoveId } from "#enums/move-id";
|
import type { MoveId } from "#enums/move-id";
|
||||||
import type { Nature } from "#enums/nature";
|
import type { Nature } from "#enums/nature";
|
||||||
import type { PokemonType } from "#enums/pokemon-type";
|
import type { PokemonType } from "#enums/pokemon-type";
|
||||||
|
import type { SpeciesId } from "#enums/species-id";
|
||||||
import type { AttackMoveResult } from "#types/attack-move-result";
|
import type { AttackMoveResult } from "#types/attack-move-result";
|
||||||
import type { IllusionData } from "#types/illusion-data";
|
import type { IllusionData } from "#types/illusion-data";
|
||||||
import type { TurnMove } from "#types/turn-move";
|
import type { TurnMove } from "#types/turn-move";
|
||||||
|
import type { CoerceNullPropertiesToUndefined } from "#types/type-helpers";
|
||||||
import { isNullOrUndefined } from "#utils/common";
|
import { isNullOrUndefined } from "#utils/common";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type that {@linkcode PokemonSpeciesForm} is converted to when an object containing it serializes it.
|
||||||
|
*/
|
||||||
|
type SerializedSpeciesForm = {
|
||||||
|
id: SpeciesId;
|
||||||
|
formIdx: number;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Permanent data that can customize a Pokemon in non-standard ways from its Species.
|
* Permanent data that can customize a Pokemon in non-standard ways from its Species.
|
||||||
* Includes abilities, nature, changed types, etc.
|
* Includes abilities, nature, changed types, etc.
|
||||||
@ -41,9 +53,59 @@ export class CustomPokemonData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deserialize a pokemon species form from an object containing `id` and `formIdx` properties.
|
||||||
|
* @param value - The value to deserialize
|
||||||
|
* @returns The `PokemonSpeciesForm` or `null` if the fields could not be properly discerned
|
||||||
|
*/
|
||||||
|
function deserializePokemonSpeciesForm(value: SerializedSpeciesForm | PokemonSpeciesForm): PokemonSpeciesForm | null {
|
||||||
|
// @ts-expect-error: We may be deserializing a PokemonSpeciesForm, but we catch later on
|
||||||
|
let { id, formIdx } = value;
|
||||||
|
|
||||||
|
if (isNullOrUndefined(id) || isNullOrUndefined(formIdx)) {
|
||||||
|
// @ts-expect-error: Typescript doesn't know that in block, `value` must be a PokemonSpeciesForm
|
||||||
|
id = value.speciesId;
|
||||||
|
// @ts-expect-error: Same as above (plus we are accessing a protected property)
|
||||||
|
formIdx = value._formIndex;
|
||||||
|
}
|
||||||
|
// If for some reason either of these fields are null/undefined, we cannot reconstruct the species form
|
||||||
|
if (isNullOrUndefined(id) || isNullOrUndefined(formIdx)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return getPokemonSpeciesForm(id, formIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SerializedIllusionData extends Omit<IllusionData, "fusionSpecies"> {
|
||||||
|
/** The id of the illusioned fusion species, or `undefined` if not a fusion */
|
||||||
|
fusionSpecies?: SpeciesId;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SerializedPokemonSummonData {
|
||||||
|
statStages: number[];
|
||||||
|
moveQueue: TurnMove[];
|
||||||
|
tags: BattlerTag[];
|
||||||
|
abilitySuppressed: boolean;
|
||||||
|
speciesForm?: SerializedSpeciesForm;
|
||||||
|
fusionSpeciesForm?: SerializedSpeciesForm;
|
||||||
|
ability?: AbilityId;
|
||||||
|
passiveAbility?: AbilityId;
|
||||||
|
gender?: Gender;
|
||||||
|
fusionGender?: Gender;
|
||||||
|
stats: number[];
|
||||||
|
moveset?: PokemonMove[];
|
||||||
|
types: PokemonType[];
|
||||||
|
addedType?: PokemonType;
|
||||||
|
illusion?: SerializedIllusionData;
|
||||||
|
illusionBroken: boolean;
|
||||||
|
berriesEatenLast: BerryType[];
|
||||||
|
moveHistory: TurnMove[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Persistent in-battle data for a {@linkcode Pokemon}.
|
* Persistent in-battle data for a {@linkcode Pokemon}.
|
||||||
* Resets on switch or new battle.
|
* Resets on switch or new battle.
|
||||||
|
*
|
||||||
|
* @sealed
|
||||||
*/
|
*/
|
||||||
export class PokemonSummonData {
|
export class PokemonSummonData {
|
||||||
/** [Atk, Def, SpAtk, SpDef, Spd, Acc, Eva] */
|
/** [Atk, Def, SpAtk, SpDef, Spd, Acc, Eva] */
|
||||||
@ -86,7 +148,7 @@ export class PokemonSummonData {
|
|||||||
*/
|
*/
|
||||||
public moveHistory: TurnMove[] = [];
|
public moveHistory: TurnMove[] = [];
|
||||||
|
|
||||||
constructor(source?: PokemonSummonData | Partial<PokemonSummonData>) {
|
constructor(source?: PokemonSummonData | SerializedPokemonSummonData) {
|
||||||
if (isNullOrUndefined(source)) {
|
if (isNullOrUndefined(source)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -97,19 +159,88 @@ export class PokemonSummonData {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (key === "speciesForm" || key === "fusionSpeciesForm") {
|
||||||
|
this[key] = deserializePokemonSpeciesForm(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "illusion" && typeof value === "object") {
|
||||||
|
// Make a copy so as not to mutate provided value
|
||||||
|
const illusionData = {
|
||||||
|
...value,
|
||||||
|
};
|
||||||
|
if (!isNullOrUndefined(illusionData.fusionSpecies)) {
|
||||||
|
switch (typeof illusionData.fusionSpecies) {
|
||||||
|
case "object":
|
||||||
|
illusionData.fusionSpecies = allSpecies[illusionData.fusionSpecies.speciesId];
|
||||||
|
break;
|
||||||
|
case "number":
|
||||||
|
illusionData.fusionSpecies = allSpecies[illusionData.fusionSpecies];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
illusionData.fusionSpecies = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this[key] = illusionData as IllusionData;
|
||||||
|
}
|
||||||
|
|
||||||
if (key === "moveset") {
|
if (key === "moveset") {
|
||||||
this.moveset = value?.map((m: any) => PokemonMove.loadMove(m));
|
this.moveset = value?.map((m: any) => PokemonMove.loadMove(m));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === "tags") {
|
if (key === "tags" && Array.isArray(value)) {
|
||||||
// load battler tags
|
// load battler tags, discarding any that are not serializable
|
||||||
this.tags = value.map((t: BattlerTag) => loadBattlerTag(t));
|
this.tags = value
|
||||||
|
.map((t: SerializableBattlerTag) => loadBattlerTag(t))
|
||||||
|
.filter((t): t is SerializableBattlerTag => t instanceof SerializableBattlerTag);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
this[key] = value;
|
this[key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize this PokemonSummonData to JSON, converting {@linkcode PokemonSpeciesForm} and {@linkcode IllusionData.fusionSpecies}
|
||||||
|
* into simpler types instead of serializing all of their fields.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* - `IllusionData.fusionSpecies` is serialized as just the species ID
|
||||||
|
* - `PokemonSpeciesForm` and `PokemonSpeciesForm.fusionSpeciesForm` are converted into {@linkcode SerializedSpeciesForm} objects
|
||||||
|
*/
|
||||||
|
public toJSON(): SerializedPokemonSummonData {
|
||||||
|
// Pokemon species forms are never saved, only the species ID.
|
||||||
|
const illusion = this.illusion;
|
||||||
|
const speciesForm = this.speciesForm;
|
||||||
|
const fusionSpeciesForm = this.fusionSpeciesForm;
|
||||||
|
const illusionSpeciesForm = illusion?.fusionSpecies;
|
||||||
|
const t = {
|
||||||
|
// the "as omit" is required to avoid TS resolving the overwritten properties to "never"
|
||||||
|
// We coerce null to undefined in the type, as the for loop below replaces `null` with `undefined`
|
||||||
|
...(this as Omit<
|
||||||
|
CoerceNullPropertiesToUndefined<PokemonSummonData>,
|
||||||
|
"speciesForm" | "fusionSpeciesForm" | "illusion"
|
||||||
|
>),
|
||||||
|
speciesForm: isNullOrUndefined(speciesForm)
|
||||||
|
? undefined
|
||||||
|
: { id: speciesForm.speciesId, formIdx: speciesForm.formIndex },
|
||||||
|
fusionSpeciesForm: isNullOrUndefined(fusionSpeciesForm)
|
||||||
|
? undefined
|
||||||
|
: { id: fusionSpeciesForm.speciesId, formIdx: fusionSpeciesForm.formIndex },
|
||||||
|
illusion: isNullOrUndefined(illusion)
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
...(this.illusion as Omit<typeof illusion, "fusionSpecies">),
|
||||||
|
fusionSpecies: illusionSpeciesForm?.speciesId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// Replace `null` with `undefined`, as `undefined` never gets serialized
|
||||||
|
for (const [key, value] of Object.entries(t)) {
|
||||||
|
if (value === null) {
|
||||||
|
t[key] = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Merge this inside `summmonData` but exclude from save if/when a save data serializer is added
|
// TODO: Merge this inside `summmonData` but exclude from save if/when a save data serializer is added
|
||||||
|
70
src/data/positional-tags/load-positional-tag.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { DelayedAttackTag, type PositionalTag, WishTag } from "#data/positional-tags/positional-tag";
|
||||||
|
import { PositionalTagType } from "#enums/positional-tag-type";
|
||||||
|
import type { ObjectValues } from "#types/type-helpers";
|
||||||
|
import type { Constructor } from "#utils/common";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the attributes of a {@linkcode PositionalTag}.
|
||||||
|
* @param tagType - The {@linkcode PositionalTagType} to create
|
||||||
|
* @param args - The arguments needed to instantize the given tag
|
||||||
|
* @returns The newly created tag.
|
||||||
|
* @remarks
|
||||||
|
* This function does not perform any checking if the added tag is valid.
|
||||||
|
*/
|
||||||
|
export function loadPositionalTag<T extends PositionalTagType>({
|
||||||
|
tagType,
|
||||||
|
...args
|
||||||
|
}: serializedPosTagMap[T]): posTagInstanceMap[T];
|
||||||
|
/**
|
||||||
|
* Load the attributes of a {@linkcode PositionalTag}.
|
||||||
|
* @param tag - The {@linkcode SerializedPositionalTag} to instantiate
|
||||||
|
* @returns The newly created tag.
|
||||||
|
* @remarks
|
||||||
|
* This function does not perform any checking if the added tag is valid.
|
||||||
|
*/
|
||||||
|
export function loadPositionalTag(tag: SerializedPositionalTag): PositionalTag;
|
||||||
|
export function loadPositionalTag<T extends PositionalTagType>({
|
||||||
|
tagType,
|
||||||
|
...rest
|
||||||
|
}: serializedPosTagMap[T]): posTagInstanceMap[T] {
|
||||||
|
// Note: We need 2 type assertions here:
|
||||||
|
// 1 because TS doesn't narrow the type of TagClass correctly based on `T`.
|
||||||
|
// It converts it into `new (DelayedAttackTag | WishTag) => DelayedAttackTag & WishTag`
|
||||||
|
const tagClass = posTagConstructorMap[tagType] as new (args: posTagParamMap[T]) => posTagInstanceMap[T];
|
||||||
|
// 2 because TS doesn't narrow the type of `rest` correctly
|
||||||
|
// (from `Omit<serializedPosTagParamMap[T], "tagType"> into `posTagParamMap[T]`)
|
||||||
|
return new tagClass(rest as unknown as posTagParamMap[T]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Const object mapping tag types to their constructors. */
|
||||||
|
const posTagConstructorMap = Object.freeze({
|
||||||
|
[PositionalTagType.DELAYED_ATTACK]: DelayedAttackTag,
|
||||||
|
[PositionalTagType.WISH]: WishTag,
|
||||||
|
}) satisfies {
|
||||||
|
// NB: This `satisfies` block ensures that all tag types have corresponding entries in the map.
|
||||||
|
[k in PositionalTagType]: Constructor<PositionalTag & { tagType: k }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Type mapping positional tag types to their constructors. */
|
||||||
|
type posTagMap = typeof posTagConstructorMap;
|
||||||
|
|
||||||
|
/** Type mapping all positional tag types to their instances. */
|
||||||
|
type posTagInstanceMap = {
|
||||||
|
[k in PositionalTagType]: InstanceType<posTagMap[k]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Type mapping all positional tag types to their constructors' parameters. */
|
||||||
|
type posTagParamMap = {
|
||||||
|
[k in PositionalTagType]: ConstructorParameters<posTagMap[k]>[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type mapping all positional tag types to their constructors' parameters, alongside the `tagType` selector.
|
||||||
|
* Equivalent to their serialized representations.
|
||||||
|
*/
|
||||||
|
export type serializedPosTagMap = {
|
||||||
|
[k in PositionalTagType]: posTagParamMap[k] & { tagType: k };
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Union type containing all serialized {@linkcode PositionalTag}s. */
|
||||||
|
export type SerializedPositionalTag = ObjectValues<serializedPosTagMap>;
|
55
src/data/positional-tags/positional-tag-manager.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { loadPositionalTag } from "#data/positional-tags/load-positional-tag";
|
||||||
|
import type { PositionalTag } from "#data/positional-tags/positional-tag";
|
||||||
|
import type { BattlerIndex } from "#enums/battler-index";
|
||||||
|
import type { PositionalTagType } from "#enums/positional-tag-type";
|
||||||
|
|
||||||
|
/** A manager for the {@linkcode PositionalTag}s in the arena. */
|
||||||
|
export class PositionalTagManager {
|
||||||
|
/**
|
||||||
|
* Array containing all pending unactivated {@linkcode PositionalTag}s,
|
||||||
|
* sorted by order of creation (oldest first).
|
||||||
|
*/
|
||||||
|
public tags: PositionalTag[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new {@linkcode PositionalTag} to the arena.
|
||||||
|
* @remarks
|
||||||
|
* This function does not perform any checking if the added tag is valid.
|
||||||
|
*/
|
||||||
|
public addTag<T extends PositionalTagType = never>(tag: Parameters<typeof loadPositionalTag<T>>[0]): void {
|
||||||
|
this.tags.push(loadPositionalTag(tag));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a new {@linkcode PositionalTag} can be added to the battlefield.
|
||||||
|
* @param tagType - The {@linkcode PositionalTagType} being created
|
||||||
|
* @param targetIndex - The {@linkcode BattlerIndex} being targeted
|
||||||
|
* @returns Whether the tag can be added.
|
||||||
|
*/
|
||||||
|
public canAddTag(tagType: PositionalTagType, targetIndex: BattlerIndex): boolean {
|
||||||
|
return !this.tags.some(t => t.tagType === tagType && t.targetIndex === targetIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrement turn counts of and trigger all pending {@linkcode PositionalTag}s on field.
|
||||||
|
* @remarks
|
||||||
|
* If multiple tags trigger simultaneously, they will activate in order of **initial creation**, regardless of current speed order.
|
||||||
|
* (Source: [Smogon](<https://www.smogon.com/forums/threads/sword-shield-battle-mechanics-research.3655528/page-64#post-9244179>))
|
||||||
|
*/
|
||||||
|
public activateAllTags(): void {
|
||||||
|
const leftoverTags: PositionalTag[] = [];
|
||||||
|
for (const tag of this.tags) {
|
||||||
|
// Check for silent removal, immediately removing invalid tags.
|
||||||
|
if (--tag.turnCount > 0) {
|
||||||
|
// tag still cooking
|
||||||
|
leftoverTags.push(tag);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag.shouldTrigger()) {
|
||||||
|
tag.trigger();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.tags = leftoverTags;
|
||||||
|
}
|
||||||
|
}
|
174
src/data/positional-tags/positional-tag.ts
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
import { globalScene } from "#app/global-scene";
|
||||||
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
|
// biome-ignore-start lint/correctness/noUnusedImports: TSDoc
|
||||||
|
import type { ArenaTag } from "#data/arena-tag";
|
||||||
|
// biome-ignore-end lint/correctness/noUnusedImports: TSDoc
|
||||||
|
import { allMoves } from "#data/data-lists";
|
||||||
|
import type { BattlerIndex } from "#enums/battler-index";
|
||||||
|
import type { MoveId } from "#enums/move-id";
|
||||||
|
import { MoveUseMode } from "#enums/move-use-mode";
|
||||||
|
import { PositionalTagType } from "#enums/positional-tag-type";
|
||||||
|
import type { Pokemon } from "#field/pokemon";
|
||||||
|
import i18next from "i18next";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baseline arguments used to construct all {@linkcode PositionalTag}s,
|
||||||
|
* the contents of which are serialized and used to construct new tags. \
|
||||||
|
* Does not contain the `tagType` parameter (which is used to select the proper class constructor during tag loading).
|
||||||
|
* @privateRemarks
|
||||||
|
* All {@linkcode PositionalTag}s are intended to implement a sub-interface of this containing their respective parameters,
|
||||||
|
* and should refrain from adding extra serializable fields not contained in said interface.
|
||||||
|
* This ensures that all tags truly "become" their respective interfaces when converted to and from JSON.
|
||||||
|
*/
|
||||||
|
export interface PositionalTagBaseArgs {
|
||||||
|
/**
|
||||||
|
* The number of turns remaining until this tag's activation. \
|
||||||
|
* Decremented by 1 at the end of each turn until reaching 0, at which point it will
|
||||||
|
* {@linkcode PositionalTag.trigger | trigger} the tag's effects and be removed.
|
||||||
|
*/
|
||||||
|
turnCount: number;
|
||||||
|
/**
|
||||||
|
* The {@linkcode BattlerIndex} targeted by this effect.
|
||||||
|
*/
|
||||||
|
targetIndex: BattlerIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@linkcode PositionalTag} is a variant of an {@linkcode ArenaTag} that targets a single *slot* of the battlefield.
|
||||||
|
* Each tag can last one or more turns, triggering various effects on removal.
|
||||||
|
* Multiple tags of the same kind can stack with one another, provided they are affecting different targets.
|
||||||
|
*/
|
||||||
|
export abstract class PositionalTag implements PositionalTagBaseArgs {
|
||||||
|
/** This tag's {@linkcode PositionalTagType | type} */
|
||||||
|
public abstract readonly tagType: PositionalTagType;
|
||||||
|
// These arguments have to be public to implement the interface, but are functionally private
|
||||||
|
// outside this and the tag manager.
|
||||||
|
// Left undocumented to inherit doc comments from the interface
|
||||||
|
public turnCount: number;
|
||||||
|
public readonly targetIndex: BattlerIndex;
|
||||||
|
|
||||||
|
constructor({ turnCount, targetIndex }: PositionalTagBaseArgs) {
|
||||||
|
this.turnCount = turnCount;
|
||||||
|
this.targetIndex = targetIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Trigger this tag's effects prior to removal. */
|
||||||
|
public abstract trigger(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether this tag should be allowed to {@linkcode trigger} and activate its effects
|
||||||
|
* upon its duration elapsing.
|
||||||
|
* @returns Whether this tag should be allowed to trigger prior to being removed.
|
||||||
|
*/
|
||||||
|
public abstract shouldTrigger(): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the {@linkcode Pokemon} currently targeted by this tag.
|
||||||
|
* @returns The {@linkcode Pokemon} located in this tag's target position, or `undefined` if none exist in it.
|
||||||
|
*/
|
||||||
|
protected getTarget(): Pokemon | undefined {
|
||||||
|
return globalScene.getField()[this.targetIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Interface containing additional properties used to construct a {@linkcode DelayedAttackTag}. */
|
||||||
|
interface DelayedAttackArgs extends PositionalTagBaseArgs {
|
||||||
|
/**
|
||||||
|
* The {@linkcode Pokemon.id | PID} of the {@linkcode Pokemon} having created this effect.
|
||||||
|
*/
|
||||||
|
sourceId: number;
|
||||||
|
/** The {@linkcode MoveId} that created this attack. */
|
||||||
|
sourceMove: MoveId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag to manage execution of delayed attacks, such as {@linkcode MoveId.FUTURE_SIGHT} or {@linkcode MoveId.DOOM_DESIRE}. \
|
||||||
|
* Delayed attacks do nothing for the first several turns after use (including the turn the move is used),
|
||||||
|
* triggering against a certain slot after the turn count has elapsed.
|
||||||
|
*/
|
||||||
|
export class DelayedAttackTag extends PositionalTag implements DelayedAttackArgs {
|
||||||
|
public override readonly tagType = PositionalTagType.DELAYED_ATTACK;
|
||||||
|
public readonly sourceMove: MoveId;
|
||||||
|
public readonly sourceId: number;
|
||||||
|
|
||||||
|
constructor({ sourceId, turnCount, targetIndex, sourceMove }: DelayedAttackArgs) {
|
||||||
|
super({ turnCount, targetIndex });
|
||||||
|
this.sourceId = sourceId;
|
||||||
|
this.sourceMove = sourceMove;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override trigger(): void {
|
||||||
|
// Bangs are justified as the `shouldTrigger` method will queue the tag for removal
|
||||||
|
// if the source or target no longer exist
|
||||||
|
const source = globalScene.getPokemonById(this.sourceId)!;
|
||||||
|
const target = this.getTarget()!;
|
||||||
|
|
||||||
|
source.turnData.extraTurns++;
|
||||||
|
globalScene.phaseManager.queueMessage(
|
||||||
|
i18next.t("moveTriggers:tookMoveAttack", {
|
||||||
|
pokemonName: getPokemonNameWithAffix(target),
|
||||||
|
moveName: allMoves[this.sourceMove].name,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
globalScene.phaseManager.unshiftNew(
|
||||||
|
"MoveEffectPhase",
|
||||||
|
this.sourceId, // TODO: Find an alternate method of passing the source pokemon without a source ID
|
||||||
|
[this.targetIndex],
|
||||||
|
allMoves[this.sourceMove],
|
||||||
|
MoveUseMode.DELAYED_ATTACK,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override shouldTrigger(): boolean {
|
||||||
|
const source = globalScene.getPokemonById(this.sourceId);
|
||||||
|
const target = this.getTarget();
|
||||||
|
// Silently disappear if either source or target are missing or happen to be the same pokemon
|
||||||
|
// (i.e. targeting oneself)
|
||||||
|
// We also need to check for fainted targets as they don't technically leave the field until _after_ the turn ends
|
||||||
|
return !!source && !!target && source !== target && !target.isFainted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Interface containing arguments used to construct a {@linkcode WishTag}. */
|
||||||
|
interface WishArgs extends PositionalTagBaseArgs {
|
||||||
|
/** The amount of {@linkcode Stat.HP | HP} to heal; set to 50% of the user's max HP during move usage. */
|
||||||
|
healHp: number;
|
||||||
|
/** The name of the {@linkcode Pokemon} having created the tag. */
|
||||||
|
pokemonName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag to implement {@linkcode MoveId.WISH | Wish}.
|
||||||
|
*/
|
||||||
|
export class WishTag extends PositionalTag implements WishArgs {
|
||||||
|
public override readonly tagType = PositionalTagType.WISH;
|
||||||
|
|
||||||
|
public readonly pokemonName: string;
|
||||||
|
public readonly healHp: number;
|
||||||
|
|
||||||
|
constructor({ turnCount, targetIndex, healHp, pokemonName }: WishArgs) {
|
||||||
|
super({ turnCount, targetIndex });
|
||||||
|
this.healHp = healHp;
|
||||||
|
this.pokemonName = pokemonName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override trigger(): void {
|
||||||
|
// TODO: Rename this locales key - wish shows a message on REMOVAL, not addition
|
||||||
|
globalScene.phaseManager.queueMessage(
|
||||||
|
i18next.t("arenaTag:wishTagOnAdd", {
|
||||||
|
pokemonNameWithAffix: this.pokemonName,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
globalScene.phaseManager.unshiftNew("PokemonHealPhase", this.targetIndex, this.healHp, null, true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override shouldTrigger(): boolean {
|
||||||
|
// Disappear if no target or target is fainted.
|
||||||
|
// The source need not exist at the time of activation (since all we need is a simple message)
|
||||||
|
// TODO: Verify whether Wish shows a message if the Pokemon it would affect is KO'd on the turn of activation
|
||||||
|
const target = this.getTarget();
|
||||||
|
return !!target && !target.isFainted();
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,12 @@
|
|||||||
import { TrainerType } from "#enums/trainer-type";
|
import { TrainerType } from "#enums/trainer-type";
|
||||||
import { toReadableString } from "#utils/common";
|
import { toPascalSnakeCase } from "#utils/strings";
|
||||||
|
|
||||||
class TrainerNameConfig {
|
class TrainerNameConfig {
|
||||||
public urls: string[];
|
public urls: string[];
|
||||||
public femaleUrls: string[] | null;
|
public femaleUrls: string[] | null;
|
||||||
|
|
||||||
constructor(type: TrainerType, ...urls: string[]) {
|
constructor(type: TrainerType, ...urls: string[]) {
|
||||||
this.urls = urls.length ? urls : [toReadableString(TrainerType[type]).replace(/ /g, "_")];
|
this.urls = urls.length ? urls : [toPascalSnakeCase(TrainerType[type])];
|
||||||
}
|
}
|
||||||
|
|
||||||
hasGenderVariant(...femaleUrls: string[]): TrainerNameConfig {
|
hasGenderVariant(...femaleUrls: string[]): TrainerNameConfig {
|
||||||
|
@ -41,15 +41,9 @@ import type {
|
|||||||
TrainerConfigs,
|
TrainerConfigs,
|
||||||
TrainerTierPools,
|
TrainerTierPools,
|
||||||
} from "#types/trainer-funcs";
|
} from "#types/trainer-funcs";
|
||||||
import {
|
import { coerceArray, isNullOrUndefined, randSeedInt, randSeedIntRange, randSeedItem } from "#utils/common";
|
||||||
coerceArray,
|
|
||||||
isNullOrUndefined,
|
|
||||||
randSeedInt,
|
|
||||||
randSeedIntRange,
|
|
||||||
randSeedItem,
|
|
||||||
toReadableString,
|
|
||||||
} from "#utils/common";
|
|
||||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
||||||
|
import { toSnakeCase, toTitleCase } from "#utils/strings";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
/** Minimum BST for Pokemon generated onto the Elite Four's teams */
|
/** Minimum BST for Pokemon generated onto the Elite Four's teams */
|
||||||
@ -140,7 +134,7 @@ export class TrainerConfig {
|
|||||||
constructor(trainerType: TrainerType, allowLegendaries?: boolean) {
|
constructor(trainerType: TrainerType, allowLegendaries?: boolean) {
|
||||||
this.trainerType = trainerType;
|
this.trainerType = trainerType;
|
||||||
this.trainerAI = new TrainerAI();
|
this.trainerAI = new TrainerAI();
|
||||||
this.name = toReadableString(TrainerType[this.getDerivedType()]);
|
this.name = toTitleCase(TrainerType[this.getDerivedType()]);
|
||||||
this.battleBgm = "battle_trainer";
|
this.battleBgm = "battle_trainer";
|
||||||
this.mixedBattleBgm = "battle_trainer";
|
this.mixedBattleBgm = "battle_trainer";
|
||||||
this.victoryBgm = "victory_trainer";
|
this.victoryBgm = "victory_trainer";
|
||||||
@ -734,7 +728,7 @@ export class TrainerConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Localize the trainer's name by converting it to lowercase and replacing spaces with underscores.
|
// Localize the trainer's name by converting it to lowercase and replacing spaces with underscores.
|
||||||
const nameForCall = this.name.toLowerCase().replace(/\s/g, "_");
|
const nameForCall = toSnakeCase(this.name);
|
||||||
this.name = i18next.t(`trainerNames:${nameForCall}`);
|
this.name = i18next.t(`trainerNames:${nameForCall}`);
|
||||||
|
|
||||||
// Set the title to "elite_four". (this is the key in the i18n file)
|
// Set the title to "elite_four". (this is the key in the i18n file)
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import type { ObjectValues } from "#types/type-helpers";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Not to be confused with an Ability Attribute.
|
* Not to be confused with an Ability Attribute.
|
||||||
* This is an object literal storing the slot that an ability can occupy.
|
* This is an object literal storing the slot that an ability can occupy.
|
||||||
@ -8,4 +10,4 @@ export const AbilityAttr = Object.freeze({
|
|||||||
ABILITY_HIDDEN: 4,
|
ABILITY_HIDDEN: 4,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type AbilityAttr = typeof AbilityAttr[keyof typeof AbilityAttr];
|
export type AbilityAttr = ObjectValues<typeof AbilityAttr>;
|
@ -15,9 +15,6 @@ export enum ArenaTagType {
|
|||||||
SPIKES = "SPIKES",
|
SPIKES = "SPIKES",
|
||||||
TOXIC_SPIKES = "TOXIC_SPIKES",
|
TOXIC_SPIKES = "TOXIC_SPIKES",
|
||||||
MIST = "MIST",
|
MIST = "MIST",
|
||||||
FUTURE_SIGHT = "FUTURE_SIGHT",
|
|
||||||
DOOM_DESIRE = "DOOM_DESIRE",
|
|
||||||
WISH = "WISH",
|
|
||||||
STEALTH_ROCK = "STEALTH_ROCK",
|
STEALTH_ROCK = "STEALTH_ROCK",
|
||||||
STICKY_WEB = "STICKY_WEB",
|
STICKY_WEB = "STICKY_WEB",
|
||||||
TRICK_ROOM = "TRICK_ROOM",
|
TRICK_ROOM = "TRICK_ROOM",
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
export enum BattlerTagType {
|
export enum BattlerTagType {
|
||||||
NONE = "NONE",
|
|
||||||
RECHARGING = "RECHARGING",
|
RECHARGING = "RECHARGING",
|
||||||
FLINCHED = "FLINCHED",
|
FLINCHED = "FLINCHED",
|
||||||
INTERRUPTED = "INTERRUPTED",
|
INTERRUPTED = "INTERRUPTED",
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import type { ObjectValues } from "#types/type-helpers";
|
||||||
|
|
||||||
export const DexAttr = Object.freeze({
|
export const DexAttr = Object.freeze({
|
||||||
NON_SHINY: 1n,
|
NON_SHINY: 1n,
|
||||||
SHINY: 2n,
|
SHINY: 2n,
|
||||||
@ -8,4 +10,4 @@ export const DexAttr = Object.freeze({
|
|||||||
VARIANT_3: 64n,
|
VARIANT_3: 64n,
|
||||||
DEFAULT_FORM: 128n,
|
DEFAULT_FORM: 128n,
|
||||||
});
|
});
|
||||||
export type DexAttr = typeof DexAttr[keyof typeof DexAttr];
|
export type DexAttr = ObjectValues<typeof DexAttr>;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Enum representation of the phase types held by implementations of {@linkcode PhasePriorityQueue}
|
* Enum representation of the phase types held by implementations of {@linkcode PhasePriorityQueue}.
|
||||||
*/
|
*/
|
||||||
|
// TODO: We currently assume these are in order
|
||||||
export enum DynamicPhaseType {
|
export enum DynamicPhaseType {
|
||||||
POST_SUMMON
|
POST_SUMMON
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
|
import type { ObjectValues } from "#types/type-helpers";
|
||||||
|
|
||||||
export const GachaType = Object.freeze({
|
export const GachaType = Object.freeze({
|
||||||
MOVE: 0,
|
MOVE: 0,
|
||||||
LEGENDARY: 1,
|
LEGENDARY: 1,
|
||||||
SHINY: 2
|
SHINY: 2
|
||||||
});
|
});
|
||||||
|
|
||||||
export type GachaType = typeof GachaType[keyof typeof GachaType];
|
export type GachaType = ObjectValues<typeof GachaType>;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import type { ObjectValues } from "#types/type-helpers";
|
||||||
|
|
||||||
/** The result of a hit check calculation */
|
/** The result of a hit check calculation */
|
||||||
export const HitCheckResult = {
|
export const HitCheckResult = {
|
||||||
/** Hit checks haven't been evaluated yet in this pass */
|
/** Hit checks haven't been evaluated yet in this pass */
|
||||||
@ -20,4 +22,4 @@ export const HitCheckResult = {
|
|||||||
ERROR: 8,
|
ERROR: 8,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type HitCheckResult = typeof HitCheckResult[keyof typeof HitCheckResult];
|
export type HitCheckResult = ObjectValues<typeof HitCheckResult>;
|
||||||
|
@ -4,15 +4,19 @@
|
|||||||
*/
|
*/
|
||||||
export enum MoveFlags {
|
export enum MoveFlags {
|
||||||
NONE = 0,
|
NONE = 0,
|
||||||
|
/**
|
||||||
|
* Whether the move makes contact.
|
||||||
|
* Set by default on all contact moves, and unset by default on all special moves.
|
||||||
|
*/
|
||||||
MAKES_CONTACT = 1 << 0,
|
MAKES_CONTACT = 1 << 0,
|
||||||
IGNORE_PROTECT = 1 << 1,
|
IGNORE_PROTECT = 1 << 1,
|
||||||
/**
|
/**
|
||||||
* Sound-based moves have the following effects:
|
* Sound-based moves have the following effects:
|
||||||
* - Pokemon with the {@linkcode AbilityId.SOUNDPROOF Soundproof Ability} are unaffected by other Pokemon's sound-based moves.
|
* - Pokemon with the {@linkcode AbilityId.SOUNDPROOF | Soundproof} Ability are unaffected by other Pokemon's sound-based moves.
|
||||||
* - Pokemon affected by {@linkcode MoveId.THROAT_CHOP Throat Chop} cannot use sound-based moves for two turns.
|
* - Pokemon affected by {@linkcode MoveId.THROAT_CHOP | Throat Chop} cannot use sound-based moves for two turns.
|
||||||
* - Sound-based moves used by a Pokemon with {@linkcode AbilityId.LIQUID_VOICE Liquid Voice} become Water-type moves.
|
* - Sound-based moves used by a Pokemon with {@linkcode AbilityId.LIQUID_VOICE | Liquid Voice} become Water-type moves.
|
||||||
* - Sound-based moves used by a Pokemon with {@linkcode AbilityId.PUNK_ROCK Punk Rock} are boosted by 30%. Pokemon with Punk Rock also take half damage from sound-based moves.
|
* - Sound-based moves used by a Pokemon with {@linkcode AbilityId.PUNK_ROCK | Punk Rock} are boosted by 30%. Pokemon with Punk Rock also take half damage from sound-based moves.
|
||||||
* - All sound-based moves (except Howl) can hit Pokemon behind an active {@linkcode MoveId.SUBSTITUTE Substitute}.
|
* - All sound-based moves (except Howl) can hit Pokemon behind an active {@linkcode MoveId.SUBSTITUTE | Substitute}.
|
||||||
*
|
*
|
||||||
* cf https://bulbapedia.bulbagarden.net/wiki/Sound-based_move
|
* cf https://bulbapedia.bulbagarden.net/wiki/Sound-based_move
|
||||||
*/
|
*/
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import type { PostDancingMoveAbAttr } from "#abilities/ability";
|
import type { PostDancingMoveAbAttr } from "#abilities/ability";
|
||||||
|
import type { DelayedAttackAttr } from "#app/@types/move-types";
|
||||||
import type { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
|
import type { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
|
||||||
|
import type { ObjectValues } from "#types/type-helpers";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enum representing all the possible means through which a given move can be executed.
|
* Enum representing all the possible means through which a given move can be executed.
|
||||||
@ -59,11 +61,20 @@ export const MoveUseMode = {
|
|||||||
* and retain the same copy prevention as {@linkcode MoveUseMode.FOLLOW_UP}, but additionally
|
* and retain the same copy prevention as {@linkcode MoveUseMode.FOLLOW_UP}, but additionally
|
||||||
* **cannot be reflected by other reflecting effects**.
|
* **cannot be reflected by other reflecting effects**.
|
||||||
*/
|
*/
|
||||||
REFLECTED: 5
|
REFLECTED: 5,
|
||||||
// TODO: Add use type TRANSPARENT for Future Sight and Doom Desire to prevent move history pushing
|
/**
|
||||||
|
* This "move" was created by a transparent effect that **does not count as using a move**,
|
||||||
|
* such as {@linkcode DelayedAttackAttr | Future Sight/Doom Desire}.
|
||||||
|
*
|
||||||
|
* In addition to inheriting the cancellation ignores and copy prevention from {@linkcode MoveUseMode.REFLECTED},
|
||||||
|
* transparent moves are ignored by **all forms of move usage checks** due to **not pushing to move history**.
|
||||||
|
* @todo Consider other means of implementing FS/DD than this - we currently only use it
|
||||||
|
* to prevent pushing to move history and avoid re-delaying the attack portion
|
||||||
|
*/
|
||||||
|
DELAYED_ATTACK: 6
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type MoveUseMode = (typeof MoveUseMode)[keyof typeof MoveUseMode];
|
export type MoveUseMode = ObjectValues<typeof MoveUseMode>;
|
||||||
|
|
||||||
// # HELPER FUNCTIONS
|
// # HELPER FUNCTIONS
|
||||||
// Please update the markdown tables if any new `MoveUseMode`s get added.
|
// Please update the markdown tables if any new `MoveUseMode`s get added.
|
||||||
@ -75,13 +86,14 @@ export type MoveUseMode = (typeof MoveUseMode)[keyof typeof MoveUseMode];
|
|||||||
* @remarks
|
* @remarks
|
||||||
* This function is equivalent to the following truth table:
|
* This function is equivalent to the following truth table:
|
||||||
*
|
*
|
||||||
* | Use Type | Returns |
|
* | Use Type | Returns |
|
||||||
* |------------------------------------|---------|
|
* |----------------------------------------|---------|
|
||||||
* | {@linkcode MoveUseMode.NORMAL} | `false` |
|
* | {@linkcode MoveUseMode.NORMAL} | `false` |
|
||||||
* | {@linkcode MoveUseMode.IGNORE_PP} | `false` |
|
* | {@linkcode MoveUseMode.IGNORE_PP} | `false` |
|
||||||
* | {@linkcode MoveUseMode.INDIRECT} | `true` |
|
* | {@linkcode MoveUseMode.INDIRECT} | `true` |
|
||||||
* | {@linkcode MoveUseMode.FOLLOW_UP} | `true` |
|
* | {@linkcode MoveUseMode.FOLLOW_UP} | `true` |
|
||||||
* | {@linkcode MoveUseMode.REFLECTED} | `true` |
|
* | {@linkcode MoveUseMode.REFLECTED} | `true` |
|
||||||
|
* | {@linkcode MoveUseMode.DELAYED_ATTACK} | `true` |
|
||||||
*/
|
*/
|
||||||
export function isVirtual(useMode: MoveUseMode): boolean {
|
export function isVirtual(useMode: MoveUseMode): boolean {
|
||||||
return useMode >= MoveUseMode.INDIRECT
|
return useMode >= MoveUseMode.INDIRECT
|
||||||
@ -95,13 +107,14 @@ export function isVirtual(useMode: MoveUseMode): boolean {
|
|||||||
* @remarks
|
* @remarks
|
||||||
* This function is equivalent to the following truth table:
|
* This function is equivalent to the following truth table:
|
||||||
*
|
*
|
||||||
* | Use Type | Returns |
|
* | Use Type | Returns |
|
||||||
* |------------------------------------|---------|
|
* |----------------------------------------|---------|
|
||||||
* | {@linkcode MoveUseMode.NORMAL} | `false` |
|
* | {@linkcode MoveUseMode.NORMAL} | `false` |
|
||||||
* | {@linkcode MoveUseMode.IGNORE_PP} | `false` |
|
* | {@linkcode MoveUseMode.IGNORE_PP} | `false` |
|
||||||
* | {@linkcode MoveUseMode.INDIRECT} | `false` |
|
* | {@linkcode MoveUseMode.INDIRECT} | `false` |
|
||||||
* | {@linkcode MoveUseMode.FOLLOW_UP} | `true` |
|
* | {@linkcode MoveUseMode.FOLLOW_UP} | `true` |
|
||||||
* | {@linkcode MoveUseMode.REFLECTED} | `true` |
|
* | {@linkcode MoveUseMode.REFLECTED} | `true` |
|
||||||
|
* | {@linkcode MoveUseMode.DELAYED_ATTACK} | `true` |
|
||||||
*/
|
*/
|
||||||
export function isIgnoreStatus(useMode: MoveUseMode): boolean {
|
export function isIgnoreStatus(useMode: MoveUseMode): boolean {
|
||||||
return useMode >= MoveUseMode.FOLLOW_UP;
|
return useMode >= MoveUseMode.FOLLOW_UP;
|
||||||
@ -115,13 +128,14 @@ export function isIgnoreStatus(useMode: MoveUseMode): boolean {
|
|||||||
* @remarks
|
* @remarks
|
||||||
* This function is equivalent to the following truth table:
|
* This function is equivalent to the following truth table:
|
||||||
*
|
*
|
||||||
* | Use Type | Returns |
|
* | Use Type | Returns |
|
||||||
* |------------------------------------|---------|
|
* |----------------------------------------|---------|
|
||||||
* | {@linkcode MoveUseMode.NORMAL} | `false` |
|
* | {@linkcode MoveUseMode.NORMAL} | `false` |
|
||||||
* | {@linkcode MoveUseMode.IGNORE_PP} | `true` |
|
* | {@linkcode MoveUseMode.IGNORE_PP} | `true` |
|
||||||
* | {@linkcode MoveUseMode.INDIRECT} | `true` |
|
* | {@linkcode MoveUseMode.INDIRECT} | `true` |
|
||||||
* | {@linkcode MoveUseMode.FOLLOW_UP} | `true` |
|
* | {@linkcode MoveUseMode.FOLLOW_UP} | `true` |
|
||||||
* | {@linkcode MoveUseMode.REFLECTED} | `true` |
|
* | {@linkcode MoveUseMode.REFLECTED} | `true` |
|
||||||
|
* | {@linkcode MoveUseMode.DELAYED_ATTACK} | `true` |
|
||||||
*/
|
*/
|
||||||
export function isIgnorePP(useMode: MoveUseMode): boolean {
|
export function isIgnorePP(useMode: MoveUseMode): boolean {
|
||||||
return useMode >= MoveUseMode.IGNORE_PP;
|
return useMode >= MoveUseMode.IGNORE_PP;
|
||||||
@ -136,14 +150,15 @@ export function isIgnorePP(useMode: MoveUseMode): boolean {
|
|||||||
* @remarks
|
* @remarks
|
||||||
* This function is equivalent to the following truth table:
|
* This function is equivalent to the following truth table:
|
||||||
*
|
*
|
||||||
* | Use Type | Returns |
|
* | Use Type | Returns |
|
||||||
* |------------------------------------|---------|
|
* |----------------------------------------|---------|
|
||||||
* | {@linkcode MoveUseMode.NORMAL} | `false` |
|
* | {@linkcode MoveUseMode.NORMAL} | `false` |
|
||||||
* | {@linkcode MoveUseMode.IGNORE_PP} | `false` |
|
* | {@linkcode MoveUseMode.IGNORE_PP} | `false` |
|
||||||
* | {@linkcode MoveUseMode.INDIRECT} | `false` |
|
* | {@linkcode MoveUseMode.INDIRECT} | `false` |
|
||||||
* | {@linkcode MoveUseMode.FOLLOW_UP} | `false` |
|
* | {@linkcode MoveUseMode.FOLLOW_UP} | `false` |
|
||||||
* | {@linkcode MoveUseMode.REFLECTED} | `true` |
|
* | {@linkcode MoveUseMode.REFLECTED} | `true` |
|
||||||
|
* | {@linkcode MoveUseMode.DELAYED_ATTACK} | `false` |
|
||||||
*/
|
*/
|
||||||
export function isReflected(useMode: MoveUseMode): boolean {
|
export function isReflected(useMode: MoveUseMode): boolean {
|
||||||
return useMode === MoveUseMode.REFLECTED;
|
return useMode === MoveUseMode.REFLECTED;
|
||||||
}
|
}
|
10
src/enums/positional-tag-type.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* Enum representing all positional tag types.
|
||||||
|
* @privateRemarks
|
||||||
|
* When adding new tag types, please update `positionalTagConstructorMap` in `src/data/positionalTags`
|
||||||
|
* with the new tag type.
|
||||||
|
*/
|
||||||
|
export enum PositionalTagType {
|
||||||
|
DELAYED_ATTACK = "DELAYED_ATTACK",
|
||||||
|
WISH = "WISH",
|
||||||
|
}
|
@ -1,3 +1,7 @@
|
|||||||
|
// biome-ignore-start lint/correctness/noUnusedImports: TSDoc imports
|
||||||
|
import type { PositionalTag } from "#data/positional-tags/positional-tag";
|
||||||
|
// biome-ignore-end lint/correctness/noUnusedImports: TSDoc imports
|
||||||
|
|
||||||
import { applyAbAttrs } from "#abilities/apply-ab-attrs";
|
import { applyAbAttrs } from "#abilities/apply-ab-attrs";
|
||||||
import { globalScene } from "#app/global-scene";
|
import { globalScene } from "#app/global-scene";
|
||||||
import Overrides from "#app/overrides";
|
import Overrides from "#app/overrides";
|
||||||
@ -7,6 +11,7 @@ import type { ArenaTag } from "#data/arena-tag";
|
|||||||
import { ArenaTrapTag, getArenaTag } from "#data/arena-tag";
|
import { ArenaTrapTag, getArenaTag } from "#data/arena-tag";
|
||||||
import { SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "#data/form-change-triggers";
|
import { SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "#data/form-change-triggers";
|
||||||
import type { PokemonSpecies } from "#data/pokemon-species";
|
import type { PokemonSpecies } from "#data/pokemon-species";
|
||||||
|
import { PositionalTagManager } from "#data/positional-tags/positional-tag-manager";
|
||||||
import { getTerrainClearMessage, getTerrainStartMessage, Terrain, TerrainType } from "#data/terrain";
|
import { getTerrainClearMessage, getTerrainStartMessage, Terrain, TerrainType } from "#data/terrain";
|
||||||
import {
|
import {
|
||||||
getLegendaryWeatherContinuesMessage,
|
getLegendaryWeatherContinuesMessage,
|
||||||
@ -38,7 +43,14 @@ export class Arena {
|
|||||||
public biomeType: BiomeId;
|
public biomeType: BiomeId;
|
||||||
public weather: Weather | null;
|
public weather: Weather | null;
|
||||||
public terrain: Terrain | null;
|
public terrain: Terrain | null;
|
||||||
public tags: ArenaTag[];
|
/** All currently-active {@linkcode ArenaTag}s on both sides of the field. */
|
||||||
|
public tags: ArenaTag[] = [];
|
||||||
|
/**
|
||||||
|
* All currently-active {@linkcode PositionalTag}s on both sides of the field,
|
||||||
|
* sorted by tag type.
|
||||||
|
*/
|
||||||
|
public positionalTagManager: PositionalTagManager = new PositionalTagManager();
|
||||||
|
|
||||||
public bgm: string;
|
public bgm: string;
|
||||||
public ignoreAbilities: boolean;
|
public ignoreAbilities: boolean;
|
||||||
public ignoringEffectSource: BattlerIndex | null;
|
public ignoringEffectSource: BattlerIndex | null;
|
||||||
@ -58,7 +70,6 @@ export class Arena {
|
|||||||
|
|
||||||
constructor(biome: BiomeId, bgm: string, playerFaints = 0) {
|
constructor(biome: BiomeId, bgm: string, playerFaints = 0) {
|
||||||
this.biomeType = biome;
|
this.biomeType = biome;
|
||||||
this.tags = [];
|
|
||||||
this.bgm = bgm;
|
this.bgm = bgm;
|
||||||
this.trainerPool = biomeTrainerPools[biome];
|
this.trainerPool = biomeTrainerPools[biome];
|
||||||
this.updatePoolsForTimeOfDay();
|
this.updatePoolsForTimeOfDay();
|
||||||
@ -676,15 +687,15 @@ export class Arena {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a new tag to the arena
|
* Add a new {@linkcode ArenaTag} to the arena, triggering overlap effects on existing tags as applicable.
|
||||||
* @param tagType {@linkcode ArenaTagType} the tag being added
|
* @param tagType - The {@linkcode ArenaTagType} of the tag to add.
|
||||||
* @param turnCount How many turns the tag lasts
|
* @param turnCount - The number of turns the newly-added tag should last.
|
||||||
* @param sourceMove {@linkcode MoveId} the move the tag came from, or `undefined` if not from a move
|
* @param sourceId - The {@linkcode Pokemon.id | PID} of the Pokemon creating the tag.
|
||||||
* @param sourceId The ID of the pokemon in play the tag came from (see {@linkcode BattleScene.getPokemonById})
|
* @param sourceMove - The {@linkcode MoveId} of the move creating the tag, or `undefined` if not from a move.
|
||||||
* @param side {@linkcode ArenaTagSide} which side(s) the tag applies to
|
* @param side - The {@linkcode ArenaTagSide}(s) to which the tag should apply; default `ArenaTagSide.BOTH`.
|
||||||
* @param quiet If a message should be queued on screen to announce the tag being added
|
* @param quiet - Whether to suppress messages produced by tag addition; default `false`.
|
||||||
* @param targetIndex The {@linkcode BattlerIndex} of the target pokemon
|
* @returns `true` if the tag was successfully added without overlapping.
|
||||||
* @returns `false` if there already exists a tag of this type in the Arena
|
// TODO: Do we need the return value here? literally nothing uses it
|
||||||
*/
|
*/
|
||||||
addTag(
|
addTag(
|
||||||
tagType: ArenaTagType,
|
tagType: ArenaTagType,
|
||||||
@ -693,7 +704,6 @@ export class Arena {
|
|||||||
sourceId: number,
|
sourceId: number,
|
||||||
side: ArenaTagSide = ArenaTagSide.BOTH,
|
side: ArenaTagSide = ArenaTagSide.BOTH,
|
||||||
quiet = false,
|
quiet = false,
|
||||||
targetIndex?: BattlerIndex,
|
|
||||||
): boolean {
|
): boolean {
|
||||||
const existingTag = this.getTagOnSide(tagType, side);
|
const existingTag = this.getTagOnSide(tagType, side);
|
||||||
if (existingTag) {
|
if (existingTag) {
|
||||||
@ -708,7 +718,7 @@ export class Arena {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// creates a new tag object
|
// creates a new tag object
|
||||||
const newTag = getArenaTag(tagType, turnCount || 0, sourceMove, sourceId, targetIndex, side);
|
const newTag = getArenaTag(tagType, turnCount || 0, sourceMove, sourceId, side);
|
||||||
if (newTag) {
|
if (newTag) {
|
||||||
newTag.onAdd(this, quiet);
|
newTag.onAdd(this, quiet);
|
||||||
this.tags.push(newTag);
|
this.tags.push(newTag);
|
||||||
@ -724,10 +734,19 @@ export class Arena {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempts to get a tag from the Arena via {@linkcode getTagOnSide} that applies to both sides
|
* Attempt to get a tag from the Arena via {@linkcode getTagOnSide} that applies to both sides
|
||||||
* @param tagType The {@linkcode ArenaTagType} or {@linkcode ArenaTag} to get
|
* @param tagType - The {@linkcode ArenaTagType} to retrieve
|
||||||
* @returns either the {@linkcode ArenaTag}, or `undefined` if it isn't there
|
* @returns The existing {@linkcode ArenaTag}, or `undefined` if not present.
|
||||||
|
* @overload
|
||||||
*/
|
*/
|
||||||
|
getTag(tagType: ArenaTagType): ArenaTag | undefined;
|
||||||
|
/**
|
||||||
|
* Attempt to get a tag from the Arena via {@linkcode getTagOnSide} that applies to both sides
|
||||||
|
* @param tagType - The constructor of the {@linkcode ArenaTag} to retrieve
|
||||||
|
* @returns The existing {@linkcode ArenaTag}, or `undefined` if not present.
|
||||||
|
* @overload
|
||||||
|
*/
|
||||||
|
getTag<T extends ArenaTag>(tagType: Constructor<T> | AbstractConstructor<T>): T | undefined;
|
||||||
getTag(tagType: ArenaTagType | Constructor<ArenaTag> | AbstractConstructor<ArenaTag>): ArenaTag | undefined {
|
getTag(tagType: ArenaTagType | Constructor<ArenaTag> | AbstractConstructor<ArenaTag>): ArenaTag | undefined {
|
||||||
return this.getTagOnSide(tagType, ArenaTagSide.BOTH);
|
return this.getTagOnSide(tagType, ArenaTagSide.BOTH);
|
||||||
}
|
}
|
||||||
|
@ -213,8 +213,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
* TODO: Stop treating this like a unique ID and stop treating 0 as no pokemon
|
* TODO: Stop treating this like a unique ID and stop treating 0 as no pokemon
|
||||||
*/
|
*/
|
||||||
public id: number;
|
public id: number;
|
||||||
public name: string;
|
/**
|
||||||
public nickname: string;
|
* The Pokemon's current nickname, or `undefined` if it currently lacks one.
|
||||||
|
* If omitted, references to this should refer to the default name for this Pokemon's species.
|
||||||
|
*/
|
||||||
|
public nickname?: string;
|
||||||
public species: PokemonSpecies;
|
public species: PokemonSpecies;
|
||||||
public formIndex: number;
|
public formIndex: number;
|
||||||
public abilityIndex: number;
|
public abilityIndex: number;
|
||||||
@ -444,7 +447,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
getNameToRender(useIllusion = true) {
|
getNameToRender(useIllusion = true) {
|
||||||
const illusion = this.summonData.illusion;
|
const illusion = this.summonData.illusion;
|
||||||
const name = useIllusion ? (illusion?.name ?? this.name) : this.name;
|
const name = useIllusion ? (illusion?.name ?? this.name) : this.name;
|
||||||
const nickname: string = useIllusion ? (illusion?.nickname ?? this.nickname) : this.nickname;
|
const nickname: string | undefined = useIllusion ? illusion?.nickname : this.nickname;
|
||||||
try {
|
try {
|
||||||
if (nickname) {
|
if (nickname) {
|
||||||
return decodeURIComponent(escape(atob(nickname))); // TODO: Remove `atob` and `escape`... eventually...
|
return decodeURIComponent(escape(atob(nickname))); // TODO: Remove `atob` and `escape`... eventually...
|
||||||
@ -5664,7 +5667,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class PlayerPokemon extends Pokemon {
|
export class PlayerPokemon extends Pokemon {
|
||||||
protected battleInfo: PlayerBattleInfo;
|
protected declare battleInfo: PlayerBattleInfo;
|
||||||
public compatibleTms: MoveId[];
|
public compatibleTms: MoveId[];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -6193,7 +6196,7 @@ export class PlayerPokemon extends Pokemon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class EnemyPokemon extends Pokemon {
|
export class EnemyPokemon extends Pokemon {
|
||||||
protected battleInfo: EnemyBattleInfo;
|
protected declare battleInfo: EnemyBattleInfo;
|
||||||
public trainerSlot: TrainerSlot;
|
public trainerSlot: TrainerSlot;
|
||||||
public aiType: AiType;
|
public aiType: AiType;
|
||||||
public bossSegments: number;
|
public bossSegments: number;
|
||||||
|
@ -23,13 +23,13 @@ import {
|
|||||||
} from "#trainers/trainer-party-template";
|
} from "#trainers/trainer-party-template";
|
||||||
import { randSeedInt, randSeedItem, randSeedWeightedItem } from "#utils/common";
|
import { randSeedInt, randSeedItem, randSeedWeightedItem } from "#utils/common";
|
||||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
||||||
|
import { toSnakeCase } from "#utils/strings";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
export class Trainer extends Phaser.GameObjects.Container {
|
export class Trainer extends Phaser.GameObjects.Container {
|
||||||
public config: TrainerConfig;
|
public config: TrainerConfig;
|
||||||
public variant: TrainerVariant;
|
public variant: TrainerVariant;
|
||||||
public partyTemplateIndex: number;
|
public partyTemplateIndex: number;
|
||||||
public name: string;
|
|
||||||
public partnerName: string;
|
public partnerName: string;
|
||||||
public nameKey: string;
|
public nameKey: string;
|
||||||
public partnerNameKey: string | undefined;
|
public partnerNameKey: string | undefined;
|
||||||
@ -170,7 +170,7 @@ export class Trainer extends Phaser.GameObjects.Container {
|
|||||||
const evilTeamTitles = ["grunt"];
|
const evilTeamTitles = ["grunt"];
|
||||||
if (this.name === "" && evilTeamTitles.some(t => name.toLocaleLowerCase().includes(t))) {
|
if (this.name === "" && evilTeamTitles.some(t => name.toLocaleLowerCase().includes(t))) {
|
||||||
// This is a evil team grunt so we localize it by only using the "name" as the title
|
// This is a evil team grunt so we localize it by only using the "name" as the title
|
||||||
title = i18next.t(`trainerClasses:${name.toLowerCase().replace(/\s/g, "_")}`);
|
title = i18next.t(`trainerClasses:${toSnakeCase(name)}`);
|
||||||
console.log("Localized grunt name: " + title);
|
console.log("Localized grunt name: " + title);
|
||||||
// Since grunts are not named we can just return the title
|
// Since grunts are not named we can just return the title
|
||||||
return title;
|
return title;
|
||||||
@ -187,7 +187,7 @@ export class Trainer extends Phaser.GameObjects.Container {
|
|||||||
}
|
}
|
||||||
// Get the localized trainer class name from the i18n file and set it as the title.
|
// Get the localized trainer class name from the i18n file and set it as the title.
|
||||||
// This is used for trainer class names, not titles like "Elite Four, Champion, etc."
|
// This is used for trainer class names, not titles like "Elite Four, Champion, etc."
|
||||||
title = i18next.t(`trainerClasses:${name.toLowerCase().replace(/\s/g, "_")}`);
|
title = i18next.t(`trainerClasses:${toSnakeCase(name)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no specific trainer slot is set.
|
// If no specific trainer slot is set.
|
||||||
@ -208,7 +208,7 @@ export class Trainer extends Phaser.GameObjects.Container {
|
|||||||
|
|
||||||
if (this.config.titleDouble && this.variant === TrainerVariant.DOUBLE && !this.config.doubleOnly) {
|
if (this.config.titleDouble && this.variant === TrainerVariant.DOUBLE && !this.config.doubleOnly) {
|
||||||
title = this.config.titleDouble;
|
title = this.config.titleDouble;
|
||||||
name = i18next.t(`trainerNames:${this.config.nameDouble.toLowerCase().replace(/\s/g, "_")}`);
|
name = i18next.t(`trainerNames:${toSnakeCase(this.config.nameDouble)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(title ? `${title} ${name}` : name);
|
console.log(title ? `${title} ${name}` : name);
|
||||||
|
@ -119,6 +119,7 @@ export class LoadingScene extends SceneBase {
|
|||||||
|
|
||||||
this.loadImage("party_bg", "ui");
|
this.loadImage("party_bg", "ui");
|
||||||
this.loadImage("party_bg_double", "ui");
|
this.loadImage("party_bg_double", "ui");
|
||||||
|
this.loadImage("party_bg_double_manage", "ui");
|
||||||
this.loadAtlas("party_slot_main", "ui");
|
this.loadAtlas("party_slot_main", "ui");
|
||||||
this.loadAtlas("party_slot", "ui");
|
this.loadAtlas("party_slot", "ui");
|
||||||
this.loadImage("party_slot_overlay_lv", "ui");
|
this.loadImage("party_slot_overlay_lv", "ui");
|
||||||
@ -126,6 +127,8 @@ export class LoadingScene extends SceneBase {
|
|||||||
this.loadAtlas("party_slot_hp_overlay", "ui");
|
this.loadAtlas("party_slot_hp_overlay", "ui");
|
||||||
this.loadAtlas("party_pb", "ui");
|
this.loadAtlas("party_pb", "ui");
|
||||||
this.loadAtlas("party_cancel", "ui");
|
this.loadAtlas("party_cancel", "ui");
|
||||||
|
this.loadAtlas("party_discard", "ui");
|
||||||
|
this.loadAtlas("party_transfer", "ui");
|
||||||
|
|
||||||
this.loadImage("summary_bg", "ui");
|
this.loadImage("summary_bg", "ui");
|
||||||
this.loadImage("summary_overlay_shiny", "ui");
|
this.loadImage("summary_overlay_shiny", "ui");
|
||||||
|
@ -462,7 +462,7 @@ export abstract class LapsingPersistentModifier extends PersistentModifier {
|
|||||||
* @see {@linkcode apply}
|
* @see {@linkcode apply}
|
||||||
*/
|
*/
|
||||||
export class DoubleBattleChanceBoosterModifier extends LapsingPersistentModifier {
|
export class DoubleBattleChanceBoosterModifier extends LapsingPersistentModifier {
|
||||||
public override type: DoubleBattleChanceBoosterModifierType;
|
public declare type: DoubleBattleChanceBoosterModifierType;
|
||||||
|
|
||||||
match(modifier: Modifier): boolean {
|
match(modifier: Modifier): boolean {
|
||||||
return modifier instanceof DoubleBattleChanceBoosterModifier && modifier.getMaxBattles() === this.getMaxBattles();
|
return modifier instanceof DoubleBattleChanceBoosterModifier && modifier.getMaxBattles() === this.getMaxBattles();
|
||||||
@ -936,7 +936,7 @@ export class EvoTrackerModifier extends PokemonHeldItemModifier {
|
|||||||
* Currently used by Shuckle Juice item
|
* Currently used by Shuckle Juice item
|
||||||
*/
|
*/
|
||||||
export class PokemonBaseStatTotalModifier extends PokemonHeldItemModifier {
|
export class PokemonBaseStatTotalModifier extends PokemonHeldItemModifier {
|
||||||
public override type: PokemonBaseStatTotalModifierType;
|
public declare type: PokemonBaseStatTotalModifierType;
|
||||||
public isTransferable = false;
|
public isTransferable = false;
|
||||||
public statModifier: 10 | -15;
|
public statModifier: 10 | -15;
|
||||||
|
|
||||||
@ -2074,7 +2074,7 @@ export abstract class ConsumablePokemonModifier extends ConsumableModifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class TerrastalizeModifier extends ConsumablePokemonModifier {
|
export class TerrastalizeModifier extends ConsumablePokemonModifier {
|
||||||
public override type: TerastallizeModifierType;
|
public declare type: TerastallizeModifierType;
|
||||||
public teraType: PokemonType;
|
public teraType: PokemonType;
|
||||||
|
|
||||||
constructor(type: TerastallizeModifierType, pokemonId: number, teraType: PokemonType) {
|
constructor(type: TerastallizeModifierType, pokemonId: number, teraType: PokemonType) {
|
||||||
@ -2318,7 +2318,7 @@ export class PokemonLevelIncrementModifier extends ConsumablePokemonModifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class TmModifier extends ConsumablePokemonModifier {
|
export class TmModifier extends ConsumablePokemonModifier {
|
||||||
public override type: TmModifierType;
|
public declare type: TmModifierType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies {@linkcode TmModifier}
|
* Applies {@linkcode TmModifier}
|
||||||
@ -2365,7 +2365,7 @@ export class RememberMoveModifier extends ConsumablePokemonModifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class EvolutionItemModifier extends ConsumablePokemonModifier {
|
export class EvolutionItemModifier extends ConsumablePokemonModifier {
|
||||||
public override type: EvolutionItemModifierType;
|
public declare type: EvolutionItemModifierType;
|
||||||
/**
|
/**
|
||||||
* Applies {@linkcode EvolutionItemModifier}
|
* Applies {@linkcode EvolutionItemModifier}
|
||||||
* @param playerPokemon The {@linkcode PlayerPokemon} that should evolve via item
|
* @param playerPokemon The {@linkcode PlayerPokemon} that should evolve via item
|
||||||
@ -2530,7 +2530,7 @@ export class ExpBoosterModifier extends PersistentModifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class PokemonExpBoosterModifier extends PokemonHeldItemModifier {
|
export class PokemonExpBoosterModifier extends PokemonHeldItemModifier {
|
||||||
public override type: PokemonExpBoosterModifierType;
|
public declare type: PokemonExpBoosterModifierType;
|
||||||
|
|
||||||
private boostMultiplier: number;
|
private boostMultiplier: number;
|
||||||
|
|
||||||
@ -2627,7 +2627,7 @@ export class ExpBalanceModifier extends PersistentModifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class PokemonFriendshipBoosterModifier extends PokemonHeldItemModifier {
|
export class PokemonFriendshipBoosterModifier extends PokemonHeldItemModifier {
|
||||||
public override type: PokemonFriendshipBoosterModifierType;
|
public declare type: PokemonFriendshipBoosterModifierType;
|
||||||
|
|
||||||
matchType(modifier: Modifier): boolean {
|
matchType(modifier: Modifier): boolean {
|
||||||
return modifier instanceof PokemonFriendshipBoosterModifier;
|
return modifier instanceof PokemonFriendshipBoosterModifier;
|
||||||
@ -2684,7 +2684,7 @@ export class PokemonNatureWeightModifier extends PokemonHeldItemModifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class PokemonMoveAccuracyBoosterModifier extends PokemonHeldItemModifier {
|
export class PokemonMoveAccuracyBoosterModifier extends PokemonHeldItemModifier {
|
||||||
public override type: PokemonMoveAccuracyBoosterModifierType;
|
public declare type: PokemonMoveAccuracyBoosterModifierType;
|
||||||
private accuracyAmount: number;
|
private accuracyAmount: number;
|
||||||
|
|
||||||
constructor(type: PokemonMoveAccuracyBoosterModifierType, pokemonId: number, accuracy: number, stackCount?: number) {
|
constructor(type: PokemonMoveAccuracyBoosterModifierType, pokemonId: number, accuracy: number, stackCount?: number) {
|
||||||
@ -2736,7 +2736,7 @@ export class PokemonMoveAccuracyBoosterModifier extends PokemonHeldItemModifier
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class PokemonMultiHitModifier extends PokemonHeldItemModifier {
|
export class PokemonMultiHitModifier extends PokemonHeldItemModifier {
|
||||||
public override type: PokemonMultiHitModifierType;
|
public declare type: PokemonMultiHitModifierType;
|
||||||
|
|
||||||
matchType(modifier: Modifier): boolean {
|
matchType(modifier: Modifier): boolean {
|
||||||
return modifier instanceof PokemonMultiHitModifier;
|
return modifier instanceof PokemonMultiHitModifier;
|
||||||
@ -2817,7 +2817,7 @@ export class PokemonMultiHitModifier extends PokemonHeldItemModifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class PokemonFormChangeItemModifier extends PokemonHeldItemModifier {
|
export class PokemonFormChangeItemModifier extends PokemonHeldItemModifier {
|
||||||
public override type: FormChangeItemModifierType;
|
public declare type: FormChangeItemModifierType;
|
||||||
public formChangeItem: FormChangeItem;
|
public formChangeItem: FormChangeItem;
|
||||||
public active: boolean;
|
public active: boolean;
|
||||||
public isTransferable = false;
|
public isTransferable = false;
|
||||||
|
@ -9,6 +9,7 @@ import { AttemptCapturePhase } from "#phases/attempt-capture-phase";
|
|||||||
import { AttemptRunPhase } from "#phases/attempt-run-phase";
|
import { AttemptRunPhase } from "#phases/attempt-run-phase";
|
||||||
import { BattleEndPhase } from "#phases/battle-end-phase";
|
import { BattleEndPhase } from "#phases/battle-end-phase";
|
||||||
import { BerryPhase } from "#phases/berry-phase";
|
import { BerryPhase } from "#phases/berry-phase";
|
||||||
|
import { CheckInterludePhase } from "#phases/check-interlude-phase";
|
||||||
import { CheckStatusEffectPhase } from "#phases/check-status-effect-phase";
|
import { CheckStatusEffectPhase } from "#phases/check-status-effect-phase";
|
||||||
import { CheckSwitchPhase } from "#phases/check-switch-phase";
|
import { CheckSwitchPhase } from "#phases/check-switch-phase";
|
||||||
import { CommandPhase } from "#phases/command-phase";
|
import { CommandPhase } from "#phases/command-phase";
|
||||||
@ -60,6 +61,7 @@ import { PartyHealPhase } from "#phases/party-heal-phase";
|
|||||||
import { PokemonAnimPhase } from "#phases/pokemon-anim-phase";
|
import { PokemonAnimPhase } from "#phases/pokemon-anim-phase";
|
||||||
import { PokemonHealPhase } from "#phases/pokemon-heal-phase";
|
import { PokemonHealPhase } from "#phases/pokemon-heal-phase";
|
||||||
import { PokemonTransformPhase } from "#phases/pokemon-transform-phase";
|
import { PokemonTransformPhase } from "#phases/pokemon-transform-phase";
|
||||||
|
import { PositionalTagPhase } from "#phases/positional-tag-phase";
|
||||||
import { PostGameOverPhase } from "#phases/post-game-over-phase";
|
import { PostGameOverPhase } from "#phases/post-game-over-phase";
|
||||||
import { PostSummonPhase } from "#phases/post-summon-phase";
|
import { PostSummonPhase } from "#phases/post-summon-phase";
|
||||||
import { PostTurnStatusEffectPhase } from "#phases/post-turn-status-effect-phase";
|
import { PostTurnStatusEffectPhase } from "#phases/post-turn-status-effect-phase";
|
||||||
@ -121,6 +123,7 @@ const PHASES = Object.freeze({
|
|||||||
AttemptRunPhase,
|
AttemptRunPhase,
|
||||||
BattleEndPhase,
|
BattleEndPhase,
|
||||||
BerryPhase,
|
BerryPhase,
|
||||||
|
CheckInterludePhase,
|
||||||
CheckStatusEffectPhase,
|
CheckStatusEffectPhase,
|
||||||
CheckSwitchPhase,
|
CheckSwitchPhase,
|
||||||
CommandPhase,
|
CommandPhase,
|
||||||
@ -170,6 +173,7 @@ const PHASES = Object.freeze({
|
|||||||
PokemonAnimPhase,
|
PokemonAnimPhase,
|
||||||
PokemonHealPhase,
|
PokemonHealPhase,
|
||||||
PokemonTransformPhase,
|
PokemonTransformPhase,
|
||||||
|
PositionalTagPhase,
|
||||||
PostGameOverPhase,
|
PostGameOverPhase,
|
||||||
PostSummonPhase,
|
PostSummonPhase,
|
||||||
PostTurnStatusEffectPhase,
|
PostTurnStatusEffectPhase,
|
||||||
@ -240,6 +244,21 @@ export class PhaseManager {
|
|||||||
this.dynamicPhaseTypes = [PostSummonPhase];
|
this.dynamicPhaseTypes = [PostSummonPhase];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all previously set phases, then add a new {@linkcode TitlePhase} to transition to the title screen.
|
||||||
|
* @param addLogin - Whether to add a new {@linkcode LoginPhase} before the {@linkcode TitlePhase}
|
||||||
|
* (but reset everything else).
|
||||||
|
* Default `false`
|
||||||
|
*/
|
||||||
|
public toTitleScreen(addLogin = false): void {
|
||||||
|
this.clearAllPhases();
|
||||||
|
|
||||||
|
if (addLogin) {
|
||||||
|
this.unshiftNew("LoginPhase");
|
||||||
|
}
|
||||||
|
this.unshiftNew("TitlePhase");
|
||||||
|
}
|
||||||
|
|
||||||
/* Phase Functions */
|
/* Phase Functions */
|
||||||
getCurrentPhase(): Phase | null {
|
getCurrentPhase(): Phase | null {
|
||||||
return this.currentPhase;
|
return this.currentPhase;
|
||||||
@ -665,4 +684,15 @@ export class PhaseManager {
|
|||||||
): void {
|
): void {
|
||||||
this.startDynamicPhase(this.create(phase, ...args));
|
this.startDynamicPhase(this.create(phase, ...args));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Prevents end of turn effects from triggering when transitioning to a new biome on a X0 wave */
|
||||||
|
public onInterlude(): void {
|
||||||
|
const phasesToRemove = ["WeatherEffectPhase", "BerryPhase", "CheckStatusEffectPhase"];
|
||||||
|
this.phaseQueue = this.phaseQueue.filter(p => !phasesToRemove.includes(p.phaseName));
|
||||||
|
|
||||||
|
const turnEndPhase = this.findPhase<TurnEndPhase>(p => p.phaseName === "TurnEndPhase");
|
||||||
|
if (turnEndPhase) {
|
||||||
|
turnEndPhase.upcomingInterlude = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
18
src/phases/check-interlude-phase.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { globalScene } from "#app/global-scene";
|
||||||
|
import { Phase } from "#app/phase";
|
||||||
|
|
||||||
|
export class CheckInterludePhase extends Phase {
|
||||||
|
public override readonly phaseName = "CheckInterludePhase";
|
||||||
|
|
||||||
|
public override start(): void {
|
||||||
|
super.start();
|
||||||
|
const { phaseManager } = globalScene;
|
||||||
|
const { waveIndex } = globalScene.currentBattle;
|
||||||
|
|
||||||
|
if (waveIndex % 10 === 0 && globalScene.getEnemyParty().every(p => p.isFainted())) {
|
||||||
|
phaseManager.onInterlude();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.end();
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,6 @@ import type { TurnCommand } from "#app/battle";
|
|||||||
import { globalScene } from "#app/global-scene";
|
import { globalScene } from "#app/global-scene";
|
||||||
import { getPokemonNameWithAffix } from "#app/messages";
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
import { speciesStarterCosts } from "#balance/starters";
|
import { speciesStarterCosts } from "#balance/starters";
|
||||||
import type { EncoreTag } from "#data/battler-tags";
|
|
||||||
import { TrappedTag } from "#data/battler-tags";
|
import { TrappedTag } from "#data/battler-tags";
|
||||||
import { applyChallenges } from "#data/challenge";
|
import { applyChallenges } from "#data/challenge";
|
||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
@ -24,59 +23,77 @@ import type { MoveTargetSet } from "#moves/move";
|
|||||||
import { getMoveTargets } from "#moves/move-utils";
|
import { getMoveTargets } from "#moves/move-utils";
|
||||||
import { FieldPhase } from "#phases/field-phase";
|
import { FieldPhase } from "#phases/field-phase";
|
||||||
import type { TurnMove } from "#types/turn-move";
|
import type { TurnMove } from "#types/turn-move";
|
||||||
import { isNullOrUndefined } from "#utils/common";
|
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
export class CommandPhase extends FieldPhase {
|
export class CommandPhase extends FieldPhase {
|
||||||
public readonly phaseName = "CommandPhase";
|
public readonly phaseName = "CommandPhase";
|
||||||
protected fieldIndex: number;
|
protected fieldIndex: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the command phase is handling a switch command
|
||||||
|
*/
|
||||||
|
private isSwitch = false;
|
||||||
|
|
||||||
constructor(fieldIndex: number) {
|
constructor(fieldIndex: number) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.fieldIndex = fieldIndex;
|
this.fieldIndex = fieldIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
/**
|
||||||
super.start();
|
* Resets the cursor to the position of {@linkcode Command.FIGHT} if any of the following are true
|
||||||
|
* - The setting to remember the last action is not enabled
|
||||||
globalScene.updateGameInfo();
|
* - This is the first turn of a mystery encounter, trainer battle, or the END biome
|
||||||
|
* - The cursor is currently on the POKEMON command
|
||||||
|
*/
|
||||||
|
private resetCursorIfNeeded(): void {
|
||||||
const commandUiHandler = globalScene.ui.handlers[UiMode.COMMAND];
|
const commandUiHandler = globalScene.ui.handlers[UiMode.COMMAND];
|
||||||
|
const { arena, commandCursorMemory, currentBattle } = globalScene;
|
||||||
|
const { battleType, turn } = currentBattle;
|
||||||
|
const { biomeType } = arena;
|
||||||
|
|
||||||
// If one of these conditions is true, we always reset the cursor to Command.FIGHT
|
// If one of these conditions is true, we always reset the cursor to Command.FIGHT
|
||||||
const cursorResetEvent =
|
const cursorResetEvent =
|
||||||
globalScene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER ||
|
battleType === BattleType.MYSTERY_ENCOUNTER || battleType === BattleType.TRAINER || biomeType === BiomeId.END;
|
||||||
globalScene.currentBattle.battleType === BattleType.TRAINER ||
|
|
||||||
globalScene.arena.biomeType === BiomeId.END;
|
|
||||||
|
|
||||||
if (commandUiHandler) {
|
if (!commandUiHandler) {
|
||||||
if (
|
return;
|
||||||
(globalScene.currentBattle.turn === 1 && (!globalScene.commandCursorMemory || cursorResetEvent)) ||
|
}
|
||||||
commandUiHandler.getCursor() === Command.POKEMON
|
if (
|
||||||
) {
|
(turn === 1 && (!commandCursorMemory || cursorResetEvent)) ||
|
||||||
commandUiHandler.setCursor(Command.FIGHT);
|
commandUiHandler.getCursor() === Command.POKEMON
|
||||||
} else {
|
) {
|
||||||
commandUiHandler.setCursor(commandUiHandler.getCursor());
|
commandUiHandler.setCursor(Command.FIGHT);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submethod of {@linkcode start} that validates field index logic for nonzero field indices.
|
||||||
|
* Must only be called if the field index is nonzero.
|
||||||
|
*/
|
||||||
|
private handleFieldIndexLogic(): void {
|
||||||
|
// If we somehow are attempting to check the right pokemon but there's only one pokemon out
|
||||||
|
// Switch back to the center pokemon. This can happen rarely in double battles with mid turn switching
|
||||||
|
// TODO: Prevent this from happening in the first place
|
||||||
|
if (globalScene.getPlayerField().filter(p => p.isActive()).length === 1) {
|
||||||
|
this.fieldIndex = FieldPosition.CENTER;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.fieldIndex) {
|
const allyCommand = globalScene.currentBattle.turnCommands[this.fieldIndex - 1];
|
||||||
// If we somehow are attempting to check the right pokemon but there's only one pokemon out
|
if (allyCommand?.command === Command.BALL || allyCommand?.command === Command.RUN) {
|
||||||
// Switch back to the center pokemon. This can happen rarely in double battles with mid turn switching
|
globalScene.currentBattle.turnCommands[this.fieldIndex] = {
|
||||||
if (globalScene.getPlayerField().filter(p => p.isActive()).length === 1) {
|
command: allyCommand?.command,
|
||||||
this.fieldIndex = FieldPosition.CENTER;
|
skip: true,
|
||||||
} else {
|
};
|
||||||
const allyCommand = globalScene.currentBattle.turnCommands[this.fieldIndex - 1];
|
|
||||||
if (allyCommand?.command === Command.BALL || allyCommand?.command === Command.RUN) {
|
|
||||||
globalScene.currentBattle.turnCommands[this.fieldIndex] = {
|
|
||||||
command: allyCommand?.command,
|
|
||||||
skip: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submethod of {@linkcode start} that sets the turn command to skip if this pokemon
|
||||||
|
* is commanding its ally via {@linkcode AbilityId.COMMANDER}.
|
||||||
|
*/
|
||||||
|
private checkCommander(): void {
|
||||||
// If the Pokemon has applied Commander's effects to its ally, skip this command
|
// If the Pokemon has applied Commander's effects to its ally, skip this command
|
||||||
if (
|
if (
|
||||||
globalScene.currentBattle?.double &&
|
globalScene.currentBattle?.double &&
|
||||||
@ -88,390 +105,521 @@ export class CommandPhase extends FieldPhase {
|
|||||||
skip: true,
|
skip: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Checks if the Pokemon is under the effects of Encore. If so, Encore can end early if the encored move has no more PP.
|
/**
|
||||||
const encoreTag = this.getPokemon().getTag(BattlerTagType.ENCORE) as EncoreTag | undefined;
|
* Clear out all unusable moves in front of the currently acting pokemon's move queue.
|
||||||
if (encoreTag) {
|
*/
|
||||||
this.getPokemon().lapseTag(BattlerTagType.ENCORE);
|
// TODO: Refactor move queue handling to ensure that this method is not necessary.
|
||||||
}
|
private clearUnusuableMoves(): void {
|
||||||
|
const playerPokemon = this.getPokemon();
|
||||||
if (globalScene.currentBattle.turnCommands[this.fieldIndex]?.skip) {
|
|
||||||
return this.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
const playerPokemon = globalScene.getPlayerField()[this.fieldIndex];
|
|
||||||
|
|
||||||
const moveQueue = playerPokemon.getMoveQueue();
|
const moveQueue = playerPokemon.getMoveQueue();
|
||||||
|
if (moveQueue.length === 0) {
|
||||||
while (
|
return;
|
||||||
moveQueue.length &&
|
|
||||||
moveQueue[0] &&
|
|
||||||
moveQueue[0].move &&
|
|
||||||
!isVirtual(moveQueue[0].useMode) &&
|
|
||||||
(!playerPokemon.getMoveset().find(m => m.moveId === moveQueue[0].move) ||
|
|
||||||
!playerPokemon
|
|
||||||
.getMoveset()
|
|
||||||
[playerPokemon.getMoveset().findIndex(m => m.moveId === moveQueue[0].move)].isUsable(
|
|
||||||
playerPokemon,
|
|
||||||
isIgnorePP(moveQueue[0].useMode),
|
|
||||||
))
|
|
||||||
) {
|
|
||||||
moveQueue.shift();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Refactor this. I did a few simple find/replace matches but this is just ABHORRENTLY structured
|
let entriesToDelete = 0;
|
||||||
if (moveQueue.length > 0) {
|
const moveset = playerPokemon.getMoveset();
|
||||||
const queuedMove = moveQueue[0];
|
for (const queuedMove of moveQueue) {
|
||||||
if (!queuedMove.move) {
|
const movesetQueuedMove = moveset.find(m => m.moveId === queuedMove.move);
|
||||||
this.handleCommand(Command.FIGHT, -1, MoveUseMode.NORMAL);
|
|
||||||
} else {
|
|
||||||
const moveIndex = playerPokemon.getMoveset().findIndex(m => m.moveId === queuedMove.move);
|
|
||||||
if (
|
|
||||||
(moveIndex > -1 &&
|
|
||||||
playerPokemon.getMoveset()[moveIndex].isUsable(playerPokemon, isIgnorePP(queuedMove.useMode))) ||
|
|
||||||
isVirtual(queuedMove.useMode)
|
|
||||||
) {
|
|
||||||
this.handleCommand(Command.FIGHT, moveIndex, queuedMove.useMode, queuedMove);
|
|
||||||
} else {
|
|
||||||
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (
|
if (
|
||||||
globalScene.currentBattle.isBattleMysteryEncounter() &&
|
queuedMove.move !== MoveId.NONE &&
|
||||||
globalScene.currentBattle.mysteryEncounter?.skipToFightInput
|
!isVirtual(queuedMove.useMode) &&
|
||||||
|
!movesetQueuedMove?.isUsable(playerPokemon, isIgnorePP(queuedMove.useMode))
|
||||||
) {
|
) {
|
||||||
globalScene.ui.clearText();
|
entriesToDelete++;
|
||||||
globalScene.ui.setMode(UiMode.FIGHT, this.fieldIndex);
|
|
||||||
} else {
|
} else {
|
||||||
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (entriesToDelete) {
|
||||||
|
moveQueue.splice(0, entriesToDelete);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: Remove `args` and clean this thing up
|
* Attempt to execute the first usable move in this Pokemon's move queue
|
||||||
* Code will need to be copied over from pkty except replacing the `virtual` and `ignorePP` args with a corresponding `MoveUseMode`.
|
* @returns Whether a queued move was successfully set to be executed.
|
||||||
*/
|
*/
|
||||||
handleCommand(command: Command, cursor: number, ...args: any[]): boolean {
|
private tryExecuteQueuedMove(): boolean {
|
||||||
|
this.clearUnusuableMoves();
|
||||||
const playerPokemon = globalScene.getPlayerField()[this.fieldIndex];
|
const playerPokemon = globalScene.getPlayerField()[this.fieldIndex];
|
||||||
|
const moveQueue = playerPokemon.getMoveQueue();
|
||||||
|
|
||||||
|
if (moveQueue.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const queuedMove = moveQueue[0];
|
||||||
|
if (queuedMove.move === MoveId.NONE) {
|
||||||
|
this.handleCommand(Command.FIGHT, -1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const moveIndex = playerPokemon.getMoveset().findIndex(m => m.moveId === queuedMove.move);
|
||||||
|
if (!isVirtual(queuedMove.useMode) && moveIndex === -1) {
|
||||||
|
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
|
||||||
|
} else {
|
||||||
|
this.handleCommand(Command.FIGHT, moveIndex, queuedMove.useMode, queuedMove);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override start(): void {
|
||||||
|
super.start();
|
||||||
|
|
||||||
|
globalScene.updateGameInfo();
|
||||||
|
this.resetCursorIfNeeded();
|
||||||
|
|
||||||
|
if (this.fieldIndex) {
|
||||||
|
this.handleFieldIndexLogic();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkCommander();
|
||||||
|
|
||||||
|
const playerPokemon = this.getPokemon();
|
||||||
|
|
||||||
|
// Note: It is OK to call this if the target is not under the effect of encore; it will simply do nothing.
|
||||||
|
playerPokemon.lapseTag(BattlerTagType.ENCORE);
|
||||||
|
|
||||||
|
if (globalScene.currentBattle.turnCommands[this.fieldIndex]?.skip) {
|
||||||
|
this.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.tryExecuteQueuedMove()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
globalScene.currentBattle.isBattleMysteryEncounter() &&
|
||||||
|
globalScene.currentBattle.mysteryEncounter?.skipToFightInput
|
||||||
|
) {
|
||||||
|
globalScene.ui.clearText();
|
||||||
|
globalScene.ui.setMode(UiMode.FIGHT, this.fieldIndex);
|
||||||
|
} else {
|
||||||
|
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submethod of {@linkcode handleFightCommand} responsible for queuing the appropriate
|
||||||
|
* error message when a move cannot be used.
|
||||||
|
* @param user - The pokemon using the move
|
||||||
|
* @param cursor - The index of the move in the moveset
|
||||||
|
*/
|
||||||
|
private queueFightErrorMessage(user: PlayerPokemon, cursor: number) {
|
||||||
|
const move = user.getMoveset()[cursor];
|
||||||
|
globalScene.ui.setMode(UiMode.MESSAGE);
|
||||||
|
|
||||||
|
// Decides between a Disabled, Not Implemented, or No PP translation message
|
||||||
|
const errorMessage = user.isMoveRestricted(move.moveId, user)
|
||||||
|
? user.getRestrictingTag(move.moveId, user)!.selectionDeniedText(user, move.moveId)
|
||||||
|
: move.getName().endsWith(" (N)")
|
||||||
|
? "battle:moveNotImplemented"
|
||||||
|
: "battle:moveNoPP";
|
||||||
|
const moveName = move.getName().replace(" (N)", ""); // Trims off the indicator
|
||||||
|
|
||||||
|
globalScene.ui.showText(
|
||||||
|
i18next.t(errorMessage, { moveName: moveName }),
|
||||||
|
null,
|
||||||
|
() => {
|
||||||
|
globalScene.ui.clearText();
|
||||||
|
globalScene.ui.setMode(UiMode.FIGHT, this.fieldIndex);
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method for {@linkcode handleFightCommand} that returns the moveID for the phase
|
||||||
|
* based on the move passed in or the cursor.
|
||||||
|
*
|
||||||
|
* Does not check if the move is usable or not, that should be handled by the caller.
|
||||||
|
*/
|
||||||
|
private computeMoveId(playerPokemon: PlayerPokemon, cursor: number, move: TurnMove | undefined): MoveId {
|
||||||
|
return move?.move ?? (cursor > -1 ? playerPokemon.getMoveset()[cursor]?.moveId : MoveId.NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the logic for executing a fight-related command
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* - Validates whether the move can be used, using struggle if not
|
||||||
|
* - Constructs the turn command and inserts it into the battle's turn commands
|
||||||
|
*
|
||||||
|
* @param command - The command to handle (FIGHT or TERA)
|
||||||
|
* @param cursor - The index that the cursor is placed on, or -1 if no move can be selected.
|
||||||
|
* @param ignorePP - Whether to ignore PP when checking if the move can be used.
|
||||||
|
* @param move - The move to force the command to use, if any.
|
||||||
|
*/
|
||||||
|
private handleFightCommand(
|
||||||
|
command: Command.FIGHT | Command.TERA,
|
||||||
|
cursor: number,
|
||||||
|
useMode: MoveUseMode = MoveUseMode.NORMAL,
|
||||||
|
move?: TurnMove,
|
||||||
|
): boolean {
|
||||||
|
const playerPokemon = this.getPokemon();
|
||||||
|
const ignorePP = isIgnorePP(useMode);
|
||||||
|
|
||||||
|
let canUse = cursor === -1 || playerPokemon.trySelectMove(cursor, ignorePP);
|
||||||
|
|
||||||
|
// Ternary here ensures we don't compute struggle conditions unless necessary
|
||||||
|
const useStruggle = canUse
|
||||||
|
? false
|
||||||
|
: cursor > -1 && !playerPokemon.getMoveset().some(m => m.isUsable(playerPokemon));
|
||||||
|
|
||||||
|
canUse ||= useStruggle;
|
||||||
|
|
||||||
|
if (!canUse) {
|
||||||
|
this.queueFightErrorMessage(playerPokemon, cursor);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const moveId = useStruggle ? MoveId.STRUGGLE : this.computeMoveId(playerPokemon, cursor, move);
|
||||||
|
|
||||||
|
const turnCommand: TurnCommand = {
|
||||||
|
command: Command.FIGHT,
|
||||||
|
cursor,
|
||||||
|
move: { move: moveId, targets: [], useMode },
|
||||||
|
args: [useMode, move],
|
||||||
|
};
|
||||||
|
const preTurnCommand: TurnCommand = {
|
||||||
|
command,
|
||||||
|
targets: [this.fieldIndex],
|
||||||
|
skip: command === Command.FIGHT,
|
||||||
|
};
|
||||||
|
|
||||||
|
const moveTargets: MoveTargetSet =
|
||||||
|
move === undefined
|
||||||
|
? getMoveTargets(playerPokemon, moveId)
|
||||||
|
: {
|
||||||
|
targets: move.targets,
|
||||||
|
multiple: move.targets.length > 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (moveId === MoveId.NONE) {
|
||||||
|
turnCommand.targets = [this.fieldIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"Move:",
|
||||||
|
MoveId[moveId],
|
||||||
|
"Move targets:",
|
||||||
|
moveTargets,
|
||||||
|
"\nPlayer Pokemon:",
|
||||||
|
getPokemonNameWithAffix(playerPokemon),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (moveTargets.targets.length > 1 && moveTargets.multiple) {
|
||||||
|
globalScene.phaseManager.unshiftNew("SelectTargetPhase", this.fieldIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (turnCommand.move && (moveTargets.targets.length <= 1 || moveTargets.multiple)) {
|
||||||
|
turnCommand.move.targets = moveTargets.targets;
|
||||||
|
} else if (
|
||||||
|
turnCommand.move &&
|
||||||
|
playerPokemon.getTag(BattlerTagType.CHARGING) &&
|
||||||
|
playerPokemon.getMoveQueue().length >= 1
|
||||||
|
) {
|
||||||
|
turnCommand.move.targets = playerPokemon.getMoveQueue()[0].targets;
|
||||||
|
} else {
|
||||||
|
globalScene.phaseManager.unshiftNew("SelectTargetPhase", this.fieldIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
globalScene.currentBattle.preTurnCommands[this.fieldIndex] = preTurnCommand;
|
||||||
|
globalScene.currentBattle.turnCommands[this.fieldIndex] = turnCommand;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the mode in preparation to show the text, and then show the text.
|
||||||
|
* Only works for parameterless i18next keys.
|
||||||
|
* @param key - The i18next key for the text to show
|
||||||
|
*/
|
||||||
|
private queueShowText(key: string): void {
|
||||||
|
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
|
||||||
|
globalScene.ui.setMode(UiMode.MESSAGE);
|
||||||
|
|
||||||
|
globalScene.ui.showText(
|
||||||
|
i18next.t(key),
|
||||||
|
null,
|
||||||
|
() => {
|
||||||
|
globalScene.ui.showText("", 0);
|
||||||
|
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method for {@linkcode handleBallCommand} that checks if a pokeball can be thrown
|
||||||
|
* and displays the appropriate error message.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* The pokeball may not be thrown if any of the following are true:
|
||||||
|
* - It is a trainer battle
|
||||||
|
* - The player is in the {@linkcode BiomeId.END | End} biome and
|
||||||
|
* - it is not classic mode; or
|
||||||
|
* - the fresh start challenge is active; or
|
||||||
|
* - the player has not caught the target before and the player is still missing more than one starter
|
||||||
|
* - The player is in a mystery encounter that disallows catching the pokemon
|
||||||
|
* @returns Whether a pokeball can be thrown
|
||||||
|
*/
|
||||||
|
private checkCanUseBall(): boolean {
|
||||||
|
const { arena, currentBattle, gameData, gameMode } = globalScene;
|
||||||
|
const { battleType } = currentBattle;
|
||||||
|
const { biomeType } = arena;
|
||||||
|
const { isClassic } = gameMode;
|
||||||
|
const { dexData } = gameData;
|
||||||
|
|
||||||
|
const someUncaughtSpeciesOnField = globalScene
|
||||||
|
.getEnemyField()
|
||||||
|
.some(p => p.isActive() && !dexData[p.species.speciesId].caughtAttr);
|
||||||
|
const missingMultipleStarters =
|
||||||
|
gameData.getStarterCount(d => !!d.caughtAttr) < Object.keys(speciesStarterCosts).length - 1;
|
||||||
|
if (
|
||||||
|
biomeType === BiomeId.END &&
|
||||||
|
(!isClassic || gameMode.isFreshStartChallenge() || (someUncaughtSpeciesOnField && missingMultipleStarters))
|
||||||
|
) {
|
||||||
|
this.queueShowText("battle:noPokeballForce");
|
||||||
|
} else if (battleType === BattleType.TRAINER) {
|
||||||
|
this.queueShowText("battle:noPokeballTrainer");
|
||||||
|
} else if (currentBattle.isBattleMysteryEncounter() && !currentBattle.mysteryEncounter!.catchAllowed) {
|
||||||
|
this.queueShowText("battle:noPokeballMysteryEncounter");
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method for {@linkcode handleCommand} that handles the logic when the selected command is to use a pokeball.
|
||||||
|
*
|
||||||
|
* @param cursor - The index of the pokeball to use
|
||||||
|
* @returns Whether the command was successfully initiated
|
||||||
|
*/
|
||||||
|
private handleBallCommand(cursor: number): boolean {
|
||||||
|
const targets = globalScene
|
||||||
|
.getEnemyField()
|
||||||
|
.filter(p => p.isActive(true))
|
||||||
|
.map(p => p.getBattlerIndex());
|
||||||
|
if (targets.length > 1) {
|
||||||
|
this.queueShowText("battle:noPokeballMulti");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.checkCanUseBall()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const numBallTypes = 5;
|
||||||
|
if (cursor < numBallTypes) {
|
||||||
|
const targetPokemon = globalScene.getEnemyPokemon();
|
||||||
|
if (
|
||||||
|
targetPokemon?.isBoss() &&
|
||||||
|
targetPokemon?.bossSegmentIndex >= 1 &&
|
||||||
|
// TODO: Decouple this hardcoded exception for wonder guard and just check the target...
|
||||||
|
!targetPokemon?.hasAbility(AbilityId.WONDER_GUARD, false, true) &&
|
||||||
|
cursor < PokeballType.MASTER_BALL
|
||||||
|
) {
|
||||||
|
this.queueShowText("battle:noPokeballStrong");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
globalScene.currentBattle.turnCommands[this.fieldIndex] = {
|
||||||
|
command: Command.BALL,
|
||||||
|
cursor: cursor,
|
||||||
|
};
|
||||||
|
globalScene.currentBattle.turnCommands[this.fieldIndex]!.targets = targets;
|
||||||
|
if (this.fieldIndex) {
|
||||||
|
globalScene.currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submethod of {@linkcode tryLeaveField} to handle the logic for effects that prevent the pokemon from leaving the field
|
||||||
|
* due to trapping abilities or effects.
|
||||||
|
*
|
||||||
|
* This method queues the proper messages in the case of trapping abilities or effects.
|
||||||
|
*
|
||||||
|
* @returns Whether the pokemon is currently trapped
|
||||||
|
*/
|
||||||
|
private handleTrap(): boolean {
|
||||||
|
const playerPokemon = this.getPokemon();
|
||||||
|
const trappedAbMessages: string[] = [];
|
||||||
|
const isSwitch = this.isSwitch;
|
||||||
|
if (!playerPokemon.isTrapped(trappedAbMessages)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (trappedAbMessages.length > 0) {
|
||||||
|
if (isSwitch) {
|
||||||
|
globalScene.ui.setMode(UiMode.MESSAGE);
|
||||||
|
}
|
||||||
|
globalScene.ui.showText(
|
||||||
|
trappedAbMessages[0],
|
||||||
|
null,
|
||||||
|
() => {
|
||||||
|
globalScene.ui.showText("", 0);
|
||||||
|
if (isSwitch) {
|
||||||
|
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const trapTag = playerPokemon.getTag(TrappedTag);
|
||||||
|
const fairyLockTag = globalScene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, ArenaTagSide.PLAYER);
|
||||||
|
|
||||||
|
if (!isSwitch) {
|
||||||
|
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
|
||||||
|
globalScene.ui.setMode(UiMode.MESSAGE);
|
||||||
|
}
|
||||||
|
if (trapTag) {
|
||||||
|
this.showNoEscapeText(trapTag, false);
|
||||||
|
} else if (fairyLockTag) {
|
||||||
|
this.showNoEscapeText(fairyLockTag, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common helper method that attempts to have the pokemon leave the field.
|
||||||
|
* Checks for trapping abilities and effects.
|
||||||
|
*
|
||||||
|
* @param cursor - The index of the option that the cursor is on
|
||||||
|
* @returns Whether the pokemon is able to leave the field, indicating the command phase should end
|
||||||
|
*/
|
||||||
|
private tryLeaveField(cursor?: number, isBatonSwitch = false): boolean {
|
||||||
|
const currentBattle = globalScene.currentBattle;
|
||||||
|
|
||||||
|
if (isBatonSwitch || !this.handleTrap()) {
|
||||||
|
currentBattle.turnCommands[this.fieldIndex] = this.isSwitch
|
||||||
|
? {
|
||||||
|
command: Command.POKEMON,
|
||||||
|
cursor,
|
||||||
|
args: [isBatonSwitch],
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
command: Command.RUN,
|
||||||
|
};
|
||||||
|
if (!this.isSwitch && this.fieldIndex) {
|
||||||
|
currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method for {@linkcode handleCommand} that handles the logic when the selected command is RUN.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* Checks if the player is allowed to flee, and if not, queues the appropriate message.
|
||||||
|
*
|
||||||
|
* The player cannot flee if:
|
||||||
|
* - The player is in the {@linkcode BiomeId.END | End} biome
|
||||||
|
* - The player is in a trainer battle
|
||||||
|
* - The player is in a mystery encounter that disallows fleeing
|
||||||
|
* - The player's pokemon is trapped by an ability or effect
|
||||||
|
* @returns Whether the pokemon is able to leave the field, indicating the command phase should end
|
||||||
|
*/
|
||||||
|
private handleRunCommand(): boolean {
|
||||||
|
const { currentBattle, arena } = globalScene;
|
||||||
|
const mysteryEncounterFleeAllowed = currentBattle.mysteryEncounter?.fleeAllowed ?? true;
|
||||||
|
if (arena.biomeType === BiomeId.END || !mysteryEncounterFleeAllowed) {
|
||||||
|
this.queueShowText("battle:noEscapeForce");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
currentBattle.battleType === BattleType.TRAINER ||
|
||||||
|
currentBattle.mysteryEncounter?.encounterMode === MysteryEncounterMode.TRAINER_BATTLE
|
||||||
|
) {
|
||||||
|
this.queueShowText("battle:noEscapeTrainer");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = this.tryLeaveField();
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a message indicating that the pokemon cannot escape, and then return to the command phase.
|
||||||
|
*/
|
||||||
|
private showNoEscapeText(tag: any, isSwitch: boolean): void {
|
||||||
|
globalScene.ui.showText(
|
||||||
|
i18next.t("battle:noEscapePokemon", {
|
||||||
|
pokemonName:
|
||||||
|
tag.sourceId && globalScene.getPokemonById(tag.sourceId)
|
||||||
|
? getPokemonNameWithAffix(globalScene.getPokemonById(tag.sourceId)!)
|
||||||
|
: "",
|
||||||
|
moveName: tag.getMoveName(),
|
||||||
|
escapeVerb: i18next.t(isSwitch ? "battle:escapeVerbSwitch" : "battle:escapeVerbFlee"),
|
||||||
|
}),
|
||||||
|
null,
|
||||||
|
() => {
|
||||||
|
globalScene.ui.showText("", 0);
|
||||||
|
if (!isSwitch) {
|
||||||
|
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overloads for handleCommand to provide a more specific signature for the different options
|
||||||
|
/**
|
||||||
|
* Process the command phase logic based on the selected command
|
||||||
|
*
|
||||||
|
* @param command - The kind of command to handle
|
||||||
|
* @param cursor - The index of option that the cursor is on, or -1 if no option is selected
|
||||||
|
* @param useMode - The mode to use for the move, if applicable. For switches, a boolean that specifies whether the switch is a Baton switch.
|
||||||
|
* @param move - For {@linkcode Command.FIGHT}, the move to use
|
||||||
|
* @returns Whether the command was successful
|
||||||
|
*/
|
||||||
|
handleCommand(command: Command.FIGHT | Command.TERA, cursor: number, useMode?: MoveUseMode, move?: TurnMove): boolean;
|
||||||
|
handleCommand(command: Command.BALL, cursor: number): boolean;
|
||||||
|
handleCommand(command: Command.POKEMON, cursor: number, useBaton: boolean): boolean;
|
||||||
|
handleCommand(command: Command.RUN, cursor: number): boolean;
|
||||||
|
handleCommand(command: Command, cursor: number, useMode?: boolean | MoveUseMode, move?: TurnMove): boolean;
|
||||||
|
|
||||||
|
public handleCommand(
|
||||||
|
command: Command,
|
||||||
|
cursor: number,
|
||||||
|
useMode: boolean | MoveUseMode = false,
|
||||||
|
move?: TurnMove,
|
||||||
|
): boolean {
|
||||||
let success = false;
|
let success = false;
|
||||||
|
|
||||||
switch (command) {
|
switch (command) {
|
||||||
// TODO: We don't need 2 args for this - moveUseMode is carried over from queuedMove
|
|
||||||
case Command.TERA:
|
case Command.TERA:
|
||||||
case Command.FIGHT: {
|
case Command.FIGHT:
|
||||||
let useStruggle = false;
|
success = this.handleFightCommand(command, cursor, typeof useMode === "boolean" ? undefined : useMode, move);
|
||||||
const turnMove: TurnMove | undefined = args.length === 2 ? (args[1] as TurnMove) : undefined;
|
|
||||||
if (
|
|
||||||
cursor === -1 ||
|
|
||||||
playerPokemon.trySelectMove(cursor, isIgnorePP(args[0] as MoveUseMode)) ||
|
|
||||||
(useStruggle = cursor > -1 && !playerPokemon.getMoveset().filter(m => m.isUsable(playerPokemon)).length)
|
|
||||||
) {
|
|
||||||
let moveId: MoveId;
|
|
||||||
if (useStruggle) {
|
|
||||||
moveId = MoveId.STRUGGLE;
|
|
||||||
} else if (turnMove !== undefined) {
|
|
||||||
moveId = turnMove.move;
|
|
||||||
} else if (cursor > -1) {
|
|
||||||
moveId = playerPokemon.getMoveset()[cursor].moveId;
|
|
||||||
} else {
|
|
||||||
moveId = MoveId.NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
const turnCommand: TurnCommand = {
|
|
||||||
command: Command.FIGHT,
|
|
||||||
cursor: cursor,
|
|
||||||
move: { move: moveId, targets: [], useMode: args[0] },
|
|
||||||
args: args,
|
|
||||||
};
|
|
||||||
const preTurnCommand: TurnCommand = {
|
|
||||||
command: command,
|
|
||||||
targets: [this.fieldIndex],
|
|
||||||
skip: command === Command.FIGHT,
|
|
||||||
};
|
|
||||||
const moveTargets: MoveTargetSet =
|
|
||||||
turnMove === undefined
|
|
||||||
? getMoveTargets(playerPokemon, moveId)
|
|
||||||
: {
|
|
||||||
targets: turnMove.targets,
|
|
||||||
multiple: turnMove.targets.length > 1,
|
|
||||||
};
|
|
||||||
if (!moveId) {
|
|
||||||
turnCommand.targets = [this.fieldIndex];
|
|
||||||
}
|
|
||||||
console.log(moveTargets, getPokemonNameWithAffix(playerPokemon));
|
|
||||||
if (moveTargets.targets.length > 1 && moveTargets.multiple) {
|
|
||||||
globalScene.phaseManager.unshiftNew("SelectTargetPhase", this.fieldIndex);
|
|
||||||
}
|
|
||||||
if (turnCommand.move && (moveTargets.targets.length <= 1 || moveTargets.multiple)) {
|
|
||||||
turnCommand.move.targets = moveTargets.targets;
|
|
||||||
} else if (
|
|
||||||
turnCommand.move &&
|
|
||||||
playerPokemon.getTag(BattlerTagType.CHARGING) &&
|
|
||||||
playerPokemon.getMoveQueue().length >= 1
|
|
||||||
) {
|
|
||||||
turnCommand.move.targets = playerPokemon.getMoveQueue()[0].targets;
|
|
||||||
} else {
|
|
||||||
globalScene.phaseManager.unshiftNew("SelectTargetPhase", this.fieldIndex);
|
|
||||||
}
|
|
||||||
globalScene.currentBattle.preTurnCommands[this.fieldIndex] = preTurnCommand;
|
|
||||||
globalScene.currentBattle.turnCommands[this.fieldIndex] = turnCommand;
|
|
||||||
success = true;
|
|
||||||
} else if (cursor < playerPokemon.getMoveset().length) {
|
|
||||||
const move = playerPokemon.getMoveset()[cursor];
|
|
||||||
globalScene.ui.setMode(UiMode.MESSAGE);
|
|
||||||
|
|
||||||
// Set the translation key for why the move cannot be selected. The reasons can be:
|
|
||||||
// - If the move has been restricted in battle (e.g., Disable).
|
|
||||||
// - If the move has no more PP.
|
|
||||||
// - If the move is restricted by a challenge.
|
|
||||||
// - If the move is not implemented
|
|
||||||
let cannotUseKey: string;
|
|
||||||
if (playerPokemon.isMoveRestricted(move.moveId, playerPokemon)) {
|
|
||||||
cannotUseKey = playerPokemon
|
|
||||||
.getRestrictingTag(move.moveId, playerPokemon)!
|
|
||||||
.selectionDeniedText(playerPokemon, move.moveId);
|
|
||||||
} else if (move.getPpRatio() === 0) {
|
|
||||||
cannotUseKey = "battle:moveNoPP";
|
|
||||||
} else if (!applyChallenges(ChallengeType.POKEMON_MOVE, move.moveId)) {
|
|
||||||
cannotUseKey = "battle:moveCannotUseChallenge";
|
|
||||||
} else if (move.getName().endsWith(" (N)")) {
|
|
||||||
cannotUseKey = "battle:moveNotImplemented";
|
|
||||||
} else {
|
|
||||||
// TODO: Consider a message that signals a being unusable for an unknown reason
|
|
||||||
cannotUseKey = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const moveName = move.getName().replace(" (N)", ""); // Trims off the indicator
|
|
||||||
|
|
||||||
globalScene.ui.showText(
|
|
||||||
i18next.t(cannotUseKey, { moveName: moveName }),
|
|
||||||
null,
|
|
||||||
() => {
|
|
||||||
globalScene.ui.clearText();
|
|
||||||
globalScene.ui.setMode(UiMode.FIGHT, this.fieldIndex);
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
case Command.BALL:
|
||||||
case Command.BALL: {
|
success = this.handleBallCommand(cursor);
|
||||||
const notInDex =
|
|
||||||
globalScene
|
|
||||||
.getEnemyField()
|
|
||||||
.filter(p => p.isActive(true))
|
|
||||||
.some(p => !globalScene.gameData.dexData[p.species.speciesId].caughtAttr) &&
|
|
||||||
globalScene.gameData.getStarterCount(d => !!d.caughtAttr) < Object.keys(speciesStarterCosts).length - 1;
|
|
||||||
if (
|
|
||||||
globalScene.arena.biomeType === BiomeId.END &&
|
|
||||||
(!globalScene.gameMode.isClassic || globalScene.gameMode.isFreshStartChallenge() || notInDex)
|
|
||||||
) {
|
|
||||||
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
|
|
||||||
globalScene.ui.setMode(UiMode.MESSAGE);
|
|
||||||
globalScene.ui.showText(
|
|
||||||
i18next.t("battle:noPokeballForce"),
|
|
||||||
null,
|
|
||||||
() => {
|
|
||||||
globalScene.ui.showText("", 0);
|
|
||||||
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
} else if (globalScene.currentBattle.battleType === BattleType.TRAINER) {
|
|
||||||
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
|
|
||||||
globalScene.ui.setMode(UiMode.MESSAGE);
|
|
||||||
globalScene.ui.showText(
|
|
||||||
i18next.t("battle:noPokeballTrainer"),
|
|
||||||
null,
|
|
||||||
() => {
|
|
||||||
globalScene.ui.showText("", 0);
|
|
||||||
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
} else if (
|
|
||||||
globalScene.currentBattle.isBattleMysteryEncounter() &&
|
|
||||||
!globalScene.currentBattle.mysteryEncounter!.catchAllowed
|
|
||||||
) {
|
|
||||||
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
|
|
||||||
globalScene.ui.setMode(UiMode.MESSAGE);
|
|
||||||
globalScene.ui.showText(
|
|
||||||
i18next.t("battle:noPokeballMysteryEncounter"),
|
|
||||||
null,
|
|
||||||
() => {
|
|
||||||
globalScene.ui.showText("", 0);
|
|
||||||
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const targets = globalScene
|
|
||||||
.getEnemyField()
|
|
||||||
.filter(p => p.isActive(true))
|
|
||||||
.map(p => p.getBattlerIndex());
|
|
||||||
if (targets.length > 1) {
|
|
||||||
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
|
|
||||||
globalScene.ui.setMode(UiMode.MESSAGE);
|
|
||||||
globalScene.ui.showText(
|
|
||||||
i18next.t("battle:noPokeballMulti"),
|
|
||||||
null,
|
|
||||||
() => {
|
|
||||||
globalScene.ui.showText("", 0);
|
|
||||||
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
} else if (cursor < 5) {
|
|
||||||
const targetPokemon = globalScene.getEnemyField().find(p => p.isActive(true));
|
|
||||||
if (
|
|
||||||
targetPokemon?.isBoss() &&
|
|
||||||
targetPokemon?.bossSegmentIndex >= 1 &&
|
|
||||||
!targetPokemon?.hasAbility(AbilityId.WONDER_GUARD, false, true) &&
|
|
||||||
cursor < PokeballType.MASTER_BALL
|
|
||||||
) {
|
|
||||||
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
|
|
||||||
globalScene.ui.setMode(UiMode.MESSAGE);
|
|
||||||
globalScene.ui.showText(
|
|
||||||
i18next.t("battle:noPokeballStrong"),
|
|
||||||
null,
|
|
||||||
() => {
|
|
||||||
globalScene.ui.showText("", 0);
|
|
||||||
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
globalScene.currentBattle.turnCommands[this.fieldIndex] = {
|
|
||||||
command: Command.BALL,
|
|
||||||
cursor: cursor,
|
|
||||||
};
|
|
||||||
globalScene.currentBattle.turnCommands[this.fieldIndex]!.targets = targets;
|
|
||||||
if (this.fieldIndex) {
|
|
||||||
globalScene.currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true;
|
|
||||||
}
|
|
||||||
success = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
case Command.POKEMON:
|
case Command.POKEMON:
|
||||||
case Command.RUN: {
|
this.isSwitch = true;
|
||||||
const isSwitch = command === Command.POKEMON;
|
success = this.tryLeaveField(cursor, typeof useMode === "boolean" ? useMode : undefined);
|
||||||
const { currentBattle, arena } = globalScene;
|
this.isSwitch = false;
|
||||||
const mysteryEncounterFleeAllowed = currentBattle.mysteryEncounter?.fleeAllowed;
|
|
||||||
if (
|
|
||||||
!isSwitch &&
|
|
||||||
(arena.biomeType === BiomeId.END ||
|
|
||||||
(!isNullOrUndefined(mysteryEncounterFleeAllowed) && !mysteryEncounterFleeAllowed))
|
|
||||||
) {
|
|
||||||
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
|
|
||||||
globalScene.ui.setMode(UiMode.MESSAGE);
|
|
||||||
globalScene.ui.showText(
|
|
||||||
i18next.t("battle:noEscapeForce"),
|
|
||||||
null,
|
|
||||||
() => {
|
|
||||||
globalScene.ui.showText("", 0);
|
|
||||||
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
} else if (
|
|
||||||
!isSwitch &&
|
|
||||||
(currentBattle.battleType === BattleType.TRAINER ||
|
|
||||||
currentBattle.mysteryEncounter?.encounterMode === MysteryEncounterMode.TRAINER_BATTLE)
|
|
||||||
) {
|
|
||||||
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
|
|
||||||
globalScene.ui.setMode(UiMode.MESSAGE);
|
|
||||||
globalScene.ui.showText(
|
|
||||||
i18next.t("battle:noEscapeTrainer"),
|
|
||||||
null,
|
|
||||||
() => {
|
|
||||||
globalScene.ui.showText("", 0);
|
|
||||||
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const batonPass = isSwitch && (args[0] as boolean);
|
|
||||||
const trappedAbMessages: string[] = [];
|
|
||||||
if (batonPass || !playerPokemon.isTrapped(trappedAbMessages)) {
|
|
||||||
currentBattle.turnCommands[this.fieldIndex] = isSwitch
|
|
||||||
? { command: Command.POKEMON, cursor: cursor, args: args }
|
|
||||||
: { command: Command.RUN };
|
|
||||||
success = true;
|
|
||||||
if (!isSwitch && this.fieldIndex) {
|
|
||||||
currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true;
|
|
||||||
}
|
|
||||||
} else if (trappedAbMessages.length > 0) {
|
|
||||||
if (!isSwitch) {
|
|
||||||
globalScene.ui.setMode(UiMode.MESSAGE);
|
|
||||||
}
|
|
||||||
globalScene.ui.showText(
|
|
||||||
trappedAbMessages[0],
|
|
||||||
null,
|
|
||||||
() => {
|
|
||||||
globalScene.ui.showText("", 0);
|
|
||||||
if (!isSwitch) {
|
|
||||||
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const trapTag = playerPokemon.getTag(TrappedTag);
|
|
||||||
const fairyLockTag = globalScene.arena.getTagOnSide(ArenaTagType.FAIRY_LOCK, ArenaTagSide.PLAYER);
|
|
||||||
|
|
||||||
if (!trapTag && !fairyLockTag) {
|
|
||||||
i18next.t(`battle:noEscape${isSwitch ? "Switch" : "Flee"}`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (!isSwitch) {
|
|
||||||
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
|
|
||||||
globalScene.ui.setMode(UiMode.MESSAGE);
|
|
||||||
}
|
|
||||||
const showNoEscapeText = (tag: any) => {
|
|
||||||
globalScene.ui.showText(
|
|
||||||
i18next.t("battle:noEscapePokemon", {
|
|
||||||
pokemonName:
|
|
||||||
tag.sourceId && globalScene.getPokemonById(tag.sourceId)
|
|
||||||
? getPokemonNameWithAffix(globalScene.getPokemonById(tag.sourceId)!)
|
|
||||||
: "",
|
|
||||||
moveName: tag.getMoveName(),
|
|
||||||
escapeVerb: isSwitch ? i18next.t("battle:escapeVerbSwitch") : i18next.t("battle:escapeVerbFlee"),
|
|
||||||
}),
|
|
||||||
null,
|
|
||||||
() => {
|
|
||||||
globalScene.ui.showText("", 0);
|
|
||||||
if (!isSwitch) {
|
|
||||||
globalScene.ui.setMode(UiMode.COMMAND, this.fieldIndex);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (trapTag) {
|
|
||||||
showNoEscapeText(trapTag);
|
|
||||||
} else if (fairyLockTag) {
|
|
||||||
showNoEscapeText(fairyLockTag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
case Command.RUN:
|
||||||
|
success = this.handleRunCommand();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
|
@ -19,7 +19,7 @@ import { MoveFlags } from "#enums/move-flags";
|
|||||||
import { MoveId } from "#enums/move-id";
|
import { MoveId } from "#enums/move-id";
|
||||||
import { MoveResult } from "#enums/move-result";
|
import { MoveResult } from "#enums/move-result";
|
||||||
import { MoveTarget } from "#enums/move-target";
|
import { MoveTarget } from "#enums/move-target";
|
||||||
import { isReflected, isVirtual, MoveUseMode } from "#enums/move-use-mode";
|
import { isReflected, MoveUseMode } from "#enums/move-use-mode";
|
||||||
import { PokemonType } from "#enums/pokemon-type";
|
import { PokemonType } from "#enums/pokemon-type";
|
||||||
import type { Pokemon } from "#field/pokemon";
|
import type { Pokemon } from "#field/pokemon";
|
||||||
import {
|
import {
|
||||||
@ -244,43 +244,19 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
globalScene.currentBattle.lastPlayerInvolved = this.fieldIndex;
|
globalScene.currentBattle.lastPlayerInvolved = this.fieldIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDelayedAttack = this.move.hasAttr("DelayedAttackAttr");
|
const move = this.move;
|
||||||
/** If the user was somehow removed from the field and it's not a delayed attack, end this phase */
|
|
||||||
if (!user.isOnField()) {
|
|
||||||
if (!isDelayedAttack) {
|
|
||||||
super.end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!user.scene) {
|
|
||||||
/*
|
|
||||||
* This happens if the Pokemon that used the delayed attack gets caught and released
|
|
||||||
* on the turn the attack would have triggered. Having access to the global scene
|
|
||||||
* in the future may solve this entirely, so for now we just cancel the hit
|
|
||||||
*/
|
|
||||||
super.end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Does an effect from this move override other effects on this turn?
|
* Does an effect from this move override other effects on this turn?
|
||||||
* e.g. Charging moves (Fly, etc.) on their first turn of use.
|
* e.g. Charging moves (Fly, etc.) on their first turn of use.
|
||||||
*/
|
*/
|
||||||
const overridden = new BooleanHolder(false);
|
const overridden = new BooleanHolder(false);
|
||||||
const move = this.move;
|
|
||||||
|
|
||||||
// Apply effects to override a move effect.
|
// Apply effects to override a move effect.
|
||||||
// Assuming single target here works as this is (currently)
|
// Assuming single target here works as this is (currently)
|
||||||
// only used for Future Sight, calling and Pledge moves.
|
// only used for Future Sight, calling and Pledge moves.
|
||||||
// TODO: change if any other move effect overrides are introduced
|
// TODO: change if any other move effect overrides are introduced
|
||||||
applyMoveAttrs(
|
applyMoveAttrs("OverrideMoveEffectAttr", user, this.getFirstTarget() ?? null, move, overridden, this.useMode);
|
||||||
"OverrideMoveEffectAttr",
|
|
||||||
user,
|
|
||||||
this.getFirstTarget() ?? null,
|
|
||||||
move,
|
|
||||||
overridden,
|
|
||||||
isVirtual(this.useMode),
|
|
||||||
);
|
|
||||||
|
|
||||||
// If other effects were overriden, stop this phase before they can be applied
|
// If other effects were overriden, stop this phase before they can be applied
|
||||||
if (overridden.value) {
|
if (overridden.value) {
|
||||||
@ -355,7 +331,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
*/
|
*/
|
||||||
private postAnimCallback(user: Pokemon, targets: Pokemon[]) {
|
private postAnimCallback(user: Pokemon, targets: Pokemon[]) {
|
||||||
// Add to the move history entry
|
// Add to the move history entry
|
||||||
if (this.firstHit) {
|
if (this.firstHit && this.useMode !== MoveUseMode.DELAYED_ATTACK) {
|
||||||
user.pushMoveHistory(this.moveHistoryEntry);
|
user.pushMoveHistory(this.moveHistoryEntry);
|
||||||
applyAbAttrs("ExecutedMoveAbAttr", { pokemon: user });
|
applyAbAttrs("ExecutedMoveAbAttr", { pokemon: user });
|
||||||
}
|
}
|
||||||
@ -663,6 +639,7 @@ export class MoveEffectPhase extends PokemonPhase {
|
|||||||
|
|
||||||
/** @returns The {@linkcode Pokemon} using this phase's invoked move */
|
/** @returns The {@linkcode Pokemon} using this phase's invoked move */
|
||||||
public getUserPokemon(): Pokemon | null {
|
public getUserPokemon(): Pokemon | null {
|
||||||
|
// TODO: Make this purely a battler index
|
||||||
if (this.battlerIndex > BattlerIndex.ENEMY_2) {
|
if (this.battlerIndex > BattlerIndex.ENEMY_2) {
|
||||||
return globalScene.getPokemonById(this.battlerIndex);
|
return globalScene.getPokemonById(this.battlerIndex);
|
||||||
}
|
}
|
||||||
|
@ -2,14 +2,12 @@ import { applyAbAttrs } from "#abilities/apply-ab-attrs";
|
|||||||
import { globalScene } from "#app/global-scene";
|
import { globalScene } from "#app/global-scene";
|
||||||
import { getPokemonNameWithAffix } from "#app/messages";
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
import Overrides from "#app/overrides";
|
import Overrides from "#app/overrides";
|
||||||
import type { DelayedAttackTag } from "#data/arena-tag";
|
|
||||||
import { CenterOfAttentionTag } from "#data/battler-tags";
|
import { CenterOfAttentionTag } from "#data/battler-tags";
|
||||||
import { SpeciesFormChangePreMoveTrigger } from "#data/form-change-triggers";
|
import { SpeciesFormChangePreMoveTrigger } from "#data/form-change-triggers";
|
||||||
import { getStatusEffectActivationText, getStatusEffectHealText } from "#data/status-effect";
|
import { getStatusEffectActivationText, getStatusEffectHealText } from "#data/status-effect";
|
||||||
import { getTerrainBlockMessage } from "#data/terrain";
|
import { getTerrainBlockMessage } from "#data/terrain";
|
||||||
import { getWeatherBlockMessage } from "#data/weather";
|
import { getWeatherBlockMessage } from "#data/weather";
|
||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
import { ArenaTagType } from "#enums/arena-tag-type";
|
|
||||||
import { BattlerIndex } from "#enums/battler-index";
|
import { BattlerIndex } from "#enums/battler-index";
|
||||||
import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
|
import { BattlerTagLapseType } from "#enums/battler-tag-lapse-type";
|
||||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
@ -297,21 +295,6 @@ export class MovePhase extends BattlePhase {
|
|||||||
// form changes happen even before we know that the move wll execute.
|
// form changes happen even before we know that the move wll execute.
|
||||||
globalScene.triggerPokemonFormChange(this.pokemon, SpeciesFormChangePreMoveTrigger);
|
globalScene.triggerPokemonFormChange(this.pokemon, SpeciesFormChangePreMoveTrigger);
|
||||||
|
|
||||||
// Check the player side arena if another delayed attack is active and hitting the same slot.
|
|
||||||
if (move.hasAttr("DelayedAttackAttr")) {
|
|
||||||
const currentTargetIndex = targets[0].getBattlerIndex();
|
|
||||||
const delayedAttackHittingSameSlot = globalScene.arena.tags.some(
|
|
||||||
tag =>
|
|
||||||
(tag.tagType === ArenaTagType.FUTURE_SIGHT || tag.tagType === ArenaTagType.DOOM_DESIRE) &&
|
|
||||||
(tag as DelayedAttackTag).targetIndex === currentTargetIndex,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (delayedAttackHittingSameSlot) {
|
|
||||||
this.failMove(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the move has any attributes that can interrupt its own use **before** displaying text.
|
// Check if the move has any attributes that can interrupt its own use **before** displaying text.
|
||||||
// TODO: This should not rely on direct return values
|
// TODO: This should not rely on direct return values
|
||||||
let failed = move.getAttrs("PreUseInterruptAttr").some(attr => attr.apply(this.pokemon, targets[0], move));
|
let failed = move.getAttrs("PreUseInterruptAttr").some(attr => attr.apply(this.pokemon, targets[0], move));
|
||||||
|
21
src/phases/positional-tag-phase.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// biome-ignore-start lint/correctness/noUnusedImports: TSDocs
|
||||||
|
import type { PositionalTag } from "#data/positional-tags/positional-tag";
|
||||||
|
import type { TurnEndPhase } from "#phases/turn-end-phase";
|
||||||
|
// biome-ignore-end lint/correctness/noUnusedImports: TSDocs
|
||||||
|
|
||||||
|
import { globalScene } from "#app/global-scene";
|
||||||
|
import { Phase } from "#app/phase";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Phase to trigger all pending post-turn {@linkcode PositionalTag}s.
|
||||||
|
* Occurs before {@linkcode TurnEndPhase} to allow for proper electrify timing.
|
||||||
|
*/
|
||||||
|
export class PositionalTagPhase extends Phase {
|
||||||
|
public readonly phaseName = "PositionalTagPhase";
|
||||||
|
|
||||||
|
public override start(): void {
|
||||||
|
globalScene.arena.positionalTagManager.activateAllTags();
|
||||||
|
super.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
@ -24,10 +24,11 @@ export class SelectStarterPhase extends Phase {
|
|||||||
globalScene.ui.setMode(UiMode.STARTER_SELECT, (starters: Starter[]) => {
|
globalScene.ui.setMode(UiMode.STARTER_SELECT, (starters: Starter[]) => {
|
||||||
globalScene.ui.clearText();
|
globalScene.ui.clearText();
|
||||||
globalScene.ui.setMode(UiMode.SAVE_SLOT, SaveSlotUiMode.SAVE, (slotId: number) => {
|
globalScene.ui.setMode(UiMode.SAVE_SLOT, SaveSlotUiMode.SAVE, (slotId: number) => {
|
||||||
|
// If clicking cancel, back out to title screen
|
||||||
if (slotId === -1) {
|
if (slotId === -1) {
|
||||||
globalScene.phaseManager.clearPhaseQueue();
|
globalScene.phaseManager.toTitleScreen();
|
||||||
globalScene.phaseManager.pushNew("TitlePhase");
|
this.end();
|
||||||
return this.end();
|
return;
|
||||||
}
|
}
|
||||||
globalScene.sessionSlotId = slotId;
|
globalScene.sessionSlotId = slotId;
|
||||||
this.initBattle(starters);
|
this.initBattle(starters);
|
||||||
|
@ -114,11 +114,11 @@ export class TitlePhase extends Phase {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Cancel button = back to title
|
||||||
options.push({
|
options.push({
|
||||||
label: i18next.t("menu:cancel"),
|
label: i18next.t("menu:cancel"),
|
||||||
handler: () => {
|
handler: () => {
|
||||||
globalScene.phaseManager.clearPhaseQueue();
|
globalScene.phaseManager.toTitleScreen();
|
||||||
globalScene.phaseManager.pushNew("TitlePhase");
|
|
||||||
super.end();
|
super.end();
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
@ -191,11 +191,12 @@ export class TitlePhase extends Phase {
|
|||||||
initDailyRun(): void {
|
initDailyRun(): void {
|
||||||
globalScene.ui.clearText();
|
globalScene.ui.clearText();
|
||||||
globalScene.ui.setMode(UiMode.SAVE_SLOT, SaveSlotUiMode.SAVE, (slotId: number) => {
|
globalScene.ui.setMode(UiMode.SAVE_SLOT, SaveSlotUiMode.SAVE, (slotId: number) => {
|
||||||
globalScene.phaseManager.clearPhaseQueue();
|
|
||||||
if (slotId === -1) {
|
if (slotId === -1) {
|
||||||
globalScene.phaseManager.pushNew("TitlePhase");
|
globalScene.phaseManager.toTitleScreen();
|
||||||
return super.end();
|
super.end();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
globalScene.phaseManager.clearPhaseQueue();
|
||||||
globalScene.sessionSlotId = slotId;
|
globalScene.sessionSlotId = slotId;
|
||||||
|
|
||||||
const generateDaily = (seed: string) => {
|
const generateDaily = (seed: string) => {
|
||||||
|
@ -18,6 +18,8 @@ import i18next from "i18next";
|
|||||||
|
|
||||||
export class TurnEndPhase extends FieldPhase {
|
export class TurnEndPhase extends FieldPhase {
|
||||||
public readonly phaseName = "TurnEndPhase";
|
public readonly phaseName = "TurnEndPhase";
|
||||||
|
public upcomingInterlude = false;
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
super.start();
|
super.start();
|
||||||
|
|
||||||
@ -59,9 +61,11 @@ export class TurnEndPhase extends FieldPhase {
|
|||||||
pokemon.tempSummonData.waveTurnCount++;
|
pokemon.tempSummonData.waveTurnCount++;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.executeForAll(handlePokemon);
|
if (!this.upcomingInterlude) {
|
||||||
|
this.executeForAll(handlePokemon);
|
||||||
|
|
||||||
globalScene.arena.lapseTags();
|
globalScene.arena.lapseTags();
|
||||||
|
}
|
||||||
|
|
||||||
if (globalScene.arena.weather && !globalScene.arena.weather.lapse()) {
|
if (globalScene.arena.weather && !globalScene.arena.weather.lapse()) {
|
||||||
globalScene.arena.trySetWeather(WeatherType.NONE);
|
globalScene.arena.trySetWeather(WeatherType.NONE);
|
||||||
|
@ -218,6 +218,10 @@ export class TurnStartPhase extends FieldPhase {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
phaseManager.pushNew("CheckInterludePhase");
|
||||||
|
|
||||||
|
// TODO: Re-order these phases to be consistent with mainline turn order:
|
||||||
|
// https://www.smogon.com/forums/threads/sword-shield-battle-mechanics-research.3655528/page-64#post-9244179
|
||||||
|
|
||||||
phaseManager.pushNew("WeatherEffectPhase");
|
phaseManager.pushNew("WeatherEffectPhase");
|
||||||
phaseManager.pushNew("BerryPhase");
|
phaseManager.pushNew("BerryPhase");
|
||||||
@ -225,12 +229,13 @@ export class TurnStartPhase extends FieldPhase {
|
|||||||
/** Add a new phase to check who should be taking status damage */
|
/** Add a new phase to check who should be taking status damage */
|
||||||
phaseManager.pushNew("CheckStatusEffectPhase", moveOrder);
|
phaseManager.pushNew("CheckStatusEffectPhase", moveOrder);
|
||||||
|
|
||||||
|
phaseManager.pushNew("PositionalTagPhase");
|
||||||
phaseManager.pushNew("TurnEndPhase");
|
phaseManager.pushNew("TurnEndPhase");
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* this.end() will call shiftPhase(), which dumps everything from PrependQueue (aka everything that is unshifted()) to the front
|
* `this.end()` will call `PhaseManager#shiftPhase()`, which dumps everything from `phaseQueuePrepend`
|
||||||
* of the queue and dequeues to start the next phase
|
* (aka everything that is queued via `unshift()`) to the front of the queue and dequeues to start the next phase.
|
||||||
* this is important since stuff like SwitchSummon, AttemptRun, AttemptCapture Phases break the "flow" and should take precedence
|
* This is important since stuff like `SwitchSummonPhase`, `AttemptRunPhase`, and `AttemptCapturePhase` break the "flow" and should take precedence
|
||||||
*/
|
*/
|
||||||
this.end();
|
this.end();
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import pkg from "#package.json";
|
import pkg from "#package.json";
|
||||||
import { camelCaseToKebabCase } from "#utils/common";
|
import { toKebabCase } from "#utils/strings";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import LanguageDetector from "i18next-browser-languagedetector";
|
import LanguageDetector from "i18next-browser-languagedetector";
|
||||||
import HttpBackend from "i18next-http-backend";
|
import HttpBackend from "i18next-http-backend";
|
||||||
@ -79,13 +79,13 @@ const fonts: Array<LoadingFontFaceProperty> = [
|
|||||||
face: new FontFace("emerald", "url(./fonts/pokemon-bw.ttf)", {
|
face: new FontFace("emerald", "url(./fonts/pokemon-bw.ttf)", {
|
||||||
unicodeRange: rangesByLanguage.japanese,
|
unicodeRange: rangesByLanguage.japanese,
|
||||||
}),
|
}),
|
||||||
only: ["en", "es", "fr", "it", "de", "pt", "ko", "ja", "ca", "da", "tr", "ro", "ru"],
|
only: ["en", "es", "fr", "it", "de", "pt", "ko", "ja", "ca", "da", "tr", "ro", "ru", "tl"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
face: new FontFace("pkmnems", "url(./fonts/pokemon-bw.ttf)", {
|
face: new FontFace("pkmnems", "url(./fonts/pokemon-bw.ttf)", {
|
||||||
unicodeRange: rangesByLanguage.japanese,
|
unicodeRange: rangesByLanguage.japanese,
|
||||||
}),
|
}),
|
||||||
only: ["en", "es", "fr", "it", "de", "pt", "ko", "ja", "ca", "da", "tr", "ro", "ru"],
|
only: ["en", "es", "fr", "it", "de", "pt", "ko", "ja", "ca", "da", "tr", "ro", "ru", "tl"],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -191,17 +191,20 @@ export async function initI18n(): Promise<void> {
|
|||||||
"tr",
|
"tr",
|
||||||
"ro",
|
"ro",
|
||||||
"ru",
|
"ru",
|
||||||
|
"tl",
|
||||||
],
|
],
|
||||||
backend: {
|
backend: {
|
||||||
loadPath(lng: string, [ns]: string[]) {
|
loadPath(lng: string, [ns]: string[]) {
|
||||||
|
// Use namespace maps where required
|
||||||
let fileName: string;
|
let fileName: string;
|
||||||
if (namespaceMap[ns]) {
|
if (namespaceMap[ns]) {
|
||||||
fileName = namespaceMap[ns];
|
fileName = namespaceMap[ns];
|
||||||
} else if (ns.startsWith("mysteryEncounters/")) {
|
} else if (ns.startsWith("mysteryEncounters/")) {
|
||||||
fileName = camelCaseToKebabCase(ns + "Dialogue");
|
fileName = toKebabCase(ns + "-dialogue"); // mystery-encounters/a-trainers-test-dialogue
|
||||||
} else {
|
} else {
|
||||||
fileName = camelCaseToKebabCase(ns);
|
fileName = toKebabCase(ns);
|
||||||
}
|
}
|
||||||
|
// ex: "./locales/en/move-anims"
|
||||||
return `./locales/${lng}/${fileName}.json?v=${pkg.version}`;
|
return `./locales/${lng}/${fileName}.json?v=${pkg.version}`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -890,7 +890,7 @@ export const achvs = {
|
|||||||
100,
|
100,
|
||||||
c =>
|
c =>
|
||||||
c instanceof FreshStartChallenge &&
|
c instanceof FreshStartChallenge &&
|
||||||
c.value > 0 &&
|
c.value === 1 &&
|
||||||
!globalScene.gameMode.challenges.some(
|
!globalScene.gameMode.challenges.some(
|
||||||
c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT].includes(c.id) && c.value > 0,
|
c => [Challenges.INVERSE_BATTLE, Challenges.FLIP_STAT].includes(c.id) && c.value > 0,
|
||||||
),
|
),
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import type { ArenaTag } from "#data/arena-tag";
|
import type { ArenaTag } from "#data/arena-tag";
|
||||||
import { loadArenaTag, SerializableArenaTag } from "#data/arena-tag";
|
import { loadArenaTag, SerializableArenaTag } from "#data/arena-tag";
|
||||||
|
import type { SerializedPositionalTag } from "#data/positional-tags/load-positional-tag";
|
||||||
import { Terrain } from "#data/terrain";
|
import { Terrain } from "#data/terrain";
|
||||||
import { Weather } from "#data/weather";
|
import { Weather } from "#data/weather";
|
||||||
import type { BiomeId } from "#enums/biome-id";
|
import type { BiomeId } from "#enums/biome-id";
|
||||||
@ -12,6 +13,7 @@ export interface SerializedArenaData {
|
|||||||
weather: NonFunctionProperties<Weather> | null;
|
weather: NonFunctionProperties<Weather> | null;
|
||||||
terrain: NonFunctionProperties<Terrain> | null;
|
terrain: NonFunctionProperties<Terrain> | null;
|
||||||
tags?: ArenaTagTypeData[];
|
tags?: ArenaTagTypeData[];
|
||||||
|
positionalTags: SerializedPositionalTag[];
|
||||||
playerTerasUsed?: number;
|
playerTerasUsed?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,6 +22,7 @@ export class ArenaData {
|
|||||||
public weather: Weather | null;
|
public weather: Weather | null;
|
||||||
public terrain: Terrain | null;
|
public terrain: Terrain | null;
|
||||||
public tags: ArenaTag[];
|
public tags: ArenaTag[];
|
||||||
|
public positionalTags: SerializedPositionalTag[] = [];
|
||||||
public playerTerasUsed: number;
|
public playerTerasUsed: number;
|
||||||
|
|
||||||
constructor(source: Arena | SerializedArenaData) {
|
constructor(source: Arena | SerializedArenaData) {
|
||||||
@ -37,11 +40,15 @@ export class ArenaData {
|
|||||||
this.biome = source.biomeType;
|
this.biome = source.biomeType;
|
||||||
this.weather = source.weather;
|
this.weather = source.weather;
|
||||||
this.terrain = source.terrain;
|
this.terrain = source.terrain;
|
||||||
|
// The assertion here is ok - we ensure that all tags are inside the `posTagConstructorMap` map,
|
||||||
|
// and that all `PositionalTags` will become their respective interfaces when serialized and de-serialized.
|
||||||
|
this.positionalTags = (source.positionalTagManager.tags as unknown as SerializedPositionalTag[]) ?? [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.biome = source.biome;
|
this.biome = source.biome;
|
||||||
this.weather = source.weather ? new Weather(source.weather.weatherType, source.weather.turnsLeft) : null;
|
this.weather = source.weather ? new Weather(source.weather.weatherType, source.weather.turnsLeft) : null;
|
||||||
this.terrain = source.terrain ? new Terrain(source.terrain.terrainType, source.terrain.turnsLeft) : null;
|
this.terrain = source.terrain ? new Terrain(source.terrain.terrainType, source.terrain.turnsLeft) : null;
|
||||||
|
this.positionalTags = source.positionalTags ?? [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ import { allMoves, allSpecies } from "#data/data-lists";
|
|||||||
import type { Egg } from "#data/egg";
|
import type { Egg } from "#data/egg";
|
||||||
import { pokemonFormChanges } from "#data/pokemon-forms";
|
import { pokemonFormChanges } from "#data/pokemon-forms";
|
||||||
import type { PokemonSpecies } from "#data/pokemon-species";
|
import type { PokemonSpecies } from "#data/pokemon-species";
|
||||||
|
import { loadPositionalTag } from "#data/positional-tags/load-positional-tag";
|
||||||
import { TerrainType } from "#data/terrain";
|
import { TerrainType } from "#data/terrain";
|
||||||
import { AbilityAttr } from "#enums/ability-attr";
|
import { AbilityAttr } from "#enums/ability-attr";
|
||||||
import { BattleType } from "#enums/battle-type";
|
import { BattleType } from "#enums/battle-type";
|
||||||
@ -1096,6 +1097,10 @@ export class GameData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
globalScene.arena.positionalTagManager.tags = sessionData.arena.positionalTags.map(tag =>
|
||||||
|
loadPositionalTag(tag),
|
||||||
|
);
|
||||||
|
|
||||||
if (globalScene.modifiers.length) {
|
if (globalScene.modifiers.length) {
|
||||||
console.warn("Existing modifiers not cleared on session load, deleting...");
|
console.warn("Existing modifiers not cleared on session load, deleting...");
|
||||||
globalScene.modifiers = [];
|
globalScene.modifiers = [];
|
||||||
@ -1454,11 +1459,10 @@ export class GameData {
|
|||||||
|
|
||||||
reader.onload = (_ => {
|
reader.onload = (_ => {
|
||||||
return e => {
|
return e => {
|
||||||
let dataName: string;
|
let dataName = GameDataType[dataType].toLowerCase();
|
||||||
let dataStr = AES.decrypt(e.target?.result?.toString()!, saveKey).toString(enc.Utf8); // TODO: is this bang correct?
|
let dataStr = AES.decrypt(e.target?.result?.toString()!, saveKey).toString(enc.Utf8); // TODO: is this bang correct?
|
||||||
let valid = false;
|
let valid = false;
|
||||||
try {
|
try {
|
||||||
dataName = GameDataType[dataType].toLowerCase();
|
|
||||||
switch (dataType) {
|
switch (dataType) {
|
||||||
case GameDataType.SYSTEM: {
|
case GameDataType.SYSTEM: {
|
||||||
dataStr = this.convertSystemDataStr(dataStr);
|
dataStr = this.convertSystemDataStr(dataStr);
|
||||||
@ -1493,7 +1497,6 @@ export class GameData {
|
|||||||
|
|
||||||
const displayError = (error: string) =>
|
const displayError = (error: string) =>
|
||||||
globalScene.ui.showText(error, null, () => globalScene.ui.showText("", 0), fixedInt(1500));
|
globalScene.ui.showText(error, null, () => globalScene.ui.showText("", 0), fixedInt(1500));
|
||||||
dataName = dataName!; // tell TS compiler that dataName is defined!
|
|
||||||
|
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
return globalScene.ui.showText(
|
return globalScene.ui.showText(
|
||||||
|
@ -981,6 +981,10 @@ export function setSetting(setting: string, value: number): boolean {
|
|||||||
label: "Română (Needs Help)",
|
label: "Română (Needs Help)",
|
||||||
handler: () => changeLocaleHandler("ro"),
|
handler: () => changeLocaleHandler("ro"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Tagalog (Needs Help)",
|
||||||
|
handler: () => changeLocaleHandler("tl"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: i18next.t("settings:back"),
|
label: i18next.t("settings:back"),
|
||||||
handler: () => cancelHandler(),
|
handler: () => cancelHandler(),
|
||||||
|
@ -6,7 +6,7 @@ import { UiMode } from "#enums/ui-mode";
|
|||||||
import type { InputFieldConfig } from "#ui/form-modal-ui-handler";
|
import type { InputFieldConfig } from "#ui/form-modal-ui-handler";
|
||||||
import { FormModalUiHandler } from "#ui/form-modal-ui-handler";
|
import { FormModalUiHandler } from "#ui/form-modal-ui-handler";
|
||||||
import type { ModalConfig } from "#ui/modal-ui-handler";
|
import type { ModalConfig } from "#ui/modal-ui-handler";
|
||||||
import { formatText } from "#utils/common";
|
import { toTitleCase } from "#utils/strings";
|
||||||
|
|
||||||
type AdminUiHandlerService = "discord" | "google";
|
type AdminUiHandlerService = "discord" | "google";
|
||||||
type AdminUiHandlerServiceMode = "Link" | "Unlink";
|
type AdminUiHandlerServiceMode = "Link" | "Unlink";
|
||||||
@ -21,9 +21,9 @@ export class AdminUiHandler extends FormModalUiHandler {
|
|||||||
private readonly httpUserNotFoundErrorCode: number = 404;
|
private readonly httpUserNotFoundErrorCode: number = 404;
|
||||||
private readonly ERR_REQUIRED_FIELD = (field: string) => {
|
private readonly ERR_REQUIRED_FIELD = (field: string) => {
|
||||||
if (field === "username") {
|
if (field === "username") {
|
||||||
return `${formatText(field)} is required`;
|
return `${toTitleCase(field)} is required`;
|
||||||
}
|
}
|
||||||
return `${formatText(field)} Id is required`;
|
return `${toTitleCase(field)} Id is required`;
|
||||||
};
|
};
|
||||||
// returns a string saying whether a username has been successfully linked/unlinked to discord/google
|
// returns a string saying whether a username has been successfully linked/unlinked to discord/google
|
||||||
private readonly SUCCESS_SERVICE_MODE = (service: string, mode: string) => {
|
private readonly SUCCESS_SERVICE_MODE = (service: string, mode: string) => {
|
||||||
|
@ -18,7 +18,8 @@ import { BattleSceneEventType } from "#events/battle-scene";
|
|||||||
import { addTextObject } from "#ui/text";
|
import { addTextObject } from "#ui/text";
|
||||||
import { TimeOfDayWidget } from "#ui/time-of-day-widget";
|
import { TimeOfDayWidget } from "#ui/time-of-day-widget";
|
||||||
import { addWindow, WindowVariant } from "#ui/ui-theme";
|
import { addWindow, WindowVariant } from "#ui/ui-theme";
|
||||||
import { fixedInt, formatText, toCamelCaseString } from "#utils/common";
|
import { fixedInt } from "#utils/common";
|
||||||
|
import { toCamelCase, toTitleCase } from "#utils/strings";
|
||||||
import type { ParseKeys } from "i18next";
|
import type { ParseKeys } from "i18next";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
@ -49,10 +50,10 @@ export function getFieldEffectText(arenaTagType: string): string {
|
|||||||
if (!arenaTagType || arenaTagType === ArenaTagType.NONE) {
|
if (!arenaTagType || arenaTagType === ArenaTagType.NONE) {
|
||||||
return arenaTagType;
|
return arenaTagType;
|
||||||
}
|
}
|
||||||
const effectName = toCamelCaseString(arenaTagType);
|
const effectName = toCamelCase(arenaTagType);
|
||||||
const i18nKey = `arenaFlyout:${effectName}` as ParseKeys;
|
const i18nKey = `arenaFlyout:${effectName}` as ParseKeys;
|
||||||
const resultName = i18next.t(i18nKey);
|
const resultName = i18next.t(i18nKey);
|
||||||
return !resultName || resultName === i18nKey ? formatText(arenaTagType) : resultName;
|
return !resultName || resultName === i18nKey ? toTitleCase(arenaTagType) : resultName;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ArenaFlyout extends Phaser.GameObjects.Container {
|
export class ArenaFlyout extends Phaser.GameObjects.Container {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { globalScene } from "#app/global-scene";
|
import { globalScene } from "#app/global-scene";
|
||||||
import { TextStyle } from "#enums/text-style";
|
import { TextStyle } from "#enums/text-style";
|
||||||
import { addTextObject } from "#ui/text";
|
import { addTextObject } from "#ui/text";
|
||||||
import { formatText } from "#utils/common";
|
import { toTitleCase } from "#utils/strings";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
const hiddenX = -150;
|
const hiddenX = -150;
|
||||||
@ -101,7 +101,7 @@ export class BgmBar extends Phaser.GameObjects.Container {
|
|||||||
|
|
||||||
getRealBgmName(bgmName: string): string {
|
getRealBgmName(bgmName: string): string {
|
||||||
return i18next.t([`bgmName:${bgmName}`, "bgmName:missing_entries"], {
|
return i18next.t([`bgmName:${bgmName}`, "bgmName:missing_entries"], {
|
||||||
name: formatText(bgmName),
|
name: toTitleCase(bgmName),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -382,8 +382,7 @@ export class GameChallengesUiHandler extends UiHandler {
|
|||||||
this.cursorObj?.setVisible(true);
|
this.cursorObj?.setVisible(true);
|
||||||
this.updateChallengeArrows(this.startCursor.visible);
|
this.updateChallengeArrows(this.startCursor.visible);
|
||||||
} else {
|
} else {
|
||||||
globalScene.phaseManager.clearPhaseQueue();
|
globalScene.phaseManager.toTitleScreen();
|
||||||
globalScene.phaseManager.pushNew("TitlePhase");
|
|
||||||
globalScene.phaseManager.getCurrentPhase()?.end();
|
globalScene.phaseManager.getCurrentPhase()?.end();
|
||||||
}
|
}
|
||||||
success = true;
|
success = true;
|
||||||
|
@ -72,6 +72,10 @@ export abstract class FormModalUiHandler extends ModalUiHandler {
|
|||||||
(hasTitle ? 31 : 5) + 20 * (config.length - 1) + 16 + this.getButtonTopMargin(),
|
(hasTitle ? 31 : 5) + 20 * (config.length - 1) + 16 + this.getButtonTopMargin(),
|
||||||
"",
|
"",
|
||||||
TextStyle.TOOLTIP_CONTENT,
|
TextStyle.TOOLTIP_CONTENT,
|
||||||
|
{
|
||||||
|
fontSize: "42px",
|
||||||
|
wordWrap: { width: 850 },
|
||||||
|
},
|
||||||
);
|
);
|
||||||
this.errorMessage.setColor(this.getTextColor(TextStyle.SUMMARY_PINK));
|
this.errorMessage.setColor(this.getTextColor(TextStyle.SUMMARY_PINK));
|
||||||
this.errorMessage.setShadowColor(this.getTextColor(TextStyle.SUMMARY_PINK, true));
|
this.errorMessage.setShadowColor(this.getTextColor(TextStyle.SUMMARY_PINK, true));
|
||||||
@ -84,20 +88,28 @@ export abstract class FormModalUiHandler extends ModalUiHandler {
|
|||||||
this.inputs = [];
|
this.inputs = [];
|
||||||
this.formLabels = [];
|
this.formLabels = [];
|
||||||
fieldsConfig.forEach((config, f) => {
|
fieldsConfig.forEach((config, f) => {
|
||||||
const label = addTextObject(10, (hasTitle ? 31 : 5) + 20 * f, config.label, TextStyle.TOOLTIP_CONTENT);
|
// The Pokédex Scan Window uses width `300` instead of `160` like the other forms
|
||||||
|
// Therefore, the label does not need to be shortened
|
||||||
|
const label = addTextObject(
|
||||||
|
10,
|
||||||
|
(hasTitle ? 31 : 5) + 20 * f,
|
||||||
|
config.label.length > 25 && this.getWidth() < 200 ? config.label.slice(0, 20) + "..." : config.label,
|
||||||
|
TextStyle.TOOLTIP_CONTENT,
|
||||||
|
);
|
||||||
label.name = "formLabel" + f;
|
label.name = "formLabel" + f;
|
||||||
|
|
||||||
this.formLabels.push(label);
|
this.formLabels.push(label);
|
||||||
this.modalContainer.add(this.formLabels[this.formLabels.length - 1]);
|
this.modalContainer.add(this.formLabels[this.formLabels.length - 1]);
|
||||||
|
|
||||||
const inputContainer = globalScene.add.container(70, (hasTitle ? 28 : 2) + 20 * f);
|
const inputWidth = label.width < 320 ? 80 : 80 - (label.width - 320) / 5.5;
|
||||||
|
const inputContainer = globalScene.add.container(70 + (80 - inputWidth), (hasTitle ? 28 : 2) + 20 * f);
|
||||||
inputContainer.setVisible(false);
|
inputContainer.setVisible(false);
|
||||||
|
|
||||||
const inputBg = addWindow(0, 0, 80, 16, false, false, 0, 0, WindowVariant.XTHIN);
|
const inputBg = addWindow(0, 0, inputWidth, 16, false, false, 0, 0, WindowVariant.XTHIN);
|
||||||
|
|
||||||
const isPassword = config?.isPassword;
|
const isPassword = config?.isPassword;
|
||||||
const isReadOnly = config?.isReadOnly;
|
const isReadOnly = config?.isReadOnly;
|
||||||
const input = addTextInputObject(4, -2, 440, 116, TextStyle.TOOLTIP_CONTENT, {
|
const input = addTextInputObject(4, -2, inputWidth * 5.5, 116, TextStyle.TOOLTIP_CONTENT, {
|
||||||
type: isPassword ? "password" : "text",
|
type: isPassword ? "password" : "text",
|
||||||
maxLength: isPassword ? 64 : 20,
|
maxLength: isPassword ? 64 : 20,
|
||||||
readOnly: isReadOnly,
|
readOnly: isReadOnly,
|
||||||
|
@ -8,7 +8,8 @@ import type { GameData } from "#system/game-data";
|
|||||||
import { addTextObject } from "#ui/text";
|
import { addTextObject } from "#ui/text";
|
||||||
import { UiHandler } from "#ui/ui-handler";
|
import { UiHandler } from "#ui/ui-handler";
|
||||||
import { addWindow } from "#ui/ui-theme";
|
import { addWindow } from "#ui/ui-theme";
|
||||||
import { formatFancyLargeNumber, getPlayTimeString, toReadableString } from "#utils/common";
|
import { formatFancyLargeNumber, getPlayTimeString } from "#utils/common";
|
||||||
|
import { toTitleCase } from "#utils/strings";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import Phaser from "phaser";
|
import Phaser from "phaser";
|
||||||
|
|
||||||
@ -502,11 +503,9 @@ export function initStatsKeys() {
|
|||||||
sourceFunc: gameData => gameData.gameStats[key].toString(),
|
sourceFunc: gameData => gameData.gameStats[key].toString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (!(displayStats[key] as DisplayStat).label_key) {
|
if (!displayStats[key].label_key) {
|
||||||
const splittableKey = key.replace(/([a-z]{2,})([A-Z]{1}(?:[^A-Z]|$))/g, "$1_$2");
|
const splittableKey = key.replace(/([a-z]{2,})([A-Z]{1}(?:[^A-Z]|$))/g, "$1_$2");
|
||||||
(displayStats[key] as DisplayStat).label_key = toReadableString(
|
displayStats[key].label_key = toTitleCase(splittableKey);
|
||||||
`${splittableKey[0].toUpperCase()}${splittableKey.slice(1)}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -152,7 +152,12 @@ export abstract class ModalUiHandler extends UiHandler {
|
|||||||
updateContainer(config?: ModalConfig): void {
|
updateContainer(config?: ModalConfig): void {
|
||||||
const [marginTop, marginRight, marginBottom, marginLeft] = this.getMargin(config);
|
const [marginTop, marginRight, marginBottom, marginLeft] = this.getMargin(config);
|
||||||
|
|
||||||
const [width, height] = [this.getWidth(config), this.getHeight(config)];
|
/**
|
||||||
|
* If the total amount of characters for the 2 buttons exceeds ~30 characters,
|
||||||
|
* the width in `registration-form-ui-handler.ts` and `login-form-ui-handler.ts` needs to be increased.
|
||||||
|
*/
|
||||||
|
const width = this.getWidth(config);
|
||||||
|
const height = this.getHeight(config);
|
||||||
this.modalContainer.setPosition(
|
this.modalContainer.setPosition(
|
||||||
(globalScene.game.canvas.width / 6 - (width + (marginRight - marginLeft))) / 2,
|
(globalScene.game.canvas.width / 6 - (width + (marginRight - marginLeft))) / 2,
|
||||||
(-globalScene.game.canvas.height / 6 - (height + (marginBottom - marginTop))) / 2,
|
(-globalScene.game.canvas.height / 6 - (height + (marginBottom - marginTop))) / 2,
|
||||||
@ -166,10 +171,14 @@ export abstract class ModalUiHandler extends UiHandler {
|
|||||||
this.titleText.setX(width / 2);
|
this.titleText.setX(width / 2);
|
||||||
this.titleText.setVisible(!!title);
|
this.titleText.setVisible(!!title);
|
||||||
|
|
||||||
for (let b = 0; b < this.buttonContainers.length; b++) {
|
if (this.buttonContainers.length > 0) {
|
||||||
const sliceWidth = width / (this.buttonContainers.length + 1);
|
const spacing = 12;
|
||||||
|
const totalWidth = this.buttonBgs.reduce((sum, bg) => sum + bg.width, 0) + spacing * (this.buttonBgs.length - 1);
|
||||||
this.buttonContainers[b].setPosition(sliceWidth * (b + 1), this.modalBg.height - (this.buttonBgs[b].height + 8));
|
let x = (this.modalBg.width - totalWidth) / 2;
|
||||||
|
this.buttonContainers.forEach((container, i) => {
|
||||||
|
container.setPosition(x + this.buttonBgs[i].width / 2, this.modalBg.height - (this.buttonBgs[i].height + 8));
|
||||||
|
x += this.buttonBgs[i].width + spacing;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler {
|
|||||||
|
|
||||||
if (context) {
|
if (context) {
|
||||||
context.font = styleOptions.fontSize + "px " + styleOptions.fontFamily;
|
context.font = styleOptions.fontSize + "px " + styleOptions.fontFamily;
|
||||||
this.transferButtonWidth = context.measureText(i18next.t("modifierSelectUiHandler:transfer")).width;
|
this.transferButtonWidth = context.measureText(i18next.t("modifierSelectUiHandler:manageItems")).width;
|
||||||
this.checkButtonWidth = context.measureText(i18next.t("modifierSelectUiHandler:checkTeam")).width;
|
this.checkButtonWidth = context.measureText(i18next.t("modifierSelectUiHandler:checkTeam")).width;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,7 +81,7 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler {
|
|||||||
this.transferButtonContainer.setVisible(false);
|
this.transferButtonContainer.setVisible(false);
|
||||||
ui.add(this.transferButtonContainer);
|
ui.add(this.transferButtonContainer);
|
||||||
|
|
||||||
const transferButtonText = addTextObject(-4, -2, i18next.t("modifierSelectUiHandler:transfer"), TextStyle.PARTY);
|
const transferButtonText = addTextObject(-4, -2, i18next.t("modifierSelectUiHandler:manageItems"), TextStyle.PARTY);
|
||||||
transferButtonText.setName("text-transfer-btn");
|
transferButtonText.setName("text-transfer-btn");
|
||||||
transferButtonText.setOrigin(1, 0);
|
transferButtonText.setOrigin(1, 0);
|
||||||
this.transferButtonContainer.add(transferButtonText);
|
this.transferButtonContainer.add(transferButtonText);
|
||||||
@ -601,7 +601,7 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler {
|
|||||||
(globalScene.game.canvas.width - this.transferButtonWidth - this.checkButtonWidth) / 6 - 30,
|
(globalScene.game.canvas.width - this.transferButtonWidth - this.checkButtonWidth) / 6 - 30,
|
||||||
OPTION_BUTTON_YPOSITION + 4,
|
OPTION_BUTTON_YPOSITION + 4,
|
||||||
);
|
);
|
||||||
ui.showText(i18next.t("modifierSelectUiHandler:transferDesc"));
|
ui.showText(i18next.t("modifierSelectUiHandler:manageItemsDesc"));
|
||||||
} else if (cursor === 2) {
|
} else if (cursor === 2) {
|
||||||
this.cursorObj.setPosition(
|
this.cursorObj.setPosition(
|
||||||
(globalScene.game.canvas.width - this.checkButtonWidth) / 6 - 10,
|
(globalScene.game.canvas.width - this.checkButtonWidth) / 6 - 10,
|
||||||
|
@ -26,7 +26,8 @@ import { MoveInfoOverlay } from "#ui/move-info-overlay";
|
|||||||
import { PokemonIconAnimHandler, PokemonIconAnimMode } from "#ui/pokemon-icon-anim-handler";
|
import { PokemonIconAnimHandler, PokemonIconAnimMode } from "#ui/pokemon-icon-anim-handler";
|
||||||
import { addBBCodeTextObject, addTextObject, getTextColor } from "#ui/text";
|
import { addBBCodeTextObject, addTextObject, getTextColor } from "#ui/text";
|
||||||
import { addWindow } from "#ui/ui-theme";
|
import { addWindow } from "#ui/ui-theme";
|
||||||
import { BooleanHolder, getLocalizedSpriteKey, randInt, toReadableString } from "#utils/common";
|
import { BooleanHolder, getLocalizedSpriteKey, randInt } from "#utils/common";
|
||||||
|
import { toTitleCase } from "#utils/strings";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import type BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext";
|
import type BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext";
|
||||||
|
|
||||||
@ -102,6 +103,11 @@ export enum PartyUiMode {
|
|||||||
* This is generally used in for Mystery Encounter or special effects that require the player to select a Pokemon
|
* This is generally used in for Mystery Encounter or special effects that require the player to select a Pokemon
|
||||||
*/
|
*/
|
||||||
SELECT,
|
SELECT,
|
||||||
|
/**
|
||||||
|
* Indicates that the party UI is open to select a party member from which items will be discarded.
|
||||||
|
* This type of selection can be cancelled.
|
||||||
|
*/
|
||||||
|
DISCARD,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum PartyOption {
|
export enum PartyOption {
|
||||||
@ -120,6 +126,7 @@ export enum PartyOption {
|
|||||||
RELEASE,
|
RELEASE,
|
||||||
RENAME,
|
RENAME,
|
||||||
SELECT,
|
SELECT,
|
||||||
|
DISCARD,
|
||||||
SCROLL_UP = 1000,
|
SCROLL_UP = 1000,
|
||||||
SCROLL_DOWN = 1001,
|
SCROLL_DOWN = 1001,
|
||||||
FORM_CHANGE_ITEM = 2000,
|
FORM_CHANGE_ITEM = 2000,
|
||||||
@ -154,6 +161,7 @@ export class PartyUiHandler extends MessageUiHandler {
|
|||||||
private partySlotsContainer: Phaser.GameObjects.Container;
|
private partySlotsContainer: Phaser.GameObjects.Container;
|
||||||
private partySlots: PartySlot[];
|
private partySlots: PartySlot[];
|
||||||
private partyCancelButton: PartyCancelButton;
|
private partyCancelButton: PartyCancelButton;
|
||||||
|
private partyDiscardModeButton: PartyDiscardModeButton;
|
||||||
private partyMessageBox: Phaser.GameObjects.NineSlice;
|
private partyMessageBox: Phaser.GameObjects.NineSlice;
|
||||||
private moveInfoOverlay: MoveInfoOverlay;
|
private moveInfoOverlay: MoveInfoOverlay;
|
||||||
|
|
||||||
@ -179,6 +187,8 @@ export class PartyUiHandler extends MessageUiHandler {
|
|||||||
private transferAll: boolean;
|
private transferAll: boolean;
|
||||||
|
|
||||||
private lastCursor = 0;
|
private lastCursor = 0;
|
||||||
|
private lastLeftPokemonCursor = 0;
|
||||||
|
private lastRightPokemonCursor = 0;
|
||||||
private selectCallback: PartySelectCallback | PartyModifierTransferSelectCallback | null;
|
private selectCallback: PartySelectCallback | PartyModifierTransferSelectCallback | null;
|
||||||
private selectFilter: PokemonSelectFilter | PokemonModifierTransferSelectFilter;
|
private selectFilter: PokemonSelectFilter | PokemonModifierTransferSelectFilter;
|
||||||
private moveSelectFilter: PokemonMoveSelectFilter;
|
private moveSelectFilter: PokemonMoveSelectFilter;
|
||||||
@ -307,6 +317,12 @@ export class PartyUiHandler extends MessageUiHandler {
|
|||||||
this.iconAnimHandler = new PokemonIconAnimHandler();
|
this.iconAnimHandler = new PokemonIconAnimHandler();
|
||||||
this.iconAnimHandler.setup();
|
this.iconAnimHandler.setup();
|
||||||
|
|
||||||
|
const partyDiscardModeButton = new PartyDiscardModeButton(60, -globalScene.game.canvas.height / 15 - 1, this);
|
||||||
|
|
||||||
|
partyContainer.add(partyDiscardModeButton);
|
||||||
|
|
||||||
|
this.partyDiscardModeButton = partyDiscardModeButton;
|
||||||
|
|
||||||
// prepare move overlay. in case it appears to be too big, set the overlayScale to .5
|
// prepare move overlay. in case it appears to be too big, set the overlayScale to .5
|
||||||
const overlayScale = 1;
|
const overlayScale = 1;
|
||||||
this.moveInfoOverlay = new MoveInfoOverlay({
|
this.moveInfoOverlay = new MoveInfoOverlay({
|
||||||
@ -348,8 +364,18 @@ export class PartyUiHandler extends MessageUiHandler {
|
|||||||
this.showMovePp = args.length > 6 && args[6];
|
this.showMovePp = args.length > 6 && args[6];
|
||||||
|
|
||||||
this.partyContainer.setVisible(true);
|
this.partyContainer.setVisible(true);
|
||||||
this.partyBg.setTexture(`party_bg${globalScene.currentBattle.double ? "_double" : ""}`);
|
if (this.isItemManageMode()) {
|
||||||
|
this.partyBg.setTexture(`party_bg${globalScene.currentBattle.double ? "_double_manage" : ""}`);
|
||||||
|
} else {
|
||||||
|
this.partyBg.setTexture(`party_bg${globalScene.currentBattle.double ? "_double" : ""}`);
|
||||||
|
}
|
||||||
|
|
||||||
this.populatePartySlots();
|
this.populatePartySlots();
|
||||||
|
// If we are currently transferring items, set the icon to its proper state and reveal the button.
|
||||||
|
if (this.isItemManageMode()) {
|
||||||
|
this.partyDiscardModeButton.toggleIcon(this.partyUiMode as PartyUiMode.MODIFIER_TRANSFER | PartyUiMode.DISCARD);
|
||||||
|
}
|
||||||
|
this.showPartyText();
|
||||||
this.setCursor(0);
|
this.setCursor(0);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -594,7 +620,7 @@ export class PartyUiHandler extends MessageUiHandler {
|
|||||||
const option = this.options[this.optionsCursor];
|
const option = this.options[this.optionsCursor];
|
||||||
if (button === Button.LEFT) {
|
if (button === Button.LEFT) {
|
||||||
/** Decrease quantity for the current item and update UI */
|
/** Decrease quantity for the current item and update UI */
|
||||||
if (this.partyUiMode === PartyUiMode.MODIFIER_TRANSFER) {
|
if (this.isItemManageMode()) {
|
||||||
this.transferQuantities[option] =
|
this.transferQuantities[option] =
|
||||||
this.transferQuantities[option] === 1
|
this.transferQuantities[option] === 1
|
||||||
? this.transferQuantitiesMax[option]
|
? this.transferQuantitiesMax[option]
|
||||||
@ -608,7 +634,7 @@ export class PartyUiHandler extends MessageUiHandler {
|
|||||||
|
|
||||||
if (button === Button.RIGHT) {
|
if (button === Button.RIGHT) {
|
||||||
/** Increase quantity for the current item and update UI */
|
/** Increase quantity for the current item and update UI */
|
||||||
if (this.partyUiMode === PartyUiMode.MODIFIER_TRANSFER) {
|
if (this.isItemManageMode()) {
|
||||||
this.transferQuantities[option] =
|
this.transferQuantities[option] =
|
||||||
this.transferQuantities[option] === this.transferQuantitiesMax[option]
|
this.transferQuantities[option] === this.transferQuantitiesMax[option]
|
||||||
? 1
|
? 1
|
||||||
@ -638,6 +664,45 @@ export class PartyUiHandler extends MessageUiHandler {
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private processDiscardMenuInput(pokemon: PlayerPokemon) {
|
||||||
|
const ui = this.getUi();
|
||||||
|
const option = this.options[this.optionsCursor];
|
||||||
|
this.clearOptions();
|
||||||
|
|
||||||
|
this.blockInput = true;
|
||||||
|
this.showText(i18next.t("partyUiHandler:discardConfirmation"), null, () => {
|
||||||
|
this.blockInput = false;
|
||||||
|
ui.setModeWithoutClear(
|
||||||
|
UiMode.CONFIRM,
|
||||||
|
() => {
|
||||||
|
ui.setMode(UiMode.PARTY);
|
||||||
|
this.doDiscard(option, pokemon);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
ui.setMode(UiMode.PARTY);
|
||||||
|
this.showPartyText();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private doDiscard(option: PartyOption, pokemon: PlayerPokemon) {
|
||||||
|
const itemModifiers = this.getTransferrableItemsFromPokemon(pokemon);
|
||||||
|
this.clearOptions();
|
||||||
|
|
||||||
|
if (option === PartyOption.ALL) {
|
||||||
|
// Discard all currently held items
|
||||||
|
for (let i = 0; i < itemModifiers.length; i++) {
|
||||||
|
globalScene.tryDiscardHeldItemModifier(itemModifiers[i], this.transferQuantities[i]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Discard the currently selected item
|
||||||
|
globalScene.tryDiscardHeldItemModifier(itemModifiers[option], this.transferQuantities[option]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private moveOptionCursor(button: Button.UP | Button.DOWN): boolean {
|
private moveOptionCursor(button: Button.UP | Button.DOWN): boolean {
|
||||||
if (button === Button.UP) {
|
if (button === Button.UP) {
|
||||||
return this.setCursor(this.optionsCursor ? this.optionsCursor - 1 : this.options.length - 1);
|
return this.setCursor(this.optionsCursor ? this.optionsCursor - 1 : this.options.length - 1);
|
||||||
@ -724,6 +789,10 @@ export class PartyUiHandler extends MessageUiHandler {
|
|||||||
return this.processModifierTransferModeInput(pokemon);
|
return this.processModifierTransferModeInput(pokemon);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.partyUiMode === PartyUiMode.DISCARD) {
|
||||||
|
return this.processDiscardMenuInput(pokemon);
|
||||||
|
}
|
||||||
|
|
||||||
// options specific to the mode (moves)
|
// options specific to the mode (moves)
|
||||||
if (this.partyUiMode === PartyUiMode.REMEMBER_MOVE_MODIFIER) {
|
if (this.partyUiMode === PartyUiMode.REMEMBER_MOVE_MODIFIER) {
|
||||||
return this.processRememberMoveModeInput(pokemon);
|
return this.processRememberMoveModeInput(pokemon);
|
||||||
@ -863,7 +932,7 @@ export class PartyUiHandler extends MessageUiHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (button === Button.LEFT || button === Button.RIGHT) {
|
if (button === Button.LEFT || button === Button.RIGHT) {
|
||||||
if (this.partyUiMode === PartyUiMode.MODIFIER_TRANSFER) {
|
if (this.isItemManageMode()) {
|
||||||
return this.processModifierTransferModeLeftRightInput(button);
|
return this.processModifierTransferModeLeftRightInput(button);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -918,10 +987,22 @@ export class PartyUiHandler extends MessageUiHandler {
|
|||||||
return !(this.partyUiMode === PartyUiMode.FAINT_SWITCH || this.partyUiMode === PartyUiMode.REVIVAL_BLESSING);
|
return !(this.partyUiMode === PartyUiMode.FAINT_SWITCH || this.partyUiMode === PartyUiMode.REVIVAL_BLESSING);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether this UI handler is responsible for managing items.
|
||||||
|
* Used to ensure proper placement of mode toggle buttons in the UI, etc.
|
||||||
|
* @returns Whether the current handler is responsible for managing items.
|
||||||
|
*/
|
||||||
|
private isItemManageMode(): boolean {
|
||||||
|
return this.partyUiMode === PartyUiMode.MODIFIER_TRANSFER || this.partyUiMode === PartyUiMode.DISCARD;
|
||||||
|
}
|
||||||
|
|
||||||
private processPartyActionInput(): boolean {
|
private processPartyActionInput(): boolean {
|
||||||
const ui = this.getUi();
|
const ui = this.getUi();
|
||||||
if (this.cursor < 6) {
|
if (this.cursor < 6) {
|
||||||
if (this.partyUiMode === PartyUiMode.MODIFIER_TRANSFER && !this.transferMode) {
|
if (
|
||||||
|
(this.partyUiMode === PartyUiMode.MODIFIER_TRANSFER && !this.transferMode) ||
|
||||||
|
this.partyUiMode === PartyUiMode.DISCARD
|
||||||
|
) {
|
||||||
/** Initialize item quantities for the selected Pokemon */
|
/** Initialize item quantities for the selected Pokemon */
|
||||||
const itemModifiers = globalScene.findModifiers(
|
const itemModifiers = globalScene.findModifiers(
|
||||||
m =>
|
m =>
|
||||||
@ -935,6 +1016,25 @@ export class PartyUiHandler extends MessageUiHandler {
|
|||||||
this.showOptions();
|
this.showOptions();
|
||||||
ui.playSelect();
|
ui.playSelect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Toggle item transfer mode to discard items or vice versa
|
||||||
|
if (this.cursor === 7) {
|
||||||
|
switch (this.partyUiMode) {
|
||||||
|
case PartyUiMode.DISCARD:
|
||||||
|
this.partyUiMode = PartyUiMode.MODIFIER_TRANSFER;
|
||||||
|
break;
|
||||||
|
case PartyUiMode.MODIFIER_TRANSFER:
|
||||||
|
this.partyUiMode = PartyUiMode.DISCARD;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ui.playError();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.partyDiscardModeButton.toggleIcon(this.partyUiMode);
|
||||||
|
ui.playSelect();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Pressing return button
|
// Pressing return button
|
||||||
if (this.cursor === 6) {
|
if (this.cursor === 6) {
|
||||||
if (!this.allowCancel()) {
|
if (!this.allowCancel()) {
|
||||||
@ -955,6 +1055,7 @@ export class PartyUiHandler extends MessageUiHandler {
|
|||||||
this.clearTransfer();
|
this.clearTransfer();
|
||||||
ui.playSelect();
|
ui.playSelect();
|
||||||
} else if (this.allowCancel()) {
|
} else if (this.allowCancel()) {
|
||||||
|
this.partyDiscardModeButton.clear();
|
||||||
if (this.selectCallback) {
|
if (this.selectCallback) {
|
||||||
const selectCallback = this.selectCallback;
|
const selectCallback = this.selectCallback;
|
||||||
this.selectCallback = null;
|
this.selectCallback = null;
|
||||||
@ -973,30 +1074,74 @@ export class PartyUiHandler extends MessageUiHandler {
|
|||||||
const slotCount = this.partySlots.length;
|
const slotCount = this.partySlots.length;
|
||||||
const battlerCount = globalScene.currentBattle.getBattlerCount();
|
const battlerCount = globalScene.currentBattle.getBattlerCount();
|
||||||
|
|
||||||
|
if (this.lastCursor < battlerCount) {
|
||||||
|
this.lastLeftPokemonCursor = this.lastCursor;
|
||||||
|
}
|
||||||
|
if (this.lastCursor >= battlerCount && this.lastCursor < 6) {
|
||||||
|
this.lastRightPokemonCursor = this.lastCursor;
|
||||||
|
}
|
||||||
|
|
||||||
let success = false;
|
let success = false;
|
||||||
switch (button) {
|
switch (button) {
|
||||||
|
// Item manage mode adds an extra 8th "toggle mode" button to the UI, located *below* both active party members.
|
||||||
|
// The following logic serves to ensure its menu behaviour matches its in-game position,
|
||||||
|
// being selected when scrolling up from the first inactive party member or down from the last active one.
|
||||||
case Button.UP:
|
case Button.UP:
|
||||||
|
if (this.isItemManageMode()) {
|
||||||
|
if (this.cursor === 1) {
|
||||||
|
success = this.setCursor(globalScene.currentBattle.double ? 0 : 7);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (this.cursor === 2) {
|
||||||
|
success = this.setCursor(globalScene.currentBattle.double ? 7 : 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (this.cursor === 6) {
|
||||||
|
success = this.setCursor(slotCount <= globalScene.currentBattle.getBattlerCount() ? 7 : slotCount - 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (this.cursor === 7) {
|
||||||
|
success = this.setCursor(globalScene.currentBattle.double && slotCount > 1 ? 1 : 0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
success = this.setCursor(this.cursor ? (this.cursor < 6 ? this.cursor - 1 : slotCount - 1) : 6);
|
success = this.setCursor(this.cursor ? (this.cursor < 6 ? this.cursor - 1 : slotCount - 1) : 6);
|
||||||
break;
|
break;
|
||||||
case Button.DOWN:
|
case Button.DOWN:
|
||||||
|
if (this.isItemManageMode()) {
|
||||||
|
if (this.cursor === 0) {
|
||||||
|
success = this.setCursor(globalScene.currentBattle.double && slotCount > 1 ? 1 : 7);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (this.cursor === 1) {
|
||||||
|
success = this.setCursor(globalScene.currentBattle.double ? 7 : slotCount > 2 ? 2 : 6);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (this.cursor === 7) {
|
||||||
|
success = this.setCursor(
|
||||||
|
slotCount > globalScene.currentBattle.getBattlerCount() ? globalScene.currentBattle.getBattlerCount() : 6,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
success = this.setCursor(this.cursor < 6 ? (this.cursor < slotCount - 1 ? this.cursor + 1 : 6) : 0);
|
success = this.setCursor(this.cursor < 6 ? (this.cursor < slotCount - 1 ? this.cursor + 1 : 6) : 0);
|
||||||
break;
|
break;
|
||||||
case Button.LEFT:
|
case Button.LEFT:
|
||||||
if (this.cursor >= battlerCount && this.cursor <= 6) {
|
if (this.cursor === 6) {
|
||||||
success = this.setCursor(0);
|
success = this.setCursor(this.isItemManageMode() ? 7 : this.lastLeftPokemonCursor);
|
||||||
|
}
|
||||||
|
if (this.cursor >= battlerCount && this.cursor < 6) {
|
||||||
|
success = this.setCursor(this.lastLeftPokemonCursor);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Button.RIGHT:
|
case Button.RIGHT:
|
||||||
if (slotCount === battlerCount) {
|
// Scrolling right from item transfer button or with no backup party members goes to cancel
|
||||||
|
if (this.cursor === 7 || slotCount <= battlerCount) {
|
||||||
success = this.setCursor(6);
|
success = this.setCursor(6);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (battlerCount >= 2 && slotCount > battlerCount && this.getCursor() === 0 && this.lastCursor === 1) {
|
if (this.cursor < battlerCount) {
|
||||||
success = this.setCursor(2);
|
success = this.setCursor(this.lastRightPokemonCursor || battlerCount);
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (slotCount > battlerCount && this.cursor < battlerCount) {
|
|
||||||
success = this.setCursor(this.lastCursor < 6 ? this.lastCursor || battlerCount : battlerCount);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1043,11 +1188,15 @@ export class PartyUiHandler extends MessageUiHandler {
|
|||||||
this.partySlots[this.lastCursor].deselect();
|
this.partySlots[this.lastCursor].deselect();
|
||||||
} else if (this.lastCursor === 6) {
|
} else if (this.lastCursor === 6) {
|
||||||
this.partyCancelButton.deselect();
|
this.partyCancelButton.deselect();
|
||||||
|
} else if (this.lastCursor === 7) {
|
||||||
|
this.partyDiscardModeButton.deselect();
|
||||||
}
|
}
|
||||||
if (cursor < 6) {
|
if (cursor < 6) {
|
||||||
this.partySlots[cursor].select();
|
this.partySlots[cursor].select();
|
||||||
} else if (cursor === 6) {
|
} else if (cursor === 6) {
|
||||||
this.partyCancelButton.select();
|
this.partyCancelButton.select();
|
||||||
|
} else if (cursor === 7) {
|
||||||
|
this.partyDiscardModeButton.select();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return changed;
|
return changed;
|
||||||
@ -1142,14 +1291,16 @@ export class PartyUiHandler extends MessageUiHandler {
|
|||||||
optionsMessage = i18next.t("partyUiHandler:selectAnotherPokemonToSplice");
|
optionsMessage = i18next.t("partyUiHandler:selectAnotherPokemonToSplice");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case PartyUiMode.DISCARD:
|
||||||
|
optionsMessage = i18next.t("partyUiHandler:changeQuantityDiscard");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.showText(optionsMessage, 0);
|
this.showText(optionsMessage, 0);
|
||||||
|
|
||||||
this.updateOptions();
|
this.updateOptions();
|
||||||
|
|
||||||
/** When an item is being selected for transfer, the message box is taller as the message occupies two lines */
|
/** When an item is being selected for transfer or discard, the message box is taller as the message occupies two lines */
|
||||||
if (this.partyUiMode === PartyUiMode.MODIFIER_TRANSFER) {
|
if (this.isItemManageMode()) {
|
||||||
this.partyMessageBox.setSize(262 - Math.max(this.optionsBg.displayWidth - 56, 0), 42);
|
this.partyMessageBox.setSize(262 - Math.max(this.optionsBg.displayWidth - 56, 0), 42);
|
||||||
} else {
|
} else {
|
||||||
this.partyMessageBox.setSize(262 - Math.max(this.optionsBg.displayWidth - 56, 0), 30);
|
this.partyMessageBox.setSize(262 - Math.max(this.optionsBg.displayWidth - 56, 0), 30);
|
||||||
@ -1158,6 +1309,20 @@ export class PartyUiHandler extends MessageUiHandler {
|
|||||||
this.setCursor(0);
|
this.setCursor(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showPartyText() {
|
||||||
|
switch (this.partyUiMode) {
|
||||||
|
case PartyUiMode.MODIFIER_TRANSFER:
|
||||||
|
this.showText(i18next.t("partyUiHandler:partyTransfer"));
|
||||||
|
break;
|
||||||
|
case PartyUiMode.DISCARD:
|
||||||
|
this.showText(i18next.t("partyUiHandler:partyDiscard"));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.showText("", 0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private allowBatonModifierSwitch(): boolean {
|
private allowBatonModifierSwitch(): boolean {
|
||||||
return !!(
|
return !!(
|
||||||
this.partyUiMode !== PartyUiMode.FAINT_SWITCH &&
|
this.partyUiMode !== PartyUiMode.FAINT_SWITCH &&
|
||||||
@ -1275,6 +1440,9 @@ export class PartyUiHandler extends MessageUiHandler {
|
|||||||
this.addCommonOptions(pokemon);
|
this.addCommonOptions(pokemon);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case PartyUiMode.DISCARD:
|
||||||
|
this.updateOptionsWithModifierTransferMode(pokemon);
|
||||||
|
break;
|
||||||
// TODO: This still needs to be broken up.
|
// TODO: This still needs to be broken up.
|
||||||
// It could use a rework differentiating different kind of switches
|
// It could use a rework differentiating different kind of switches
|
||||||
// to treat baton passing separately from switching on faint.
|
// to treat baton passing separately from switching on faint.
|
||||||
@ -1380,7 +1548,8 @@ export class PartyUiHandler extends MessageUiHandler {
|
|||||||
optionName = "↓";
|
optionName = "↓";
|
||||||
} else if (
|
} else if (
|
||||||
(this.partyUiMode !== PartyUiMode.REMEMBER_MOVE_MODIFIER &&
|
(this.partyUiMode !== PartyUiMode.REMEMBER_MOVE_MODIFIER &&
|
||||||
(this.partyUiMode !== PartyUiMode.MODIFIER_TRANSFER || this.transferMode)) ||
|
(this.partyUiMode !== PartyUiMode.MODIFIER_TRANSFER || this.transferMode) &&
|
||||||
|
this.partyUiMode !== PartyUiMode.DISCARD) ||
|
||||||
option === PartyOption.CANCEL
|
option === PartyOption.CANCEL
|
||||||
) {
|
) {
|
||||||
switch (option) {
|
switch (option) {
|
||||||
@ -1409,7 +1578,7 @@ export class PartyUiHandler extends MessageUiHandler {
|
|||||||
if (this.localizedOptions.includes(option)) {
|
if (this.localizedOptions.includes(option)) {
|
||||||
optionName = i18next.t(`partyUiHandler:${PartyOption[option]}`);
|
optionName = i18next.t(`partyUiHandler:${PartyOption[option]}`);
|
||||||
} else {
|
} else {
|
||||||
optionName = toReadableString(PartyOption[option]);
|
optionName = toTitleCase(PartyOption[option]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -1443,7 +1612,7 @@ export class PartyUiHandler extends MessageUiHandler {
|
|||||||
const itemModifiers = this.getItemModifiers(pokemon);
|
const itemModifiers = this.getItemModifiers(pokemon);
|
||||||
const itemModifier = itemModifiers[option];
|
const itemModifier = itemModifiers[option];
|
||||||
if (
|
if (
|
||||||
this.partyUiMode === PartyUiMode.MODIFIER_TRANSFER &&
|
this.isItemManageMode() &&
|
||||||
this.transferQuantitiesMax[option] > 1 &&
|
this.transferQuantitiesMax[option] > 1 &&
|
||||||
!this.transferMode &&
|
!this.transferMode &&
|
||||||
itemModifier !== undefined &&
|
itemModifier !== undefined &&
|
||||||
@ -1473,7 +1642,6 @@ export class PartyUiHandler extends MessageUiHandler {
|
|||||||
optionText.x = 15 - this.optionsBg.width;
|
optionText.x = 15 - this.optionsBg.width;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
startTransfer(): void {
|
startTransfer(): void {
|
||||||
this.transferMode = true;
|
this.transferMode = true;
|
||||||
this.transferCursor = this.cursor;
|
this.transferCursor = this.cursor;
|
||||||
@ -1607,7 +1775,7 @@ export class PartyUiHandler extends MessageUiHandler {
|
|||||||
this.eraseOptionsCursor();
|
this.eraseOptionsCursor();
|
||||||
|
|
||||||
this.partyMessageBox.setSize(262, 30);
|
this.partyMessageBox.setSize(262, 30);
|
||||||
this.showText("", 0);
|
this.showPartyText();
|
||||||
}
|
}
|
||||||
|
|
||||||
eraseOptionsCursor() {
|
eraseOptionsCursor() {
|
||||||
@ -1662,7 +1830,9 @@ class PartySlot extends Phaser.GameObjects.Container {
|
|||||||
? -184 +
|
? -184 +
|
||||||
(globalScene.currentBattle.double ? -40 : 0) +
|
(globalScene.currentBattle.double ? -40 : 0) +
|
||||||
(28 + (globalScene.currentBattle.double ? 8 : 0)) * slotIndex
|
(28 + (globalScene.currentBattle.double ? 8 : 0)) * slotIndex
|
||||||
: -124 + (globalScene.currentBattle.double ? -8 : 0) + slotIndex * 64,
|
: partyUiMode === PartyUiMode.MODIFIER_TRANSFER
|
||||||
|
? -124 + (globalScene.currentBattle.double ? -20 : 0) + slotIndex * 55
|
||||||
|
: -124 + (globalScene.currentBattle.double ? -8 : 0) + slotIndex * 64,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.slotIndex = slotIndex;
|
this.slotIndex = slotIndex;
|
||||||
@ -1917,7 +2087,6 @@ class PartySlot extends Phaser.GameObjects.Container {
|
|||||||
|
|
||||||
class PartyCancelButton extends Phaser.GameObjects.Container {
|
class PartyCancelButton extends Phaser.GameObjects.Container {
|
||||||
private selected: boolean;
|
private selected: boolean;
|
||||||
|
|
||||||
private partyCancelBg: Phaser.GameObjects.Sprite;
|
private partyCancelBg: Phaser.GameObjects.Sprite;
|
||||||
private partyCancelPb: Phaser.GameObjects.Sprite;
|
private partyCancelPb: Phaser.GameObjects.Sprite;
|
||||||
|
|
||||||
@ -1964,3 +2133,96 @@ class PartyCancelButton extends Phaser.GameObjects.Container {
|
|||||||
this.partyCancelPb.setFrame("party_pb");
|
this.partyCancelPb.setFrame("party_pb");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class PartyDiscardModeButton extends Phaser.GameObjects.Container {
|
||||||
|
private selected: boolean;
|
||||||
|
private transferIcon: Phaser.GameObjects.Sprite;
|
||||||
|
private discardIcon: Phaser.GameObjects.Sprite;
|
||||||
|
private textBox: Phaser.GameObjects.Text;
|
||||||
|
private party: PartyUiHandler;
|
||||||
|
|
||||||
|
constructor(x: number, y: number, party: PartyUiHandler) {
|
||||||
|
super(globalScene, x, y);
|
||||||
|
|
||||||
|
this.setup(party);
|
||||||
|
}
|
||||||
|
|
||||||
|
setup(party: PartyUiHandler) {
|
||||||
|
this.transferIcon = globalScene.add.sprite(0, 0, "party_transfer");
|
||||||
|
this.discardIcon = globalScene.add.sprite(0, 0, "party_discard");
|
||||||
|
this.textBox = addTextObject(-8, -7, i18next.t("partyUiHandler:TRANSFER"), TextStyle.PARTY);
|
||||||
|
this.party = party;
|
||||||
|
|
||||||
|
this.add(this.transferIcon);
|
||||||
|
this.add(this.discardIcon);
|
||||||
|
this.add(this.textBox);
|
||||||
|
|
||||||
|
this.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
select() {
|
||||||
|
if (this.selected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selected = true;
|
||||||
|
|
||||||
|
this.party.showText(i18next.t("partyUiHandler:changeMode"));
|
||||||
|
|
||||||
|
this.transferIcon.setFrame("selected");
|
||||||
|
this.discardIcon.setFrame("selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
deselect() {
|
||||||
|
if (!this.selected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selected = false;
|
||||||
|
this.party.showPartyText();
|
||||||
|
|
||||||
|
this.transferIcon.setFrame("normal");
|
||||||
|
this.discardIcon.setFrame("normal");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the current mode deals with transferring items, toggle the discard items button's name and assets.
|
||||||
|
* @param partyMode - The current {@linkcode PartyUiMode}
|
||||||
|
* @remarks
|
||||||
|
* This will also reveal the button if it is currently hidden.
|
||||||
|
*/
|
||||||
|
public toggleIcon(partyMode: PartyUiMode.MODIFIER_TRANSFER | PartyUiMode.DISCARD): void {
|
||||||
|
this.setActive(true).setVisible(true);
|
||||||
|
switch (partyMode) {
|
||||||
|
case PartyUiMode.MODIFIER_TRANSFER:
|
||||||
|
this.transferIcon.setVisible(true);
|
||||||
|
this.discardIcon.setVisible(false);
|
||||||
|
this.textBox.setVisible(true);
|
||||||
|
this.textBox.setText(i18next.t("partyUiHandler:TRANSFER"));
|
||||||
|
this.setPosition(
|
||||||
|
globalScene.currentBattle.double ? 64 : 60,
|
||||||
|
globalScene.currentBattle.double ? -48 : -globalScene.game.canvas.height / 15 - 1,
|
||||||
|
);
|
||||||
|
this.transferIcon.displayWidth = this.textBox.text.length * 9 + 3;
|
||||||
|
break;
|
||||||
|
case PartyUiMode.DISCARD:
|
||||||
|
this.transferIcon.setVisible(false);
|
||||||
|
this.discardIcon.setVisible(true);
|
||||||
|
this.textBox.setVisible(true);
|
||||||
|
this.textBox.setText(i18next.t("partyUiHandler:DISCARD"));
|
||||||
|
this.setPosition(
|
||||||
|
globalScene.currentBattle.double ? 64 : 60,
|
||||||
|
globalScene.currentBattle.double ? -48 : -globalScene.game.canvas.height / 15 - 1,
|
||||||
|
);
|
||||||
|
this.discardIcon.displayWidth = this.textBox.text.length * 9 + 3;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.setActive(false).setVisible(false);
|
||||||
|
this.transferIcon.setVisible(false);
|
||||||
|
this.discardIcon.setVisible(false);
|
||||||
|
this.textBox.setVisible(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -54,16 +54,10 @@ import { PokedexInfoOverlay } from "#ui/pokedex-info-overlay";
|
|||||||
import { StatsContainer } from "#ui/stats-container";
|
import { StatsContainer } from "#ui/stats-container";
|
||||||
import { addBBCodeTextObject, addTextObject, getTextColor, getTextStyleOptions } from "#ui/text";
|
import { addBBCodeTextObject, addTextObject, getTextColor, getTextStyleOptions } from "#ui/text";
|
||||||
import { addWindow } from "#ui/ui-theme";
|
import { addWindow } from "#ui/ui-theme";
|
||||||
import {
|
import { BooleanHolder, getLocalizedSpriteKey, isNullOrUndefined, padInt, rgbHexToRgba } from "#utils/common";
|
||||||
BooleanHolder,
|
|
||||||
getLocalizedSpriteKey,
|
|
||||||
isNullOrUndefined,
|
|
||||||
padInt,
|
|
||||||
rgbHexToRgba,
|
|
||||||
toReadableString,
|
|
||||||
} from "#utils/common";
|
|
||||||
import { getEnumValues } from "#utils/enums";
|
import { getEnumValues } from "#utils/enums";
|
||||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
||||||
|
import { toTitleCase } from "#utils/strings";
|
||||||
import { argbFromRgba } from "@material/material-color-utilities";
|
import { argbFromRgba } from "@material/material-color-utilities";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import type BBCodeText from "phaser3-rex-plugins/plugins/gameobjects/tagtext/bbcodetext/BBCodeText";
|
import type BBCodeText from "phaser3-rex-plugins/plugins/gameobjects/tagtext/bbcodetext/BBCodeText";
|
||||||
@ -2620,7 +2614,7 @@ export class PokedexPageUiHandler extends MessageUiHandler {
|
|||||||
|
|
||||||
// Setting growth rate text
|
// Setting growth rate text
|
||||||
if (isFormCaught) {
|
if (isFormCaught) {
|
||||||
let growthReadable = toReadableString(GrowthRate[species.growthRate]);
|
let growthReadable = toTitleCase(GrowthRate[species.growthRate]);
|
||||||
const growthAux = growthReadable.replace(" ", "_");
|
const growthAux = growthReadable.replace(" ", "_");
|
||||||
if (i18next.exists("growth:" + growthAux)) {
|
if (i18next.exists("growth:" + growthAux)) {
|
||||||
growthReadable = i18next.t(("growth:" + growthAux) as any);
|
growthReadable = i18next.t(("growth:" + growthAux) as any);
|
||||||
|
@ -158,8 +158,11 @@ export class PokedexScanUiHandler extends FormModalUiHandler {
|
|||||||
|
|
||||||
if (super.show(args)) {
|
if (super.show(args)) {
|
||||||
const config = args[0] as ModalConfig;
|
const config = args[0] as ModalConfig;
|
||||||
this.inputs[0].resize(1150, 116);
|
const label = this.formLabels[0];
|
||||||
this.inputContainers[0].list[0].width = 200;
|
|
||||||
|
const inputWidth = label.width < 420 ? 200 : 200 - (label.width - 420) / 5.75;
|
||||||
|
this.inputs[0].resize(inputWidth * 5.75, 116);
|
||||||
|
this.inputContainers[0].list[0].width = inputWidth;
|
||||||
if (args[1] && typeof (args[1] as PlayerPokemon).getNameToRender === "function") {
|
if (args[1] && typeof (args[1] as PlayerPokemon).getNameToRender === "function") {
|
||||||
this.inputs[0].text = (args[1] as PlayerPokemon).getNameToRender();
|
this.inputs[0].text = (args[1] as PlayerPokemon).getNameToRender();
|
||||||
} else {
|
} else {
|
||||||
|
@ -8,19 +8,6 @@ import type { ModalConfig } from "#ui/modal-ui-handler";
|
|||||||
import { addTextObject } from "#ui/text";
|
import { addTextObject } from "#ui/text";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
interface LanguageSetting {
|
|
||||||
inputFieldFontSize?: string;
|
|
||||||
warningMessageFontSize?: string;
|
|
||||||
errorMessageFontSize?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const languageSettings: { [key: string]: LanguageSetting } = {
|
|
||||||
"es-ES": {
|
|
||||||
inputFieldFontSize: "50px",
|
|
||||||
errorMessageFontSize: "40px",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export class RegistrationFormUiHandler extends FormModalUiHandler {
|
export class RegistrationFormUiHandler extends FormModalUiHandler {
|
||||||
getModalTitle(_config?: ModalConfig): string {
|
getModalTitle(_config?: ModalConfig): string {
|
||||||
return i18next.t("menu:register");
|
return i18next.t("menu:register");
|
||||||
@ -35,7 +22,7 @@ export class RegistrationFormUiHandler extends FormModalUiHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getButtonTopMargin(): number {
|
getButtonTopMargin(): number {
|
||||||
return 8;
|
return 12;
|
||||||
}
|
}
|
||||||
|
|
||||||
getButtonLabels(_config?: ModalConfig): string[] {
|
getButtonLabels(_config?: ModalConfig): string[] {
|
||||||
@ -76,18 +63,9 @@ export class RegistrationFormUiHandler extends FormModalUiHandler {
|
|||||||
setup(): void {
|
setup(): void {
|
||||||
super.setup();
|
super.setup();
|
||||||
|
|
||||||
this.modalContainer.list.forEach((child: Phaser.GameObjects.GameObject) => {
|
|
||||||
if (child instanceof Phaser.GameObjects.Text && child !== this.titleText) {
|
|
||||||
const inputFieldFontSize = languageSettings[i18next.resolvedLanguage!]?.inputFieldFontSize;
|
|
||||||
if (inputFieldFontSize) {
|
|
||||||
child.setFontSize(inputFieldFontSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const warningMessageFontSize = languageSettings[i18next.resolvedLanguage!]?.warningMessageFontSize ?? "42px";
|
|
||||||
const label = addTextObject(10, 87, i18next.t("menu:registrationAgeWarning"), TextStyle.TOOLTIP_CONTENT, {
|
const label = addTextObject(10, 87, i18next.t("menu:registrationAgeWarning"), TextStyle.TOOLTIP_CONTENT, {
|
||||||
fontSize: warningMessageFontSize,
|
fontSize: "42px",
|
||||||
|
wordWrap: { width: 850 },
|
||||||
});
|
});
|
||||||
|
|
||||||
this.modalContainer.add(label);
|
this.modalContainer.add(label);
|
||||||
@ -107,10 +85,6 @@ export class RegistrationFormUiHandler extends FormModalUiHandler {
|
|||||||
const onFail = error => {
|
const onFail = error => {
|
||||||
globalScene.ui.setMode(UiMode.REGISTRATION_FORM, Object.assign(config, { errorMessage: error?.trim() }));
|
globalScene.ui.setMode(UiMode.REGISTRATION_FORM, Object.assign(config, { errorMessage: error?.trim() }));
|
||||||
globalScene.ui.playError();
|
globalScene.ui.playError();
|
||||||
const errorMessageFontSize = languageSettings[i18next.resolvedLanguage!]?.errorMessageFontSize;
|
|
||||||
if (errorMessageFontSize) {
|
|
||||||
this.errorMessage.setFontSize(errorMessageFontSize);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
if (!this.inputs[0].text) {
|
if (!this.inputs[0].text) {
|
||||||
return onFail(i18next.t("menu:emptyUsername"));
|
return onFail(i18next.t("menu:emptyUsername"));
|
||||||
|
@ -10,6 +10,7 @@ import { ScrollBar } from "#ui/scroll-bar";
|
|||||||
import { addTextObject } from "#ui/text";
|
import { addTextObject } from "#ui/text";
|
||||||
import { UiHandler } from "#ui/ui-handler";
|
import { UiHandler } from "#ui/ui-handler";
|
||||||
import { addWindow } from "#ui/ui-theme";
|
import { addWindow } from "#ui/ui-theme";
|
||||||
|
import { toCamelCase } from "#utils/strings";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
export interface InputsIcons {
|
export interface InputsIcons {
|
||||||
@ -88,12 +89,6 @@ export abstract class AbstractControlSettingsUiHandler extends UiHandler {
|
|||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
private camelize(string: string): string {
|
|
||||||
return string
|
|
||||||
.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => (index === 0 ? word.toLowerCase() : word.toUpperCase()))
|
|
||||||
.replace(/\s+/g, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup UI elements.
|
* Setup UI elements.
|
||||||
*/
|
*/
|
||||||
@ -210,14 +205,15 @@ export abstract class AbstractControlSettingsUiHandler extends UiHandler {
|
|||||||
|
|
||||||
settingFiltered.forEach((setting, s) => {
|
settingFiltered.forEach((setting, s) => {
|
||||||
// Convert the setting key from format 'Key_Name' to 'Key name' for display.
|
// Convert the setting key from format 'Key_Name' to 'Key name' for display.
|
||||||
const settingName = setting.replace(/_/g, " ");
|
// TODO: IDK if this can be followed by both an underscore and a space, so leaving it as a regex matching both for now
|
||||||
|
const i18nKey = toCamelCase(setting.replace(/Alt(_| )/, ""));
|
||||||
|
|
||||||
// Create and add a text object for the setting name to the scene.
|
// Create and add a text object for the setting name to the scene.
|
||||||
const isLock = this.settingBlacklisted.includes(this.setting[setting]);
|
const isLock = this.settingBlacklisted.includes(this.setting[setting]);
|
||||||
const labelStyle = isLock ? TextStyle.SETTINGS_LOCKED : TextStyle.SETTINGS_LABEL;
|
const labelStyle = isLock ? TextStyle.SETTINGS_LOCKED : TextStyle.SETTINGS_LABEL;
|
||||||
|
const isAlt = setting.includes("Alt");
|
||||||
let labelText: string;
|
let labelText: string;
|
||||||
const i18nKey = this.camelize(settingName.replace("Alt ", ""));
|
if (isAlt) {
|
||||||
if (settingName.toLowerCase().includes("alt")) {
|
|
||||||
labelText = `${i18next.t(`settings:${i18nKey}`)}${i18next.t("settings:alt")}`;
|
labelText = `${i18next.t(`settings:${i18nKey}`)}${i18next.t("settings:alt")}`;
|
||||||
} else {
|
} else {
|
||||||
labelText = i18next.t(`settings:${i18nKey}`);
|
labelText = i18next.t(`settings:${i18nKey}`);
|
||||||
|
@ -117,6 +117,12 @@ export class SettingsDisplayUiHandler extends AbstractSettingsUiHandler {
|
|||||||
label: "Română (Needs Help)",
|
label: "Română (Needs Help)",
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
case "tl":
|
||||||
|
this.settings[languageIndex].options[0] = {
|
||||||
|
value: "Tagalog",
|
||||||
|
label: "Tagalog (Needs Help)",
|
||||||
|
};
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
this.settings[languageIndex].options[0] = {
|
this.settings[languageIndex].options[0] = {
|
||||||
value: "English",
|
value: "English",
|
||||||
|
@ -15,7 +15,8 @@ import {
|
|||||||
import { AbstractControlSettingsUiHandler } from "#ui/abstract-control-settings-ui-handler";
|
import { AbstractControlSettingsUiHandler } from "#ui/abstract-control-settings-ui-handler";
|
||||||
import { NavigationManager } from "#ui/navigation-menu";
|
import { NavigationManager } from "#ui/navigation-menu";
|
||||||
import { addTextObject } from "#ui/text";
|
import { addTextObject } from "#ui/text";
|
||||||
import { reverseValueToKeySetting, truncateString } from "#utils/common";
|
import { truncateString } from "#utils/common";
|
||||||
|
import { toPascalSnakeCase } from "#utils/strings";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -101,7 +102,7 @@ export class SettingsKeyboardUiHandler extends AbstractControlSettingsUiHandler
|
|||||||
}
|
}
|
||||||
const cursor = this.cursor + this.scrollCursor; // Calculate the absolute cursor position.
|
const cursor = this.cursor + this.scrollCursor; // Calculate the absolute cursor position.
|
||||||
const selection = this.settingLabels[cursor].text;
|
const selection = this.settingLabels[cursor].text;
|
||||||
const key = reverseValueToKeySetting(selection);
|
const key = toPascalSnakeCase(selection);
|
||||||
const settingName = SettingKeyboard[key];
|
const settingName = SettingKeyboard[key];
|
||||||
const activeConfig = this.getActiveConfig();
|
const activeConfig = this.getActiveConfig();
|
||||||
const success = deleteBind(this.getActiveConfig(), settingName);
|
const success = deleteBind(this.getActiveConfig(), settingName);
|
||||||
|
@ -69,10 +69,10 @@ import {
|
|||||||
padInt,
|
padInt,
|
||||||
randIntRange,
|
randIntRange,
|
||||||
rgbHexToRgba,
|
rgbHexToRgba,
|
||||||
toReadableString,
|
|
||||||
} from "#utils/common";
|
} from "#utils/common";
|
||||||
import type { StarterPreferences } from "#utils/data";
|
import type { StarterPreferences } from "#utils/data";
|
||||||
import { loadStarterPreferences, saveStarterPreferences } from "#utils/data";
|
import { loadStarterPreferences, saveStarterPreferences } from "#utils/data";
|
||||||
|
import { toTitleCase } from "#utils/strings";
|
||||||
import { argbFromRgba } from "@material/material-color-utilities";
|
import { argbFromRgba } from "@material/material-color-utilities";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import type { GameObjects } from "phaser";
|
import type { GameObjects } from "phaser";
|
||||||
@ -176,6 +176,10 @@ const languageSettings: { [key: string]: LanguageSetting } = {
|
|||||||
starterInfoYOffset: 0.5,
|
starterInfoYOffset: 0.5,
|
||||||
starterInfoXPos: 26,
|
starterInfoXPos: 26,
|
||||||
},
|
},
|
||||||
|
tl: {
|
||||||
|
starterInfoTextSize: "56px",
|
||||||
|
instructionTextSize: "38px",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const valueReductionMax = 2;
|
const valueReductionMax = 2;
|
||||||
@ -3527,7 +3531,7 @@ export class StarterSelectUiHandler extends MessageUiHandler {
|
|||||||
this.pokemonLuckLabelText.setVisible(this.pokemonLuckText.visible);
|
this.pokemonLuckLabelText.setVisible(this.pokemonLuckText.visible);
|
||||||
|
|
||||||
//Growth translate
|
//Growth translate
|
||||||
let growthReadable = toReadableString(GrowthRate[species.growthRate]);
|
let growthReadable = toTitleCase(GrowthRate[species.growthRate]);
|
||||||
const growthAux = growthReadable.replace(" ", "_");
|
const growthAux = growthReadable.replace(" ", "_");
|
||||||
if (i18next.exists("growth:" + growthAux)) {
|
if (i18next.exists("growth:" + growthAux)) {
|
||||||
growthReadable = i18next.t(("growth:" + growthAux) as any);
|
growthReadable = i18next.t(("growth:" + growthAux) as any);
|
||||||
@ -4303,7 +4307,10 @@ export class StarterSelectUiHandler extends MessageUiHandler {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
tryExit(): boolean {
|
/**
|
||||||
|
* Attempt to back out of the starter selection screen into the appropriate parent modal
|
||||||
|
*/
|
||||||
|
tryExit(): void {
|
||||||
this.blockInput = true;
|
this.blockInput = true;
|
||||||
const ui = this.getUi();
|
const ui = this.getUi();
|
||||||
|
|
||||||
@ -4317,12 +4324,13 @@ export class StarterSelectUiHandler extends MessageUiHandler {
|
|||||||
UiMode.CONFIRM,
|
UiMode.CONFIRM,
|
||||||
() => {
|
() => {
|
||||||
ui.setMode(UiMode.STARTER_SELECT);
|
ui.setMode(UiMode.STARTER_SELECT);
|
||||||
globalScene.phaseManager.clearPhaseQueue();
|
// Non-challenge modes go directly back to title, while challenge modes go to the selection screen.
|
||||||
if (globalScene.gameMode.isChallenge) {
|
if (!globalScene.gameMode.isChallenge) {
|
||||||
|
globalScene.phaseManager.toTitleScreen();
|
||||||
|
} else {
|
||||||
|
globalScene.phaseManager.clearPhaseQueue();
|
||||||
globalScene.phaseManager.pushNew("SelectChallengePhase");
|
globalScene.phaseManager.pushNew("SelectChallengePhase");
|
||||||
globalScene.phaseManager.pushNew("EncounterPhase");
|
globalScene.phaseManager.pushNew("EncounterPhase");
|
||||||
} else {
|
|
||||||
globalScene.phaseManager.pushNew("TitlePhase");
|
|
||||||
}
|
}
|
||||||
this.clearText();
|
this.clearText();
|
||||||
globalScene.phaseManager.getCurrentPhase()?.end();
|
globalScene.phaseManager.getCurrentPhase()?.end();
|
||||||
@ -4333,8 +4341,6 @@ export class StarterSelectUiHandler extends MessageUiHandler {
|
|||||||
19,
|
19,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tryStart(manualTrigger = false): boolean {
|
tryStart(manualTrigger = false): boolean {
|
||||||
|
@ -35,9 +35,9 @@ import {
|
|||||||
isNullOrUndefined,
|
isNullOrUndefined,
|
||||||
padInt,
|
padInt,
|
||||||
rgbHexToRgba,
|
rgbHexToRgba,
|
||||||
toReadableString,
|
|
||||||
} from "#utils/common";
|
} from "#utils/common";
|
||||||
import { getEnumValues } from "#utils/enums";
|
import { getEnumValues } from "#utils/enums";
|
||||||
|
import { toTitleCase } from "#utils/strings";
|
||||||
import { argbFromRgba } from "@material/material-color-utilities";
|
import { argbFromRgba } from "@material/material-color-utilities";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
@ -962,8 +962,8 @@ export class SummaryUiHandler extends UiHandler {
|
|||||||
this.passiveContainer?.descriptionText?.setVisible(false);
|
this.passiveContainer?.descriptionText?.setVisible(false);
|
||||||
|
|
||||||
const closeFragment = getBBCodeFrag("", TextStyle.WINDOW_ALT);
|
const closeFragment = getBBCodeFrag("", TextStyle.WINDOW_ALT);
|
||||||
const rawNature = toReadableString(Nature[this.pokemon?.getNature()!]); // TODO: is this bang correct?
|
const rawNature = toTitleCase(Nature[this.pokemon?.getNature()!]); // TODO: is this bang correct?
|
||||||
const nature = `${getBBCodeFrag(toReadableString(getNatureName(this.pokemon?.getNature()!)), TextStyle.SUMMARY_RED)}${closeFragment}`; // TODO: is this bang correct?
|
const nature = `${getBBCodeFrag(toTitleCase(getNatureName(this.pokemon?.getNature()!)), TextStyle.SUMMARY_RED)}${closeFragment}`; // TODO: is this bang correct?
|
||||||
|
|
||||||
const memoString = i18next.t("pokemonSummary:memoString", {
|
const memoString = i18next.t("pokemonSummary:memoString", {
|
||||||
metFragment: i18next.t(
|
metFragment: i18next.t(
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { pokerogueApi } from "#api/pokerogue-api";
|
import { pokerogueApi } from "#api/pokerogue-api";
|
||||||
import { MoneyFormat } from "#enums/money-format";
|
import { MoneyFormat } from "#enums/money-format";
|
||||||
import { MoveId } from "#enums/move-id";
|
|
||||||
import type { Variant } from "#sprites/variant";
|
import type { Variant } from "#sprites/variant";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
@ -10,19 +9,6 @@ export const MissingTextureKey = "__MISSING";
|
|||||||
|
|
||||||
// TODO: Draft tests for these utility functions
|
// TODO: Draft tests for these utility functions
|
||||||
// TODO: Break up this file
|
// TODO: Break up this file
|
||||||
/**
|
|
||||||
* Convert a `snake_case` string in any capitalization (such as one from an enum reverse mapping)
|
|
||||||
* into a readable `Title Case` version.
|
|
||||||
* @param str - The snake case string to be converted.
|
|
||||||
* @returns The result of converting `str` into title case.
|
|
||||||
*/
|
|
||||||
export function toReadableString(str: string): string {
|
|
||||||
return str
|
|
||||||
.replace(/_/g, " ")
|
|
||||||
.split(" ")
|
|
||||||
.map(s => capitalizeFirstLetter(s.toLowerCase()))
|
|
||||||
.join(" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function randomString(length: number, seeded = false) {
|
export function randomString(length: number, seeded = false) {
|
||||||
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
@ -278,7 +264,7 @@ export function formatMoney(format: MoneyFormat, amount: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function formatStat(stat: number, forHp = false): string {
|
export function formatStat(stat: number, forHp = false): string {
|
||||||
return formatLargeNumber(stat, forHp ? 100000 : 1000000);
|
return formatLargeNumber(stat, forHp ? 100_000 : 1_000_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function executeIf<T>(condition: boolean, promiseFunc: () => Promise<T>): Promise<T | null> {
|
export function executeIf<T>(condition: boolean, promiseFunc: () => Promise<T>): Promise<T | null> {
|
||||||
@ -359,31 +345,6 @@ export function fixedInt(value: number): number {
|
|||||||
return new FixedInt(value) as unknown as number;
|
return new FixedInt(value) as unknown as number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats a string to title case
|
|
||||||
* @param unformattedText Text to be formatted
|
|
||||||
* @returns the formatted string
|
|
||||||
*/
|
|
||||||
export function formatText(unformattedText: string): string {
|
|
||||||
const text = unformattedText.split("_");
|
|
||||||
for (let i = 0; i < text.length; i++) {
|
|
||||||
text[i] = text[i].charAt(0).toUpperCase() + text[i].substring(1).toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
return text.join(" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toCamelCaseString(unformattedText: string): string {
|
|
||||||
if (!unformattedText) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return unformattedText
|
|
||||||
.split(/[_ ]/)
|
|
||||||
.filter(f => f)
|
|
||||||
.map((f, i) => (i ? `${f[0].toUpperCase()}${f.slice(1).toLowerCase()}` : f.toLowerCase()))
|
|
||||||
.join("");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function rgbToHsv(r: number, g: number, b: number) {
|
export function rgbToHsv(r: number, g: number, b: number) {
|
||||||
const v = Math.max(r, g, b);
|
const v = Math.max(r, g, b);
|
||||||
const c = v - Math.min(r, g, b);
|
const c = v - Math.min(r, g, b);
|
||||||
@ -475,6 +436,7 @@ export function hasAllLocalizedSprites(lang?: string): boolean {
|
|||||||
case "ja":
|
case "ja":
|
||||||
case "ca":
|
case "ca":
|
||||||
case "ru":
|
case "ru":
|
||||||
|
case "tl":
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
@ -510,41 +472,6 @@ export function truncateString(str: string, maxLength = 10) {
|
|||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a space-separated string into a capitalized and underscored string.
|
|
||||||
* @param input - The string to be converted.
|
|
||||||
* @returns The converted string with words capitalized and separated by underscores.
|
|
||||||
*/
|
|
||||||
export function reverseValueToKeySetting(input: string) {
|
|
||||||
// Split the input string into an array of words
|
|
||||||
const words = input.split(" ");
|
|
||||||
// Capitalize the first letter of each word and convert the rest to lowercase
|
|
||||||
const capitalizedWords = words.map((word: string) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase());
|
|
||||||
// Join the capitalized words with underscores and return the result
|
|
||||||
return capitalizedWords.join("_");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Capitalize a string.
|
|
||||||
* @param str - The string to be capitalized.
|
|
||||||
* @param sep - The separator between the words of the string.
|
|
||||||
* @param lowerFirstChar - Whether the first character of the string should be lowercase or not.
|
|
||||||
* @param returnWithSpaces - Whether the returned string should have spaces between the words or not.
|
|
||||||
* @returns The capitalized string.
|
|
||||||
*/
|
|
||||||
export function capitalizeString(str: string, sep: string, lowerFirstChar = true, returnWithSpaces = false) {
|
|
||||||
if (str) {
|
|
||||||
const splitedStr = str.toLowerCase().split(sep);
|
|
||||||
|
|
||||||
for (let i = +lowerFirstChar; i < splitedStr?.length; i++) {
|
|
||||||
splitedStr[i] = splitedStr[i].charAt(0).toUpperCase() + splitedStr[i].substring(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return returnWithSpaces ? splitedStr.join(" ") : splitedStr.join("");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Report whether a given value is nullish (`null`/`undefined`).
|
* Report whether a given value is nullish (`null`/`undefined`).
|
||||||
* @param val - The value whose nullishness is being checked
|
* @param val - The value whose nullishness is being checked
|
||||||
@ -554,15 +481,6 @@ export function isNullOrUndefined(val: any): val is null | undefined {
|
|||||||
return val === null || val === undefined;
|
return val === null || val === undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Capitalize the first letter of a string.
|
|
||||||
* @param str - The string whose first letter is being capitalized
|
|
||||||
* @return The original string with its first letter capitalized
|
|
||||||
*/
|
|
||||||
export function capitalizeFirstLetter(str: string) {
|
|
||||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function is used in the context of a Pokémon battle game to calculate the actual integer damage value from a float result.
|
* This function is used in the context of a Pokémon battle game to calculate the actual integer damage value from a float result.
|
||||||
* Many damage calculation formulas involve various parameters and result in float values.
|
* Many damage calculation formulas involve various parameters and result in float values.
|
||||||
@ -597,26 +515,6 @@ export function isBetween(num: number, min: number, max: number): boolean {
|
|||||||
return min <= num && num <= max;
|
return min <= num && num <= max;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper method to return the animation filename for a given move
|
|
||||||
*
|
|
||||||
* @param move the move for which the animation filename is needed
|
|
||||||
*/
|
|
||||||
export function animationFileName(move: MoveId): string {
|
|
||||||
return MoveId[move].toLowerCase().replace(/_/g, "-");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transforms a camelCase string into a kebab-case string
|
|
||||||
* @param str The camelCase string
|
|
||||||
* @returns A kebab-case string
|
|
||||||
*
|
|
||||||
* @source {@link https://stackoverflow.com/a/67243723/}
|
|
||||||
*/
|
|
||||||
export function camelCaseToKebabCase(str: string): string {
|
|
||||||
return str.replace(/[A-Z]+(?![a-z])|[A-Z]/g, (s, o) => (o ? "-" : "") + s.toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get the localized shiny descriptor for the provided variant
|
/** Get the localized shiny descriptor for the provided variant
|
||||||
* @param variant - The variant to get the shiny descriptor for
|
* @param variant - The variant to get the shiny descriptor for
|
||||||
* @returns The localized shiny descriptor
|
* @returns The localized shiny descriptor
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type { EnumOrObject, EnumValues, NormalEnum, TSNumericEnum } from "#app/@types/enum-types";
|
import type { EnumOrObject, NormalEnum, TSNumericEnum } from "#types/enum-types";
|
||||||
import type { InferKeys } from "#app/@types/type-helpers";
|
import type { InferKeys, ObjectValues } from "#types/type-helpers";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the string keys of an Enum object, excluding reverse-mapped numbers.
|
* Return the string keys of an Enum object, excluding reverse-mapped numbers.
|
||||||
@ -61,7 +61,7 @@ export function getEnumValues<E extends EnumOrObject>(enumType: TSNumericEnum<E>
|
|||||||
* If multiple keys map to the same value, the first one (in insertion order) will be retrieved,
|
* If multiple keys map to the same value, the first one (in insertion order) will be retrieved,
|
||||||
* but the return type will be the union of ALL their corresponding keys.
|
* but the return type will be the union of ALL their corresponding keys.
|
||||||
*/
|
*/
|
||||||
export function enumValueToKey<T extends EnumOrObject, V extends EnumValues<T>>(
|
export function enumValueToKey<T extends EnumOrObject, V extends ObjectValues<T>>(
|
||||||
object: NormalEnum<T>,
|
object: NormalEnum<T>,
|
||||||
val: V,
|
val: V,
|
||||||
): InferKeys<T, V> {
|
): InferKeys<T, V> {
|
||||||
|
181
src/utils/strings.ts
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
// TODO: Standardize file and path casing to remove the need for all these different casing methods
|
||||||
|
|
||||||
|
// #region Split string code
|
||||||
|
|
||||||
|
// Regexps involved with splitting words in various case formats.
|
||||||
|
// Sourced from https://www.npmjs.com/package/change-case (with slight tweaking here and there)
|
||||||
|
|
||||||
|
/** Regex to split at word boundaries.*/
|
||||||
|
const SPLIT_LOWER_UPPER_RE = /([\p{Ll}\d])(\p{Lu})/gu;
|
||||||
|
/** Regex to split around single-letter uppercase words.*/
|
||||||
|
const SPLIT_UPPER_UPPER_RE = /(\p{Lu})([\p{Lu}][\p{Ll}])/gu;
|
||||||
|
/** Regexp involved with stripping non-word delimiters from the result. */
|
||||||
|
const DELIM_STRIP_REGEXP = /[-_ ]+/giu;
|
||||||
|
// The replacement value for splits.
|
||||||
|
const SPLIT_REPLACE_VALUE = "$1\0$2";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split any cased string into an array of its constituent words.
|
||||||
|
* @param string - The string to be split
|
||||||
|
* @returns The new string, delimited at each instance of one or more spaces, underscores, hyphens
|
||||||
|
* or lower-to-upper boundaries.
|
||||||
|
* @remarks
|
||||||
|
* **DO NOT USE THIS FUNCTION!**
|
||||||
|
* Exported only to allow for testing.
|
||||||
|
* @todo Consider tests into [in-source testing](https://vitest.dev/guide/in-source.html) and converting this to unexported
|
||||||
|
*/
|
||||||
|
export function splitWords(value: string): string[] {
|
||||||
|
let result = value.trim();
|
||||||
|
result = result.replace(SPLIT_LOWER_UPPER_RE, SPLIT_REPLACE_VALUE).replace(SPLIT_UPPER_UPPER_RE, SPLIT_REPLACE_VALUE);
|
||||||
|
result = result.replace(DELIM_STRIP_REGEXP, "\0");
|
||||||
|
|
||||||
|
// Trim the delimiter from around the output string
|
||||||
|
return trimFromStartAndEnd(result, "\0").split(/\0/g);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to remove one or more sequences of characters from either end of a string.
|
||||||
|
* @param str - The string to replace
|
||||||
|
* @param charToTrim - The string to remove
|
||||||
|
* @returns The result of removing all instances of {@linkcode charsToTrim} from either end of {@linkcode str}.
|
||||||
|
*/
|
||||||
|
function trimFromStartAndEnd(str: string, charToTrim: string): string {
|
||||||
|
let start = 0;
|
||||||
|
let end = str.length;
|
||||||
|
const blockLength = charToTrim.length;
|
||||||
|
|
||||||
|
while (str.startsWith(charToTrim, start)) {
|
||||||
|
start += blockLength;
|
||||||
|
}
|
||||||
|
if (start - end === blockLength) {
|
||||||
|
// Occurs if the ENTIRE string is made up of charToTrim (at which point we return nothing)
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
while (str.endsWith(charToTrim, end)) {
|
||||||
|
end -= blockLength;
|
||||||
|
}
|
||||||
|
return str.slice(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
// #endregion Split String code
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Capitalize the first letter of a string.
|
||||||
|
* @param str - The string whose first letter is to be capitalized
|
||||||
|
* @return The original string with its first letter capitalized.
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* console.log(capitalizeFirstLetter("consectetur adipiscing elit")); // returns "Consectetur adipiscing elit"
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function capitalizeFirstLetter(str: string) {
|
||||||
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to convert a string into `Title Case` (such as one used for console logs).
|
||||||
|
* @param str - The string being converted
|
||||||
|
* @returns The result of converting `str` into title case.
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* console.log(toTitleCase("lorem ipsum dolor sit amet")); // returns "Lorem Ipsum Dolor Sit Amet"
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function toTitleCase(str: string): string {
|
||||||
|
return splitWords(str)
|
||||||
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to convert a string into `camelCase` (such as one used for i18n keys).
|
||||||
|
* @param str - The string being converted
|
||||||
|
* @returns The result of converting `str` into camel case.
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* console.log(toCamelCase("BIG_ANGRY_TRAINER")); // returns "bigAngryTrainer"
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function toCamelCase(str: string) {
|
||||||
|
return splitWords(str)
|
||||||
|
.map((word, index) =>
|
||||||
|
index === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to convert a string into `PascalCase`.
|
||||||
|
* @param str - The string being converted
|
||||||
|
* @returns The result of converting `str` into pascal case.
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* console.log(toPascalCase("hi how was your day")); // returns "HiHowWasYourDay"
|
||||||
|
* ```
|
||||||
|
* @remarks
|
||||||
|
*/
|
||||||
|
export function toPascalCase(str: string) {
|
||||||
|
return splitWords(str)
|
||||||
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to convert a string into `kebab-case` (such as one used for filenames).
|
||||||
|
* @param str - The string being converted
|
||||||
|
* @returns The result of converting `str` into kebab case.
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* console.log(toKebabCase("not_kebab-caSe String")); // returns "not-kebab-case-string"
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function toKebabCase(str: string): string {
|
||||||
|
return splitWords(str)
|
||||||
|
.map(word => word.toLowerCase())
|
||||||
|
.join("-");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to convert a string into `snake_case` (such as one used for filenames).
|
||||||
|
* @param str - The string being converted
|
||||||
|
* @returns The result of converting `str` into snake case.
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* console.log(toSnakeCase("not-in snake_CaSe")); // returns "not_in_snake_case"
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function toSnakeCase(str: string) {
|
||||||
|
return splitWords(str)
|
||||||
|
.map(word => word.toLowerCase())
|
||||||
|
.join("_");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to convert a string into `UPPER_SNAKE_CASE`.
|
||||||
|
* @param str - The string being converted
|
||||||
|
* @returns The result of converting `str` into upper snake case.
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* console.log(toUpperSnakeCase("apples bananas_oranGes-PearS")); // returns "APPLES_BANANAS_ORANGES_PEARS"
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function toUpperSnakeCase(str: string) {
|
||||||
|
return splitWords(str)
|
||||||
|
.map(word => word.toUpperCase())
|
||||||
|
.join("_");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to convert a string into `Pascal_Snake_Case`.
|
||||||
|
* @param str - The string being converted
|
||||||
|
* @returns The result of converting `str` into pascal snake case.
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* console.log(toPascalSnakeCase("apples-bananas_oranGes Pears")); // returns "Apples_Bananas_Oranges_Pears"
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function toPascalSnakeCase(str: string) {
|
||||||
|
return splitWords(str)
|
||||||
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||||
|
.join("_");
|
||||||
|
}
|
26
test/@types/vitest.d.ts
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import type { Pokemon } from "#field/pokemon";
|
||||||
|
import type { PokemonType } from "#enums/pokemon-type";
|
||||||
|
import type { expect } from "vitest";
|
||||||
|
import type { toHaveTypesOptions } from "#test/test-utils/matchers/to-have-types";
|
||||||
|
|
||||||
|
declare module "vitest" {
|
||||||
|
interface Assertion {
|
||||||
|
/**
|
||||||
|
* Matcher to check if an array contains EXACTLY the given items (in any order).
|
||||||
|
*
|
||||||
|
* Different from {@linkcode expect.arrayContaining} as the latter only requires the array contain
|
||||||
|
* _at least_ the listed items.
|
||||||
|
*
|
||||||
|
* @param expected - The expected contents of the array, in any order.
|
||||||
|
* @see {@linkcode expect.arrayContaining}
|
||||||
|
*/
|
||||||
|
toEqualArrayUnsorted<E>(expected: E[]): void;
|
||||||
|
/**
|
||||||
|
* Matcher to check if a {@linkcode Pokemon}'s current typing includes the given types.
|
||||||
|
*
|
||||||
|
* @param expected - The expected types (in any order).
|
||||||
|
* @param options - The options passed to the matcher.
|
||||||
|
*/
|
||||||
|
toHaveTypes(expected: PokemonType[], options?: toHaveTypesOptions): void;
|
||||||
|
}
|
||||||
|
}
|
72
test/abilities/truant.test.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
|
import { AbilityId } from "#enums/ability-id";
|
||||||
|
import { MoveId } from "#enums/move-id";
|
||||||
|
import { MoveResult } from "#enums/move-result";
|
||||||
|
import { SpeciesId } from "#enums/species-id";
|
||||||
|
import { GameManager } from "#test/test-utils/game-manager";
|
||||||
|
import i18next from "i18next";
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
describe("Ability - Truant", () => {
|
||||||
|
let phaserGame: Phaser.Game;
|
||||||
|
let game: GameManager;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
phaserGame = new Phaser.Game({
|
||||||
|
type: Phaser.HEADLESS,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
game.phaseInterceptor.restoreOg();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
game = new GameManager(phaserGame);
|
||||||
|
game.override
|
||||||
|
.battleStyle("single")
|
||||||
|
.criticalHits(false)
|
||||||
|
.moveset([MoveId.SPLASH, MoveId.TACKLE])
|
||||||
|
.ability(AbilityId.TRUANT)
|
||||||
|
.enemySpecies(SpeciesId.MAGIKARP)
|
||||||
|
.enemyAbility(AbilityId.BALL_FETCH)
|
||||||
|
.enemyMoveset(MoveId.SPLASH);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should loaf around and prevent using moves every other turn", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||||
|
|
||||||
|
const player = game.field.getPlayerPokemon();
|
||||||
|
const enemy = game.field.getEnemyPokemon();
|
||||||
|
|
||||||
|
// Turn 1: Splash succeeds
|
||||||
|
game.move.select(MoveId.SPLASH);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(player.getLastXMoves(1)[0]).toEqual(
|
||||||
|
expect.objectContaining({ move: MoveId.SPLASH, result: MoveResult.SUCCESS }),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Turn 2: Truant activates, cancelling tackle and displaying message
|
||||||
|
game.move.select(MoveId.TACKLE);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(player.getLastXMoves(1)[0]).toEqual(expect.objectContaining({ move: MoveId.NONE, result: MoveResult.FAIL }));
|
||||||
|
expect(enemy.hp).toBe(enemy.getMaxHp());
|
||||||
|
expect(game.textInterceptor.logs).toContain(
|
||||||
|
i18next.t("battlerTags:truantLapse", {
|
||||||
|
pokemonNameWithAffix: getPokemonNameWithAffix(player),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Turn 3: Truant didn't activate, tackle worked
|
||||||
|
game.move.select(MoveId.TACKLE);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(player.getLastXMoves(1)[0]).toEqual(
|
||||||
|
expect.objectContaining({ move: MoveId.TACKLE, result: MoveResult.SUCCESS }),
|
||||||
|
);
|
||||||
|
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
|
||||||
|
});
|
||||||
|
});
|
13
test/matchers.setup.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { toEqualArrayUnsorted } from "#test/test-utils/matchers/to-equal-array-unsorted";
|
||||||
|
import { toHaveTypes } from "#test/test-utils/matchers/to-have-types";
|
||||||
|
import { expect } from "vitest";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Setup file for custom matchers.
|
||||||
|
* Make sure to define the call signatures in `test/@types/vitest.d.ts` too!
|
||||||
|
*/
|
||||||
|
|
||||||
|
expect.extend({
|
||||||
|
toEqualArrayUnsorted,
|
||||||
|
toHaveTypes,
|
||||||
|
});
|