Merge branch 'beta' into stuff-cheeks-implementation

This commit is contained in:
geeilhan 2024-12-01 07:20:27 +01:00 committed by GitHub
commit 07373caf0b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
90 changed files with 3859 additions and 6779 deletions

87
package-lock.json generated
View File

@ -16,6 +16,7 @@
"i18next-http-backend": "^2.6.1",
"i18next-korean-postposition-processor": "^1.0.0",
"json-stable-stringify": "^1.1.0",
"jszip": "^3.10.1",
"phaser": "^3.70.0",
"phaser3-rex-plugins": "^1.1.84"
},
@ -2723,6 +2724,11 @@
"node": ">= 0.6"
}
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"node_modules/cross-fetch": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz",
@ -4045,6 +4051,11 @@
"node": ">= 4"
}
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
},
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@ -4072,6 +4083,11 @@
"node": ">=0.8.19"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/ini": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz",
@ -4481,6 +4497,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -4648,6 +4675,14 @@
"node": ">= 0.8.0"
}
},
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
@ -5237,6 +5272,11 @@
"dev": true,
"license": "BlueOak-1.0.0"
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
},
"node_modules/papaparse": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz",
@ -5485,6 +5525,11 @@
"node": ">= 0.8.0"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"node_modules/prompts": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
@ -5551,6 +5596,25 @@
}
]
},
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/readable-stream/node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
},
"node_modules/rechoir": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz",
@ -5741,6 +5805,11 @@
"tslib": "^2.1.0"
}
},
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/safe-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz",
@ -5800,6 +5869,11 @@
"node": ">= 0.4"
}
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -5917,6 +5991,14 @@
"dev": true,
"license": "MIT"
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@ -6473,6 +6555,11 @@
"requires-port": "^1.0.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/vite": {
"version": "5.4.8",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz",

View File

@ -55,6 +55,7 @@
"i18next-http-backend": "^2.6.1",
"i18next-korean-postposition-processor": "^1.0.0",
"json-stable-stringify": "^1.1.0",
"jszip": "^3.10.1",
"phaser": "^3.70.0",
"phaser3-rex-plugins": "^1.1.84"
},

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -1,776 +1,299 @@
{
"textures": [
{
"image": "47.png",
"format": "RGBA8888",
"size": {
"w": 230,
"h": 230
},
"scale": 1,
"frames": [
{
"filename": "0009.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 6,
"y": 12,
"w": 56,
"h": 49
},
"frame": {
"x": 0,
"y": 0,
"w": 56,
"h": 49
}
},
{
"filename": "0010.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 6,
"y": 12,
"w": 56,
"h": 49
},
"frame": {
"x": 0,
"y": 0,
"w": 56,
"h": 49
}
},
{
"filename": "0027.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 0,
"y": 12,
"w": 62,
"h": 51
},
"frame": {
"x": 56,
"y": 0,
"w": 62,
"h": 51
}
},
{
"filename": "0028.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 0,
"y": 12,
"w": 62,
"h": 51
},
"frame": {
"x": 56,
"y": 0,
"w": 62,
"h": 51
}
},
{
"filename": "0007.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 7,
"y": 8,
"w": 55,
"h": 53
},
"frame": {
"x": 118,
"y": 0,
"w": 55,
"h": 53
}
},
{
"filename": "0008.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 7,
"y": 8,
"w": 55,
"h": 53
},
"frame": {
"x": 118,
"y": 0,
"w": 55,
"h": 53
}
},
{
"filename": "0011.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 7,
"y": 7,
"w": 55,
"h": 54
},
"frame": {
"x": 173,
"y": 0,
"w": 55,
"h": 54
}
},
{
"filename": "0012.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 7,
"y": 7,
"w": 55,
"h": 54
},
"frame": {
"x": 173,
"y": 0,
"w": 55,
"h": 54
}
},
{
"filename": "0005.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 8,
"y": 5,
"w": 53,
"h": 56
},
"frame": {
"x": 0,
"y": 49,
"w": 53,
"h": 56
}
},
{
"filename": "0006.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 8,
"y": 5,
"w": 53,
"h": 56
},
"frame": {
"x": 0,
"y": 49,
"w": 53,
"h": 56
}
},
{
"filename": "0025.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 0,
"y": 8,
"w": 62,
"h": 55
},
"frame": {
"x": 53,
"y": 51,
"w": 62,
"h": 55
}
},
{
"filename": "0026.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 0,
"y": 8,
"w": 62,
"h": 55
},
"frame": {
"x": 53,
"y": 51,
"w": 62,
"h": 55
}
},
{
"filename": "0013.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 8,
"y": 4,
"w": 53,
"h": 57
},
"frame": {
"x": 115,
"y": 53,
"w": 53,
"h": 57
}
},
{
"filename": "0014.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 8,
"y": 4,
"w": 53,
"h": 57
},
"frame": {
"x": 115,
"y": 53,
"w": 53,
"h": 57
}
},
{
"filename": "0029.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 0,
"y": 7,
"w": 62,
"h": 56
},
"frame": {
"x": 168,
"y": 54,
"w": 62,
"h": 56
}
},
{
"filename": "0030.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 0,
"y": 7,
"w": 62,
"h": 56
},
"frame": {
"x": 168,
"y": 54,
"w": 62,
"h": 56
}
},
{
"filename": "0023.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 0,
"y": 5,
"w": 61,
"h": 58
},
"frame": {
"x": 0,
"y": 106,
"w": 61,
"h": 58
}
},
{
"filename": "0024.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 0,
"y": 5,
"w": 61,
"h": 58
},
"frame": {
"x": 0,
"y": 106,
"w": 61,
"h": 58
}
},
{
"filename": "0003.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 6,
"y": 2,
"w": 54,
"h": 59
},
"frame": {
"x": 61,
"y": 106,
"w": 54,
"h": 59
}
},
{
"filename": "0004.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 6,
"y": 2,
"w": 54,
"h": 59
},
"frame": {
"x": 61,
"y": 106,
"w": 54,
"h": 59
}
},
{ "frames": [
{
"filename": "0001.png",
"frame": { "x": 0, "y": 58, "w": 55, "h": 57 },
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 2,
"y": 2,
"w": 57,
"h": 59
},
"frame": {
"x": 115,
"y": 110,
"w": 57,
"h": 59
}
"spriteSourceSize": { "x": 6, "y": 5, "w": 55, "h": 57 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0002.png",
"frame": { "x": 0, "y": 58, "w": 55, "h": 57 },
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 2,
"y": 2,
"w": 57,
"h": 59
},
"frame": {
"x": 115,
"y": 110,
"w": 57,
"h": 59
}
"spriteSourceSize": { "x": 6, "y": 5, "w": 55, "h": 57 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0019.png",
"filename": "0003.png",
"frame": { "x": 166, "y": 114, "w": 52, "h": 56 },
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 2,
"y": 2,
"w": 57,
"h": 59
},
"frame": {
"x": 115,
"y": 110,
"w": 57,
"h": 59
}
"spriteSourceSize": { "x": 9, "y": 5, "w": 52, "h": 56 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0020.png",
"filename": "0004.png",
"frame": { "x": 166, "y": 114, "w": 52, "h": 56 },
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
"spriteSourceSize": { "x": 9, "y": 5, "w": 52, "h": 56 },
"sourceSize": { "w": 65, "h": 65 }
},
"spriteSourceSize": {
"x": 2,
"y": 2,
"w": 57,
"h": 59
{
"filename": "0005.png",
"frame": { "x": 0, "y": 169, "w": 51, "h": 54 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 11, "y": 7, "w": 51, "h": 54 },
"sourceSize": { "w": 65, "h": 65 }
},
"frame": {
"x": 115,
"y": 110,
"w": 57,
"h": 59
}
{
"filename": "0006.png",
"frame": { "x": 0, "y": 169, "w": 51, "h": 54 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 11, "y": 7, "w": 51, "h": 54 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0007.png",
"frame": { "x": 104, "y": 166, "w": 53, "h": 52 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 10, "y": 9, "w": 53, "h": 52 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0008.png",
"frame": { "x": 104, "y": 166, "w": 53, "h": 52 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 10, "y": 9, "w": 53, "h": 52 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0009.png",
"frame": { "x": 157, "y": 170, "w": 55, "h": 49 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 9, "y": 12, "w": 55, "h": 49 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0010.png",
"frame": { "x": 157, "y": 170, "w": 55, "h": 49 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 9, "y": 12, "w": 55, "h": 49 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0011.png",
"frame": { "x": 0, "y": 115, "w": 53, "h": 54 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 10, "y": 8, "w": 53, "h": 54 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0012.png",
"frame": { "x": 0, "y": 115, "w": 53, "h": 54 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 10, "y": 8, "w": 53, "h": 54 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0013.png",
"frame": { "x": 53, "y": 116, "w": 51, "h": 56 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 11, "y": 6, "w": 51, "h": 56 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0014.png",
"frame": { "x": 53, "y": 116, "w": 51, "h": 56 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 11, "y": 6, "w": 51, "h": 56 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0015.png",
"frame": { "x": 114, "y": 109, "w": 52, "h": 57 },
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 6,
"y": 2,
"w": 54,
"h": 59
},
"frame": {
"x": 172,
"y": 110,
"w": 54,
"h": 59
}
"spriteSourceSize": { "x": 9, "y": 5, "w": 52, "h": 57 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0016.png",
"frame": { "x": 114, "y": 109, "w": 52, "h": 57 },
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 6,
"y": 2,
"w": 54,
"h": 59
},
"frame": {
"x": 172,
"y": 110,
"w": 54,
"h": 59
}
},
{
"filename": "0031.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 0,
"y": 4,
"w": 61,
"h": 59
},
"frame": {
"x": 0,
"y": 164,
"w": 61,
"h": 59
}
},
{
"filename": "0032.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 0,
"y": 4,
"w": 61,
"h": 59
},
"frame": {
"x": 0,
"y": 164,
"w": 61,
"h": 59
}
"spriteSourceSize": { "x": 9, "y": 5, "w": 52, "h": 57 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0017.png",
"frame": { "x": 59, "y": 57, "w": 55, "h": 59 },
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 2,
"y": 0,
"w": 57,
"h": 61
},
"frame": {
"x": 61,
"y": 169,
"w": 57,
"h": 61
}
"spriteSourceSize": { "x": 6, "y": 3, "w": 55, "h": 59 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0018.png",
"frame": { "x": 59, "y": 57, "w": 55, "h": 59 },
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 2,
"y": 0,
"w": 57,
"h": 61
},
"frame": {
"x": 61,
"y": 169,
"w": 57,
"h": 61
}
"spriteSourceSize": { "x": 6, "y": 3, "w": 55, "h": 59 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0035.png",
"filename": "0019.png",
"frame": { "x": 0, "y": 58, "w": 55, "h": 57 },
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 2,
"y": 0,
"w": 57,
"h": 61
},
"frame": {
"x": 61,
"y": 169,
"w": 57,
"h": 61
}
"spriteSourceSize": { "x": 6, "y": 5, "w": 55, "h": 57 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0036.png",
"filename": "0020.png",
"frame": { "x": 0, "y": 58, "w": 55, "h": 57 },
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 2,
"y": 0,
"w": 57,
"h": 61
},
"frame": {
"x": 61,
"y": 169,
"w": 57,
"h": 61
}
"spriteSourceSize": { "x": 6, "y": 5, "w": 55, "h": 57 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0021.png",
"frame": { "x": 178, "y": 56, "w": 57, "h": 58 },
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 0,
"y": 2,
"w": 60,
"h": 61
},
"frame": {
"x": 118,
"y": 169,
"w": 60,
"h": 61
}
"spriteSourceSize": { "x": 4, "y": 5, "w": 57, "h": 58 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0022.png",
"frame": { "x": 178, "y": 56, "w": 57, "h": 58 },
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
"spriteSourceSize": { "x": 4, "y": 5, "w": 57, "h": 58 },
"sourceSize": { "w": 65, "h": 65 }
},
"spriteSourceSize": {
"x": 0,
"y": 2,
"w": 60,
"h": 61
{
"filename": "0023.png",
"frame": { "x": 119, "y": 0, "w": 59, "h": 57 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 3, "y": 7, "w": 59, "h": 57 },
"sourceSize": { "w": 65, "h": 65 }
},
"frame": {
"x": 118,
"y": 169,
"w": 60,
"h": 61
}
{
"filename": "0024.png",
"frame": { "x": 119, "y": 0, "w": 59, "h": 57 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 3, "y": 7, "w": 59, "h": 57 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0025.png",
"frame": { "x": 178, "y": 0, "w": 60, "h": 56 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 3, "y": 9, "w": 60, "h": 56 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0026.png",
"frame": { "x": 178, "y": 0, "w": 60, "h": 56 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 3, "y": 9, "w": 60, "h": 56 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0027.png",
"frame": { "x": 114, "y": 57, "w": 62, "h": 52 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 2, "y": 12, "w": 62, "h": 52 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0028.png",
"frame": { "x": 114, "y": 57, "w": 62, "h": 52 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 2, "y": 12, "w": 62, "h": 52 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0029.png",
"frame": { "x": 59, "y": 0, "w": 60, "h": 57 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 3, "y": 8, "w": 60, "h": 57 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0030.png",
"frame": { "x": 59, "y": 0, "w": 60, "h": 57 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 3, "y": 8, "w": 60, "h": 57 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0031.png",
"frame": { "x": 0, "y": 0, "w": 59, "h": 58 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 3, "y": 6, "w": 59, "h": 58 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0032.png",
"frame": { "x": 0, "y": 0, "w": 59, "h": 58 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 3, "y": 6, "w": 59, "h": 58 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0033.png",
"frame": { "x": 178, "y": 56, "w": 57, "h": 58 },
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 0,
"y": 2,
"w": 60,
"h": 61
},
"frame": {
"x": 118,
"y": 169,
"w": 60,
"h": 61
}
"spriteSourceSize": { "x": 4, "y": 5, "w": 57, "h": 58 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0034.png",
"frame": { "x": 178, "y": 56, "w": 57, "h": 58 },
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
"spriteSourceSize": { "x": 4, "y": 5, "w": 57, "h": 58 },
"sourceSize": { "w": 65, "h": 65 }
},
"spriteSourceSize": {
"x": 0,
"y": 2,
"w": 60,
"h": 61
{
"filename": "0035.png",
"frame": { "x": 59, "y": 57, "w": 55, "h": 59 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 6, "y": 3, "w": 55, "h": 59 },
"sourceSize": { "w": 65, "h": 65 }
},
"frame": {
"x": 118,
"y": 169,
"w": 60,
"h": 61
}
}
]
{
"filename": "0036.png",
"frame": { "x": 59, "y": 57, "w": 55, "h": 59 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 6, "y": 3, "w": 55, "h": 59 },
"sourceSize": { "w": 65, "h": 65 }
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:b28fe643197bcc1def0e0ac2ba9f4e67:516d08c8e1ff13b49a109b082ef12860:fe45e2d628a6cef0908f7b82468c8798$"
"app": "https://www.aseprite.org/",
"version": "1.3.8.1-x64",
"image": "47.png",
"format": "I8",
"size": { "w": 238, "h": 223 },
"scale": "1"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -1,776 +1,299 @@
{
"textures": [
{
"image": "47.png",
"format": "RGBA8888",
"size": {
"w": 230,
"h": 230
},
"scale": 1,
"frames": [
{
"filename": "0009.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 6,
"y": 12,
"w": 56,
"h": 49
},
"frame": {
"x": 0,
"y": 0,
"w": 56,
"h": 49
}
},
{
"filename": "0010.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 6,
"y": 12,
"w": 56,
"h": 49
},
"frame": {
"x": 0,
"y": 0,
"w": 56,
"h": 49
}
},
{
"filename": "0027.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 0,
"y": 12,
"w": 62,
"h": 51
},
"frame": {
"x": 56,
"y": 0,
"w": 62,
"h": 51
}
},
{
"filename": "0028.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 0,
"y": 12,
"w": 62,
"h": 51
},
"frame": {
"x": 56,
"y": 0,
"w": 62,
"h": 51
}
},
{
"filename": "0007.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 7,
"y": 8,
"w": 55,
"h": 53
},
"frame": {
"x": 118,
"y": 0,
"w": 55,
"h": 53
}
},
{
"filename": "0008.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 7,
"y": 8,
"w": 55,
"h": 53
},
"frame": {
"x": 118,
"y": 0,
"w": 55,
"h": 53
}
},
{
"filename": "0011.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 7,
"y": 7,
"w": 55,
"h": 54
},
"frame": {
"x": 173,
"y": 0,
"w": 55,
"h": 54
}
},
{
"filename": "0012.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 7,
"y": 7,
"w": 55,
"h": 54
},
"frame": {
"x": 173,
"y": 0,
"w": 55,
"h": 54
}
},
{
"filename": "0005.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 8,
"y": 5,
"w": 53,
"h": 56
},
"frame": {
"x": 0,
"y": 49,
"w": 53,
"h": 56
}
},
{
"filename": "0006.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 8,
"y": 5,
"w": 53,
"h": 56
},
"frame": {
"x": 0,
"y": 49,
"w": 53,
"h": 56
}
},
{
"filename": "0025.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 0,
"y": 8,
"w": 62,
"h": 55
},
"frame": {
"x": 53,
"y": 51,
"w": 62,
"h": 55
}
},
{
"filename": "0026.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 0,
"y": 8,
"w": 62,
"h": 55
},
"frame": {
"x": 53,
"y": 51,
"w": 62,
"h": 55
}
},
{
"filename": "0013.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 8,
"y": 4,
"w": 53,
"h": 57
},
"frame": {
"x": 115,
"y": 53,
"w": 53,
"h": 57
}
},
{
"filename": "0014.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 8,
"y": 4,
"w": 53,
"h": 57
},
"frame": {
"x": 115,
"y": 53,
"w": 53,
"h": 57
}
},
{
"filename": "0029.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 0,
"y": 7,
"w": 62,
"h": 56
},
"frame": {
"x": 168,
"y": 54,
"w": 62,
"h": 56
}
},
{
"filename": "0030.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 0,
"y": 7,
"w": 62,
"h": 56
},
"frame": {
"x": 168,
"y": 54,
"w": 62,
"h": 56
}
},
{
"filename": "0023.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 0,
"y": 5,
"w": 61,
"h": 58
},
"frame": {
"x": 0,
"y": 106,
"w": 61,
"h": 58
}
},
{
"filename": "0024.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 0,
"y": 5,
"w": 61,
"h": 58
},
"frame": {
"x": 0,
"y": 106,
"w": 61,
"h": 58
}
},
{
"filename": "0003.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 6,
"y": 2,
"w": 54,
"h": 59
},
"frame": {
"x": 61,
"y": 106,
"w": 54,
"h": 59
}
},
{
"filename": "0004.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 6,
"y": 2,
"w": 54,
"h": 59
},
"frame": {
"x": 61,
"y": 106,
"w": 54,
"h": 59
}
},
{ "frames": [
{
"filename": "0001.png",
"frame": { "x": 0, "y": 58, "w": 55, "h": 57 },
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 2,
"y": 2,
"w": 57,
"h": 59
},
"frame": {
"x": 115,
"y": 110,
"w": 57,
"h": 59
}
"spriteSourceSize": { "x": 6, "y": 5, "w": 55, "h": 57 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0002.png",
"frame": { "x": 0, "y": 58, "w": 55, "h": 57 },
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 2,
"y": 2,
"w": 57,
"h": 59
},
"frame": {
"x": 115,
"y": 110,
"w": 57,
"h": 59
}
"spriteSourceSize": { "x": 6, "y": 5, "w": 55, "h": 57 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0019.png",
"filename": "0003.png",
"frame": { "x": 166, "y": 114, "w": 52, "h": 56 },
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 2,
"y": 2,
"w": 57,
"h": 59
},
"frame": {
"x": 115,
"y": 110,
"w": 57,
"h": 59
}
"spriteSourceSize": { "x": 9, "y": 5, "w": 52, "h": 56 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0020.png",
"filename": "0004.png",
"frame": { "x": 166, "y": 114, "w": 52, "h": 56 },
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
"spriteSourceSize": { "x": 9, "y": 5, "w": 52, "h": 56 },
"sourceSize": { "w": 65, "h": 65 }
},
"spriteSourceSize": {
"x": 2,
"y": 2,
"w": 57,
"h": 59
{
"filename": "0005.png",
"frame": { "x": 0, "y": 169, "w": 51, "h": 54 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 11, "y": 7, "w": 51, "h": 54 },
"sourceSize": { "w": 65, "h": 65 }
},
"frame": {
"x": 115,
"y": 110,
"w": 57,
"h": 59
}
{
"filename": "0006.png",
"frame": { "x": 0, "y": 169, "w": 51, "h": 54 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 11, "y": 7, "w": 51, "h": 54 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0007.png",
"frame": { "x": 104, "y": 166, "w": 53, "h": 52 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 10, "y": 9, "w": 53, "h": 52 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0008.png",
"frame": { "x": 104, "y": 166, "w": 53, "h": 52 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 10, "y": 9, "w": 53, "h": 52 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0009.png",
"frame": { "x": 157, "y": 170, "w": 55, "h": 49 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 9, "y": 12, "w": 55, "h": 49 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0010.png",
"frame": { "x": 157, "y": 170, "w": 55, "h": 49 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 9, "y": 12, "w": 55, "h": 49 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0011.png",
"frame": { "x": 0, "y": 115, "w": 53, "h": 54 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 10, "y": 8, "w": 53, "h": 54 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0012.png",
"frame": { "x": 0, "y": 115, "w": 53, "h": 54 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 10, "y": 8, "w": 53, "h": 54 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0013.png",
"frame": { "x": 53, "y": 116, "w": 51, "h": 56 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 11, "y": 6, "w": 51, "h": 56 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0014.png",
"frame": { "x": 53, "y": 116, "w": 51, "h": 56 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 11, "y": 6, "w": 51, "h": 56 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0015.png",
"frame": { "x": 114, "y": 109, "w": 52, "h": 57 },
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 6,
"y": 2,
"w": 54,
"h": 59
},
"frame": {
"x": 172,
"y": 110,
"w": 54,
"h": 59
}
"spriteSourceSize": { "x": 9, "y": 5, "w": 52, "h": 57 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0016.png",
"frame": { "x": 114, "y": 109, "w": 52, "h": 57 },
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 6,
"y": 2,
"w": 54,
"h": 59
},
"frame": {
"x": 172,
"y": 110,
"w": 54,
"h": 59
}
},
{
"filename": "0031.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 0,
"y": 4,
"w": 61,
"h": 59
},
"frame": {
"x": 0,
"y": 164,
"w": 61,
"h": 59
}
},
{
"filename": "0032.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 0,
"y": 4,
"w": 61,
"h": 59
},
"frame": {
"x": 0,
"y": 164,
"w": 61,
"h": 59
}
"spriteSourceSize": { "x": 9, "y": 5, "w": 52, "h": 57 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0017.png",
"frame": { "x": 59, "y": 57, "w": 55, "h": 59 },
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 2,
"y": 0,
"w": 57,
"h": 61
},
"frame": {
"x": 61,
"y": 169,
"w": 57,
"h": 61
}
"spriteSourceSize": { "x": 6, "y": 3, "w": 55, "h": 59 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0018.png",
"frame": { "x": 59, "y": 57, "w": 55, "h": 59 },
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 2,
"y": 0,
"w": 57,
"h": 61
},
"frame": {
"x": 61,
"y": 169,
"w": 57,
"h": 61
}
"spriteSourceSize": { "x": 6, "y": 3, "w": 55, "h": 59 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0035.png",
"filename": "0019.png",
"frame": { "x": 0, "y": 58, "w": 55, "h": 57 },
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 2,
"y": 0,
"w": 57,
"h": 61
},
"frame": {
"x": 61,
"y": 169,
"w": 57,
"h": 61
}
"spriteSourceSize": { "x": 6, "y": 5, "w": 55, "h": 57 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0036.png",
"filename": "0020.png",
"frame": { "x": 0, "y": 58, "w": 55, "h": 57 },
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 2,
"y": 0,
"w": 57,
"h": 61
},
"frame": {
"x": 61,
"y": 169,
"w": 57,
"h": 61
}
"spriteSourceSize": { "x": 6, "y": 5, "w": 55, "h": 57 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0021.png",
"frame": { "x": 178, "y": 56, "w": 57, "h": 58 },
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 0,
"y": 2,
"w": 60,
"h": 61
},
"frame": {
"x": 118,
"y": 169,
"w": 60,
"h": 61
}
"spriteSourceSize": { "x": 4, "y": 5, "w": 57, "h": 58 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0022.png",
"frame": { "x": 178, "y": 56, "w": 57, "h": 58 },
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
"spriteSourceSize": { "x": 4, "y": 5, "w": 57, "h": 58 },
"sourceSize": { "w": 65, "h": 65 }
},
"spriteSourceSize": {
"x": 0,
"y": 2,
"w": 60,
"h": 61
{
"filename": "0023.png",
"frame": { "x": 119, "y": 0, "w": 59, "h": 57 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 3, "y": 7, "w": 59, "h": 57 },
"sourceSize": { "w": 65, "h": 65 }
},
"frame": {
"x": 118,
"y": 169,
"w": 60,
"h": 61
}
{
"filename": "0024.png",
"frame": { "x": 119, "y": 0, "w": 59, "h": 57 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 3, "y": 7, "w": 59, "h": 57 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0025.png",
"frame": { "x": 178, "y": 0, "w": 60, "h": 56 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 3, "y": 9, "w": 60, "h": 56 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0026.png",
"frame": { "x": 178, "y": 0, "w": 60, "h": 56 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 3, "y": 9, "w": 60, "h": 56 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0027.png",
"frame": { "x": 114, "y": 57, "w": 62, "h": 52 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 2, "y": 12, "w": 62, "h": 52 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0028.png",
"frame": { "x": 114, "y": 57, "w": 62, "h": 52 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 2, "y": 12, "w": 62, "h": 52 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0029.png",
"frame": { "x": 59, "y": 0, "w": 60, "h": 57 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 3, "y": 8, "w": 60, "h": 57 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0030.png",
"frame": { "x": 59, "y": 0, "w": 60, "h": 57 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 3, "y": 8, "w": 60, "h": 57 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0031.png",
"frame": { "x": 0, "y": 0, "w": 59, "h": 58 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 3, "y": 6, "w": 59, "h": 58 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0032.png",
"frame": { "x": 0, "y": 0, "w": 59, "h": 58 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 3, "y": 6, "w": 59, "h": 58 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0033.png",
"frame": { "x": 178, "y": 56, "w": 57, "h": 58 },
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
},
"spriteSourceSize": {
"x": 0,
"y": 2,
"w": 60,
"h": 61
},
"frame": {
"x": 118,
"y": 169,
"w": 60,
"h": 61
}
"spriteSourceSize": { "x": 4, "y": 5, "w": 57, "h": 58 },
"sourceSize": { "w": 65, "h": 65 }
},
{
"filename": "0034.png",
"frame": { "x": 178, "y": 56, "w": 57, "h": 58 },
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 62,
"h": 63
"spriteSourceSize": { "x": 4, "y": 5, "w": 57, "h": 58 },
"sourceSize": { "w": 65, "h": 65 }
},
"spriteSourceSize": {
"x": 0,
"y": 2,
"w": 60,
"h": 61
{
"filename": "0035.png",
"frame": { "x": 59, "y": 57, "w": 55, "h": 59 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 6, "y": 3, "w": 55, "h": 59 },
"sourceSize": { "w": 65, "h": 65 }
},
"frame": {
"x": 118,
"y": 169,
"w": 60,
"h": 61
}
}
]
{
"filename": "0036.png",
"frame": { "x": 59, "y": 57, "w": 55, "h": 59 },
"rotated": false,
"trimmed": true,
"spriteSourceSize": { "x": 6, "y": 3, "w": 55, "h": 59 },
"sourceSize": { "w": 65, "h": 65 }
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:38ba9918eb8f68ab2190b03c6512ef47:46578d6dd482a1b04fa7c2884107a0f5:fe45e2d628a6cef0908f7b82468c8798$"
"app": "https://www.aseprite.org/",
"version": "1.3.8.1-x64",
"image": "47.png",
"format": "I8",
"size": { "w": 238, "h": 223 },
"scale": "1"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@ -4,7 +4,7 @@ import Pokemon, { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon";
import PokemonSpecies, { allSpecies, getPokemonSpecies, PokemonSpeciesFilter } from "#app/data/pokemon-species";
import { Constructor, isNullOrUndefined, randSeedInt } from "#app/utils";
import * as Utils from "#app/utils";
import { ConsumableModifier, ConsumablePokemonModifier, DoubleBattleChanceBoosterModifier, ExpBalanceModifier, ExpShareModifier, FusePokemonModifier, HealingBoosterModifier, Modifier, ModifierBar, ModifierPredicate, MultipleParticipantExpBonusModifier, overrideHeldItems, overrideModifiers, PersistentModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, RememberMoveModifier, TerastallizeModifier, TurnHeldItemTransferModifier } from "./modifier/modifier";
import { ConsumableModifier, ConsumablePokemonModifier, DoubleBattleChanceBoosterModifier, ExpBalanceModifier, ExpShareModifier, FusePokemonModifier, HealingBoosterModifier, Modifier, ModifierBar, ModifierPredicate, MultipleParticipantExpBonusModifier, PersistentModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, RememberMoveModifier, TerastallizeModifier, TurnHeldItemTransferModifier } from "./modifier/modifier";
import { PokeballType } from "#enums/pokeball";
import { initCommonAnims, initMoveAnim, loadCommonAnimAssets, loadMoveAnimAssets, populateAnims } from "#app/data/battle-anims";
import { Phase } from "#app/phase";
@ -47,7 +47,7 @@ import PokemonInfoContainer from "#app/ui/pokemon-info-container";
import { biomeDepths, getBiomeName } from "#app/data/balance/biomes";
import { SceneBase } from "#app/scene-base";
import CandyBar from "#app/ui/candy-bar";
import { Variant, variantData } from "#app/data/variant";
import { Variant, variantColorCache, variantData, VariantSet } from "#app/data/variant";
import { Localizable } from "#app/interfaces/locales";
import Overrides from "#app/overrides";
import { InputsController } from "#app/inputs-controller";
@ -345,6 +345,33 @@ export default class BattleScene extends SceneBase {
this.load.atlas(key, `images/pokemon/${variant ? "variant/" : ""}${experimental ? "exp/" : ""}${atlasPath}.png`, `images/pokemon/${variant ? "variant/" : ""}${experimental ? "exp/" : ""}${atlasPath}.json`);
}
/**
* Load the variant assets for the given sprite and stores them in {@linkcode variantColorCache}
*/
loadPokemonVariantAssets(spriteKey: string, fileRoot: string, variant?: Variant) {
const useExpSprite = this.experimentalSprites && this.hasExpSprite(spriteKey);
if (useExpSprite) {
fileRoot = `exp/${fileRoot}`;
}
let variantConfig = variantData;
fileRoot.split("/").map(p => variantConfig ? variantConfig = variantConfig[p] : null);
const variantSet = variantConfig as VariantSet;
if (variantSet && (variant !== undefined && variantSet[variant] === 1)) {
const populateVariantColors = (key: string): Promise<void> => {
return new Promise(resolve => {
if (variantColorCache.hasOwnProperty(key)) {
return resolve();
}
this.cachedFetch(`./images/pokemon/variant/${fileRoot}.json`).then(res => res.json()).then(c => {
variantColorCache[key] = c;
resolve();
});
});
};
populateVariantColors(spriteKey);
}
}
async preload() {
if (DEBUG_RNG) {
const scene = this;
@ -891,7 +918,7 @@ export default class BattleScene extends SceneBase {
return pokemon;
}
addEnemyPokemon(species: PokemonSpecies, level: integer, trainerSlot: TrainerSlot, boss: boolean = false, dataSource?: PokemonData, postProcess?: (enemyPokemon: EnemyPokemon) => void): EnemyPokemon {
addEnemyPokemon(species: PokemonSpecies, level: integer, trainerSlot: TrainerSlot, boss: boolean = false, shinyLock: boolean = false, dataSource?: PokemonData, postProcess?: (enemyPokemon: EnemyPokemon) => void): EnemyPokemon {
if (Overrides.OPP_LEVEL_OVERRIDE > 0) {
level = Overrides.OPP_LEVEL_OVERRIDE;
}
@ -901,13 +928,11 @@ export default class BattleScene extends SceneBase {
boss = this.getEncounterBossSegments(this.currentBattle.waveIndex, level, species) > 1;
}
const pokemon = new EnemyPokemon(this, species, level, trainerSlot, boss, dataSource);
const pokemon = new EnemyPokemon(this, species, level, trainerSlot, boss, shinyLock, dataSource);
if (Overrides.OPP_FUSION_OVERRIDE) {
pokemon.generateFusionSpecies();
}
overrideModifiers(this, false);
overrideHeldItems(this, pokemon, false);
if (boss && !dataSource) {
const secondaryIvs = Utils.getIvsFromId(Utils.randSeedInt(4294967296));
@ -3008,7 +3033,8 @@ export default class BattleScene extends SceneBase {
}
validateAchv(achv: Achv, args?: unknown[]): boolean {
if (!this.gameData.achvUnlocks.hasOwnProperty(achv.id) && achv.validate(this, args)) {
if ((!this.gameData.achvUnlocks.hasOwnProperty(achv.id) || Overrides.ACHIEVEMENTS_REUNLOCK_OVERRIDE)
&& achv.validate(this, args)) {
this.gameData.achvUnlocks[achv.id] = new Date().getTime();
this.ui.achvBar.showAchv(achv);
if (vouchers.hasOwnProperty(achv.id)) {

View File

@ -3720,16 +3720,16 @@ export class PostTurnHurtIfSleepingAbAttr extends PostTurnAbAttr {
/**
* Deals damage to all sleeping opponents equal to 1/8 of their max hp (min 1)
* @param {Pokemon} pokemon Pokemon that has this ability
* @param {boolean} passive N/A
* @param {boolean} simulated true if applying in a simulated call.
* @param {any[]} args N/A
* @returns {boolean} true if any opponents are sleeping
* @param pokemon Pokemon that has this ability
* @param passive N/A
* @param simulated `true` if applying in a simulated call.
* @param args N/A
* @returns `true` if any opponents are sleeping
*/
applyPostTurn(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean | Promise<boolean> {
let hadEffect: boolean = false;
for (const opp of pokemon.getOpponents()) {
if ((opp.status?.effect === StatusEffect.SLEEP || opp.hasAbility(Abilities.COMATOSE)) && !opp.hasAbilityWithAttr(BlockNonDirectDamageAbAttr)) {
if ((opp.status?.effect === StatusEffect.SLEEP || opp.hasAbility(Abilities.COMATOSE)) && !opp.hasAbilityWithAttr(BlockNonDirectDamageAbAttr) && !opp.switchOutStatus) {
if (!simulated) {
opp.damageAndUpdate(Utils.toDmgValue(opp.getMaxHp() / 8), HitResult.OTHER);
pokemon.scene.queueMessage(i18next.t("abilityTriggers:badDreams", { pokemonName: getPokemonNameWithAffix(opp) }));
@ -4112,9 +4112,13 @@ export class PostBattleAbAttr extends AbAttr {
}
export class PostBattleLootAbAttr extends PostBattleAbAttr {
/**
* @param args - `[0]`: boolean for if the battle ended in a victory
* @returns `true` if successful
*/
applyPostBattle(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean {
const postBattleLoot = pokemon.scene.currentBattle.postBattleLoot;
if (!simulated && postBattleLoot.length) {
if (!simulated && postBattleLoot.length && args[0]) {
const randItem = Utils.randSeedItem(postBattleLoot);
//@ts-ignore - TODO see below
if (pokemon.scene.tryTransferHeldItemModifier(randItem, pokemon, true, 1, true, undefined, false)) { // TODO: fix. This is a promise!?
@ -4575,28 +4579,28 @@ export class MoneyAbAttr extends PostBattleAbAttr {
/**
* @param pokemon {@linkcode Pokemon} that is the user of this ability.
* @param passive N/A
* @param args N/A
* @returns true
* @param args - `[0]`: boolean for if the battle ended in a victory
* @returns `true` if successful
*/
applyPostBattle(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean {
if (!simulated) {
if (!simulated && args[0]) {
pokemon.scene.currentBattle.moneyScattered += pokemon.scene.getWaveMoneyAmount(0.2);
}
return true;
}
return false;
}
}
/**
* Applies a stat change after a Pokémon is summoned,
* conditioned on the presence of a specific arena tag.
*
* @extends {PostSummonStatStageChangeAbAttr}
* @extends PostSummonStatStageChangeAbAttr
*/
export class PostSummonStatStageChangeOnArenaAbAttr extends PostSummonStatStageChangeAbAttr {
/**
* The type of arena tag that conditions the stat change.
* @private
* @type {ArenaTagType}
*/
private tagType: ArenaTagType;
@ -4972,7 +4976,7 @@ class ForceSwitchOutHelper {
pokemon.scene.clearEnemyHeldItemModifiers();
if (switchOutTarget.hp) {
pokemon.scene.pushPhase(new BattleEndPhase(pokemon.scene));
pokemon.scene.pushPhase(new BattleEndPhase(pokemon.scene, false));
pokemon.scene.pushPhase(new NewBattlePhase(pokemon.scene));
}
}
@ -5709,9 +5713,7 @@ export function initAbilities() {
.condition(getSheerForceHitDisableAbCondition()),
new Ability(Abilities.SHEER_FORCE, 5)
.attr(MovePowerBoostAbAttr, (user, target, move) => move.chance >= 1, 5461 / 4096)
.attr(MoveEffectChanceMultiplierAbAttr, 0)
.edgeCase() // Should disable shell bell and Meloetta's relic song transformation
.edgeCase(), // Should disable life orb, eject button, red card, kee/maranga berry if they get implemented
.attr(MoveEffectChanceMultiplierAbAttr, 0), // Should disable life orb, eject button, red card, kee/maranga berry if they get implemented
new Ability(Abilities.CONTRARY, 5)
.attr(StatStageChangeMultiplierAbAttr, -1)
.ignorable(),
@ -5779,9 +5781,10 @@ export function initAbilities() {
.attr(WonderSkinAbAttr)
.ignorable(),
new Ability(Abilities.ANALYTIC, 5)
.attr(MovePowerBoostAbAttr, (user, target, move) =>
!!target?.getLastXMoves(1).find(m => m.turn === target?.scene.currentBattle.turn)
|| user?.scene.currentBattle.turnCommands[target?.getBattlerIndex() ?? BattlerIndex.ATTACKER]?.command !== Command.FIGHT, 1.3),
.attr(MovePowerBoostAbAttr, (user, target, move) => {
const movePhase = user?.scene.findPhase((phase) => phase instanceof MovePhase && phase.pokemon.id !== user.id);
return Utils.isNullOrUndefined(movePhase);
}, 1.3),
new Ability(Abilities.ILLUSION, 5)
.attr(UncopiableAbilityAbAttr)
.attr(UnswappableAbilityAbAttr)
@ -5929,10 +5932,10 @@ export function initAbilities() {
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, Stat.DEF, 1),
new Ability(Abilities.WIMP_OUT, 7)
.attr(PostDamageForceSwitchAbAttr)
.edgeCase(), // Should not trigger when hurting itself in confusion
.edgeCase(), // Should not trigger when hurting itself in confusion, causes Fake Out to fail turn 1 and succeed turn 2 if pokemon is switched out before battle start via playing in Switch Mode
new Ability(Abilities.EMERGENCY_EXIT, 7)
.attr(PostDamageForceSwitchAbAttr)
.edgeCase(), // Should not trigger when hurting itself in confusion
.edgeCase(), // Should not trigger when hurting itself in confusion, causes Fake Out to fail turn 1 and succeed turn 2 if pokemon is switched out before battle start via playing in Switch Mode
new Ability(Abilities.WATER_COMPACTION, 7)
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => user.getMoveType(move) === Type.WATER && move.category !== MoveCategory.STATUS, Stat.DEF, 2),
new Ability(Abilities.MERCILESS, 7)
@ -5948,7 +5951,7 @@ export function initAbilities() {
.bypassFaint()
.partial(), // Meteor form should protect against status effects and yawn
new Ability(Abilities.STAKEOUT, 7)
.attr(MovePowerBoostAbAttr, (user, target, move) => user?.scene.currentBattle.turnCommands[target?.getBattlerIndex() ?? BattlerIndex.ATTACKER]?.command === Command.POKEMON, 2),
.attr(MovePowerBoostAbAttr, (user, target, move) => !!target?.turnData.switchedInThisTurn, 2),
new Ability(Abilities.WATER_BUBBLE, 7)
.attr(ReceivedTypeDamageMultiplierAbAttr, Type.FIRE, 0.5)
.attr(MoveTypePowerBoostAbAttr, Type.WATER, 2)
@ -6340,8 +6343,7 @@ export function initAbilities() {
.attr(IgnoreOpponentStatStagesAbAttr, [ Stat.EVA ])
.ignorable(),
new Ability(Abilities.SUPERSWEET_SYRUP, 9)
.attr(PostSummonStatStageChangeAbAttr, [ Stat.EVA ], -1)
.condition(getOncePerBattleCondition(Abilities.SUPERSWEET_SYRUP)),
.attr(PostSummonStatStageChangeAbAttr, [ Stat.EVA ], -1),
new Ability(Abilities.HOSPITALITY, 9)
.attr(PostSummonAllyHealAbAttr, 4, true),
new Ability(Abilities.TOXIC_CHAIN, 9)

View File

@ -1144,7 +1144,7 @@ class FireGrassPledgeTag extends ArenaTag {
? arena.scene.getPlayerField()
: arena.scene.getEnemyField();
field.filter(pokemon => !pokemon.isOfType(Type.FIRE)).forEach(pokemon => {
field.filter(pokemon => !pokemon.isOfType(Type.FIRE) && !pokemon.switchOutStatus).forEach(pokemon => {
// "{pokemonNameWithAffix} was hurt by the sea of fire!"
pokemon.scene.queueMessage(i18next.t("arenaTag:fireGrassPledgeLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
// TODO: Replace this with a proper animation

View File

@ -0,0 +1,46 @@
import { Species } from "#enums/species";
/**
* A list of all {@link https://bulbapedia.bulbagarden.net/wiki/Paradox_Pok%C3%A9mon | Paradox Pokemon}, NOT including the legendaries Miraidon and Koraidon.
*/
export const NON_LEGEND_PARADOX_POKEMON = [
Species.GREAT_TUSK,
Species.SCREAM_TAIL,
Species.BRUTE_BONNET,
Species.FLUTTER_MANE,
Species.SLITHER_WING,
Species.SANDY_SHOCKS,
Species.ROARING_MOON,
Species.WALKING_WAKE,
Species.GOUGING_FIRE,
Species.RAGING_BOLT,
Species.IRON_TREADS,
Species.IRON_BUNDLE,
Species.IRON_HANDS,
Species.IRON_JUGULIS,
Species.IRON_MOTH,
Species.IRON_THORNS,
Species.IRON_VALIANT,
Species.IRON_LEAVES,
Species.IRON_BOULDER,
Species.IRON_CROWN,
];
/**
* A list of all {@link https://bulbapedia.bulbagarden.net/wiki/Ultra_Beast | Ultra Beasts}, NOT including legendaries such as Necrozma or the Cosmog line.
*
* Note that all of these Ultra Beasts are still considered Sub-Legendary.
*/
export const NON_LEGEND_ULTRA_BEASTS = [
Species.NIHILEGO,
Species.BUZZWOLE,
Species.PHEROMOSA,
Species.XURKITREE,
Species.CELESTEELA,
Species.KARTANA,
Species.GUZZLORD,
Species.POIPOLE,
Species.NAGANADEL,
Species.STAKATAKA,
Species.BLACEPHALON,
];

View File

@ -3,10 +3,10 @@ import { Species } from "#enums/species";
export const POKERUS_STARTER_COUNT = 5;
// #region Friendship constants
export const CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER = 2;
export const FRIENDSHIP_GAIN_FROM_BATTLE = 2;
export const FRIENDSHIP_GAIN_FROM_RARE_CANDY = 5;
export const FRIENDSHIP_LOSS_FROM_FAINT = 10;
export const CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER = 3;
export const FRIENDSHIP_GAIN_FROM_BATTLE = 3;
export const FRIENDSHIP_GAIN_FROM_RARE_CANDY = 6;
export const FRIENDSHIP_LOSS_FROM_FAINT = 5;
/**
* Function to get the cumulative friendship threshold at which a candy is earned
@ -16,19 +16,19 @@ export const FRIENDSHIP_LOSS_FROM_FAINT = 10;
export function getStarterValueFriendshipCap(starterCost: number): number {
switch (starterCost) {
case 1:
return 20;
return 25;
case 2:
return 40;
return 50;
case 3:
return 60;
return 75;
case 4:
return 100;
case 5:
return 140;
return 150;
case 6:
return 200;
case 7:
return 280;
return 300;
case 8:
case 9:
return 450;

View File

@ -1085,10 +1085,6 @@ export class OctolockTag extends TrappedTag {
super(BattlerTagType.OCTOLOCK, BattlerTagLapseType.TURN_END, 1, Moves.OCTOLOCK, sourceId);
}
canAdd(pokemon: Pokemon): boolean {
return !pokemon.getTag(BattlerTagType.OCTOLOCK);
}
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
const shouldLapse = lapseType !== BattlerTagLapseType.CUSTOM || super.lapse(pokemon, lapseType);

View File

@ -1415,16 +1415,40 @@ export class UserHpDamageAttr extends FixedDamageAttr {
}
export class TargetHalfHpDamageAttr extends FixedDamageAttr {
// the initial amount of hp the target had before the first hit
// used for multi lens
private initialHp: number;
constructor() {
super(0);
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
(args[0] as Utils.IntegerHolder).value = Utils.toDmgValue(target.hp / 2);
// first, determine if the hit is coming from multi lens or not
const lensCount = user.getHeldItems().find(i => i instanceof PokemonMultiHitModifier)?.getStackCount() ?? 0;
if (lensCount <= 0) {
// no multi lenses; we can just halve the target's hp and call it a day
(args[0] as Utils.NumberHolder).value = Utils.toDmgValue(target.hp / 2);
return true;
}
// figure out what hit # we're on
switch (user.turnData.hitCount - user.turnData.hitsLeft) {
case 0:
// first hit of move; update initialHp tracker
this.initialHp = target.hp;
default:
// multi lens added hit; use initialHp tracker to ensure correct damage
(args[0] as Utils.NumberHolder).value = Utils.toDmgValue(this.initialHp / 2);
return true;
break;
case lensCount + 1:
// parental bond added hit; calc damage as normal
(args[0] as Utils.NumberHolder).value = Utils.toDmgValue(target.hp / 2);
return true;
break;
}
}
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
return target.getHpRatio() > 0.5 ? Math.floor(((target.getHpRatio() - 0.5) * -24) + 4) : -20;
}
@ -1873,7 +1897,7 @@ export class FlameBurstAttr extends MoveEffectAttr {
applyAbAttrs(BlockNonDirectDamageAbAttr, targetAlly, cancelled);
}
if (cancelled.value || !targetAlly) {
if (cancelled.value || !targetAlly || targetAlly.switchOutStatus) {
return false;
}
@ -5997,50 +6021,113 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
* Check if Wimp Out/Emergency Exit activates due to being hit by U-turn or Volt Switch
* If it did, the user of U-turn or Volt Switch will not be switched out.
*/
if (target.getAbility().hasAttr(PostDamageForceSwitchAbAttr) &&
(move.id === Moves.U_TURN || move.id === Moves.VOLT_SWITCH || move.id === Moves.FLIP_TURN)
if (target.getAbility().hasAttr(PostDamageForceSwitchAbAttr)
&& [ Moves.U_TURN, Moves.VOLT_SWITCH, Moves.FLIP_TURN ].includes(move.id)
) {
if (this.hpDroppedBelowHalf(target)) {
return false;
}
}
// Switch out logic for the player's Pokemon
if (switchOutTarget.scene.getPlayerParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) {
// Find indices of off-field Pokemon that are eligible to be switched into
const eligibleNewIndices: number[] = [];
switchOutTarget.scene.getPlayerParty().forEach((pokemon, index) => {
if (pokemon.isAllowedInBattle() && !pokemon.isOnField()) {
eligibleNewIndices.push(index);
}
});
if (eligibleNewIndices.length < 1) {
return false;
}
if (switchOutTarget.hp > 0) {
if (this.switchType === SwitchType.FORCE_SWITCH) {
switchOutTarget.leaveField(true);
const slotIndex = eligibleNewIndices[user.randSeedInt(eligibleNewIndices.length)];
user.scene.prependToPhase(
new SwitchSummonPhase(
user.scene,
this.switchType,
switchOutTarget.getFieldIndex(),
slotIndex,
false,
true
),
MoveEndPhase
);
} else {
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
user.scene.prependToPhase(new SwitchPhase(user.scene, this.switchType, switchOutTarget.getFieldIndex(), true, true), MoveEndPhase);
user.scene.prependToPhase(
new SwitchPhase(
user.scene,
this.switchType,
switchOutTarget.getFieldIndex(),
true,
true
),
MoveEndPhase
);
return true;
}
}
return false;
} else if (user.scene.currentBattle.battleType !== BattleType.WILD) {
// Switch out logic for trainer battles
if (switchOutTarget.scene.getEnemyParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) {
} else if (user.scene.currentBattle.battleType !== BattleType.WILD) { // Switch out logic for enemy trainers
// Find indices of off-field Pokemon that are eligible to be switched into
const eligibleNewIndices: number[] = [];
switchOutTarget.scene.getEnemyParty().forEach((pokemon, index) => {
if (pokemon.isAllowedInBattle() && !pokemon.isOnField()) {
eligibleNewIndices.push(index);
}
});
if (eligibleNewIndices.length < 1) {
return false;
}
if (switchOutTarget.hp > 0) {
// for opponent switching out
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
user.scene.prependToPhase(new SwitchSummonPhase(user.scene, this.switchType, switchOutTarget.getFieldIndex(),
(user.scene.currentBattle.trainer ? user.scene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0),
false, false), MoveEndPhase);
}
if (this.switchType === SwitchType.FORCE_SWITCH) {
switchOutTarget.leaveField(true);
const slotIndex = eligibleNewIndices[user.randSeedInt(eligibleNewIndices.length)];
user.scene.prependToPhase(
new SwitchSummonPhase(
user.scene,
this.switchType,
switchOutTarget.getFieldIndex(),
slotIndex,
false,
false
),
MoveEndPhase
);
} else {
switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH);
user.scene.prependToPhase(
new SwitchSummonPhase(
user.scene,
this.switchType,
switchOutTarget.getFieldIndex(),
(user.scene.currentBattle.trainer ? user.scene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0),
false,
false
),
MoveEndPhase
);
}
}
} else { // Switch out logic for wild pokemon
/**
* Check if Wimp Out/Emergency Exit activates due to being hit by U-turn or Volt Switch
* If it did, the user of U-turn or Volt Switch will not be switched out.
*/
if (target.getAbility().hasAttr(PostDamageForceSwitchAbAttr) &&
(move.id === Moves.U_TURN || move.id === Moves.VOLT_SWITCH) || move.id === Moves.FLIP_TURN) {
if (target.getAbility().hasAttr(PostDamageForceSwitchAbAttr)
&& [ Moves.U_TURN, Moves.VOLT_SWITCH, Moves.FLIP_TURN ].includes(move.id)
) {
if (this.hpDroppedBelowHalf(target)) {
return false;
}
}
// Switch out logic for everything else (eg: WILD battles)
if (user.scene.currentBattle.waveIndex % 10 === 0) {
return false;
}
@ -6065,7 +6152,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
user.scene.clearEnemyHeldItemModifiers();
if (switchOutTarget.hp) {
user.scene.pushPhase(new BattleEndPhase(user.scene));
user.scene.pushPhase(new BattleEndPhase(user.scene, false));
user.scene.pushPhase(new NewBattlePhase(user.scene));
}
}
@ -7484,6 +7571,8 @@ const failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Po
return party.some(pokemon => pokemon.isActive() && !pokemon.isOnField());
};
const failIfGhostTypeCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => !target.isOfType(Type.GHOST);
export type MoveAttrFilter = (attr: MoveAttr) => boolean;
function applyMoveAttrsInternal(attrFilter: MoveAttrFilter, user: Pokemon | null, target: Pokemon | null, move: Move, args: any[]): Promise<void> {
@ -7869,11 +7958,10 @@ export function initMoves() {
.windMove(),
new AttackMove(Moves.WING_ATTACK, Type.FLYING, MoveCategory.PHYSICAL, 60, 100, 35, -1, 0, 1),
new StatusMove(Moves.WHIRLWIND, Type.NORMAL, -1, 20, -1, -6, 1)
.attr(ForceSwitchOutAttr)
.attr(ForceSwitchOutAttr, false, SwitchType.FORCE_SWITCH)
.ignoresSubstitute()
.hidesTarget()
.windMove()
.partial(), // Should force random switches
.windMove(),
new ChargingAttackMove(Moves.FLY, Type.FLYING, MoveCategory.PHYSICAL, 90, 95, 15, -1, 0, 1)
.chargeText(i18next.t("moveTriggers:flewUpHigh", { pokemonName: "{USER}" }))
.chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING)
@ -7949,10 +8037,9 @@ export function initMoves() {
.soundBased()
.target(MoveTarget.ALL_NEAR_ENEMIES),
new StatusMove(Moves.ROAR, Type.NORMAL, -1, 20, -1, -6, 1)
.attr(ForceSwitchOutAttr)
.attr(ForceSwitchOutAttr, false, SwitchType.FORCE_SWITCH)
.soundBased()
.hidesTarget()
.partial(), // Should force random switching
.hidesTarget(),
new StatusMove(Moves.SING, Type.NORMAL, 55, 15, -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.SLEEP)
.soundBased(),
@ -8294,6 +8381,7 @@ export function initMoves() {
new AttackMove(Moves.THIEF, Type.DARK, MoveCategory.PHYSICAL, 60, 100, 25, -1, 0, 2)
.attr(StealHeldItemChanceAttr, 0.3),
new StatusMove(Moves.SPIDER_WEB, Type.BUG, -1, 10, -1, 0, 2)
.condition(failIfGhostTypeCondition)
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1),
new StatusMove(Moves.MIND_READER, Type.NORMAL, -1, 5, -1, 0, 2)
.attr(IgnoreAccuracyAttr),
@ -8430,6 +8518,7 @@ export function initMoves() {
new AttackMove(Moves.STEEL_WING, Type.STEEL, MoveCategory.PHYSICAL, 70, 90, 25, 10, 0, 2)
.attr(StatStageChangeAttr, [ Stat.DEF ], 1, true),
new StatusMove(Moves.MEAN_LOOK, Type.NORMAL, -1, 5, -1, 0, 2)
.condition(failIfGhostTypeCondition)
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1),
new StatusMove(Moves.ATTRACT, Type.NORMAL, 100, 15, -1, 0, 2)
.attr(AddBattlerTagAttr, BattlerTagType.INFATUATED)
@ -8809,6 +8898,7 @@ export function initMoves() {
new SelfStatusMove(Moves.IRON_DEFENSE, Type.STEEL, -1, 15, -1, 0, 3)
.attr(StatStageChangeAttr, [ Stat.DEF ], 2, true),
new StatusMove(Moves.BLOCK, Type.NORMAL, -1, 5, -1, 0, 3)
.condition(failIfGhostTypeCondition)
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1),
new StatusMove(Moves.HOWL, Type.NORMAL, -1, 40, -1, 0, 3)
.attr(StatStageChangeAttr, [ Stat.ATK ], 1)
@ -9314,8 +9404,8 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.ATK ], 1, true)
.attr(StatStageChangeAttr, [ Stat.SPD ], 2, true),
new AttackMove(Moves.CIRCLE_THROW, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 90, 10, -1, -6, 5)
.attr(ForceSwitchOutAttr)
.partial(), // Should force random switches
.attr(ForceSwitchOutAttr, false, SwitchType.FORCE_SWITCH)
.hidesTarget(),
new AttackMove(Moves.INCINERATE, Type.FIRE, MoveCategory.SPECIAL, 60, 100, 15, -1, 0, 5)
.target(MoveTarget.ALL_NEAR_ENEMIES)
.attr(RemoveHeldItemAttr, true),
@ -9383,9 +9473,8 @@ export function initMoves() {
new AttackMove(Moves.FROST_BREATH, Type.ICE, MoveCategory.SPECIAL, 60, 90, 10, 100, 0, 5)
.attr(CritOnlyAttr),
new AttackMove(Moves.DRAGON_TAIL, Type.DRAGON, MoveCategory.PHYSICAL, 60, 90, 10, -1, -6, 5)
.attr(ForceSwitchOutAttr)
.hidesTarget()
.partial(), // Should force random switches
.attr(ForceSwitchOutAttr, false, SwitchType.FORCE_SWITCH)
.hidesTarget(),
new SelfStatusMove(Moves.WORK_UP, Type.NORMAL, -1, 30, -1, 0, 5)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, true),
new AttackMove(Moves.ELECTROWEB, Type.ELECTRIC, MoveCategory.SPECIAL, 55, 95, 15, 100, 0, 5)
@ -10101,6 +10190,7 @@ export function initMoves() {
.attr(EatBerryAttr)
.target(MoveTarget.ALL),
new StatusMove(Moves.OCTOLOCK, Type.FIGHTING, 100, 15, -1, 0, 8)
.condition(failIfGhostTypeCondition)
.attr(AddBattlerTagAttr, BattlerTagType.OCTOLOCK, false, true, 1),
new AttackMove(Moves.BOLT_BEAK, Type.ELECTRIC, MoveCategory.PHYSICAL, 85, 100, 10, -1, 0, 8)
.attr(FirstAttackDoublePowerAttr),
@ -10643,6 +10733,7 @@ export function initMoves() {
new AttackMove(Moves.TWIN_BEAM, Type.PSYCHIC, MoveCategory.SPECIAL, 40, 100, 10, -1, 0, 9)
.attr(MultiHitAttr, MultiHitType._2),
new AttackMove(Moves.RAGE_FIST, Type.GHOST, MoveCategory.PHYSICAL, 50, 100, 10, -1, 0, 9)
.partial() // Counter resets every wave instead of on arena reset
.attr(HitCountPowerAttr)
.punchingMove(),
new AttackMove(Moves.ARMOR_CANNON, Type.FIRE, MoveCategory.SPECIAL, 120, 100, 5, -1, 0, 9)

View File

@ -216,6 +216,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter =
species: getPokemonSpecies(Species.GREEDENT),
isBoss: true,
bossSegments: 3,
shiny: false, // Shiny lock because of consistency issues between the different options
moveSet: [ Moves.THRASH, Moves.BODY_PRESS, Moves.STUFF_CHEEKS, Moves.CRUNCH ],
modifierConfigs: bossModifierConfigs,
tags: [ BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON ],
@ -353,9 +354,9 @@ export const AbsoluteAvariceEncounter: MysteryEncounter =
})
.withOptionPhase(async (scene: BattleScene) => {
// Let it have the food
// Greedent joins the team, level equal to 2 below highest party member
// Greedent joins the team, level equal to 2 below highest party member (shiny locked)
const level = getHighestLevelPlayerPokemon(scene, false, true).level - 2;
const greedent = new EnemyPokemon(scene, getPokemonSpecies(Species.GREEDENT), level, TrainerSlot.NONE, false);
const greedent = new EnemyPokemon(scene, getPokemonSpecies(Species.GREEDENT), level, TrainerSlot.NONE, false, true);
greedent.moveset = [ new PokemonMove(Moves.THRASH), new PokemonMove(Moves.BODY_PRESS), new PokemonMove(Moves.STUFF_CHEEKS), new PokemonMove(Moves.SLACK_OFF) ];
greedent.passive = true;

View File

@ -98,7 +98,9 @@ export const BerriesAboundEncounter: MysteryEncounter =
tint: 0.25,
x: -5,
repeat: true,
isPokemon: true
isPokemon: true,
isShiny: bossPokemon.shiny,
variant: bossPokemon.variant
}
];

View File

@ -276,6 +276,8 @@ export const ClowningAroundEncounter: MysteryEncounter =
generateItemsOfTier(scene, mostHeldItemsPokemon, numBerries, "Berries");
// Shuffle Transferable held items in the same tier (only shuffles Ultra and Rogue atm)
// For the purpose of this ME, Soothe Bells and Lucky Eggs are counted as Ultra tier
// And Golden Eggs as Rogue tier
let numUltra = 0;
let numRogue = 0;
items.filter(m => m.isTransferable && !(m instanceof BerryModifier))
@ -285,7 +287,7 @@ export const ClowningAroundEncounter: MysteryEncounter =
if (type.id === "GOLDEN_EGG" || tier === ModifierTier.ROGUE) {
numRogue += m.stackCount;
scene.removeModifier(m);
} else if (type.id === "LUCKY_EGG" || tier === ModifierTier.ULTRA) {
} else if (type.id === "LUCKY_EGG" || type.id === "SOOTHE_BELL" || tier === ModifierTier.ULTRA) {
numUltra += m.stackCount;
scene.removeModifier(m);
}
@ -456,7 +458,6 @@ function generateItemsOfTier(scene: BattleScene, pokemon: PlayerPokemon, numItem
[ modifierTypes.LEFTOVERS, 4 ],
[ modifierTypes.SHELL_BELL, 4 ],
[ modifierTypes.SOUL_DEW, 10 ],
[ modifierTypes.SOOTHE_BELL, 3 ],
[ modifierTypes.SCOPE_LENS, 1 ],
[ modifierTypes.BATON, 1 ],
[ modifierTypes.FOCUS_BAND, 5 ],

View File

@ -92,9 +92,13 @@ export const DancingLessonsEncounter: MysteryEncounter =
.withCatchAllowed(true)
.withFleeAllowed(false)
.withOnVisualsStart((scene: BattleScene) => {
const danceAnim = new EncounterBattleAnim(EncounterAnim.DANCE, scene.getEnemyPokemon()!, scene.getPlayerPokemon()!);
danceAnim.play(scene);
const oricorio = scene.getEnemyPokemon()!;
const danceAnim = new EncounterBattleAnim(EncounterAnim.DANCE, oricorio, scene.getPlayerPokemon()!);
danceAnim.play(scene, false, () => {
if (oricorio.shiny) {
oricorio.sparkle();
}
});
return true;
})
.withIntroDialogue([
@ -136,7 +140,7 @@ export const DancingLessonsEncounter: MysteryEncounter =
}
const oricorioData = new PokemonData(enemyPokemon);
const oricorio = scene.addEnemyPokemon(species, level, TrainerSlot.NONE, false, oricorioData);
const oricorio = scene.addEnemyPokemon(species, level, TrainerSlot.NONE, false, false, oricorioData);
// Adds a real Pokemon sprite to the field (required for the animation)
scene.getEnemyParty().forEach(enemyPokemon => {

View File

@ -8,7 +8,7 @@ import { getPokemonSpecies } from "#app/data/pokemon-species";
import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter";
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { EnemyPartyConfig, EnemyPokemonConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, } from "../utils/encounter-phase-utils";
import { getRandomPlayerPokemon, getRandomSpeciesByStarterTier } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { getRandomPlayerPokemon, getRandomSpeciesByStarterCost } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase";
@ -174,7 +174,7 @@ export const DarkDealEncounter: MysteryEncounter =
const roll = randSeedInt(100);
const starterTier: number | [number, number] =
roll >= 65 ? 6 : roll >= 15 ? 7 : roll >= 5 ? 8 : [ 9, 10 ];
const bossSpecies = getPokemonSpecies(getRandomSpeciesByStarterTier(starterTier, excludedBosses, bossTypes));
const bossSpecies = getPokemonSpecies(getRandomSpeciesByStarterCost(starterTier, excludedBosses, bossTypes));
const pokemonConfig: EnemyPokemonConfig = {
species: bossSpecies,
isBoss: true,

View File

@ -114,7 +114,9 @@ export const FightOrFlightEncounter: MysteryEncounter =
tint: 0.25,
x: -5,
repeat: true,
isPokemon: true
isPokemon: true,
isShiny: bossPokemon.shiny,
variant: bossPokemon.variant
},
];

View File

@ -194,10 +194,10 @@ async function summonPlayerPokemon(scene: BattleScene) {
playerAnimationPromise = summonPlayerPokemonAnimation(scene, playerPokemon);
});
// Also loads Wobbuffet data
// Also loads Wobbuffet data (cannot be shiny)
const enemySpecies = getPokemonSpecies(Species.WOBBUFFET);
scene.currentBattle.enemyParty = [];
const wobbuffet = scene.addEnemyPokemon(enemySpecies, encounter.misc.playerPokemon.level, TrainerSlot.NONE, false);
const wobbuffet = scene.addEnemyPokemon(enemySpecies, encounter.misc.playerPokemon.level, TrainerSlot.NONE, false, true);
wobbuffet.ivs = [ 0, 0, 0, 0, 0, 0 ];
wobbuffet.setNature(Nature.MILD);
wobbuffet.setAlpha(0);

View File

@ -12,8 +12,7 @@ import PokemonSpecies, { allSpecies, getPokemonSpecies } from "#app/data/pokemon
import { getTypeRgb } from "#app/data/type";
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import * as Utils from "#app/utils";
import { IntegerHolder, isNullOrUndefined, randInt, randSeedInt, randSeedShuffle } from "#app/utils";
import { NumberHolder, isNullOrUndefined, randInt, randSeedInt, randSeedShuffle } from "#app/utils";
import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon";
import { HiddenAbilityRateBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier, ShinyRateBoosterModifier, SpeciesStatBoosterModifier } from "#app/modifier/modifier";
import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler";
@ -27,6 +26,7 @@ import { trainerNamePools } from "#app/data/trainer-names";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { addPokemonDataToDexAndValidateAchievements } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import type { PokeballType } from "#enums/pokeball";
import { doShinySparkleAnim } from "#app/field/anims";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/globalTradeSystem";
@ -230,7 +230,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter =
const tradePokemon = new EnemyPokemon(scene, randomTradeOption, pokemon.level, TrainerSlot.NONE, false);
// Extra shiny roll at 1/128 odds (boosted by events and charms)
if (!tradePokemon.shiny) {
const shinyThreshold = new Utils.IntegerHolder(WONDER_TRADE_SHINY_CHANCE);
const shinyThreshold = new NumberHolder(WONDER_TRADE_SHINY_CHANCE);
if (scene.eventManager.isEventActive()) {
shinyThreshold.value *= scene.eventManager.getShinyMultiplier();
}
@ -247,7 +247,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter =
const hiddenIndex = tradePokemon.species.ability2 ? 2 : 1;
if (tradePokemon.species.abilityHidden) {
if (tradePokemon.abilityIndex < hiddenIndex) {
const hiddenAbilityChance = new IntegerHolder(64);
const hiddenAbilityChance = new NumberHolder(64);
scene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance);
const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value);
@ -797,6 +797,14 @@ function doTradeReceivedSequence(scene: BattleScene, receivedPokemon: PlayerPoke
receivedPokeballSprite.x = tradeBaseBg.displayWidth / 2;
receivedPokeballSprite.y = tradeBaseBg.displayHeight / 2 - 100;
// Received pokemon sparkles
let pokemonShinySparkle: Phaser.GameObjects.Sprite;
if (receivedPokemon.shiny) {
pokemonShinySparkle = scene.add.sprite(receivedPokemonSprite.x, receivedPokemonSprite.y, "shiny");
pokemonShinySparkle.setVisible(false);
tradeContainer.add(pokemonShinySparkle);
}
const BASE_ANIM_DURATION = 1000;
// Pokeball falls to the screen
@ -835,6 +843,11 @@ function doTradeReceivedSequence(scene: BattleScene, receivedPokemon: PlayerPoke
scale: 1,
alpha: 0,
onComplete: () => {
if (receivedPokemon.shiny) {
scene.time.delayedCall(500, () => {
doShinySparkleAnim(scene, pokemonShinySparkle, receivedPokemon.variant);
});
}
receivedPokeballSprite.destroy();
scene.time.delayedCall(2000, () => resolve());
}

View File

@ -9,9 +9,9 @@ import { EnemyPokemon } from "#app/field/pokemon";
import { PokeballType } from "#enums/pokeball";
import { PlayerGender } from "#enums/player-gender";
import { IntegerHolder, randSeedInt } from "#app/utils";
import { getPokemonSpecies } from "#app/data/pokemon-species";
import PokemonSpecies, { getPokemonSpecies } from "#app/data/pokemon-species";
import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements";
import { doPlayerFlee, doPokemonFlee, getRandomSpeciesByStarterTier, trainerThrowPokeball } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { doPlayerFlee, doPokemonFlee, getRandomSpeciesByStarterCost, trainerThrowPokeball } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { getPokemonNameWithAffix } from "#app/messages";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
@ -19,6 +19,7 @@ import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode
import { ScanIvsPhase } from "#app/phases/scan-ivs-phase";
import { SummonPhase } from "#app/phases/summon-phase";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { NON_LEGEND_PARADOX_POKEMON } from "#app/data/balance/special-species-groups";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/safariZone";
@ -261,7 +262,7 @@ async function summonSafariPokemon(scene: BattleScene) {
let enemySpecies;
let pokemon;
scene.executeWithSeedOffset(() => {
enemySpecies = getPokemonSpecies(getRandomSpeciesByStarterTier([ 0, 5 ], undefined, undefined, false, false, false));
enemySpecies = getSafariSpeciesSpawn();
const level = scene.currentBattle.getLevelForWave();
enemySpecies = getPokemonSpecies(enemySpecies.getWildSpeciesForLevel(level, true, false, scene.gameMode));
pokemon = scene.addEnemyPokemon(enemySpecies, level, TrainerSlot.NONE, false);
@ -526,3 +527,10 @@ async function doEndTurn(scene: BattleScene, cursorIndex: number) {
initSubsequentOptionSelect(scene, { overrideOptions: safariZoneGameOptions, startingCursorIndex: cursorIndex, hideDescription: true });
}
}
/**
* @returns A random species that has at most 5 starter cost and is not Mythical, Paradox, etc.
*/
export function getSafariSpeciesSpawn(): PokemonSpecies {
return getPokemonSpecies(getRandomSpeciesByStarterCost([ 0, 5 ], NON_LEGEND_PARADOX_POKEMON, undefined, false, false, false));
}

View File

@ -60,6 +60,7 @@ export const SlumberingSnorlaxEncounter: MysteryEncounter =
const pokemonConfig: EnemyPokemonConfig = {
species: bossSpecies,
isBoss: true,
shiny: false, // Shiny lock because shiny is rolled only if the battle option is picked
status: [ StatusEffect.SLEEP, 5 ], // Extra turns on timer for Snorlax's start of fight moves
moveSet: [ Moves.REST, Moves.SLEEP_TALK, Moves.CRUNCH, Moves.GIGA_IMPACT ],
modifierConfigs: [

View File

@ -21,7 +21,6 @@ import { EggSourceType } from "#enums/egg-source-types";
import { EggTier } from "#enums/egg-type";
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { achvs } from "#app/system/achv";
import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import { Type } from "#enums/type";
import { getPokeballTintColor } from "#app/data/pokeball";
@ -520,12 +519,6 @@ function removePokemonFromPartyAndStoreHeldItems(scene: BattleScene, encounter:
];
}
function checkAchievement(scene: BattleScene) {
if (scene.arena.biomeType === Biome.SPACE) {
scene.validateAchv(achvs.BREEDERS_IN_SPACE);
}
}
function restorePartyAndHeldItems(scene: BattleScene) {
const encounter = scene.currentBattle.mysteryEncounter!;
// Restore original party
@ -617,8 +610,6 @@ function onGameOver(scene: BattleScene) {
function doPostEncounterCleanup(scene: BattleScene) {
const encounter = scene.currentBattle.mysteryEncounter!;
if (!encounter.misc.encounterFailed) {
// Give achievement if in Space biome
checkAchievement(scene);
// Give 20 friendship to the chosen pokemon
encounter.misc.chosenPokemon.addFriendship(FRIENDSHIP_ADDED);
restorePartyAndHeldItems(scene);

View File

@ -4,8 +4,8 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import BattleScene from "#app/battle-scene";
import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter";
import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements";
import { catchPokemon, getRandomSpeciesByStarterTier, getSpriteKeysFromPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { getPokemonSpecies } from "#app/data/pokemon-species";
import { catchPokemon, getRandomSpeciesByStarterCost, getSpriteKeysFromPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import PokemonSpecies, { getPokemonSpecies } from "#app/data/pokemon-species";
import { speciesStarterCosts } from "#app/data/balance/starters";
import { Species } from "#enums/species";
import { PokeballType } from "#enums/pokeball";
@ -17,6 +17,7 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { Abilities } from "#enums/abilities";
import { NON_LEGEND_PARADOX_POKEMON } from "#app/data/balance/special-species-groups";
/** the i18n namespace for this encounter */
const namespace = "mysteryEncounters/thePokemonSalesman";
@ -60,24 +61,22 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter =
.withOnInit((scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter!;
let species = getPokemonSpecies(getRandomSpeciesByStarterTier([ 0, 5 ], undefined, undefined, false, false, false));
let species = getSalesmanSpeciesOffer();
let tries = 0;
// Reroll any species that don't have HAs
while ((isNullOrUndefined(species.abilityHidden) || species.abilityHidden === Abilities.NONE) && tries < 5) {
species = getPokemonSpecies(getRandomSpeciesByStarterTier([ 0, 5 ], undefined, undefined, false, false, false));
species = getSalesmanSpeciesOffer();
tries++;
}
let pokemon: PlayerPokemon;
if (randSeedInt(SHINY_MAGIKARP_WEIGHT) === 0 || isNullOrUndefined(species.abilityHidden) || species.abilityHidden === Abilities.NONE) {
// If no HA mon found or you roll 1%, give shiny Magikarp
// If no HA mon found or you roll 1%, give shiny Magikarp with random variant
species = getPokemonSpecies(Species.MAGIKARP);
const hiddenIndex = species.ability2 ? 2 : 1;
pokemon = new PlayerPokemon(scene, species, 5, hiddenIndex, species.formIndex, undefined, true, 0);
pokemon = new PlayerPokemon(scene, species, 5, 2, species.formIndex, undefined, true);
} else {
const hiddenIndex = species.ability2 ? 2 : 1;
pokemon = new PlayerPokemon(scene, species, 5, hiddenIndex, species.formIndex);
pokemon = new PlayerPokemon(scene, species, 5, 2, species.formIndex);
}
pokemon.generateAndPopulateMoveset();
@ -87,7 +86,9 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter =
fileRoot: fileRoot,
hasShadow: true,
repeat: true,
isPokemon: true
isPokemon: true,
isShiny: pokemon.shiny,
variant: pokemon.variant
});
const starterTier = speciesStarterCosts[species.speciesId];
@ -164,3 +165,10 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter =
}
)
.build();
/**
* @returns A random species that has at most 5 starter cost and is not Mythical, Paradox, etc.
*/
export function getSalesmanSpeciesOffer(): PokemonSpecies {
return getPokemonSpecies(getRandomSpeciesByStarterCost([ 0, 5 ], NON_LEGEND_PARADOX_POKEMON, undefined, false, false, false));
}

View File

@ -79,6 +79,7 @@ export const TheStrongStuffEncounter: MysteryEncounter =
species: getPokemonSpecies(Species.SHUCKLE),
isBoss: true,
bossSegments: 5,
shiny: false, // Shiny lock because shiny is rolled only if the battle option is picked
customPokemonData: new CustomPokemonData({ spriteScale: 1.25 }),
nature: Nature.BOLD,
moveSet: [ Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER ],

View File

@ -61,11 +61,12 @@ export const TrashToTreasureEncounter: MysteryEncounter =
.withOnInit((scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter!;
// Calculate boss mon
// Calculate boss mon (shiny locked)
const bossSpecies = getPokemonSpecies(Species.GARBODOR);
const pokemonConfig: EnemyPokemonConfig = {
species: bossSpecies,
isBoss: true,
shiny: false, // Shiny lock because of custom intro sprite
formIndex: 1, // Gmax
bossSegmentModifier: 1, // +1 Segment from normal
moveSet: [ Moves.PAYBACK, Moves.GUNK_SHOT, Moves.STOMPING_TANTRUM, Moves.DRAIN_PUNCH ]

View File

@ -100,7 +100,9 @@ export const UncommonBreedEncounter: MysteryEncounter =
hasShadow: true,
x: -5,
repeat: true,
isPokemon: true
isPokemon: true,
isShiny: pokemon.shiny,
variant: pokemon.variant
},
];
@ -113,13 +115,15 @@ export const UncommonBreedEncounter: MysteryEncounter =
const encounter = scene.currentBattle.mysteryEncounter!;
const pokemonSprite = encounter.introVisuals!.getSprites();
scene.tweens.add({ // Bounce at the end
// Bounce at the end, then shiny sparkle if the Pokemon is shiny
scene.tweens.add({
targets: pokemonSprite,
duration: 300,
ease: "Cubic.easeOut",
yoyo: true,
y: "-=20",
loop: 1,
onComplete: () => encounter.introVisuals?.playShinySparkles()
});
scene.time.delayedCall(500, () => scene.playSound("battle_anims/PRSFX- Spotlight2"));

View File

@ -184,7 +184,7 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig:
dataSource = config.dataSource;
enemySpecies = config.species;
isBoss = config.isBoss;
battle.enemyParty[e] = scene.addEnemyPokemon(enemySpecies, level, TrainerSlot.TRAINER, isBoss, dataSource);
battle.enemyParty[e] = scene.addEnemyPokemon(enemySpecies, level, TrainerSlot.TRAINER, isBoss, false, dataSource);
} else {
battle.enemyParty[e] = battle.trainer.genPartyMember(e);
}
@ -202,7 +202,7 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig:
enemySpecies = scene.randomSpecies(battle.waveIndex, level, true);
}
battle.enemyParty[e] = scene.addEnemyPokemon(enemySpecies, level, TrainerSlot.NONE, isBoss, dataSource);
battle.enemyParty[e] = scene.addEnemyPokemon(enemySpecies, level, TrainerSlot.NONE, isBoss, false, dataSource);
}
}
@ -731,7 +731,7 @@ export function handleMysteryEncounterVictory(scene: BattleScene, addHealPhase:
scene.pushPhase(new MysteryEncounterRewardsPhase(scene, addHealPhase));
scene.pushPhase(new EggLapsePhase(scene));
} else if (!scene.getEnemyParty().find(p => encounter.encounterMode !== MysteryEncounterMode.TRAINER_BATTLE ? p.isOnField() : !p?.isFainted(true))) {
scene.pushPhase(new BattleEndPhase(scene));
scene.pushPhase(new BattleEndPhase(scene, true));
if (encounter.encounterMode === MysteryEncounterMode.TRAINER_BATTLE) {
scene.pushPhase(new TrainerVictoryPhase(scene));
}

View File

@ -207,7 +207,7 @@ export function getHighestStatTotalPlayerPokemon(scene: BattleScene, isAllowed:
* @param allowMythical
* @returns
*/
export function getRandomSpeciesByStarterTier(starterTiers: number | [number, number], excludedSpecies?: Species[], types?: Type[], allowSubLegendary: boolean = true, allowLegendary: boolean = true, allowMythical: boolean = true): Species {
export function getRandomSpeciesByStarterCost(starterTiers: number | [number, number], excludedSpecies?: Species[], types?: Type[], allowSubLegendary: boolean = true, allowLegendary: boolean = true, allowMythical: boolean = true): Species {
let min = Array.isArray(starterTiers) ? starterTiers[0] : starterTiers;
let max = Array.isArray(starterTiers) ? starterTiers[1] : starterTiers;

View File

@ -351,6 +351,10 @@ export class MeloettaFormChangePostMoveTrigger extends SpeciesFormChangePostMove
if (pokemon.scene.gameMode.hasChallenge(Challenges.SINGLE_TYPE)) {
return false;
} else {
// Meloetta will not transform if it has the ability Sheer Force when using Relic Song
if (pokemon.hasAbility(Abilities.SHEER_FORCE)) {
return false;
}
return super.canChange(pokemon);
}
}

View File

@ -15,7 +15,7 @@ import { EvolutionLevel, SpeciesWildEvolutionDelay, pokemonEvolutions, pokemonPr
import { Type } from "#enums/type";
import { LevelMoves, pokemonFormLevelMoves, pokemonFormLevelMoves as pokemonSpeciesFormLevelMoves, pokemonSpeciesLevelMoves } from "#app/data/balance/pokemon-level-moves";
import { Stat } from "#enums/stat";
import { Variant, VariantSet, variantColorCache, variantData } from "#app/data/variant";
import { Variant, VariantSet, variantData } from "#app/data/variant";
import { speciesStarterCosts, POKERUS_STARTER_COUNT } from "#app/data/balance/starters";
import { SpeciesFormKey } from "#enums/species-form-key";
@ -511,29 +511,8 @@ export abstract class PokemonSpeciesForm {
} else {
scene.anims.get(spriteKey).frameRate = 10;
}
let spritePath = this.getSpriteAtlasPath(female, formIndex, shiny, variant).replace("variant/", "").replace(/_[1-3]$/, "");
const useExpSprite = scene.experimentalSprites && scene.hasExpSprite(spriteKey);
if (useExpSprite) {
spritePath = `exp/${spritePath}`;
}
let config = variantData;
spritePath.split("/").map(p => config ? config = config[p] : null);
const variantSet = config as VariantSet;
if (variantSet && (variant !== undefined && variantSet[variant] === 1)) {
const populateVariantColors = (key: string): Promise<void> => {
return new Promise(resolve => {
if (variantColorCache.hasOwnProperty(key)) {
return resolve();
}
scene.cachedFetch(`./images/pokemon/variant/${spritePath}.json`).then(res => res.json()).then(c => {
variantColorCache[key] = c;
resolve();
});
});
};
populateVariantColors(spriteKey).then(() => resolve());
return;
}
const spritePath = this.getSpriteAtlasPath(female, formIndex, shiny, variant).replace("variant/", "").replace(/_[1-3]$/, "");
scene.loadPokemonVariantAssets(spriteKey, spritePath, variant);
resolve();
});
if (startLoad) {

View File

@ -1173,16 +1173,28 @@ export function getRandomPartyMemberFunc(speciesPool: Species[], trainerSlot: Tr
if (!ignoreEvolution) {
species = getPokemonSpecies(species).getTrainerSpeciesForLevel(level, true, strength, scene.currentBattle.waveIndex);
}
return scene.addEnemyPokemon(getPokemonSpecies(species), level, trainerSlot, undefined, undefined, postProcess);
return scene.addEnemyPokemon(getPokemonSpecies(species), level, trainerSlot, undefined, false, undefined, postProcess);
};
}
function getSpeciesFilterRandomPartyMemberFunc(speciesFilter: PokemonSpeciesFilter, trainerSlot: TrainerSlot = TrainerSlot.TRAINER, allowLegendaries?: boolean, postProcess?: (EnemyPokemon: EnemyPokemon) => void): PartyMemberFunc {
const originalSpeciesFilter = speciesFilter;
speciesFilter = (species: PokemonSpecies) => (allowLegendaries || (!species.legendary && !species.subLegendary && !species.mythical)) && !species.isTrainerForbidden() && originalSpeciesFilter(species);
return (scene: BattleScene, level: integer, strength: PartyMemberStrength) => {
const ret = scene.addEnemyPokemon(getPokemonSpecies(scene.randomSpecies(scene.currentBattle.waveIndex, level, false, speciesFilter).getTrainerSpeciesForLevel(level, true, strength, scene.currentBattle.waveIndex)), level, trainerSlot, undefined, undefined, postProcess);
return ret;
function getSpeciesFilterRandomPartyMemberFunc(
originalSpeciesFilter: PokemonSpeciesFilter,
trainerSlot: TrainerSlot = TrainerSlot.TRAINER,
allowLegendaries?: boolean,
postProcess?: (EnemyPokemon: EnemyPokemon) => void
): PartyMemberFunc {
const speciesFilter = (species: PokemonSpecies): boolean => {
const notLegendary = !species.legendary && !species.subLegendary && !species.mythical;
return (allowLegendaries || notLegendary) && !species.isTrainerForbidden() && originalSpeciesFilter(species);
};
return (scene: BattleScene, level: number, strength: PartyMemberStrength) => {
const waveIndex = scene.currentBattle.waveIndex;
const species = getPokemonSpecies(scene.randomSpecies(waveIndex, level, false, speciesFilter)
.getTrainerSpeciesForLevel(level, true, strength, waveIndex));
return scene.addEnemyPokemon(species, level, trainerSlot, undefined, false, undefined, postProcess);
};
}

View File

@ -10,5 +10,7 @@ export enum SwitchType {
/** Transfers stat stages and other effects from the returning Pokemon to the switched in Pokemon */
BATON_PASS,
/** Transfers the returning Pokemon's Substitute to the switched in Pokemon */
SHED_TAIL
SHED_TAIL,
/** Force switchout to a random party member */
FORCE_SWITCH,
}

View File

@ -1,6 +1,7 @@
import BattleScene from "../battle-scene";
import BattleScene from "#app/battle-scene";
import { PokeballType } from "#enums/pokeball";
import * as Utils from "../utils";
import { Variant } from "#app/data/variant";
import { getFrameMs, randGauss } from "#app/utils";
export function addPokeballOpenParticles(scene: BattleScene, x: number, y: number, pokeballType: PokeballType): void {
switch (pokeballType) {
@ -127,7 +128,7 @@ function doFanOutParticle(scene: BattleScene, trigIndex: integer, x: integer, y:
const particleTimer = scene.tweens.addCounter({
repeat: -1,
duration: Utils.getFrameMs(1),
duration: getFrameMs(1),
onRepeat: () => {
updateParticle();
}
@ -159,7 +160,7 @@ export function addPokeballCaptureStars(scene: BattleScene, pokeball: Phaser.Gam
}
});
const dist = Utils.randGauss(25);
const dist = randGauss(25);
scene.tweens.add({
targets: particle,
x: pokeball.x + dist,
@ -185,3 +186,31 @@ export function sin(index: integer, amplitude: integer): number {
export function cos(index: integer, amplitude: integer): number {
return amplitude * Math.cos(index * (Math.PI / 128));
}
/**
* Play the shiny sparkle animation and sound effect for the given sprite
* First ensures that the animation has been properly initialized
* @param sparkleSprite the Sprite to play the animation on
* @param variant which shiny {@linkcode variant} to play the animation for
*/
export function doShinySparkleAnim(scene: BattleScene, sparkleSprite: Phaser.GameObjects.Sprite, variant: Variant) {
const keySuffix = variant ? `_${variant + 1}` : "";
const spriteKey = `shiny${keySuffix}`;
const animationKey = `sparkle${keySuffix}`;
// Make sure the animation exists, and create it if not
if (!scene.anims.exists(animationKey)) {
const frameNames = scene.anims.generateFrameNames(spriteKey, { suffix: ".png", end: 34 });
scene.anims.create({
key: `sparkle${keySuffix}`,
frames: frameNames,
frameRate: 32,
showOnStart: true,
hideOnComplete: true,
});
}
// Play the animation
sparkleSprite.play(animationKey);
scene.playSound("se/sparkle");
}

View File

@ -707,7 +707,7 @@ export class Arena {
case Biome.METROPOLIS:
return 141.470;
case Biome.FOREST:
return 4.294;
return 0.341;
case Biome.SEA:
return 0.024;
case Biome.SWAMP:

View File

@ -1,10 +1,12 @@
import { GameObjects } from "phaser";
import BattleScene from "../battle-scene";
import MysteryEncounter from "../data/mystery-encounters/mystery-encounter";
import BattleScene from "#app/battle-scene";
import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter";
import { Species } from "#enums/species";
import { isNullOrUndefined } from "#app/utils";
import { getSpriteKeysFromSpecies } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import PlayAnimationConfig = Phaser.Types.Animations.PlayAnimationConfig;
import { Variant } from "#app/data/variant";
import { doShinySparkleAnim } from "#app/field/anims";
type KnownFileRoot =
| "arenas"
@ -59,6 +61,10 @@ export class MysteryEncounterSpriteConfig {
scale?: number;
/** If you are using a Pokemon sprite, set to `true`. This will ensure variant, form, gender, shiny sprites are loaded properly */
isPokemon?: boolean;
/** If using a Pokemon shiny sprite, needs to be set to ensure the correct variant assets get loaded and displayed */
isShiny?: boolean;
/** If using a Pokemon shiny sprite, needs to be set to ensure the correct variant assets get loaded and displayed */
variant?: Variant;
/** If you are using an item sprite, set to `true` */
isItem?: boolean;
/** The sprites alpha. `0` - `1` The lower the number, the more transparent */
@ -74,6 +80,7 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con
public encounter: MysteryEncounter;
public spriteConfigs: MysteryEncounterSpriteConfig[];
public enterFromRight: boolean;
private shinySparkleSprites: { sprite: Phaser.GameObjects.Sprite, variant: Variant }[];
constructor(scene: BattleScene, encounter: MysteryEncounter) {
super(scene, -72, 76);
@ -86,7 +93,7 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con
};
if (!isNullOrUndefined(result.species)) {
const keys = getSpriteKeysFromSpecies(result.species);
const keys = getSpriteKeysFromSpecies(result.species, undefined, undefined, result.isShiny, result.variant);
result.spriteKey = keys.spriteKey;
result.fileRoot = keys.fileRoot;
result.isPokemon = true;
@ -120,18 +127,36 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con
// Sprites with custom X or Y defined will not count for normal spacing requirements
const spacingValue = Math.round((maxX - minX) / Math.max(this.spriteConfigs.filter(s => !s.x && !s.y).length, 1));
this.shinySparkleSprites = [];
const shinySparkleSprites = scene.add.container(0, 0);
this.spriteConfigs?.forEach((config) => {
const { spriteKey, isItem, hasShadow, scale, x, y, yShadow, alpha } = config;
const { spriteKey, isItem, hasShadow, scale, x, y, yShadow, alpha, isPokemon, isShiny, variant } = config;
let sprite: GameObjects.Sprite;
let tintSprite: GameObjects.Sprite;
let pokemonShinySparkle: Phaser.GameObjects.Sprite | undefined;
if (!isItem) {
sprite = getSprite(spriteKey, hasShadow, yShadow);
tintSprite = getSprite(spriteKey);
} else {
if (isItem) {
sprite = getItemSprite(spriteKey, hasShadow, yShadow);
tintSprite = getItemSprite(spriteKey);
} else {
sprite = getSprite(spriteKey, hasShadow, yShadow);
tintSprite = getSprite(spriteKey);
if (isPokemon && isShiny) {
// Set Pipeline for shiny variant
sprite.setPipelineData("spriteKey", spriteKey);
tintSprite.setPipelineData("spriteKey", spriteKey);
sprite.setPipelineData("shiny", true);
sprite.setPipelineData("variant", variant);
tintSprite.setPipelineData("shiny", true);
tintSprite.setPipelineData("variant", variant);
// Create Sprite for shiny Sparkle
pokemonShinySparkle = scene.add.sprite(sprite.x, sprite.y, "shiny");
pokemonShinySparkle.setOrigin(0.5, 1);
pokemonShinySparkle.setVisible(false);
this.shinySparkleSprites.push({ sprite: pokemonShinySparkle, variant: variant ?? 0 });
shinySparkleSprites.add(pokemonShinySparkle);
}
}
sprite.setVisible(!config.hidden);
@ -165,6 +190,11 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con
}
}
if (!isNullOrUndefined(pokemonShinySparkle)) {
// Offset the sparkle to match the Pokemon's position
pokemonShinySparkle.setPosition(sprite.x, sprite.y);
}
if (!isNullOrUndefined(alpha)) {
sprite.setAlpha(alpha);
tintSprite.setAlpha(alpha);
@ -173,6 +203,7 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con
this.add(sprite);
this.add(tintSprite);
});
this.add(shinySparkleSprites);
}
/**
@ -187,6 +218,9 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con
this.spriteConfigs.forEach((config) => {
if (config.isPokemon) {
this.scene.loadPokemonAtlas(config.spriteKey, config.fileRoot);
if (config.isShiny) {
this.scene.loadPokemonVariantAssets(config.spriteKey, config.fileRoot, config.variant);
}
} else if (config.isItem) {
this.scene.loadAtlas("items", "");
} else {
@ -240,11 +274,21 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con
this.getSprites().map((sprite, i) => {
if (!this.spriteConfigs[i].isItem) {
sprite.setTexture(this.spriteConfigs[i].spriteKey).setFrame(0);
if (sprite.texture.frameTotal > 1) {
// Show the first animation frame for a smooth transition when the animation starts.
const firstFrame = sprite.texture.frames["0001.png"];
sprite.setFrame(firstFrame ?? 0);
}
}
});
this.getTintSprites().map((tintSprite, i) => {
if (!this.spriteConfigs[i].isItem) {
tintSprite.setTexture(this.spriteConfigs[i].spriteKey).setFrame(0);
if (tintSprite.texture.frameTotal > 1) {
// Show the first frame for a smooth transition when the animation starts.
const firstFrame = tintSprite.texture.frames["0001.png"];
tintSprite.setFrame(firstFrame ?? 0);
}
}
});
@ -288,6 +332,17 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con
return true;
}
/**
* Play shiny sparkle animations if there are shiny Pokemon
*/
playShinySparkles() {
for (const sparkleConfig of this.shinySparkleSprites) {
this.scene.time.delayedCall(500, () => {
doShinySparkleAnim(this.scene, sparkleConfig.sprite, sparkleConfig.variant);
});
}
}
/**
* For sprites with animation and that do not have animation disabled, will begin frame animation
*/

View File

@ -23,7 +23,7 @@ import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "#app/data/balance/
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag, AutotomizedTag, PowerTrickTag } from "../data/battler-tags";
import { WeatherType } from "#enums/weather-type";
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag";
import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, AlliedFieldDamageReductionAbAttr, PostDamageAbAttr, applyPostDamageAbAttrs, PostDamageForceSwitchAbAttr, CommanderAbAttr, applyPostItemLostAbAttrs, PostItemLostAbAttr } from "#app/data/ability";
import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, AlliedFieldDamageReductionAbAttr, PostDamageAbAttr, applyPostDamageAbAttrs, CommanderAbAttr, applyPostItemLostAbAttrs, PostItemLostAbAttr } from "#app/data/ability";
import PokemonData from "#app/system/pokemon-data";
import { BattlerIndex } from "#app/battle";
import { Mode } from "#app/ui/ui";
@ -69,6 +69,7 @@ import { SpeciesFormKey } from "#enums/species-form-key";
import { BASE_HIDDEN_ABILITY_CHANCE, BASE_SHINY_CHANCE, SHINY_EPIC_CHANCE, SHINY_VARIANT_CHANCE } from "#app/data/balance/rates";
import { Nature } from "#enums/nature";
import { StatusEffect } from "#enums/status-effect";
import { doShinySparkleAnim } from "#app/field/anims";
export enum FieldPosition {
CENTER,
@ -325,6 +326,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (!this.scene) {
return false;
}
if (this.switchOutStatus) {
return false;
}
return this.scene.field.getIndex(this) > -1;
}
@ -670,21 +674,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
initShinySparkle(): void {
const keySuffix = this.variant ? `_${this.variant + 1}` : "";
const key = `shiny${keySuffix}`;
const shinySparkle = this.scene.addFieldSprite(0, 0, key);
const shinySparkle = this.scene.addFieldSprite(0, 0, "shiny");
shinySparkle.setVisible(false);
shinySparkle.setOrigin(0.5, 1);
const frameNames = this.scene.anims.generateFrameNames(key, { suffix: ".png", end: 34 });
if (!(this.scene.anims.exists(`sparkle${keySuffix}`))) {
this.scene.anims.create({
key: `sparkle${keySuffix}`,
frames: frameNames,
frameRate: 32,
showOnStart: true,
hideOnComplete: true,
});
}
this.add(shinySparkle);
this.shinySparkle = shinySparkle;
@ -1583,7 +1575,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
const trappedByAbility = new Utils.BooleanHolder(false);
const opposingField = this.isPlayer() ? this.scene.getEnemyField() : this.scene.getPlayerField();
/**
* Contains opposing Pokemon (Enemy/Player Pokemon) depending on perspective
* Afterwards, it filters out Pokemon that have been switched out of the field so trapped abilities/moves do not trigger
*/
const opposingFieldUnfiltered = this.isPlayer() ? this.scene.getEnemyField() : this.scene.getPlayerField();
const opposingField = opposingFieldUnfiltered.filter(enemyPkm => enemyPkm.switchOutStatus === false);
opposingField.forEach((opponent) =>
applyCheckTrappedAbAttrs(CheckTrappedAbAttr, opponent, trappedByAbility, this, trappedAbMessages, simulated)
@ -1968,6 +1965,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
/**
* Function that tries to set a Pokemon shiny based on seed.
* For manual use only, usually to roll a Pokemon's shiny chance a second time.
* If it rolls shiny, also sets a random variant and give the Pokemon the associated luck.
*
* The base shiny odds are {@linkcode BASE_SHINY_CHANCE} / `65536`
* @param thresholdOverride number that is divided by `2^16` (`65536`) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm)
@ -1993,6 +1991,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.shiny = randSeedInt(65536) < shinyThreshold.value;
if (this.shiny) {
this.variant = this.generateShinyVariant();
this.luck = this.variant + 1 + (this.fusionShiny ? this.fusionVariant + 1 : 0);
this.initShinySparkle();
}
@ -2618,8 +2618,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
};
}
// If the attack deals fixed damaged, return a result with that much damage
const fixedDamage = new Utils.IntegerHolder(0);
// If the attack deals fixed damage, return a result with that much damage
const fixedDamage = new Utils.NumberHolder(0);
applyMoveAttrs(FixedDamageAttr, source, this, move, fixedDamage);
if (fixedDamage.value) {
const multiLensMultiplier = new Utils.NumberHolder(1);
@ -2896,14 +2896,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.turnData.damageTaken += damage;
this.battleData.hitCount++;
// Multi-Lens and Parental Bond check for Wimp Out/Emergency Exit
if (this.hasAbilityWithAttr(PostDamageForceSwitchAbAttr)) {
const multiHitModifier = source.getHeldItems().find(m => m instanceof PokemonMultiHitModifier);
if (multiHitModifier || source.hasAbilityWithAttr(AddSecondStrikeAbAttr)) {
applyPostDamageAbAttrs(PostDamageAbAttr, this, damage, this.hasPassive(), false, [], source);
}
}
const attackResult = { move: move.id, result: result as DamageResult, damage: damage, critical: isCritical, sourceId: source.id, sourceBattlerIndex: source.getBattlerIndex() };
this.turnData.attacksReceived.unshift(attackResult);
if (source.isPlayer() && !this.isPlayer()) {
@ -3004,13 +2996,22 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* @param ignoreFaintPhase boolean to ignore adding a FaintPhase, passsed to damage()
* @returns integer of damage done
*/
damageAndUpdate(damage: integer, result?: DamageResult, critical: boolean = false, ignoreSegments: boolean = false, preventEndure: boolean = false, ignoreFaintPhase: boolean = false, source?: Pokemon): integer {
damageAndUpdate(damage: number, result?: DamageResult, critical: boolean = false, ignoreSegments: boolean = false, preventEndure: boolean = false, ignoreFaintPhase: boolean = false, source?: Pokemon): number {
const damagePhase = new DamageAnimPhase(this.scene, this.getBattlerIndex(), damage, result as DamageResult, critical);
this.scene.unshiftPhase(damagePhase);
if (this.switchOutStatus && source) {
damage = 0;
}
damage = this.damage(damage, ignoreSegments, preventEndure, ignoreFaintPhase);
// Damage amount may have changed, but needed to be queued before calling damage function
damagePhase.updateAmount(damage);
/**
* Run PostDamageAbAttr from any source of damage that is not from a multi-hit
* Multi-hits are handled in move-effect-phase.ts for PostDamageAbAttr
*/
if (!source || source.turnData.hitCount <= 1) {
applyPostDamageAbAttrs(PostDamageAbAttr, this, damage, this.hasPassive(), false, [], source);
}
return damage;
}
@ -3793,8 +3794,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
sparkle(): void {
if (this.shinySparkle) {
this.shinySparkle.play(`sparkle${this.variant ? `_${this.variant + 1}` : ""}`);
this.scene.playSound("se/sparkle");
doShinySparkleAnim(this.scene, this.shinySparkle, this.variant);
}
}
@ -4637,12 +4637,13 @@ export class EnemyPokemon extends Pokemon {
public aiType: AiType;
public bossSegments: integer;
public bossSegmentIndex: integer;
/** To indicate of the instance was populated with a dataSource -> e.g. loaded & populated from session data */
/** To indicate if the instance was populated with a dataSource -> e.g. loaded & populated from session data */
public readonly isPopulatedFromDataSource: boolean;
constructor(scene: BattleScene, species: PokemonSpecies, level: integer, trainerSlot: TrainerSlot, boss: boolean, dataSource?: PokemonData) {
super(scene, 236, 84, species, level, dataSource?.abilityIndex, dataSource?.formIndex,
dataSource?.gender, dataSource ? dataSource.shiny : false, dataSource ? dataSource.variant : undefined, undefined, dataSource ? dataSource.nature : undefined, dataSource);
constructor(scene: BattleScene, species: PokemonSpecies, level: integer, trainerSlot: TrainerSlot, boss: boolean, shinyLock: boolean = false, dataSource?: PokemonData) {
super(scene, 236, 84, species, level, dataSource?.abilityIndex, dataSource?.formIndex, dataSource?.gender,
(!shinyLock && dataSource) ? dataSource.shiny : false, (!shinyLock && dataSource) ? dataSource.variant : undefined,
undefined, dataSource ? dataSource.nature : undefined, dataSource);
this.trainerSlot = trainerSlot;
this.isPopulatedFromDataSource = !!dataSource; // if a dataSource is provided, then it was populated from dataSource
@ -4671,12 +4672,15 @@ export class EnemyPokemon extends Pokemon {
if (!dataSource) {
this.generateAndPopulateMoveset();
if (shinyLock || Overrides.OPP_SHINY_OVERRIDE === false) {
this.shiny = false;
} else {
this.trySetShiny();
if (Overrides.OPP_SHINY_OVERRIDE) {
}
if (!this.shiny && Overrides.OPP_SHINY_OVERRIDE) {
this.shiny = true;
this.initShinySparkle();
} else if (Overrides.OPP_SHINY_OVERRIDE === false) {
this.shiny = false;
}
if (this.shiny) {

View File

@ -1702,7 +1702,8 @@ const modifierPool: ModifierPool = {
new WeightedModifierType(modifierTypes.EVOLUTION_ITEM, (party: Pokemon[]) => {
return Math.min(Math.ceil(party[0].scene.currentBattle.waveIndex / 15), 8);
}, 8),
new WeightedModifierType(modifierTypes.MAP, (party: Pokemon[]) => party[0].scene.gameMode.isClassic && party[0].scene.currentBattle.waveIndex < 180 ? 1 : 0, 1),
new WeightedModifierType(modifierTypes.MAP, (party: Pokemon[]) => party[0].scene.gameMode.isClassic && party[0].scene.currentBattle.waveIndex < 180 ? 2 : 0, 2),
new WeightedModifierType(modifierTypes.SOOTHE_BELL, 2),
new WeightedModifierType(modifierTypes.TM_GREAT, 3),
new WeightedModifierType(modifierTypes.MEMORY_MUSHROOM, (party: Pokemon[]) => {
if (!party.find(p => p.getLearnableLevelMoves().length)) {
@ -1730,8 +1731,14 @@ const modifierPool: ModifierPool = {
new WeightedModifierType(modifierTypes.EVIOLITE, (party: Pokemon[]) => {
const { gameMode, gameData } = party[0].scene;
if (gameMode.isDaily || (!gameMode.isFreshStartChallenge() && gameData.isUnlocked(Unlockables.EVIOLITE))) {
return party.some(p => ((p.getSpeciesForm(true).speciesId in pokemonEvolutions) || (p.isFusion() && (p.getFusionSpeciesForm(true).speciesId in pokemonEvolutions)))
&& !p.getHeldItems().some(i => i instanceof EvolutionStatBoosterModifier) && !p.isMax()) ? 10 : 0;
return party.some(p => {
// Check if Pokemon's species (or fusion species, if applicable) can evolve or if they're G-Max'd
if (!p.isMax() && ((p.getSpeciesForm(true).speciesId in pokemonEvolutions) || (p.isFusion() && (p.getFusionSpeciesForm(true).speciesId in pokemonEvolutions)))) {
// Check if Pokemon is already holding an Eviolite
return !p.getHeldItems().some(i => i.type.id === "EVIOLITE");
}
return false;
}) ? 10 : 0;
}
return 0;
}),
@ -1794,7 +1801,6 @@ const modifierPool: ModifierPool = {
new WeightedModifierType(modifierTypes.SOUL_DEW, 7),
//new WeightedModifierType(modifierTypes.OVAL_CHARM, 6),
new WeightedModifierType(modifierTypes.CATCHING_CHARM, (party: Pokemon[]) => !party[0].scene.gameMode.isFreshStartChallenge() && party[0].scene.gameData.getSpeciesCount(d => !!d.caughtAttr) > 100 ? 4 : 0, 4),
new WeightedModifierType(modifierTypes.SOOTHE_BELL, 4),
new WeightedModifierType(modifierTypes.ABILITY_CHARM, skipInClassicAfterWave(189, 6)),
new WeightedModifierType(modifierTypes.FOCUS_BAND, 5),
new WeightedModifierType(modifierTypes.KINGS_ROCK, 3),

View File

@ -18,7 +18,6 @@ import type { VoucherType } from "#app/system/voucher";
import { Command } from "#app/ui/command-ui-handler";
import { addTextObject, TextStyle } from "#app/ui/text";
import { BooleanHolder, hslToHex, isNullOrUndefined, NumberHolder, toDmgValue } from "#app/utils";
import { Abilities } from "#enums/abilities";
import { BattlerTagType } from "#enums/battler-tag-type";
import { BerryType } from "#enums/berry-type";
import { Moves } from "#enums/moves";
@ -726,22 +725,6 @@ export abstract class PokemonHeldItemModifier extends PersistentModifier {
return 1;
}
//Applies to items with chance of activating secondary effects ie Kings Rock
getSecondaryChanceMultiplier(pokemon: Pokemon): number {
// Temporary quickfix to stop game from freezing when the opponet uses u-turn while holding on to king's rock
if (!pokemon.getLastXMoves()[0]) {
return 1;
}
const sheerForceAffected = allMoves[pokemon.getLastXMoves()[0].move].chance >= 0 && pokemon.hasAbility(Abilities.SHEER_FORCE);
if (sheerForceAffected) {
return 0;
} else if (pokemon.hasAbility(Abilities.SERENE_GRACE)) {
return 2;
}
return 1;
}
getMaxStackCount(scene: BattleScene, forThreshold?: boolean): number {
const pokemon = this.getPokemon(scene);
if (!pokemon) {
@ -1614,9 +1597,16 @@ export class BypassSpeedChanceModifier extends PokemonHeldItemModifier {
}
}
/**
* Class for Pokemon held items like King's Rock
* Because King's Rock can be stacked in PokeRogue, unlike mainline, it does not receive a boost from Abilities.SERENE_GRACE
*/
export class FlinchChanceModifier extends PokemonHeldItemModifier {
private chance: number;
constructor(type: ModifierType, pokemonId: number, stackCount?: number) {
super(type, pokemonId, stackCount);
this.chance = 10;
}
matchType(modifier: Modifier) {
@ -1644,7 +1634,8 @@ export class FlinchChanceModifier extends PokemonHeldItemModifier {
* @returns `true` if {@linkcode FlinchChanceModifier} has been applied
*/
override apply(pokemon: Pokemon, flinched: BooleanHolder): boolean {
if (!flinched.value && pokemon.randSeedInt(10) < (this.getStackCount() * this.getSecondaryChanceMultiplier(pokemon))) {
// The check for pokemon.battleSummonData is to ensure that a crash doesn't occur when a Pokemon with King's Rock procs a flinch
if (pokemon.battleSummonData && !flinched.value && pokemon.randSeedInt(100) < (this.getStackCount() * this.chance)) {
flinched.value = true;
return true;
}
@ -1652,7 +1643,7 @@ export class FlinchChanceModifier extends PokemonHeldItemModifier {
return false;
}
getMaxHeldItemCount(pokemon: Pokemon): number {
getMaxHeldItemCount(_pokemon: Pokemon): number {
return 3;
}
}

View File

@ -86,6 +86,8 @@ class DefaultOverrides {
readonly ITEM_UNLOCK_OVERRIDE: Unlockables[] = [];
/** Set to `true` to show all tutorials */
readonly BYPASS_TUTORIAL_SKIP_OVERRIDE: boolean = false;
/** Set to `true` to be able to re-earn already unlocked achievements */
readonly ACHIEVEMENTS_REUNLOCK_OVERRIDE: boolean = false;
/** Set to `true` to force Paralysis and Freeze to always activate, or `false` to force them to not activate */
readonly STATUS_ACTIVATION_OVERRIDE: boolean | null = null;

View File

@ -52,7 +52,7 @@ export class AttemptRunPhase extends PokemonPhase {
enemyPokemon.trySetStatus(StatusEffect.FAINT);
});
this.scene.pushPhase(new BattleEndPhase(this.scene));
this.scene.pushPhase(new BattleEndPhase(this.scene, false));
this.scene.pushPhase(new NewBattlePhase(this.scene));
} else {
playerPokemon.turnData.failedRunAway = true;

View File

@ -8,7 +8,7 @@ export class BattleEndPhase extends BattlePhase {
/** If true, will increment battles won */
isVictory: boolean;
constructor(scene: BattleScene, isVictory: boolean = true) {
constructor(scene: BattleScene, isVictory: boolean) {
super(scene);
this.isVictory = isVictory;
@ -17,16 +17,17 @@ export class BattleEndPhase extends BattlePhase {
start() {
super.start();
this.scene.gameData.gameStats.battles++;
if (this.scene.gameMode.isEndless && this.scene.currentBattle.waveIndex + 1 > this.scene.gameData.gameStats.highestEndlessWave) {
this.scene.gameData.gameStats.highestEndlessWave = this.scene.currentBattle.waveIndex + 1;
}
if (this.isVictory) {
this.scene.currentBattle.addBattleScore(this.scene);
this.scene.gameData.gameStats.battles++;
if (this.scene.currentBattle.trainer) {
this.scene.gameData.gameStats.trainersDefeated++;
}
if (this.scene.gameMode.isEndless && this.scene.currentBattle.waveIndex + 1 > this.scene.gameData.gameStats.highestEndlessWave) {
this.scene.gameData.gameStats.highestEndlessWave = this.scene.currentBattle.waveIndex + 1;
}
}
// Endless graceful end
@ -42,7 +43,7 @@ export class BattleEndPhase extends BattlePhase {
}
for (const pokemon of this.scene.getPokemonAllowedInBattle()) {
applyPostBattleAbAttrs(PostBattleAbAttr, pokemon);
applyPostBattleAbAttrs(PostBattleAbAttr, pokemon, false, this.isVictory);
}
if (this.scene.currentBattle.moneyScattered) {

View File

@ -5,7 +5,6 @@ import { getPokemonNameWithAffix } from "#app/messages";
import { Mode } from "#app/ui/ui";
import i18next from "i18next";
import { BattlePhase } from "./battle-phase";
import { PostSummonPhase } from "./post-summon-phase";
import { SummonMissingPhase } from "./summon-missing-phase";
import { SwitchPhase } from "./switch-phase";
import { SwitchType } from "#enums/switch-type";
@ -54,7 +53,6 @@ export class CheckSwitchPhase extends BattlePhase {
this.scene.ui.showText(i18next.t("battle:switchQuestion", { pokemonName: this.useName ? getPokemonNameWithAffix(pokemon) : i18next.t("battle:pokemon") }), null, () => {
this.scene.ui.setMode(Mode.CONFIRM, () => {
this.scene.ui.setMode(Mode.MESSAGE);
this.scene.tryRemovePhase(p => p instanceof PostSummonPhase && p.player && p.fieldIndex === this.fieldIndex);
this.scene.unshiftPhase(new SwitchPhase(this.scene, SwitchType.INITIAL_SWITCH, this.fieldIndex, false, true));
this.end();
}, () => {

View File

@ -14,6 +14,7 @@ import SoundFade from "phaser3-rex-plugins/plugins/soundfade";
import * as Utils from "#app/utils";
import { EggLapsePhase } from "./egg-lapse-phase";
import { EggHatchData } from "#app/data/egg-hatch-data";
import { doShinySparkleAnim } from "#app/field/anims";
/**
@ -341,8 +342,7 @@ export class EggHatchPhase extends Phase {
this.pokemon.cry();
if (isShiny) {
this.scene.time.delayedCall(Utils.fixedInt(500), () => {
this.pokemonShinySparkle.play(`sparkle${this.pokemon.variant ? `_${this.pokemon.variant + 1}` : ""}`);
this.scene.playSound("se/sparkle");
doShinySparkleAnim(this.scene, this.pokemonShinySparkle, this.pokemon.variant);
});
}
this.scene.time.delayedCall(Utils.fixedInt(!this.skipped ? !isShiny ? 1250 : 1750 : !isShiny ? 250 : 750), () => {

View File

@ -34,7 +34,7 @@ export class EggLapsePhase extends Phase {
if (eggsToHatchCount >= this.minEggsToSkip && this.scene.eggSkipPreference === 1) {
this.scene.ui.showText(i18next.t("battle:eggHatching"), 0, () => {
// show prompt for skip, blocking inputs for 1 second
this.scene.ui.showText(i18next.t("battle:eggSkipPrompt"), 0);
this.scene.ui.showText(i18next.t("battle:eggSkipPrompt", { eggsToHatch: eggsToHatchCount }), 0);
this.scene.ui.setModeWithoutClear(Mode.CONFIRM, () => {
this.hatchEggsSkipped(eggsToHatch);
this.showSummary();

View File

@ -34,6 +34,7 @@ import { Biome } from "#enums/biome";
import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
import { PlayerGender } from "#enums/player-gender";
import { Species } from "#enums/species";
import { overrideHeldItems, overrideModifiers } from "#app/modifier/modifier";
import i18next from "i18next";
import { WEIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/mystery-encounters";
@ -216,6 +217,11 @@ export class EncounterPhase extends BattlePhase {
if (!this.loaded && battle.battleType !== BattleType.MYSTERY_ENCOUNTER) {
regenerateModifierPoolThresholds(this.scene.getEnemyField(), battle.battleType === BattleType.TRAINER ? ModifierPoolType.TRAINER : ModifierPoolType.WILD);
this.scene.generateEnemyModifiers();
overrideModifiers(this.scene, false);
this.scene.getEnemyField().forEach(enemy => {
overrideHeldItems(this.scene, enemy, false);
});
}
this.scene.ui.setMode(Mode.MESSAGE).then(() => {
@ -379,6 +385,9 @@ export class EncounterPhase extends BattlePhase {
if (encounter.onVisualsStart) {
encounter.onVisualsStart(this.scene);
} else if (encounter.spriteConfigs && introVisuals) {
// If the encounter doesn't have any special visual intro, show sparkle for shiny Pokemon
introVisuals.playShinySparkles();
}
const doEncounter = () => {

View File

@ -4,11 +4,13 @@ import {
AddSecondStrikeAbAttr,
AlwaysHitAbAttr,
applyPostAttackAbAttrs,
applyPostDamageAbAttrs,
applyPostDefendAbAttrs,
applyPreAttackAbAttrs,
IgnoreMoveEffectsAbAttr,
MaxMultiHitAbAttr,
PostAttackAbAttr,
PostDamageAbAttr,
PostDefendAbAttr,
TypeImmunityAbAttr,
} from "#app/data/ability";
@ -228,9 +230,11 @@ export class MoveEffectPhase extends PokemonPhase {
* If the move missed a target, stop all future hits against that target
* and move on to the next target (if there is one).
*/
if (isCommanding || (!isImmune && !isProtected && !targetHitChecks[target.getBattlerIndex()])) {
if (target.switchOutStatus || isCommanding || (!isImmune && !isProtected && !targetHitChecks[target.getBattlerIndex()])) {
this.stopMultiHit(target);
if (!target.switchOutStatus) {
this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: getPokemonNameWithAffix(target) }));
}
if (moveHistoryEntry.result === MoveResult.PENDING) {
moveHistoryEntry.result = MoveResult.MISS;
}
@ -299,6 +303,13 @@ export class MoveEffectPhase extends PokemonPhase {
*/
if (lastHit) {
this.scene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger);
/**
* Multi-Lens, Multi Hit move and Parental Bond check for PostDamageAbAttr
* other damage source are calculated in damageAndUpdate in pokemon.ts
*/
if (user.turnData.hitCount > 1) {
applyPostDamageAbAttrs(PostDamageAbAttr, target, 0, target.hasPassive(), false, [], user);
}
}
/**

View File

@ -27,9 +27,12 @@ export class PostSummonPhase extends PokemonPhase {
pokemon.lapseTag(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON);
}
applyPostSummonAbAttrs(PostSummonAbAttr, pokemon).then(() => this.end());
applyPostSummonAbAttrs(PostSummonAbAttr, pokemon)
.then(() => {
const field = pokemon.isPlayer() ? this.scene.getPlayerField() : this.scene.getEnemyField();
field.forEach((p) => applyAbAttrs(CommanderAbAttr, p, null, false));
this.end();
});
}
}

View File

@ -16,7 +16,7 @@ export class PostTurnStatusEffectPhase extends PokemonPhase {
start() {
const pokemon = this.getPokemon();
if (pokemon?.isActive(true) && pokemon.status && pokemon.status.isPostTurn()) {
if (pokemon?.isActive(true) && pokemon.status && pokemon.status.isPostTurn() && !pokemon.switchOutStatus) {
pokemon.status.incrementTurn();
const cancelled = new Utils.BooleanHolder(false);
applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled);

View File

@ -3,6 +3,7 @@ import PartyUiHandler, { PartyOption, PartyUiMode } from "#app/ui/party-ui-handl
import { Mode } from "#app/ui/ui";
import { SwitchType } from "#enums/switch-type";
import { BattlePhase } from "./battle-phase";
import { PostSummonPhase } from "./post-summon-phase";
import { SwitchSummonPhase } from "./switch-summon-phase";
/**
@ -63,6 +64,9 @@ export class SwitchPhase extends BattlePhase {
this.scene.ui.setMode(Mode.PARTY, this.isModal ? PartyUiMode.FAINT_SWITCH : PartyUiMode.POST_BATTLE_SWITCH, fieldIndex, (slotIndex: integer, option: PartyOption) => {
if (slotIndex >= this.scene.currentBattle.getBattlerCount() && slotIndex < 6) {
// Remove any pre-existing PostSummonPhase under the same field index.
// Pre-existing PostSummonPhases may occur when this phase is invoked during a prompt to switch at the start of a wave.
this.scene.tryRemovePhase(p => p instanceof PostSummonPhase && p.player && p.fieldIndex === this.fieldIndex);
const switchType = (option === PartyOption.PASS_BATON) ? SwitchType.BATON_PASS : this.switchType;
this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, switchType, fieldIndex, slotIndex, this.doReturn));
}

View File

@ -1,5 +1,5 @@
import BattleScene from "#app/battle-scene";
import { applyPreSwitchOutAbAttrs, PreSwitchOutAbAttr } from "#app/data/ability";
import { applyPreSwitchOutAbAttrs, PostDamageForceSwitchAbAttr, PreSwitchOutAbAttr } from "#app/data/ability";
import { allMoves, ForceSwitchOutAttr } from "#app/data/move";
import { getPokeballTintColor } from "#app/data/pokeball";
import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms";
@ -166,10 +166,11 @@ export class SwitchSummonPhase extends SummonPhase {
const currentCommand = pokemon.scene.currentBattle.turnCommands[this.fieldIndex]?.command;
const lastPokemonIsForceSwitchedAndNotFainted = lastUsedMove?.hasAttr(ForceSwitchOutAttr) && !this.lastPokemon.isFainted();
const lastPokemonHasForceSwitchAbAttr = this.lastPokemon.hasAbilityWithAttr(PostDamageForceSwitchAbAttr) && !this.lastPokemon.isFainted();
// Compensate for turn spent summoning
// Or compensate for force switch move if switched out pokemon is not fainted
if (currentCommand === Command.POKEMON || lastPokemonIsForceSwitchedAndNotFainted) {
if (currentCommand === Command.POKEMON || lastPokemonIsForceSwitchedAndNotFainted || lastPokemonHasForceSwitchAbAttr) {
pokemon.battleSummonData.turnCount--;
pokemon.battleSummonData.waveTurnCount--;
}

View File

@ -9,6 +9,8 @@ import { BattlePhase } from "./battle-phase";
import { ModifierRewardPhase } from "./modifier-reward-phase";
import { MoneyRewardPhase } from "./money-reward-phase";
import { TrainerSlot } from "#app/data/trainer-config";
import { Biome } from "#app/enums/biome";
import { achvs } from "#app/system/achv";
export class TrainerVictoryPhase extends BattlePhase {
constructor(scene: BattleScene) {
@ -34,11 +36,17 @@ export class TrainerVictoryPhase extends BattlePhase {
}
const trainerType = this.scene.currentBattle.trainer?.config.trainerType!; // TODO: is this bang correct?
// Validate Voucher for boss trainers
if (vouchers.hasOwnProperty(TrainerType[trainerType])) {
if (!this.scene.validateVoucher(vouchers[TrainerType[trainerType]]) && this.scene.currentBattle.trainer?.config.isBoss) {
this.scene.unshiftPhase(new ModifierRewardPhase(this.scene, [ modifierTypes.VOUCHER, modifierTypes.VOUCHER, modifierTypes.VOUCHER_PLUS, modifierTypes.VOUCHER_PREMIUM ][vouchers[TrainerType[trainerType]].voucherType]));
}
}
// Breeders in Space achievement
if (this.scene.arena.biomeType === Biome.SPACE
&& (trainerType === TrainerType.BREEDER || trainerType === TrainerType.EXPERT_POKEMON_BREEDER)) {
this.scene.validateAchv(achvs.BREEDERS_IN_SPACE);
}
this.scene.ui.showText(i18next.t("battle:trainerDefeated", { trainerName: this.scene.currentBattle.trainer?.getName(TrainerSlot.NONE, true) }), null, () => {
const victoryMessages = this.scene.currentBattle.trainer?.getVictoryMessages()!; // TODO: is this bang correct?

View File

@ -23,6 +23,7 @@ export class TurnEndPhase extends FieldPhase {
this.scene.eventTarget.dispatchEvent(new TurnEndEvent(this.scene.currentBattle.turn));
const handlePokemon = (pokemon: Pokemon) => {
if (!pokemon.switchOutStatus) {
pokemon.lapseTags(BattlerTagLapseType.TURN_END);
this.scene.applyModifiers(TurnHealModifier, pokemon.isPlayer(), pokemon);
@ -38,6 +39,7 @@ export class TurnEndPhase extends FieldPhase {
}
applyPostTurnAbAttrs(PostTurnAbAttr, pokemon);
}
this.scene.applyModifiers(TurnStatusEffectModifier, pokemon.isPlayer(), pokemon);

View File

@ -41,7 +41,7 @@ export class VictoryPhase extends PokemonPhase {
}
if (!this.scene.getEnemyParty().find(p => this.scene.currentBattle.battleType === BattleType.WILD ? p.isOnField() : !p?.isFainted(true))) {
this.scene.pushPhase(new BattleEndPhase(this.scene));
this.scene.pushPhase(new BattleEndPhase(this.scene, true));
if (this.scene.currentBattle.battleType === BattleType.TRAINER) {
this.scene.pushPhase(new TrainerVictoryPhase(this.scene));
}

View File

@ -51,7 +51,7 @@ export class WeatherEffectPhase extends CommonAnimPhase {
};
this.executeForAll((pokemon: Pokemon) => {
const immune = !pokemon || !!pokemon.getTypes(true, true).filter(t => this.weather?.isTypeDamageImmune(t)).length;
const immune = !pokemon || !!pokemon.getTypes(true, true).filter(t => this.weather?.isTypeDamageImmune(t)).length || pokemon.switchOutStatus;
if (!immune) {
inflictDamage(pokemon);
}
@ -59,8 +59,12 @@ export class WeatherEffectPhase extends CommonAnimPhase {
}
}
this.scene.ui.showText(getWeatherLapseMessage(this.weather.weatherType)!, null, () => { // TODO: is this bang correct?
this.executeForAll((pokemon: Pokemon) => applyPostWeatherLapseAbAttrs(PostWeatherLapseAbAttr, pokemon, this.weather));
this.scene.ui.showText(getWeatherLapseMessage(this.weather.weatherType) ?? "", null, () => {
this.executeForAll((pokemon: Pokemon) => {
if (!pokemon.switchOutStatus) {
applyPostWeatherLapseAbAttrs(PostWeatherLapseAbAttr, pokemon, this.weather);
}
});
super.start();
});

View File

@ -358,7 +358,7 @@ export const achvs = {
MONO_FAIRY: new ChallengeAchv("MONO_FAIRY", "", "MONO_FAIRY.description", "fairy_feather", 100, (c, scene) => c instanceof SingleTypeChallenge && c.value === 18 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
FRESH_START: new ChallengeAchv("FRESH_START", "", "FRESH_START.description", "reviver_seed", 100, (c, scene) => c instanceof FreshStartChallenge && c.value > 0 && !scene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0)),
INVERSE_BATTLE: new ChallengeAchv("INVERSE_BATTLE", "", "INVERSE_BATTLE.description", "inverse", 100, c => c instanceof InverseBattleChallenge && c.value > 0),
BREEDERS_IN_SPACE: new Achv("BREEDERS_IN_SPACE", "", "BREEDERS_IN_SPACE.description", "moon_stone", 100).setSecret(),
BREEDERS_IN_SPACE: new Achv("BREEDERS_IN_SPACE", "", "BREEDERS_IN_SPACE.description", "moon_stone", 50).setSecret(),
};
export function initAchievements() {

View File

@ -171,7 +171,7 @@ export default class PokemonData {
playerPokemon.nickname = this.nickname;
}
})
: scene.addEnemyPokemon(species, this.level, battleType === BattleType.TRAINER ? !double || !(partyMemberIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER : TrainerSlot.NONE, this.boss, this);
: scene.addEnemyPokemon(species, this.level, battleType === BattleType.TRAINER ? !double || !(partyMemberIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER : TrainerSlot.NONE, this.boss, false, this);
if (this.summonData) {
ret.primeSummonData(this.summonData);
}

View File

@ -1,9 +1,9 @@
import { Mode } from "#app/ui/ui";
import i18next from "i18next";
import BattleScene from "../../battle-scene";
import { hasTouchscreen } from "../../touch-controls";
import { updateWindowType } from "../../ui/ui-theme";
import { CandyUpgradeNotificationChangedEvent } from "../../events/battle-scene";
import BattleScene from "#app/battle-scene";
import { hasTouchscreen } from "#app/touch-controls";
import { updateWindowType } from "#app/ui/ui-theme";
import { CandyUpgradeNotificationChangedEvent } from "#app/events/battle-scene";
import SettingsUiHandler from "#app/ui/settings/settings-ui-handler";
import { EaseType } from "#enums/ease-type";
import { MoneyFormat } from "#enums/money-format";
@ -44,6 +44,7 @@ const OFF_ON: SettingOption[] = [
label: i18next.t("settings:on")
}
];
const AUTO_DISABLED: SettingOption[] = [
{
value: "Auto",
@ -55,6 +56,19 @@ const AUTO_DISABLED: SettingOption[] = [
}
];
const TOUCH_CONTROLS_OPTIONS: SettingOption[] = [
{
value: "Auto",
label: i18next.t("settings:auto")
},
{
value: "Disabled",
label: i18next.t("settings:disabled"),
needConfirmation: true,
confirmationMessage: i18next.t("settings:confirmDisableTouch")
}
];
const SHOP_CURSOR_TARGET_OPTIONS: SettingOption[] = [
{
value: "Rewards",
@ -100,7 +114,9 @@ export enum SettingType {
type SettingOption = {
value: string,
label: string
label: string,
needConfirmation?: boolean,
confirmationMessage?: string
};
export interface Setting {
@ -344,13 +360,6 @@ export const Setting: Array<Setting> = [
default: 1,
type: SettingType.GENERAL
},
{
key: SettingKeys.Touch_Controls,
label: i18next.t("settings:touchControls"),
options: AUTO_DISABLED,
default: 0,
type: SettingType.GENERAL
},
{
key: SettingKeys.Vibration,
label: i18next.t("settings:vibrations"),
@ -358,6 +367,28 @@ export const Setting: Array<Setting> = [
default: 0,
type: SettingType.GENERAL
},
{
key: SettingKeys.Touch_Controls,
label: i18next.t("settings:touchControls"),
options: TOUCH_CONTROLS_OPTIONS,
default: 0,
type: SettingType.GENERAL,
isHidden: () => !hasTouchscreen()
},
{
key: SettingKeys.Move_Touch_Controls,
label: i18next.t("settings:moveTouchControls"),
options: [
{
value: "Configure",
label: i18next.t("settings:change")
}
],
default: 0,
type: SettingType.GENERAL,
activatable: true,
isHidden: () => !hasTouchscreen()
},
{
key: SettingKeys.Language,
label: i18next.t("settings:language"),
@ -643,20 +674,6 @@ export const Setting: Array<Setting> = [
type: SettingType.AUDIO,
requireReload: true
},
{
key: SettingKeys.Move_Touch_Controls,
label: i18next.t("settings:moveTouchControls"),
options: [
{
value: "Configure",
label: i18next.t("settings:change")
}
],
default: 0,
type: SettingType.GENERAL,
activatable: true,
isHidden: () => !hasTouchscreen()
},
{
key: SettingKeys.Shop_Cursor_Target,
label: i18next.t("settings:shopCursorTarget"),
@ -849,7 +866,7 @@ export function setSetting(scene: BattleScene, setting: string, value: integer):
if (scene.ui) {
const cancelHandler = () => {
scene.ui.revertMode();
(scene.ui.getHandler() as SettingsUiHandler).setOptionCursor(0, 0, true);
(scene.ui.getHandler() as SettingsUiHandler).setOptionCursor(-1, 0, true);
};
const changeLocaleHandler = (locale: string): boolean => {
try {

View File

@ -0,0 +1,81 @@
import { BattlerIndex } from "#app/battle";
import { isBetween, toDmgValue } from "#app/utils";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Abilities - Analytic", () => {
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
.moveset([ Moves.SPLASH, Moves.TACKLE ])
.ability(Abilities.ANALYTIC)
.battleType("single")
.disableCrits()
.startingLevel(200)
.enemyLevel(200)
.enemySpecies(Species.SNORLAX)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("should increase damage if the user moves last", async () => {
await game.classicMode.startBattle([ Species.ARCEUS ]);
const enemy = game.scene.getEnemyPokemon()!;
game.move.select(Moves.TACKLE);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.toNextTurn();
const damage1 = enemy.getInverseHp();
enemy.hp = enemy.getMaxHp();
game.move.select(Moves.TACKLE);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.phaseInterceptor.to("BerryPhase");
expect(isBetween(enemy.getInverseHp(), toDmgValue(damage1 * 1.3) - 3, toDmgValue(damage1 * 1.3) + 3)).toBe(true);
});
it("should increase damage only if the user moves last in doubles", async () => {
game.override.battleType("double");
await game.classicMode.startBattle([ Species.GENGAR, Species.SHUCKLE ]);
const [ enemy, ] = game.scene.getEnemyField();
game.move.select(Moves.TACKLE, 0, BattlerIndex.ENEMY);
game.move.select(Moves.SPLASH, 1);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]);
await game.toNextTurn();
const damage1 = enemy.getInverseHp();
enemy.hp = enemy.getMaxHp();
game.move.select(Moves.TACKLE, 0, BattlerIndex.ENEMY);
game.move.select(Moves.SPLASH, 1);
await game.setTurnOrder([ BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER ]);
await game.toNextTurn();
expect(isBetween(enemy.getInverseHp(), toDmgValue(damage1 * 1.3) - 3, toDmgValue(damage1 * 1.3) + 3)).toBe(true);
enemy.hp = enemy.getMaxHp();
game.move.select(Moves.TACKLE, 0, BattlerIndex.ENEMY);
game.move.select(Moves.SPLASH, 1);
await game.setTurnOrder([ BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.PLAYER, BattlerIndex.ENEMY_2 ]);
await game.phaseInterceptor.to("BerryPhase");
expect(enemy.getInverseHp()).toBe(damage1);
});
});

View File

@ -1,9 +1,10 @@
import { allAbilities } from "#app/data/ability";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, it, expect } from "vitest";
import { afterEach, beforeAll, beforeEach, describe, it, expect, vi } from "vitest";
describe("Abilities - Arena Trap", () => {
let phaserGame: Phaser.Game;
@ -55,4 +56,39 @@ describe("Abilities - Arena Trap", () => {
expect(game.scene.getEnemyField().length).toBe(2);
});
/**
* This checks if the Player Pokemon is able to switch out/run away after the Enemy Pokemon with {@linkcode Abilities.ARENA_TRAP}
* is forcefully moved out of the field from moves such as Roar {@linkcode Moves.ROAR}
*
* Note: It should be able to switch out/run away
*/
it("should lift if pokemon with this ability leaves the field", async () => {
game.override
.battleType("double")
.enemyMoveset(Moves.SPLASH)
.moveset([ Moves.ROAR, Moves.SPLASH ])
.ability(Abilities.BALL_FETCH);
await game.classicMode.startBattle([ Species.MAGIKARP, Species.SUDOWOODO, Species.LUNATONE ]);
const [ enemy1, enemy2 ] = game.scene.getEnemyField();
const [ player1, player2 ] = game.scene.getPlayerField();
vi.spyOn(enemy1, "getAbility").mockReturnValue(allAbilities[Abilities.ARENA_TRAP]);
game.move.select(Moves.ROAR);
game.move.select(Moves.SPLASH, 1);
// This runs the fist command phase where the moves are selected
await game.toNextTurn();
// During the next command phase the player pokemons should not be trapped anymore
game.move.select(Moves.SPLASH);
game.move.select(Moves.SPLASH, 1);
await game.toNextTurn();
expect(player1.isTrapped()).toBe(false);
expect(player2.isTrapped()).toBe(false);
expect(enemy1.isOnField()).toBe(false);
expect(enemy2.isOnField()).toBe(true);
});
});

View File

@ -0,0 +1,74 @@
import type { CommandPhase } from "#app/phases/command-phase";
import { Command } from "#app/ui/command-ui-handler";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
describe("Abilities - Honey Gather", () => {
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
.moveset([ Moves.SPLASH, Moves.ROAR, Moves.THUNDERBOLT ])
.startingLevel(100)
.ability(Abilities.HONEY_GATHER)
.passiveAbility(Abilities.RUN_AWAY)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("should give money when winning a battle", async () => {
await game.classicMode.startBattle([ Species.MILOTIC ]);
game.scene.money = 1000;
game.move.select(Moves.THUNDERBOLT);
await game.toNextWave();
expect(game.scene.money).toBeGreaterThan(1000);
});
it("should not give money when the enemy pokemon flees", async () => {
await game.classicMode.startBattle([ Species.MILOTIC ]);
game.scene.money = 1000;
game.move.select(Moves.ROAR);
await game.toNextTurn();
expect(game.scene.money).toBe(1000);
expect(game.scene.currentBattle.waveIndex).toBe(2);
});
it("should not give money when the player flees", async () => {
await game.classicMode.startBattle([ Species.MILOTIC ]);
game.scene.money = 1000;
// something weird is going on with the test framework, so this is required to prevent a crash
const enemy = game.scene.getEnemyPokemon()!;
vi.spyOn(enemy, "scene", "get").mockReturnValue(game.scene);
const commandPhase = game.scene.getCurrentPhase() as CommandPhase;
commandPhase.handleCommand(Command.RUN, 0);
await game.toNextTurn();
expect(game.scene.money).toBe(1000);
expect(game.scene.currentBattle.waveIndex).toBe(2);
});
});

View File

@ -1,15 +1,12 @@
import { BattlerIndex } from "#app/battle";
import { applyAbAttrs, MoveEffectChanceMultiplierAbAttr } from "#app/data/ability";
import { Stat } from "#enums/stat";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import * as Utils from "#app/utils";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { allMoves } from "#app/data/move";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { FlinchAttr } from "#app/data/move";
describe("Abilities - Serene Grace", () => {
let phaserGame: Phaser.Game;
@ -27,66 +24,26 @@ describe("Abilities - Serene Grace", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
const movesToUse = [ Moves.AIR_SLASH, Moves.TACKLE ];
game.override.battleType("single");
game.override.enemySpecies(Species.ONIX);
game.override.startingLevel(100);
game.override.moveset(movesToUse);
game.override.enemyMoveset([ Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE ]);
game.override
.battleType("single")
.ability(Abilities.SERENE_GRACE)
.moveset([ Moves.AIR_SLASH, Moves.TACKLE ])
.enemyLevel(10)
.enemyMoveset([ Moves.SPLASH ]);
});
it("Move chance without Serene Grace", async () => {
const moveToUse = Moves.AIR_SLASH;
await game.startBattle([
Species.PIDGEOT
]);
it("Serene Grace should double the secondary effect chance of a move", async () => {
await game.classicMode.startBattle([ Species.SHUCKLE ]);
const airSlashMove = allMoves[Moves.AIR_SLASH];
const airSlashFlinchAttr = airSlashMove.getAttrs(FlinchAttr)[0];
vi.spyOn(airSlashFlinchAttr, "getMoveChance");
game.scene.getEnemyParty()[0].stats[Stat.SPDEF] = 10000;
expect(game.scene.getPlayerParty()[0].formIndex).toBe(0);
game.move.select(moveToUse);
game.move.select(Moves.AIR_SLASH);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to(MoveEffectPhase, false);
await game.move.forceHit();
await game.phaseInterceptor.to("BerryPhase");
// Check chance of Air Slash without Serene Grace
const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
const move = phase.move.getMove();
expect(move.id).toBe(Moves.AIR_SLASH);
const chance = new Utils.IntegerHolder(move.chance);
console.log(move.chance + " Their ability is " + phase.getUserPokemon()!.getAbility().name);
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
expect(chance.value).toBe(30);
}, 20000);
it("Move chance with Serene Grace", async () => {
const moveToUse = Moves.AIR_SLASH;
game.override.ability(Abilities.SERENE_GRACE);
await game.startBattle([
Species.TOGEKISS
]);
game.scene.getEnemyParty()[0].stats[Stat.SPDEF] = 10000;
expect(game.scene.getPlayerParty()[0].formIndex).toBe(0);
game.move.select(moveToUse);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to(MoveEffectPhase, false);
// Check chance of Air Slash with Serene Grace
const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
const move = phase.move.getMove();
expect(move.id).toBe(Moves.AIR_SLASH);
const chance = new Utils.IntegerHolder(move.chance);
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
expect(chance.value).toBe(60);
}, 20000);
//TODO King's Rock Interaction Unit Test
expect(airSlashFlinchAttr.getMoveChance).toHaveLastReturnedWith(60);
});
});

View File

@ -1,15 +1,13 @@
import { BattlerIndex } from "#app/battle";
import { applyAbAttrs, applyPostDefendAbAttrs, applyPreAttackAbAttrs, MoveEffectChanceMultiplierAbAttr, MovePowerBoostAbAttr, PostDefendTypeChangeAbAttr } from "#app/data/ability";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import { NumberHolder } from "#app/utils";
import { Type } from "#app/enums/type";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { Stat } from "#enums/stat";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { allMoves } from "#app/data/move";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { allMoves, FlinchAttr } from "#app/data/move";
describe("Abilities - Sheer Force", () => {
let phaserGame: Phaser.Game;
@ -27,143 +25,91 @@ describe("Abilities - Sheer Force", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
const movesToUse = [ Moves.AIR_SLASH, Moves.BIND, Moves.CRUSH_CLAW, Moves.TACKLE ];
game.override.battleType("single");
game.override.enemySpecies(Species.ONIX);
game.override.startingLevel(100);
game.override.moveset(movesToUse);
game.override.enemyMoveset([ Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE ]);
game.override
.battleType("single")
.ability(Abilities.SHEER_FORCE)
.enemySpecies(Species.ONIX)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset([ Moves.SPLASH ])
.disableCrits();
});
it("Sheer Force", async () => {
const moveToUse = Moves.AIR_SLASH;
game.override.ability(Abilities.SHEER_FORCE);
const SHEER_FORCE_MULT = 5461 / 4096;
it("Sheer Force should boost the power of the move but disable secondary effects", async () => {
game.override.moveset([ Moves.AIR_SLASH ]);
await game.classicMode.startBattle([ Species.SHUCKLE ]);
const airSlashMove = allMoves[Moves.AIR_SLASH];
vi.spyOn(airSlashMove, "calculateBattlePower");
const airSlashFlinchAttr = airSlashMove.getAttrs(FlinchAttr)[0];
vi.spyOn(airSlashFlinchAttr, "getMoveChance");
game.move.select(Moves.AIR_SLASH);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.move.forceHit();
await game.phaseInterceptor.to("BerryPhase", false);
expect(airSlashMove.calculateBattlePower).toHaveLastReturnedWith(airSlashMove.power * SHEER_FORCE_MULT);
expect(airSlashFlinchAttr.getMoveChance).toHaveLastReturnedWith(0);
});
it("Sheer Force does not affect the base damage or secondary effects of binding moves", async () => {
game.override.moveset([ Moves.BIND ]);
await game.classicMode.startBattle([ Species.SHUCKLE ]);
const bindMove = allMoves[Moves.BIND];
vi.spyOn(bindMove, "calculateBattlePower");
game.move.select(Moves.BIND);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.move.forceHit();
await game.phaseInterceptor.to("BerryPhase", false);
expect(bindMove.calculateBattlePower).toHaveLastReturnedWith(bindMove.power);
}, 20000);
it("Sheer Force does not boost the base damage of moves with no secondary effect", async () => {
game.override.moveset([ Moves.TACKLE ]);
await game.classicMode.startBattle([ Species.PIDGEOT ]);
game.scene.getEnemyPokemon()!.stats[Stat.SPDEF] = 10000;
expect(game.scene.getPlayerPokemon()!.formIndex).toBe(0);
game.move.select(moveToUse);
const tackleMove = allMoves[Moves.TACKLE];
vi.spyOn(tackleMove, "calculateBattlePower");
game.move.select(Moves.TACKLE);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to(MoveEffectPhase, false);
await game.move.forceHit();
await game.phaseInterceptor.to("BerryPhase", false);
const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
const move = phase.move.getMove();
expect(move.id).toBe(Moves.AIR_SLASH);
expect(tackleMove.calculateBattlePower).toHaveLastReturnedWith(tackleMove.power);
});
//Verify the move is boosted and has no chance of secondary effects
const power = new NumberHolder(move.power);
const chance = new NumberHolder(move.chance);
it("Sheer Force can disable the on-hit activation of specific abilities", async () => {
game.override
.moveset([ Moves.HEADBUTT ])
.enemySpecies(Species.SQUIRTLE)
.enemyLevel(10)
.enemyAbility(Abilities.COLOR_CHANGE);
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power);
expect(chance.value).toBe(0);
expect(power.value).toBe(move.power * 5461 / 4096);
}, 20000);
it("Sheer Force with exceptions including binding moves", async () => {
const moveToUse = Moves.BIND;
game.override.ability(Abilities.SHEER_FORCE);
await game.classicMode.startBattle([ Species.PIDGEOT ]);
const enemyPokemon = game.scene.getEnemyPokemon();
const headbuttMove = allMoves[Moves.HEADBUTT];
vi.spyOn(headbuttMove, "calculateBattlePower");
const headbuttFlinchAttr = headbuttMove.getAttrs(FlinchAttr)[0];
vi.spyOn(headbuttFlinchAttr, "getMoveChance");
game.scene.getEnemyPokemon()!.stats[Stat.DEF] = 10000;
expect(game.scene.getPlayerPokemon()!.formIndex).toBe(0);
game.move.select(moveToUse);
game.move.select(Moves.HEADBUTT);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to(MoveEffectPhase, false);
await game.move.forceHit();
await game.phaseInterceptor.to("BerryPhase", false);
const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
const move = phase.move.getMove();
expect(move.id).toBe(Moves.BIND);
//Binding moves and other exceptions are not affected by Sheer Force and have a chance.value of -1
const power = new NumberHolder(move.power);
const chance = new NumberHolder(move.chance);
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power);
expect(chance.value).toBe(-1);
expect(power.value).toBe(move.power);
}, 20000);
it("Sheer Force with moves with no secondary effect", async () => {
const moveToUse = Moves.TACKLE;
game.override.ability(Abilities.SHEER_FORCE);
await game.classicMode.startBattle([ Species.PIDGEOT ]);
game.scene.getEnemyPokemon()!.stats[Stat.DEF] = 10000;
expect(game.scene.getPlayerPokemon()!.formIndex).toBe(0);
game.move.select(moveToUse);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to(MoveEffectPhase, false);
const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
const move = phase.move.getMove();
expect(move.id).toBe(Moves.TACKLE);
//Binding moves and other exceptions are not affected by Sheer Force and have a chance.value of -1
const power = new NumberHolder(move.power);
const chance = new NumberHolder(move.chance);
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power);
expect(chance.value).toBe(-1);
expect(power.value).toBe(move.power);
}, 20000);
it("Sheer Force Disabling Specific Abilities", async () => {
const moveToUse = Moves.CRUSH_CLAW;
game.override.enemyAbility(Abilities.COLOR_CHANGE);
game.override.startingHeldItems([{ name: "KINGS_ROCK", count: 1 }]);
game.override.ability(Abilities.SHEER_FORCE);
await game.startBattle([ Species.PIDGEOT ]);
game.scene.getEnemyPokemon()!.stats[Stat.DEF] = 10000;
expect(game.scene.getPlayerPokemon()!.formIndex).toBe(0);
game.move.select(moveToUse);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to(MoveEffectPhase, false);
const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
const move = phase.move.getMove();
expect(move.id).toBe(Moves.CRUSH_CLAW);
//Disable color change due to being hit by Sheer Force
const power = new NumberHolder(move.power);
const chance = new NumberHolder(move.chance);
const user = phase.getUserPokemon()!;
const target = phase.getFirstTarget()!;
const opponentType = target.getTypes()[0];
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, false, chance, move, target, false);
applyPreAttackAbAttrs(MovePowerBoostAbAttr, user, target, move, false, power);
applyPostDefendAbAttrs(PostDefendTypeChangeAbAttr, target, user, move, target.apply(user, move));
expect(chance.value).toBe(0);
expect(power.value).toBe(move.power * 5461 / 4096);
expect(target.getTypes().length).toBe(2);
expect(target.getTypes()[0]).toBe(opponentType);
}, 20000);
expect(enemyPokemon?.getTypes()[0]).toBe(Type.WATER);
expect(headbuttMove.calculateBattlePower).toHaveLastReturnedWith(headbuttMove.power * SHEER_FORCE_MULT);
expect(headbuttFlinchAttr.getMoveChance).toHaveLastReturnedWith(0);
});
it("Two Pokemon with abilities disabled by Sheer Force hitting each other should not cause a crash", async () => {
const moveToUse = Moves.CRUNCH;
@ -191,5 +137,19 @@ describe("Abilities - Sheer Force", () => {
expect(onix.getTypes()).toStrictEqual(expectedTypes);
});
//TODO King's Rock Interaction Unit Test
it("Sheer Force should disable Meloetta's transformation from Relic Song", async () => {
game.override
.ability(Abilities.SHEER_FORCE)
.moveset([ Moves.RELIC_SONG ])
.enemyMoveset([ Moves.SPLASH ])
.enemyLevel(100);
await game.classicMode.startBattle([ Species.MELOETTA ]);
const playerPokemon = game.scene.getPlayerPokemon();
const formKeyStart = playerPokemon?.getFormKey();
game.move.select(Moves.RELIC_SONG);
await game.phaseInterceptor.to("TurnEndPhase");
expect(formKeyStart).toBe(playerPokemon?.getFormKey());
});
});

View File

@ -0,0 +1,85 @@
import { BattlerIndex } from "#app/battle";
import { isBetween } from "#app/utils";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Abilities - Stakeout", () => {
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
.moveset([ Moves.SPLASH, Moves.SURF ])
.ability(Abilities.STAKEOUT)
.battleType("single")
.disableCrits()
.startingLevel(100)
.enemyLevel(100)
.enemySpecies(Species.SNORLAX)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset([ Moves.SPLASH, Moves.FLIP_TURN ])
.startingWave(5);
});
it("should do double damage to a pokemon that switched out", async () => {
await game.classicMode.startBattle([ Species.MILOTIC ]);
const [ enemy1, ] = game.scene.getEnemyParty();
game.move.select(Moves.SURF);
await game.forceEnemyMove(Moves.SPLASH);
await game.toNextTurn();
const damage1 = enemy1.getInverseHp();
enemy1.hp = enemy1.getMaxHp();
game.move.select(Moves.SPLASH);
game.forceEnemyToSwitch();
await game.toNextTurn();
game.move.select(Moves.SURF);
game.forceEnemyToSwitch();
await game.toNextTurn();
expect(enemy1.isFainted()).toBe(false);
expect(isBetween(enemy1.getInverseHp(), (damage1 * 2) - 5, (damage1 * 2) + 5)).toBe(true);
});
it("should do double damage to a pokemon that switched out via U-Turn/etc", async () => {
await game.classicMode.startBattle([ Species.MILOTIC ]);
const [ enemy1, ] = game.scene.getEnemyParty();
game.move.select(Moves.SURF);
await game.forceEnemyMove(Moves.SPLASH);
await game.toNextTurn();
const damage1 = enemy1.getInverseHp();
enemy1.hp = enemy1.getMaxHp();
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.FLIP_TURN);
await game.toNextTurn();
game.move.select(Moves.SURF);
await game.forceEnemyMove(Moves.FLIP_TURN);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.toNextTurn();
expect(enemy1.isFainted()).toBe(false);
expect(isBetween(enemy1.getInverseHp(), (damage1 * 2) - 5, (damage1 * 2) + 5)).toBe(true);
});
});

View File

@ -632,4 +632,34 @@ describe("Abilities - Wimp Out", () => {
const hasFled = enemyPokemon.switchOutStatus;
expect(isVisible && !hasFled).toBe(true);
});
it("wimp out will not skip battles when triggered in a double battle", async () => {
const wave = 2;
game.override
.enemyMoveset(Moves.SPLASH)
.enemySpecies(Species.WIMPOD)
.enemyAbility(Abilities.WIMP_OUT)
.moveset([ Moves.MATCHA_GOTCHA, Moves.FALSE_SWIPE ])
.startingLevel(50)
.enemyLevel(1)
.battleType("double")
.startingWave(wave);
await game.classicMode.startBattle([
Species.RAICHU,
Species.PIKACHU
]);
const [ wimpod0, wimpod1 ] = game.scene.getEnemyField();
game.move.select(Moves.FALSE_SWIPE, 0, BattlerIndex.ENEMY);
game.move.select(Moves.MATCHA_GOTCHA, 1);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]);
await game.phaseInterceptor.to("TurnEndPhase");
expect(wimpod0.hp).toBeGreaterThan(0);
expect(wimpod0.switchOutStatus).toBe(true);
expect(wimpod0.isFainted()).toBe(false);
expect(wimpod1.isFainted()).toBe(true);
await game.toNextWave();
expect(game.scene.currentBattle.waveIndex).toBe(wave + 1);
});
});

View File

@ -1,9 +1,8 @@
import BattleScene from "#app/battle-scene";
import { describe, expect, it, vi } from "vitest";
import Pokemon from "#app/field/pokemon";
import { BattlerTag, BattlerTagLapseType, OctolockTag, TrappedTag } from "#app/data/battler-tags";
import { BattlerTagLapseType, OctolockTag, TrappedTag } from "#app/data/battler-tags";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { BattlerTagType } from "#app/enums/battler-tag-type";
import { Stat } from "#enums/stat";
vi.mock("#app/battle-scene.js");
@ -33,30 +32,4 @@ describe("BattlerTag - OctolockTag", () => {
it ("traps its target (extends TrappedTag)", async () => {
expect(new OctolockTag(1)).toBeInstanceOf(TrappedTag);
});
it("can be added to pokemon who are not octolocked", async => {
const mockPokemon = {
getTag: vi.fn().mockReturnValue(undefined) as Pokemon["getTag"],
} as Pokemon;
const subject = new OctolockTag(1);
expect(subject.canAdd(mockPokemon)).toBeTruthy();
expect(mockPokemon.getTag).toHaveBeenCalledTimes(1);
expect(mockPokemon.getTag).toHaveBeenCalledWith(BattlerTagType.OCTOLOCK);
});
it("cannot be added to pokemon who are octolocked", async => {
const mockPokemon = {
getTag: vi.fn().mockReturnValue(new BattlerTag(null!, null!, null!, null!)) as Pokemon["getTag"],
} as Pokemon;
const subject = new OctolockTag(1);
expect(subject.canAdd(mockPokemon)).toBeFalsy();
expect(mockPokemon.getTag).toHaveBeenCalledTimes(1);
expect(mockPokemon.getTag).toHaveBeenCalledWith(BattlerTagType.OCTOLOCK);
});
});

View File

@ -135,4 +135,57 @@ describe("Items - Multi Lens", () => {
expect(damageResults[0]).toBe(Math.floor(playerPokemon.level * 0.75));
expect(damageResults[1]).toBe(Math.floor(playerPokemon.level * 0.25));
});
it("should result in correct damage for hp% attacks with 1 lens", async () => {
game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }])
.moveset(Moves.SUPER_FANG)
.ability(Abilities.COMPOUND_EYES)
.enemyLevel(1000)
.enemySpecies(Species.BLISSEY); // allows for unrealistically high levels of accuracy
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.SUPER_FANG);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to("MoveEndPhase");
expect(enemyPokemon.getHpRatio()).toBeCloseTo(0.5, 5);
});
it("should result in correct damage for hp% attacks with 2 lenses", async () => {
game.override.startingHeldItems([{ name: "MULTI_LENS", count: 2 }])
.moveset(Moves.SUPER_FANG)
.ability(Abilities.COMPOUND_EYES)
.enemyMoveset(Moves.SPLASH)
.enemyLevel(1000)
.enemySpecies(Species.BLISSEY); // allows for unrealistically high levels of accuracy
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.SUPER_FANG);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to("MoveEndPhase");
expect(enemyPokemon.getHpRatio()).toBeCloseTo(0.5, 5);
});
it("should result in correct damage for hp% attacks with 2 lenses + Parental Bond", async () => {
game.override.startingHeldItems([{ name: "MULTI_LENS", count: 2 }])
.moveset(Moves.SUPER_FANG)
.ability(Abilities.PARENTAL_BOND)
.passiveAbility(Abilities.COMPOUND_EYES)
.enemyMoveset(Moves.SPLASH)
.enemyLevel(1000)
.enemySpecies(Species.BLISSEY); // allows for unrealistically high levels of accuracy
await game.classicMode.startBattle([ Species.MAGIKARP ]);
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.SUPER_FANG);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to("MoveEndPhase");
expect(enemyPokemon.getHpRatio()).toBeCloseTo(0.25, 5);
});
});

View File

@ -1,5 +1,9 @@
import { BattlerIndex } from "#app/battle";
import { allMoves } from "#app/data/move";
import { Status } from "#app/data/status-effect";
import { Challenges } from "#enums/challenges";
import { StatusEffect } from "#enums/status-effect";
import { Type } from "#enums/type";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
@ -193,4 +197,122 @@ describe("Moves - Dragon Tail", () => {
expect(dratini.hp).toBe(Math.floor(dratini.getMaxHp() / 2));
expect(game.scene.getPlayerField().length).toBe(1);
});
it("should force switches randomly", async () => {
game.override.enemyMoveset(Moves.DRAGON_TAIL)
.startingLevel(100)
.enemyLevel(1);
await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]);
const [ bulbasaur, charmander, squirtle ] = game.scene.getPlayerParty();
// Turn 1: Mock an RNG call that calls for switching to 1st backup Pokemon (Charmander)
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
return min;
});
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.DRAGON_TAIL);
await game.toNextTurn();
expect(bulbasaur.isOnField()).toBe(false);
expect(charmander.isOnField()).toBe(true);
expect(squirtle.isOnField()).toBe(false);
expect(bulbasaur.getInverseHp()).toBeGreaterThan(0);
// Turn 2: Mock an RNG call that calls for switching to 2nd backup Pokemon (Squirtle)
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
return min + 1;
});
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(bulbasaur.isOnField()).toBe(false);
expect(charmander.isOnField()).toBe(false);
expect(squirtle.isOnField()).toBe(true);
expect(charmander.getInverseHp()).toBeGreaterThan(0);
});
it("should not force a switch to a challenge-ineligible Pokemon", async () => {
game.override.enemyMoveset(Moves.DRAGON_TAIL)
.startingLevel(100)
.enemyLevel(1);
// Mono-Water challenge, Eevee is ineligible
game.challengeMode.addChallenge(Challenges.SINGLE_TYPE, Type.WATER + 1, 0);
await game.challengeMode.startBattle([ Species.LAPRAS, Species.EEVEE, Species.TOXAPEX, Species.PRIMARINA ]);
const [ lapras, eevee, toxapex, primarina ] = game.scene.getPlayerParty();
// Turn 1: Mock an RNG call that would normally call for switching to Eevee, but it is ineligible
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
return min;
});
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(lapras.isOnField()).toBe(false);
expect(eevee.isOnField()).toBe(false);
expect(toxapex.isOnField()).toBe(true);
expect(primarina.isOnField()).toBe(false);
expect(lapras.getInverseHp()).toBeGreaterThan(0);
});
it("should not force a switch to a fainted Pokemon", async () => {
game.override.enemyMoveset([ Moves.SPLASH, Moves.DRAGON_TAIL ])
.startingLevel(100)
.enemyLevel(1);
await game.classicMode.startBattle([ Species.LAPRAS, Species.EEVEE, Species.TOXAPEX, Species.PRIMARINA ]);
const [ lapras, eevee, toxapex, primarina ] = game.scene.getPlayerParty();
// Turn 1: Eevee faints
eevee.hp = 0;
eevee.status = new Status(StatusEffect.FAINT);
expect(eevee.isFainted()).toBe(true);
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.SPLASH);
await game.toNextTurn();
// Turn 2: Mock an RNG call that would normally call for switching to Eevee, but it is fainted
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
return min;
});
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.DRAGON_TAIL);
await game.toNextTurn();
expect(lapras.isOnField()).toBe(false);
expect(eevee.isOnField()).toBe(false);
expect(toxapex.isOnField()).toBe(true);
expect(primarina.isOnField()).toBe(false);
expect(lapras.getInverseHp()).toBeGreaterThan(0);
});
it("should not force a switch if there are no available Pokemon to switch into", async () => {
game.override.enemyMoveset([ Moves.SPLASH, Moves.DRAGON_TAIL ])
.startingLevel(100)
.enemyLevel(1);
await game.classicMode.startBattle([ Species.LAPRAS, Species.EEVEE ]);
const [ lapras, eevee ] = game.scene.getPlayerParty();
// Turn 1: Eevee faints
eevee.hp = 0;
eevee.status = new Status(StatusEffect.FAINT);
expect(eevee.isFainted()).toBe(true);
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.SPLASH);
await game.toNextTurn();
// Turn 2: Mock an RNG call that would normally call for switching to Eevee, but it is fainted
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
return min;
});
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.DRAGON_TAIL);
await game.toNextTurn();
expect(lapras.isOnField()).toBe(true);
expect(eevee.isOnField()).toBe(false);
expect(lapras.getInverseHp()).toBeGreaterThan(0);
});
});

View File

@ -6,7 +6,7 @@ import { Abilities } from "#app/enums/abilities";
import { Moves } from "#app/enums/moves";
import { Species } from "#app/enums/species";
import * as Messages from "#app/messages";
import { TerastallizeModifier } from "#app/modifier/modifier";
import { TerastallizeModifier, overrideHeldItems } from "#app/modifier/modifier";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
@ -15,15 +15,17 @@ function testMoveEffectiveness(game: GameManager, move: Moves, targetSpecies: Sp
expected: number, targetAbility: Abilities = Abilities.BALL_FETCH, teraType?: Type): void {
// Suppress getPokemonNameWithAffix because it calls on a null battle spec
vi.spyOn(Messages, "getPokemonNameWithAffix").mockReturnValue("");
game.override.enemyAbility(targetAbility);
if (teraType !== undefined) {
game.override.enemyHeldItems([{ name:"TERA_SHARD", type: teraType }]);
}
game.override
.enemyAbility(targetAbility)
.enemyHeldItems([{ name:"TERA_SHARD", type: teraType }]);
const user = game.scene.addPlayerPokemon(getPokemonSpecies(Species.SNORLAX), 5);
const target = game.scene.addEnemyPokemon(getPokemonSpecies(targetSpecies), 5, TrainerSlot.NONE);
if (teraType !== undefined) {
overrideHeldItems(game.scene, target, false);
}
expect(target.getMoveEffectiveness(user, allMoves[move])).toBe(expected);
user.destroy();
target.destroy();

View File

@ -1,11 +1,8 @@
import { Stat } from "#enums/stat";
import { TrappedTag } from "#app/data/battler-tags";
import { CommandPhase } from "#app/phases/command-phase";
import { MoveEndPhase } from "#app/phases/move-end-phase";
import { TurnInitPhase } from "#app/phases/turn-init-phase";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { Stat } from "#enums/stat";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
@ -27,12 +24,13 @@ describe("Moves - Octolock", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
game.override.battleType("single")
.enemySpecies(Species.RATTATA)
game.override
.battleType("single")
.enemySpecies(Species.MAGIKARP)
.enemyMoveset(Moves.SPLASH)
.enemyAbility(Abilities.BALL_FETCH)
.startingLevel(2000)
.moveset([ Moves.OCTOLOCK, Moves.SPLASH ])
.moveset([ Moves.OCTOLOCK, Moves.SPLASH, Moves.TRICK_OR_TREAT ])
.ability(Abilities.BALL_FETCH);
});
@ -43,16 +41,15 @@ describe("Moves - Octolock", () => {
// use Octolock and advance to init phase of next turn to check for stat changes
game.move.select(Moves.OCTOLOCK);
await game.phaseInterceptor.to(TurnInitPhase);
await game.toNextTurn();
expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(-1);
expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(-1);
// take a second turn to make sure stat changes occur again
await game.phaseInterceptor.to(CommandPhase);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
await game.phaseInterceptor.to(TurnInitPhase);
expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(-2);
expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(-2);
});
@ -65,7 +62,7 @@ describe("Moves - Octolock", () => {
// use Octolock and advance to init phase of next turn to check for stat changes
game.move.select(Moves.OCTOLOCK);
await game.phaseInterceptor.to(TurnInitPhase);
await game.toNextTurn();
expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0);
expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(-1);
@ -79,7 +76,7 @@ describe("Moves - Octolock", () => {
// use Octolock and advance to init phase of next turn to check for stat changes
game.move.select(Moves.OCTOLOCK);
await game.phaseInterceptor.to(TurnInitPhase);
await game.toNextTurn();
expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0);
expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(0);
@ -93,7 +90,7 @@ describe("Moves - Octolock", () => {
// use Octolock and advance to init phase of next turn to check for stat changes
game.move.select(Moves.OCTOLOCK);
await game.phaseInterceptor.to(TurnInitPhase);
await game.toNextTurn();
expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0);
expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(0);
@ -110,7 +107,44 @@ describe("Moves - Octolock", () => {
game.move.select(Moves.OCTOLOCK);
// after Octolock - enemy should be trapped
await game.phaseInterceptor.to(MoveEndPhase);
await game.phaseInterceptor.to("MoveEndPhase");
expect(enemyPokemon.findTag(t => t instanceof TrappedTag)).toBeDefined();
});
it("does not work on ghost type pokemon", async () => {
game.override.enemyMoveset(Moves.OCTOLOCK);
await game.classicMode.startBattle([ Species.GASTLY ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
// before Octolock - player should not be trapped
expect(playerPokemon.findTag(t => t instanceof TrappedTag)).toBeUndefined();
game.move.select(Moves.SPLASH);
await game.toNextTurn();
// after Octolock - player should still not be trapped, and no stat loss
expect(playerPokemon.findTag(t => t instanceof TrappedTag)).toBeUndefined();
expect(playerPokemon.getStatStage(Stat.DEF)).toBe(0);
expect(playerPokemon.getStatStage(Stat.SPDEF)).toBe(0);
});
it("does not work on pokemon with added ghost type via Trick-or-Treat", async () => {
await game.classicMode.startBattle([ Species.FEEBAS ]);
const enemy = game.scene.getEnemyPokemon()!;
// before Octolock - pokemon should not be trapped
expect(enemy.findTag(t => t instanceof TrappedTag)).toBeUndefined();
game.move.select(Moves.TRICK_OR_TREAT);
await game.toNextTurn();
game.move.select(Moves.OCTOLOCK);
await game.toNextTurn();
// after Octolock - pokemon should still not be trapped, and no stat loss
expect(enemy.findTag(t => t instanceof TrappedTag)).toBeUndefined();
expect(enemy.getStatStage(Stat.DEF)).toBe(0);
expect(enemy.getStatStage(Stat.SPDEF)).toBe(0);
});
});

View File

@ -26,7 +26,7 @@ describe("Moves - Shell Side Arm", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([ Moves.SHELL_SIDE_ARM ])
.moveset([ Moves.SHELL_SIDE_ARM, Moves.SPLASH ])
.battleType("single")
.startingLevel(100)
.enemyLevel(100)
@ -69,6 +69,9 @@ describe("Moves - Shell Side Arm", () => {
vi.spyOn(shellSideArmAttr, "apply");
game.move.select(Moves.SPLASH);
await game.toNextTurn();
game.move.select(Moves.SHELL_SIDE_ARM);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.phaseInterceptor.to("BerryPhase", false);

View File

@ -1,11 +1,15 @@
import { BattlerTagType } from "#app/enums/battler-tag-type";
import { BattlerTagType } from "#enums/battler-tag-type";
import { Challenges } from "#enums/challenges";
import { Type } from "#enums/type";
import { MoveResult } from "#app/field/pokemon";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { Status } from "#app/data/status-effect";
import { StatusEffect } from "#enums/status-effect";
describe("Moves - Whirlwind", () => {
let phaserGame: Phaser.Game;
@ -25,8 +29,9 @@ describe("Moves - Whirlwind", () => {
game = new GameManager(phaserGame);
game.override
.battleType("single")
.moveset(Moves.SPLASH)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.WHIRLWIND)
.enemyMoveset([ Moves.SPLASH, Moves.WHIRLWIND ])
.enemySpecies(Species.PIDGEY);
});
@ -41,10 +46,114 @@ describe("Moves - Whirlwind", () => {
const staraptor = game.scene.getPlayerPokemon()!;
game.move.select(move);
await game.forceEnemyMove(Moves.WHIRLWIND);
await game.phaseInterceptor.to("BerryPhase", false);
expect(staraptor.findTag((t) => t.tagType === BattlerTagType.FLYING)).toBeDefined();
expect(game.scene.getEnemyPokemon()!.getLastXMoves(1)[0].result).toBe(MoveResult.MISS);
});
it("should force switches randomly", async () => {
await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]);
const [ bulbasaur, charmander, squirtle ] = game.scene.getPlayerParty();
// Turn 1: Mock an RNG call that calls for switching to 1st backup Pokemon (Charmander)
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
return min;
});
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.WHIRLWIND);
await game.toNextTurn();
expect(bulbasaur.isOnField()).toBe(false);
expect(charmander.isOnField()).toBe(true);
expect(squirtle.isOnField()).toBe(false);
// Turn 2: Mock an RNG call that calls for switching to 2nd backup Pokemon (Squirtle)
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
return min + 1;
});
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.WHIRLWIND);
await game.toNextTurn();
expect(bulbasaur.isOnField()).toBe(false);
expect(charmander.isOnField()).toBe(false);
expect(squirtle.isOnField()).toBe(true);
});
it("should not force a switch to a challenge-ineligible Pokemon", async () => {
// Mono-Water challenge, Eevee is ineligible
game.challengeMode.addChallenge(Challenges.SINGLE_TYPE, Type.WATER + 1, 0);
await game.challengeMode.startBattle([ Species.LAPRAS, Species.EEVEE, Species.TOXAPEX, Species.PRIMARINA ]);
const [ lapras, eevee, toxapex, primarina ] = game.scene.getPlayerParty();
// Turn 1: Mock an RNG call that would normally call for switching to Eevee, but it is ineligible
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
return min;
});
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.WHIRLWIND);
await game.toNextTurn();
expect(lapras.isOnField()).toBe(false);
expect(eevee.isOnField()).toBe(false);
expect(toxapex.isOnField()).toBe(true);
expect(primarina.isOnField()).toBe(false);
});
it("should not force a switch to a fainted Pokemon", async () => {
await game.classicMode.startBattle([ Species.LAPRAS, Species.EEVEE, Species.TOXAPEX, Species.PRIMARINA ]);
const [ lapras, eevee, toxapex, primarina ] = game.scene.getPlayerParty();
// Turn 1: Eevee faints
eevee.hp = 0;
eevee.status = new Status(StatusEffect.FAINT);
expect(eevee.isFainted()).toBe(true);
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.SPLASH);
await game.toNextTurn();
// Turn 2: Mock an RNG call that would normally call for switching to Eevee, but it is fainted
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
return min;
});
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.WHIRLWIND);
await game.toNextTurn();
expect(lapras.isOnField()).toBe(false);
expect(eevee.isOnField()).toBe(false);
expect(toxapex.isOnField()).toBe(true);
expect(primarina.isOnField()).toBe(false);
});
it("should not force a switch if there are no available Pokemon to switch into", async () => {
await game.classicMode.startBattle([ Species.LAPRAS, Species.EEVEE ]);
const [ lapras, eevee ] = game.scene.getPlayerParty();
// Turn 1: Eevee faints
eevee.hp = 0;
eevee.status = new Status(StatusEffect.FAINT);
expect(eevee.isFainted()).toBe(true);
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.SPLASH);
await game.toNextTurn();
// Turn 2: Mock an RNG call that would normally call for switching to Eevee, but it is fainted
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
return min;
});
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.WHIRLWIND);
await game.toNextTurn();
expect(lapras.isOnField()).toBe(true);
expect(eevee.isOnField()).toBe(false);
});
});

View File

@ -266,6 +266,9 @@ describe("Clowning Around - Mystery Encounter", () => {
// 5 Lucky Egg on lead (ultra)
itemType = generateModifierType(scene, modifierTypes.LUCKY_EGG) as PokemonHeldItemModifierType;
await addItemToPokemon(scene, scene.getPlayerParty()[0], 5, itemType);
// 3 Soothe Bell on lead (great tier, but counted as ultra by this ME)
itemType = generateModifierType(scene, modifierTypes.SOOTHE_BELL) as PokemonHeldItemModifierType;
await addItemToPokemon(scene, scene.getPlayerParty()[0], 3, itemType);
// 5 Soul Dew on lead (rogue)
itemType = generateModifierType(scene, modifierTypes.SOUL_DEW) as PokemonHeldItemModifierType;
await addItemToPokemon(scene, scene.getPlayerParty()[0], 5, itemType);
@ -286,7 +289,7 @@ describe("Clowning Around - Mystery Encounter", () => {
const rogueCountAfter = leadItemsAfter
.filter(m => m.type.tier === ModifierTier.ROGUE)
.reduce((a, b) => a + b.stackCount, 0);
expect(ultraCountAfter).toBe(10);
expect(ultraCountAfter).toBe(13);
expect(rogueCountAfter).toBe(7);
const secondItemsAfter = scene.getPlayerParty()[1].getHeldItems();

View File

@ -18,6 +18,7 @@ import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
import { Mode } from "#app/ui/ui";
import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler";
import { ModifierTier } from "#app/modifier/modifier-tier";
import * as Utils from "#app/utils";
const namespace = "mysteryEncounters/globalTradeSystem";
const defaultParty = [ Species.LAPRAS, Species.GENGAR, Species.ABRA ];
@ -176,6 +177,23 @@ describe("Global Trade System - Mystery Encounter", () => {
expect(defaultParty.includes(speciesAfter!)).toBeFalsy();
});
it("Should roll for shiny twice, with random variant and associated luck", async () => {
// This ensures that the first shiny roll gets ignored, to test the ME rerolling for shiny
game.override.enemyShiny(false);
await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, defaultParty);
vi.spyOn(Utils, "randSeedInt").mockReturnValue(1); // force shiny on reroll
await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1 });
const receivedPokemon = scene.getPlayerParty().at(-1)!;
expect(receivedPokemon.shiny).toBeTruthy();
expect(receivedPokemon.variant).toBeDefined();
expect(receivedPokemon.luck).toBe(receivedPokemon.variant + 1);
});
it("should leave encounter without battle", async () => {
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");

View File

@ -0,0 +1,173 @@
import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters";
import { Biome } from "#enums/biome";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption } from "#test/mystery-encounter/encounter-test-utils";
import BattleScene from "#app/battle-scene";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils";
import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter";
import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases";
import { getSafariSpeciesSpawn, SafariZoneEncounter } from "#app/data/mystery-encounters/encounters/safari-zone-encounter";
import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { NON_LEGEND_PARADOX_POKEMON } from "#app/data/balance/special-species-groups";
const namespace = "mysteryEncounters/safariZone";
const defaultParty = [ Species.LAPRAS, Species.GENGAR, Species.ABRA ];
const defaultBiome = Biome.SWAMP;
const defaultWave = 45;
describe("Safari Zone - Mystery Encounter", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
let scene: BattleScene;
beforeAll(() => {
phaserGame = new Phaser.Game({ type: Phaser.HEADLESS });
});
beforeEach(async () => {
game = new GameManager(phaserGame);
scene = game.scene;
game.override.mysteryEncounterChance(100);
game.override.startingWave(defaultWave);
game.override.startingBiome(defaultBiome);
game.override.disableTrainerWaves();
const biomeMap = new Map<Biome, MysteryEncounterType[]>([
[ Biome.VOLCANO, [ MysteryEncounterType.FIGHT_OR_FLIGHT ]],
[ Biome.FOREST, [ MysteryEncounterType.SAFARI_ZONE ]],
[ Biome.SWAMP, [ MysteryEncounterType.SAFARI_ZONE ]],
[ Biome.JUNGLE, [ MysteryEncounterType.SAFARI_ZONE ]],
]);
vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(biomeMap);
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
vi.clearAllMocks();
vi.resetAllMocks();
});
it("should have the correct properties", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.SAFARI_ZONE, defaultParty);
expect(SafariZoneEncounter.encounterType).toBe(MysteryEncounterType.SAFARI_ZONE);
expect(SafariZoneEncounter.encounterTier).toBe(MysteryEncounterTier.GREAT);
expect(SafariZoneEncounter.dialogue).toBeDefined();
expect(SafariZoneEncounter.dialogue.intro).toStrictEqual([
{ text: `${namespace}:intro` },
]);
expect(SafariZoneEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}:title`);
expect(SafariZoneEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}:description`);
expect(SafariZoneEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}:query`);
expect(SafariZoneEncounter.options.length).toBe(2);
});
it("should not spawn outside of the forest, swamp, or jungle biomes", async () => {
game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT);
game.override.startingBiome(Biome.VOLCANO);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.SAFARI_ZONE);
});
it("should initialize fully", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = new MysteryEncounter(SafariZoneEncounter);
const encounter = scene.currentBattle.mysteryEncounter!;
scene.currentBattle.waveIndex = defaultWave;
const { onInit } = encounter;
expect(encounter.onInit).toBeDefined();
encounter.populateDialogueTokensFromRequirements(scene);
const onInitResult = onInit!(scene);
expect(onInitResult).toBe(true);
});
describe("Option 1 - Enter", () => {
it("should have the correct properties", () => {
const option = SafariZoneEncounter.options[0];
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT);
expect(option.dialogue).toBeDefined();
expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}:option.1.label`,
buttonTooltip: `${namespace}:option.1.tooltip`,
selected: [
{
text: `${namespace}:option.1.selected`,
},
],
});
});
it("should NOT be selectable if the player doesn't have enough money", async () => {
game.scene.money = 0;
await game.runToMysteryEncounter(MysteryEncounterType.SAFARI_ZONE, defaultParty);
await game.phaseInterceptor.to(MysteryEncounterPhase, false);
const encounterPhase = scene.getCurrentPhase();
expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name);
const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase;
vi.spyOn(mysteryEncounterPhase, "continueEncounter");
vi.spyOn(mysteryEncounterPhase, "handleOptionSelect");
vi.spyOn(scene.ui, "playError");
await runSelectMysteryEncounterOption(game, 1);
expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name);
expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled
expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled();
expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled();
});
it("should not spawn any Paradox Pokemon", async () => {
const NUM_ROLLS = 2000; // As long as this is greater than total number of species, this should cover all possible RNG rolls
let rngSweepProgress = 0; // Will simulate full range of RNG rolls by steadily increasing from 0 to 1
vi.spyOn(Phaser.Math.RND, "realInRange").mockImplementation((min: number, max: number) => {
return rngSweepProgress * (max - min) + min;
});
vi.spyOn(Phaser.Math.RND, "shuffle").mockImplementation((arr: any[]) => arr);
for (let i = 0; i < NUM_ROLLS; i++) {
rngSweepProgress = (2 * i + 1) / (2 * NUM_ROLLS);
const simSpecies = getSafariSpeciesSpawn().speciesId;
expect(NON_LEGEND_PARADOX_POKEMON).not.toContain(simSpecies);
}
});
// TODO: Tests for player actions inside the Safari Zone (Pokeball, Mud, Bait, Flee)
});
describe("Option 2 - Leave", () => {
it("should have the correct properties", () => {
const option = SafariZoneEncounter.options[1];
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT);
expect(option.dialogue).toBeDefined();
expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}:option.2.label`,
buttonTooltip: `${namespace}:option.2.tooltip`,
selected: [
{
text: `${namespace}:option.2.selected`,
},
],
});
});
it("should leave encounter without battle", async () => {
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
await game.runToMysteryEncounter(MysteryEncounterType.SAFARI_ZONE, defaultParty);
await runMysteryEncounterToEnd(game, 2);
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
});
});
});

View File

@ -18,6 +18,7 @@ import { TheExpertPokemonBreederEncounter } from "#app/data/mystery-encounters/e
import { TrainerType } from "#enums/trainer-type";
import { EggTier } from "#enums/egg-type";
import { PostMysteryEncounterPhase } from "#app/phases/mystery-encounter-phases";
import { FRIENDSHIP_GAIN_FROM_BATTLE } from "#app/data/balance/starters";
const namespace = "mysteryEncounters/theExpertPokemonBreeder";
const defaultParty = [ Species.LAPRAS, Species.GENGAR, Species.ABRA ];
@ -182,7 +183,10 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => {
await game.phaseInterceptor.to(PostMysteryEncounterPhase);
const friendshipAfter = scene.currentBattle.mysteryEncounter!.misc.pokemon1.friendship;
expect(friendshipAfter).toBe(friendshipBefore + 20 + 2); // +2 extra for friendship gained from winning battle
// 20 from ME + extra from winning battle (that extra is not accurate to what happens in game.
// The Pokemon normally gets FRIENDSHIP_GAIN_FROM_BATTLE 3 times, once for each defeated Pokemon
// but due to how skipBattleRunMysteryEncounterRewardsPhase is implemented, it only receives it once)
expect(friendshipAfter).toBe(friendshipBefore + 20 + FRIENDSHIP_GAIN_FROM_BATTLE);
});
});
@ -261,7 +265,7 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => {
await game.phaseInterceptor.to(PostMysteryEncounterPhase);
const friendshipAfter = scene.currentBattle.mysteryEncounter!.misc.pokemon2.friendship;
expect(friendshipAfter).toBe(friendshipBefore + 20 + 2); // +2 extra for friendship gained from winning battle
expect(friendshipAfter).toBe(friendshipBefore + 20 + FRIENDSHIP_GAIN_FROM_BATTLE); // 20 from ME + extra for friendship gained from winning battle
});
});
@ -340,7 +344,7 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => {
await game.phaseInterceptor.to(PostMysteryEncounterPhase);
const friendshipAfter = scene.currentBattle.mysteryEncounter!.misc.pokemon3.friendship;
expect(friendshipAfter).toBe(friendshipBefore + 20 + 2); // +2 extra for friendship gained from winning battle
expect(friendshipAfter).toBe(friendshipBefore + 20 + FRIENDSHIP_GAIN_FROM_BATTLE); // 20 + extra for friendship gained from winning battle
});
});
});

View File

@ -9,11 +9,12 @@ import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption } from "#test
import BattleScene from "#app/battle-scene";
import { PlayerPokemon } from "#app/field/pokemon";
import { HUMAN_TRANSITABLE_BIOMES } from "#app/data/mystery-encounters/mystery-encounters";
import { ThePokemonSalesmanEncounter } from "#app/data/mystery-encounters/encounters/the-pokemon-salesman-encounter";
import { getSalesmanSpeciesOffer, ThePokemonSalesmanEncounter } from "#app/data/mystery-encounters/encounters/the-pokemon-salesman-encounter";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils";
import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases";
import { NON_LEGEND_PARADOX_POKEMON } from "#app/data/balance/special-species-groups";
const namespace = "mysteryEncounters/thePokemonSalesman";
const defaultParty = [ Species.LAPRAS, Species.GENGAR, Species.ABRA ];
@ -122,7 +123,7 @@ describe("The Pokemon Salesman - Mystery Encounter", () => {
});
});
it("Should update the player's money properly", async () => {
it("should update the player's money properly", async () => {
const initialMoney = 20000;
scene.money = initialMoney;
const updateMoneySpy = vi.spyOn(EncounterPhaseUtils, "updatePlayerMoney");
@ -136,7 +137,7 @@ describe("The Pokemon Salesman - Mystery Encounter", () => {
expect(scene.money).toBe(initialMoney - price);
});
it("Should add the Pokemon to the party", async () => {
it("should add the Pokemon to the party", async () => {
scene.money = 20000;
await game.runToMysteryEncounter(MysteryEncounterType.THE_POKEMON_SALESMAN, defaultParty);
@ -152,6 +153,18 @@ describe("The Pokemon Salesman - Mystery Encounter", () => {
expect(newlyPurchasedPokemon!.moveset.length > 0).toBeTruthy();
});
it("should give the purchased Pokemon its HA or make it shiny", async () => {
scene.money = 20000;
await game.runToMysteryEncounter(MysteryEncounterType.THE_POKEMON_SALESMAN, defaultParty);
await runMysteryEncounterToEnd(game, 1);
const newlyPurchasedPokemon = scene.getPlayerParty()[scene.getPlayerParty().length - 1];
const isshiny = newlyPurchasedPokemon.shiny;
const hasHA = newlyPurchasedPokemon.abilityIndex === 2;
expect(isshiny || hasHA).toBeTruthy();
expect(isshiny && hasHA).toBeFalsy();
});
it("should be disabled if player does not have enough money", async () => {
scene.money = 0;
await game.runToMysteryEncounter(MysteryEncounterType.THE_POKEMON_SALESMAN, defaultParty);
@ -172,6 +185,22 @@ describe("The Pokemon Salesman - Mystery Encounter", () => {
expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled();
});
it("should not offer any Paradox Pokemon", async () => {
const NUM_ROLLS = 2000; // As long as this is greater than total number of species, this should cover all possible RNG rolls
let rngSweepProgress = 0; // Will simulate full range of RNG rolls by steadily increasing from 0 to 1
vi.spyOn(Phaser.Math.RND, "realInRange").mockImplementation((min: number, max: number) => {
return rngSweepProgress * (max - min) + min;
});
vi.spyOn(Phaser.Math.RND, "shuffle").mockImplementation((arr: any[]) => arr);
for (let i = 0; i < NUM_ROLLS; i++) {
rngSweepProgress = (2 * i + 1) / (2 * NUM_ROLLS);
const simSpecies = getSalesmanSpeciesOffer().speciesId;
expect(NON_LEGEND_PARADOX_POKEMON).not.toContain(simSpecies);
}
});
it("should leave encounter without battle", async () => {
scene.money = 20000;
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");

View File

@ -109,6 +109,7 @@ describe("The Strong Stuff - Mystery Encounter", () => {
species: getPokemonSpecies(Species.SHUCKLE),
isBoss: true,
bossSegments: 5,
shiny: false,
customPokemonData: new CustomPokemonData({ spriteScale: 1.25 }),
nature: Nature.BOLD,
moveSet: [ Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER ],

View File

@ -92,6 +92,7 @@ describe("Trash to Treasure - Mystery Encounter", () => {
{
species: getPokemonSpecies(Species.GARBODOR),
isBoss: true,
shiny: false,
formIndex: 1,
bossSegmentModifier: 1,
moveSet: [ Moves.PAYBACK, Moves.GUNK_SHOT, Moves.STOMPING_TANTRUM, Moves.DRAIN_PUNCH ],

View File

@ -2,7 +2,7 @@ import BattleScene from "#app/battle-scene";
import { speciesStarterCosts } from "#app/data/balance/starters";
import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter";
import { getEncounterText, queueEncounterMessage, showEncounterDialogue, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { getHighestLevelPlayerPokemon, getLowestLevelPlayerPokemon, getRandomPlayerPokemon, getRandomSpeciesByStarterTier, koPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { getHighestLevelPlayerPokemon, getLowestLevelPlayerPokemon, getRandomPlayerPokemon, getRandomSpeciesByStarterCost, koPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { getPokemonSpecies } from "#app/data/pokemon-species";
import { Type } from "#enums/type";
import { MessagePhase } from "#app/phases/message-phase";
@ -204,9 +204,9 @@ describe("Mystery Encounter Utils", () => {
});
});
describe("getRandomSpeciesByStarterTier", () => {
describe("getRandomSpeciesByStarterCost", () => {
it("gets species for a starter tier", () => {
const result = getRandomSpeciesByStarterTier(5);
const result = getRandomSpeciesByStarterCost(5);
const pokeSpecies = getPokemonSpecies(result);
expect(pokeSpecies.speciesId).toBe(result);
@ -214,7 +214,7 @@ describe("Mystery Encounter Utils", () => {
});
it("gets species for a starter tier range", () => {
const result = getRandomSpeciesByStarterTier([ 5, 8 ]);
const result = getRandomSpeciesByStarterCost([ 5, 8 ]);
const pokeSpecies = getPokemonSpecies(result);
expect(pokeSpecies.speciesId).toBe(result);
@ -224,14 +224,14 @@ describe("Mystery Encounter Utils", () => {
it("excludes species from search", () => {
// Only 9 tiers are: Koraidon, Miraidon, Arceus, Rayquaza, Kyogre, Groudon, Zacian
const result = getRandomSpeciesByStarterTier(9, [ Species.KORAIDON, Species.MIRAIDON, Species.ARCEUS, Species.RAYQUAZA, Species.KYOGRE, Species.GROUDON ]);
const result = getRandomSpeciesByStarterCost(9, [ Species.KORAIDON, Species.MIRAIDON, Species.ARCEUS, Species.RAYQUAZA, Species.KYOGRE, Species.GROUDON ]);
const pokeSpecies = getPokemonSpecies(result);
expect(pokeSpecies.speciesId).toBe(Species.ZACIAN);
});
it("gets species of specified types", () => {
// Only 9 tiers are: Koraidon, Miraidon, Arceus, Rayquaza, Kyogre, Groudon, Zacian
const result = getRandomSpeciesByStarterTier(9, undefined, [ Type.GROUND ]);
const result = getRandomSpeciesByStarterCost(9, undefined, [ Type.GROUND ]);
const pokeSpecies = getPokemonSpecies(result);
expect(pokeSpecies.speciesId).toBe(Species.GROUDON);
});

View File

@ -8,6 +8,7 @@ import { addTextObject, TextStyle } from "./text";
import { addWindow } from "./ui-theme";
import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler";
import { pokerogueApi } from "#app/plugins/api/pokerogue-api";
import JSZip from "jszip";
interface BuildInteractableImageOpts {
scale?: number;
@ -27,6 +28,7 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
private googleImage: Phaser.GameObjects.Image;
private discordImage: Phaser.GameObjects.Image;
private usernameInfoImage: Phaser.GameObjects.Image;
private saveDownloadImage: Phaser.GameObjects.Image;
private externalPartyContainer: Phaser.GameObjects.Container;
private infoContainer: Phaser.GameObjects.Container;
private externalPartyBg: Phaser.GameObjects.NineSlice;
@ -46,7 +48,13 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
scale: 0.5
});
this.saveDownloadImage = this.buildInteractableImage("saving_icon", "save-download-icon", {
x: 0,
scale: 0.75
});
this.infoContainer.add(this.usernameInfoImage);
this.infoContainer.add(this.saveDownloadImage);
this.getUi().add(this.infoContainer);
this.infoContainer.setVisible(false);
this.infoContainer.disableInteractive();
@ -160,7 +168,7 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
this.infoContainer.setVisible(false);
this.setMouseCursorStyle("default"); //reset cursor
[ this.discordImage, this.googleImage, this.usernameInfoImage ].forEach((img) => img.off("pointerdown"));
[ this.discordImage, this.googleImage, this.usernameInfoImage, this.saveDownloadImage ].forEach((img) => img.off("pointerdown"));
}
private processExternalProvider(config: ModalConfig): void {
@ -178,6 +186,7 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
this.infoContainer.setVisible(true);
this.getUi().moveTo(this.infoContainer, this.getUi().length - 1);
this.usernameInfoImage.setPositionRelative(this.infoContainer, 0, 0);
this.saveDownloadImage.setPositionRelative(this.infoContainer, 20, 0);
this.discordImage.on("pointerdown", () => {
const redirectUri = encodeURIComponent(`${import.meta.env.VITE_SERVER_URL}/auth/discord/callback`);
@ -229,6 +238,34 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
}
});
this.saveDownloadImage.on("pointerdown", () => {
// find all data_ and sessionData keys, put them in a .txt file and download everything in a single zip
const localStorageKeys = Object.keys(localStorage); // this gets the keys for localStorage
const keyToFind = "data_";
const sessionKeyToFind = "sessionData";
const dataKeys = localStorageKeys.filter(ls => ls.indexOf(keyToFind) >= 0);
const sessionKeys = localStorageKeys.filter(ls => ls.indexOf(sessionKeyToFind) >= 0);
if (dataKeys.length > 0 || sessionKeys.length > 0) {
const zip = new JSZip();
for (let i = 0; i < dataKeys.length; i++) {
zip.file(dataKeys[i] + ".prsv", localStorage.getItem(dataKeys[i])!);
}
for (let i = 0; i < sessionKeys.length; i++) {
zip.file(sessionKeys[i] + ".prsv", localStorage.getItem(sessionKeys[i])!);
}
zip.generateAsync({ type: "blob" }).then(content => {
const url = URL.createObjectURL(content);
const a = document.createElement("a");
a.href = url;
a.download = "pokerogue_saves.zip";
a.click();
URL.revokeObjectURL(url);
});
} else {
return onFail(this.ERR_NO_SAVES);
}
});
this.externalPartyContainer.setAlpha(0);
this.scene.tweens.add({
targets: this.externalPartyContainer,

View File

@ -313,6 +313,11 @@ export default class PokemonInfoContainer extends Phaser.GameObjects.Container {
this.pokemonShinyNewIcon.setShadowColor(getTextColor(TextStyle.SUMMARY_BLUE, true, this.scene.uiTheme));
const newShinyOrVariant = ((newShiny & caughtAttr) === BigInt(0)) || ((newVariant & caughtAttr) === BigInt(0));
this.pokemonShinyNewIcon.setVisible(!!newShinyOrVariant);
} else if ((caughtAttr & DexAttr.NON_SHINY) === BigInt(0) && ((caughtAttr & DexAttr.SHINY) === DexAttr.SHINY)) { //If the player has *only* caught any shiny variant of this species, not a non-shiny
this.pokemonShinyNewIcon.setVisible(true);
this.pokemonShinyNewIcon.setText("(+)");
this.pokemonShinyNewIcon.setColor(getTextColor(TextStyle.SUMMARY_BLUE, false, this.scene.uiTheme));
this.pokemonShinyNewIcon.setShadowColor(getTextColor(TextStyle.SUMMARY_BLUE, true, this.scene.uiTheme));
} else {
this.pokemonShinyNewIcon.setVisible(false);
}

View File

@ -1,8 +1,7 @@
import BattleScene from "#app/battle-scene";
import { hasTouchscreen, isMobile } from "#app/touch-controls";
import { TextStyle, addTextObject } from "#app/ui/text";
import { Mode } from "#app/ui/ui";
import UiHandler from "#app/ui/ui-handler";
import MessageUiHandler from "#app/ui/message-ui-handler";
import { addWindow } from "#app/ui/ui-theme";
import { ScrollBar } from "#app/ui/scroll-bar";
import { Button } from "#enums/buttons";
@ -15,9 +14,10 @@ import i18next from "i18next";
/**
* Abstract class for handling UI elements related to settings.
*/
export default class AbstractSettingsUiHandler extends UiHandler {
export default class AbstractSettingsUiHandler extends MessageUiHandler {
private settingsContainer: Phaser.GameObjects.Container;
private optionsContainer: Phaser.GameObjects.Container;
private messageBoxContainer: Phaser.GameObjects.Container;
private navigationContainer: NavigationMenu;
private scrollCursor: number;
@ -135,6 +135,23 @@ export default class AbstractSettingsUiHandler extends UiHandler {
this.scrollBar = new ScrollBar(this.scene, this.optionsBg.width - 9, this.optionsBg.y + 5, 4, this.optionsBg.height - 11, this.rowsToDisplay);
this.scrollBar.setTotalRows(this.settings.length);
// Two-lines message box
this.messageBoxContainer = this.scene.add.container(0, this.scene.scaledCanvas.height);
this.messageBoxContainer.setName("settings-message-box");
this.messageBoxContainer.setVisible(false);
const settingsMessageBox = addWindow(this.scene, 0, -1, this.scene.scaledCanvas.width - 2, 48);
settingsMessageBox.setOrigin(0, 1);
this.messageBoxContainer.add(settingsMessageBox);
const messageText = addTextObject(this.scene, 8, -40, "", TextStyle.WINDOW, { maxLines: 2 });
messageText.setWordWrapWidth(this.scene.game.canvas.width - 60);
messageText.setName("settings-message");
messageText.setOrigin(0, 0);
this.messageBoxContainer.add(messageText);
this.message = messageText;
this.settingsContainer.add(this.optionsBg);
this.settingsContainer.add(this.scrollBar);
this.settingsContainer.add(this.navigationContainer);
@ -144,6 +161,7 @@ export default class AbstractSettingsUiHandler extends UiHandler {
this.settingsContainer.add(iconCancel);
this.settingsContainer.add(actionText);
this.settingsContainer.add(cancelText);
this.settingsContainer.add(this.messageBoxContainer);
ui.add(this.settingsContainer);
@ -326,18 +344,16 @@ export default class AbstractSettingsUiHandler extends UiHandler {
/**
* Set the option cursor to the specified position.
*
* @param settingIndex - The index of the setting.
* @param settingIndex - The index of the setting or -1 to change the current setting
* @param cursor - The cursor position to set.
* @param save - Whether to save the setting to local storage.
* @returns `true` if the option cursor was set successfully.
*/
setOptionCursor(settingIndex: number, cursor: number, save?: boolean): boolean {
const setting = this.settings[settingIndex];
if (setting.key === SettingKeys.Touch_Controls && cursor && hasTouchscreen() && isMobile()) {
this.getUi().playError();
return false;
if (settingIndex === -1) {
settingIndex = this.cursor + this.scrollCursor;
}
const setting = this.settings[settingIndex];
const lastCursor = this.optionCursors[settingIndex];
@ -352,10 +368,34 @@ export default class AbstractSettingsUiHandler extends UiHandler {
newValueLabel.setShadowColor(this.getTextColor(TextStyle.SETTINGS_SELECTED, true));
if (save) {
const saveSetting = () => {
this.scene.gameData.saveSetting(setting.key, cursor);
if (this.reloadSettings.includes(setting)) {
if (setting.requireReload) {
this.reloadRequired = true;
}
};
// For settings that ask for confirmation, display confirmation message and a Yes/No prompt before saving the setting
if (setting.options[cursor].needConfirmation) {
const confirmUpdateSetting = () => {
this.scene.ui.revertMode();
this.showText("");
saveSetting();
};
const cancelUpdateSetting = () => {
this.scene.ui.revertMode();
this.showText("");
// Put the cursor back to its previous position without saving or asking for confirmation again
this.setOptionCursor(settingIndex, lastCursor, false);
};
const confirmationMessage = setting.options[cursor].confirmationMessage ?? i18next.t("settings:defaultConfirmMessage");
this.scene.ui.showText(confirmationMessage, null, () => {
this.scene.ui.setOverlayMode(Mode.CONFIRM, confirmUpdateSetting, cancelUpdateSetting, null, null, 1, 750);
});
} else {
saveSetting();
}
}
return true;
@ -421,4 +461,9 @@ export default class AbstractSettingsUiHandler extends UiHandler {
}
this.cursorObj = null;
}
override showText(text: string, delay?: integer, callback?: Function, callbackDelay?: integer, prompt?: boolean, promptDelay?: integer) {
this.messageBoxContainer.setVisible(!!text?.length);
super.showText(text, delay, callback, callbackDelay, prompt, promptDelay);
}
}

View File

@ -51,6 +51,7 @@ import { Abilities } from "#enums/abilities";
import { getPassiveCandyCount, getValueReductionCandyCounts, getSameSpeciesEggCandyCounts } from "#app/data/balance/starters";
import { BooleanHolder, capitalizeString, fixedInt, getLocalizedSpriteKey, isNullOrUndefined, NumberHolder, padInt, randIntRange, rgbHexToRgba, toReadableString } from "#app/utils";
import type { Nature } from "#enums/nature";
import { PLAYER_PARTY_MAX_SIZE } from "#app/constants";
export type StarterSelectCallback = (starters: Starter[]) => void;
@ -1462,7 +1463,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
const currentPartyValue = this.starterSpecies.map(s => s.generation).reduce((total: number, gen: number, i: number) => total += this.scene.gameData.getSpeciesStarterValue(this.starterSpecies[i].speciesId), 0);
const newCost = this.scene.gameData.getSpeciesStarterValue(this.lastSpecies.speciesId);
if (!isDupe && isValidForChallenge.value && currentPartyValue + newCost <= this.getValueLimit() && this.starterSpecies.length < 6) { // this checks to make sure the pokemon doesn't exist in your party, it's valid for the challenge and that it won't go over the cost limit; if it meets all these criteria it will add it to your party
if (!isDupe && isValidForChallenge.value && currentPartyValue + newCost <= this.getValueLimit() && this.starterSpecies.length < PLAYER_PARTY_MAX_SIZE) { // this checks to make sure the pokemon doesn't exist in your party, it's valid for the challenge and that it won't go over the cost limit; if it meets all these criteria it will add it to your party
options = [
{
label: i18next.t("starterSelectUiHandler:addToParty"),