diff --git a/eslint.config.js b/eslint.config.js index 1cea5563a78..0da9cc604bf 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -5,6 +5,7 @@ import importX from 'eslint-plugin-import-x'; export default [ { + name: "eslint-config", files: ["src/**/*.{ts,tsx,js,jsx}"], ignores: ["dist/*", "build/*", "coverage/*", "public/*", ".github/*", "node_modules/*", ".vscode/*"], languageOptions: { @@ -48,5 +49,22 @@ export default [ "no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1, "maxBOF": 0 }], // Disallows multiple empty lines "@typescript-eslint/consistent-type-imports": "error", // Enforces type-only imports wherever possible } + }, + { + name: "eslint-tests", + files: ["src/test/**/**.test.ts"], + languageOptions: { + parser: parser, + parserOptions: { + "project": ["./tsconfig.json"] + } + }, + plugins: { + "@typescript-eslint": tseslint + }, + rules: { + "@typescript-eslint/no-floating-promises": "error", // Require Promise-like statements to be handled appropriately. - https://typescript-eslint.io/rules/no-floating-promises/ + "@typescript-eslint/no-misused-promises": "error", // Disallow Promises in places not designed to handle them. - https://typescript-eslint.io/rules/no-misused-promises/ + } } ] diff --git a/public/images/events/valentines2025event-de.png b/public/images/events/valentines2025event-de.png new file mode 100644 index 00000000000..0ec3bfe704b Binary files /dev/null and b/public/images/events/valentines2025event-de.png differ diff --git a/public/images/events/valentines2025event-en.png b/public/images/events/valentines2025event-en.png new file mode 100644 index 00000000000..0ec3bfe704b Binary files /dev/null and b/public/images/events/valentines2025event-en.png differ diff --git a/public/images/events/valentines2025event-es-ES.png b/public/images/events/valentines2025event-es-ES.png new file mode 100644 index 00000000000..0ec3bfe704b Binary files /dev/null and b/public/images/events/valentines2025event-es-ES.png differ diff --git a/public/images/events/valentines2025event-fr.png b/public/images/events/valentines2025event-fr.png new file mode 100644 index 00000000000..0ec3bfe704b Binary files /dev/null and b/public/images/events/valentines2025event-fr.png differ diff --git a/public/images/events/valentines2025event-it.png b/public/images/events/valentines2025event-it.png new file mode 100644 index 00000000000..0ec3bfe704b Binary files /dev/null and b/public/images/events/valentines2025event-it.png differ diff --git a/public/images/events/valentines2025event-ja.png b/public/images/events/valentines2025event-ja.png new file mode 100644 index 00000000000..0ec3bfe704b Binary files /dev/null and b/public/images/events/valentines2025event-ja.png differ diff --git a/public/images/events/valentines2025event-ko.png b/public/images/events/valentines2025event-ko.png new file mode 100644 index 00000000000..0ec3bfe704b Binary files /dev/null and b/public/images/events/valentines2025event-ko.png differ diff --git a/public/images/events/valentines2025event-pt-BR.png b/public/images/events/valentines2025event-pt-BR.png new file mode 100644 index 00000000000..0ec3bfe704b Binary files /dev/null and b/public/images/events/valentines2025event-pt-BR.png differ diff --git a/public/images/events/valentines2025event-zh-CN.png b/public/images/events/valentines2025event-zh-CN.png new file mode 100644 index 00000000000..0ec3bfe704b Binary files /dev/null and b/public/images/events/valentines2025event-zh-CN.png differ diff --git a/public/images/pokemon/335.json b/public/images/pokemon/335.json index 0279e0fba5a..a9313fcec5d 100644 --- a/public/images/pokemon/335.json +++ b/public/images/pokemon/335.json @@ -1,1910 +1,547 @@ -{ - "textures": [ - { - "image": "335.png", - "format": "RGBA8888", - "size": { - "w": 366, - "h": 366 - }, - "scale": 1, - "frames": [ - { - "filename": "0013.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 68, - "h": 63 - }, - "frame": { - "x": 0, - "y": 0, - "w": 68, - "h": 63 - } - }, - { - "filename": "0014.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 68, - "h": 63 - }, - "frame": { - "x": 0, - "y": 0, - "w": 68, - "h": 63 - } - }, - { - "filename": "0035.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 68, - "h": 63 - }, - "frame": { - "x": 0, - "y": 0, - "w": 68, - "h": 63 - } - }, - { - "filename": "0056.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 68, - "h": 63 - }, - "frame": { - "x": 0, - "y": 0, - "w": 68, - "h": 63 - } - }, - { - "filename": "0079.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 5, - "y": 0, - "w": 65, - "h": 66 - }, - "frame": { - "x": 0, - "y": 63, - "w": 65, - "h": 66 - } - }, - { - "filename": "0077.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 7, - "y": 3, - "w": 67, - "h": 63 - }, - "frame": { - "x": 68, - "y": 0, - "w": 67, - "h": 63 - } - }, - { - "filename": "0078.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 7, - "y": 3, - "w": 67, - "h": 63 - }, - "frame": { - "x": 68, - "y": 0, - "w": 67, - "h": 63 - } - }, - { - "filename": "0012.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 5, - "w": 68, - "h": 61 - }, - "frame": { - "x": 65, - "y": 63, - "w": 68, - "h": 61 - } - }, - { - "filename": "0033.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 5, - "w": 68, - "h": 61 - }, - "frame": { - "x": 65, - "y": 63, - "w": 68, - "h": 61 - } - }, - { - "filename": "0034.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 5, - "w": 68, - "h": 61 - }, - "frame": { - "x": 65, - "y": 63, - "w": 68, - "h": 61 - } - }, - { - "filename": "0055.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 5, - "w": 68, - "h": 61 - }, - "frame": { - "x": 65, - "y": 63, - "w": 68, - "h": 61 - } - }, - { - "filename": "0015.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 64, - "h": 63 - }, - "frame": { - "x": 0, - "y": 129, - "w": 64, - "h": 63 - } - }, - { - "filename": "0036.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 64, - "h": 63 - }, - "frame": { - "x": 0, - "y": 129, - "w": 64, - "h": 63 - } - }, - { - "filename": "0057.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 64, - "h": 63 - }, - "frame": { - "x": 0, - "y": 129, - "w": 64, - "h": 63 - } - }, - { - "filename": "0058.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 64, - "h": 63 - }, - "frame": { - "x": 0, - "y": 129, - "w": 64, - "h": 63 - } - }, - { - "filename": "0080.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 3, - "y": 0, - "w": 61, - "h": 66 - }, - "frame": { - "x": 0, - "y": 192, - "w": 61, - "h": 66 - } - }, - { - "filename": "0085.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 0, - "w": 61, - "h": 66 - }, - "frame": { - "x": 0, - "y": 258, - "w": 61, - "h": 66 - } - }, - { - "filename": "0086.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 0, - "w": 61, - "h": 66 - }, - "frame": { - "x": 0, - "y": 258, - "w": 61, - "h": 66 - } - }, - { - "filename": "0069.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 0, - "y": 4, - "w": 64, - "h": 62 - }, - "frame": { - "x": 135, - "y": 0, - "w": 64, - "h": 62 - } - }, - { - "filename": "0070.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 0, - "y": 4, - "w": 64, - "h": 62 - }, - "frame": { - "x": 135, - "y": 0, - "w": 64, - "h": 62 - } - }, - { - "filename": "0076.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 8, - "y": 5, - "w": 65, - "h": 61 - }, - "frame": { - "x": 199, - "y": 0, - "w": 65, - "h": 61 - } - }, - { - "filename": "0011.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 1, - "y": 8, - "w": 67, - "h": 58 - }, - "frame": { - "x": 264, - "y": 0, - "w": 67, - "h": 58 - } - }, - { - "filename": "0032.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 1, - "y": 8, - "w": 67, - "h": 58 - }, - "frame": { - "x": 264, - "y": 0, - "w": 67, - "h": 58 - } - }, - { - "filename": "0053.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 1, - "y": 8, - "w": 67, - "h": 58 - }, - "frame": { - "x": 264, - "y": 0, - "w": 67, - "h": 58 - } - }, - { - "filename": "0054.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 1, - "y": 8, - "w": 67, - "h": 58 - }, - "frame": { - "x": 264, - "y": 0, - "w": 67, - "h": 58 - } - }, - { - "filename": "0009.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 7, - "w": 64, - "h": 59 - }, - "frame": { - "x": 65, - "y": 124, - "w": 64, - "h": 59 - } - }, - { - "filename": "0010.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 7, - "w": 64, - "h": 59 - }, - "frame": { - "x": 65, - "y": 124, - "w": 64, - "h": 59 - } - }, - { - "filename": "0031.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 7, - "w": 64, - "h": 59 - }, - "frame": { - "x": 65, - "y": 124, - "w": 64, - "h": 59 - } - }, - { - "filename": "0052.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 7, - "w": 64, - "h": 59 - }, - "frame": { - "x": 65, - "y": 124, - "w": 64, - "h": 59 - } - }, - { - "filename": "0071.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 0, - "y": 5, - "w": 64, - "h": 61 - }, - "frame": { - "x": 64, - "y": 183, - "w": 64, - "h": 61 - } - }, - { - "filename": "0087.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 1, - "w": 62, - "h": 65 - }, - "frame": { - "x": 61, - "y": 244, - "w": 62, - "h": 65 - } - }, - { - "filename": "0075.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 6, - "y": 9, - "w": 63, - "h": 57 - }, - "frame": { - "x": 61, - "y": 309, - "w": 63, - "h": 57 - } - }, - { - "filename": "0088.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 2, - "w": 61, - "h": 64 - }, - "frame": { - "x": 123, - "y": 244, - "w": 61, - "h": 64 - } - }, - { - "filename": "0072.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 1, - "y": 8, - "w": 63, - "h": 58 - }, - "frame": { - "x": 124, - "y": 308, - "w": 63, - "h": 58 - } - }, - { - "filename": "0008.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 3, - "y": 5, - "w": 61, - "h": 61 - }, - "frame": { - "x": 128, - "y": 183, - "w": 61, - "h": 61 - } - }, - { - "filename": "0029.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 3, - "y": 5, - "w": 61, - "h": 61 - }, - "frame": { - "x": 128, - "y": 183, - "w": 61, - "h": 61 - } - }, - { - "filename": "0030.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 3, - "y": 5, - "w": 61, - "h": 61 - }, - "frame": { - "x": 128, - "y": 183, - "w": 61, - "h": 61 - } - }, - { - "filename": "0051.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 3, - "y": 5, - "w": 61, - "h": 61 - }, - "frame": { - "x": 128, - "y": 183, - "w": 61, - "h": 61 - } - }, - { - "filename": "0001.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0002.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0003.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0004.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0005.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0006.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0007.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0016.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0021.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0022.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0023.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0024.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0025.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0026.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0027.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0028.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0037.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0038.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0043.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0044.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0045.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0046.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0047.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0048.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0049.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0050.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0059.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0064.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0073.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 9, - "w": 62, - "h": 57 - }, - "frame": { - "x": 187, - "y": 307, - "w": 62, - "h": 57 - } - }, - { - "filename": "0074.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 9, - "w": 62, - "h": 57 - }, - "frame": { - "x": 187, - "y": 307, - "w": 62, - "h": 57 - } - }, - { - "filename": "0068.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 3, - "w": 62, - "h": 63 - }, - "frame": { - "x": 264, - "y": 58, - "w": 62, - "h": 63 - } - }, - { - "filename": "0017.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 55, - "h": 63 - }, - "frame": { - "x": 133, - "y": 63, - "w": 55, - "h": 63 - } - }, - { - "filename": "0018.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 55, - "h": 63 - }, - "frame": { - "x": 133, - "y": 63, - "w": 55, - "h": 63 - } - }, - { - "filename": "0039.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 55, - "h": 63 - }, - "frame": { - "x": 133, - "y": 63, - "w": 55, - "h": 63 - } - }, - { - "filename": "0060.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 55, - "h": 63 - }, - "frame": { - "x": 133, - "y": 63, - "w": 55, - "h": 63 - } - }, - { - "filename": "0081.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 0, - "w": 55, - "h": 66 - }, - "frame": { - "x": 188, - "y": 62, - "w": 55, - "h": 66 - } - }, - { - "filename": "0082.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 0, - "w": 55, - "h": 66 - }, - "frame": { - "x": 188, - "y": 62, - "w": 55, - "h": 66 - } - }, - { - "filename": "0083.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 0, - "w": 58, - "h": 66 - }, - "frame": { - "x": 189, - "y": 128, - "w": 58, - "h": 66 - } - }, - { - "filename": "0084.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 0, - "w": 60, - "h": 66 - }, - "frame": { - "x": 247, - "y": 121, - "w": 60, - "h": 66 - } - }, - { - "filename": "0020.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 59, - "h": 63 - }, - "frame": { - "x": 307, - "y": 121, - "w": 59, - "h": 63 - } - }, - { - "filename": "0041.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 59, - "h": 63 - }, - "frame": { - "x": 307, - "y": 121, - "w": 59, - "h": 63 - } - }, - { - "filename": "0042.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 59, - "h": 63 - }, - "frame": { - "x": 307, - "y": 121, - "w": 59, - "h": 63 - } - }, - { - "filename": "0063.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 59, - "h": 63 - }, - "frame": { - "x": 307, - "y": 121, - "w": 59, - "h": 63 - } - }, - { - "filename": "0065.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 247, - "y": 187, - "w": 60, - "h": 63 - } - }, - { - "filename": "0066.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 247, - "y": 187, - "w": 60, - "h": 63 - } - }, - { - "filename": "0089.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 247, - "y": 187, - "w": 60, - "h": 63 - } - }, - { - "filename": "0090.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 247, - "y": 187, - "w": 60, - "h": 63 - } - }, - { - "filename": "0019.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 58, - "h": 63 - }, - "frame": { - "x": 307, - "y": 184, - "w": 58, - "h": 63 - } - }, - { - "filename": "0040.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 58, - "h": 63 - }, - "frame": { - "x": 307, - "y": 184, - "w": 58, - "h": 63 - } - }, - { - "filename": "0061.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 58, - "h": 63 - }, - "frame": { - "x": 307, - "y": 184, - "w": 58, - "h": 63 - } - }, - { - "filename": "0062.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 58, - "h": 63 - }, - "frame": { - "x": 307, - "y": 184, - "w": 58, - "h": 63 - } - }, - { - "filename": "0067.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 249, - "y": 250, - "w": 60, - "h": 63 - } - } - ] - } - ], - "meta": { - "app": "https://www.codeandweb.com/texturepacker", - "version": "3.0", - "smartupdate": "$TexturePacker:SmartUpdate:9c4e9647cd30b406386dcfa45795951c:b817a280fcd689ce74ea32e378a31e74:40bb9f4809624b12bf79bbfe664bea73$" - } +{ "frames": [ + { + "filename": "0001.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0002.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0003.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0004.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0005.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0006.png", + "frame": { "x": 0, "y": 185, "w": 59, "h": 59 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 4, "y": 7, "w": 59, "h": 59 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0007.png", + "frame": { "x": 119, "y": 182, "w": 62, "h": 57 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 9, "w": 62, "h": 57 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0008.png", + "frame": { "x": 119, "y": 125, "w": 64, "h": 57 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 10, "w": 64, "h": 57 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0009.png", + "frame": { "x": 195, "y": 0, "w": 66, "h": 60 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 7, "w": 66, "h": 60 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0010.png", + "frame": { "x": 129, "y": 0, "w": 66, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 66, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0011.png", + "frame": { "x": 320, "y": 0, "w": 62, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 62, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0012.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0013.png", + "frame": { "x": 0, "y": 244, "w": 53, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 53, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0014.png", + "frame": { "x": 59, "y": 188, "w": 56, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 56, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0015.png", + "frame": { "x": 306, "y": 187, "w": 57, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 57, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0016.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0017.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0018.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0019.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0020.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0021.png", + "frame": { "x": 0, "y": 185, "w": 59, "h": 59 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 4, "y": 7, "w": 59, "h": 59 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0022.png", + "frame": { "x": 119, "y": 182, "w": 62, "h": 57 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 9, "w": 62, "h": 57 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0023.png", + "frame": { "x": 119, "y": 125, "w": 64, "h": 57 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 10, "w": 64, "h": 57 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0024.png", + "frame": { "x": 195, "y": 0, "w": 66, "h": 60 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 7, "w": 66, "h": 60 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0025.png", + "frame": { "x": 129, "y": 0, "w": 66, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 66, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0026.png", + "frame": { "x": 320, "y": 0, "w": 62, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 62, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0027.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0028.png", + "frame": { "x": 0, "y": 244, "w": 53, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 53, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0029.png", + "frame": { "x": 59, "y": 188, "w": 56, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 56, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0030.png", + "frame": { "x": 306, "y": 187, "w": 57, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 57, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0031.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0032.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0033.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0034.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0035.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0036.png", + "frame": { "x": 0, "y": 185, "w": 59, "h": 59 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 4, "y": 7, "w": 59, "h": 59 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0037.png", + "frame": { "x": 119, "y": 182, "w": 62, "h": 57 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 9, "w": 62, "h": 57 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0038.png", + "frame": { "x": 119, "y": 125, "w": 64, "h": 57 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 10, "w": 64, "h": 57 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0039.png", + "frame": { "x": 195, "y": 0, "w": 66, "h": 60 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 7, "w": 66, "h": 60 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0040.png", + "frame": { "x": 129, "y": 0, "w": 66, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 66, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0041.png", + "frame": { "x": 320, "y": 0, "w": 62, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 62, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0042.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0043.png", + "frame": { "x": 0, "y": 244, "w": 53, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 53, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0044.png", + "frame": { "x": 59, "y": 188, "w": 56, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 56, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0045.png", + "frame": { "x": 306, "y": 187, "w": 57, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 57, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0046.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0047.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0048.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0049.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0050.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0051.png", + "frame": { "x": 248, "y": 129, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0052.png", + "frame": { "x": 188, "y": 123, "w": 60, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 5, "w": 60, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0053.png", + "frame": { "x": 0, "y": 125, "w": 61, "h": 60 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 2, "y": 6, "w": 61, "h": 60 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0054.png", + "frame": { "x": 0, "y": 66, "w": 63, "h": 59 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 0, "y": 7, "w": 63, "h": 59 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0055.png", + "frame": { "x": 234, "y": 190, "w": 61, "h": 56 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 2, "y": 10, "w": 61, "h": 56 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0056.png", + "frame": { "x": 234, "y": 246, "w": 60, "h": 55 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 11, "w": 60, "h": 55 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0057.png", + "frame": { "x": 115, "y": 239, "w": 61, "h": 56 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 7, "y": 11, "w": 61, "h": 56 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0058.png", + "frame": { "x": 63, "y": 62, "w": 62, "h": 60 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 10, "y": 7, "w": 62, "h": 60 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0059.png", + "frame": { "x": 63, "y": 0, "w": 66, "h": 62 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 8, "y": 4, "w": 66, "h": 62 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0060.png", + "frame": { "x": 0, "y": 0, "w": 63, "h": 66 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 6, "y": 0, "w": 63, "h": 66 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0061.png", + "frame": { "x": 261, "y": 0, "w": 59, "h": 66 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 4, "y": 0, "w": 59, "h": 66 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0062.png", + "frame": { "x": 181, "y": 184, "w": 53, "h": 66 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 0, "w": 53, "h": 66 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0063.png", + "frame": { "x": 63, "y": 122, "w": 56, "h": 66 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 0, "w": 56, "h": 66 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0064.png", + "frame": { "x": 320, "y": 61, "w": 58, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 1, "w": 58, "h": 65 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0065.png", + "frame": { "x": 129, "y": 61, "w": 59, "h": 64 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 2, "w": 59, "h": 64 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0066.png", + "frame": { "x": 195, "y": 60, "w": 60, "h": 63 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 3, "w": 60, "h": 63 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0067.png", + "frame": { "x": 255, "y": 66, "w": 59, "h": 63 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 3, "w": 59, "h": 63 }, + "sourceSize": { "w": 74, "h": 67 } + } + ], + "meta": { + "app": "https://www.aseprite.org/", + "version": "1.3.9.2-x64", + "image": "335.png", + "format": "I8", + "size": { "w": 382, "h": 305 }, + "scale": "1" + } } diff --git a/public/images/pokemon/335.png b/public/images/pokemon/335.png index e5d051dd850..65b56582339 100644 Binary files a/public/images/pokemon/335.png and b/public/images/pokemon/335.png differ diff --git a/public/images/pokemon/back/658.json b/public/images/pokemon/back/658.json index 050b63e3592..1d8893e2d5d 100644 --- a/public/images/pokemon/back/658.json +++ b/public/images/pokemon/back/658.json @@ -1,19 +1,19 @@ { "frames": [ { "filename": "0001.png", - "frame": { "x": 0, "y": 0, "w": 77, "h": 77 }, + "frame": { "x": 0, "y": 0, "w": 77, "h": 65 }, "rotated": false, "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 77, "h": 77 }, - "sourceSize": { "w": 77, "h": 77 }, + "spriteSourceSize": { "x": 0, "y": 0, "w": 77, "h": 65 }, + "sourceSize": { "w": 77, "h": 65 }, "duration": 100 } ], "meta": { "app": "https://www.aseprite.org/", - "version": "1.3.7-x64", + "version": "1.3.9.2-x64", "format": "I8", - "size": { "w": 77, "h": 77 }, + "size": { "w": 77, "h": 65 }, "scale": "1" } } diff --git a/public/images/pokemon/back/658.png b/public/images/pokemon/back/658.png index ea24d9a6336..be286b88666 100644 Binary files a/public/images/pokemon/back/658.png and b/public/images/pokemon/back/658.png differ diff --git a/public/images/pokemon/back/shiny/658.json b/public/images/pokemon/back/shiny/658.json index 050b63e3592..867e1d2d3d2 100644 --- a/public/images/pokemon/back/shiny/658.json +++ b/public/images/pokemon/back/shiny/658.json @@ -1,11 +1,11 @@ { "frames": [ { "filename": "0001.png", - "frame": { "x": 0, "y": 0, "w": 77, "h": 77 }, + "frame": { "x": 0, "y": 0, "w": 77, "h": 65 }, "rotated": false, "trimmed": false, - "spriteSourceSize": { "x": 0, "y": 0, "w": 77, "h": 77 }, - "sourceSize": { "w": 77, "h": 77 }, + "spriteSourceSize": { "x": 0, "y": 0, "w": 77, "h": 65 }, + "sourceSize": { "w": 77, "h": 65 }, "duration": 100 } ], @@ -13,7 +13,7 @@ "app": "https://www.aseprite.org/", "version": "1.3.7-x64", "format": "I8", - "size": { "w": 77, "h": 77 }, + "size": { "w": 77, "h": 65 }, "scale": "1" } } diff --git a/public/images/pokemon/back/shiny/658.png b/public/images/pokemon/back/shiny/658.png index 21519b6a145..239aaafb6ce 100644 Binary files a/public/images/pokemon/back/shiny/658.png and b/public/images/pokemon/back/shiny/658.png differ diff --git a/public/images/pokemon/shiny/335.json b/public/images/pokemon/shiny/335.json index ca797f1d7a4..80c43b41c12 100644 --- a/public/images/pokemon/shiny/335.json +++ b/public/images/pokemon/shiny/335.json @@ -1,1910 +1,523 @@ -{ - "textures": [ - { - "image": "335.png", - "format": "RGBA8888", - "size": { - "w": 366, - "h": 366 - }, - "scale": 1, - "frames": [ - { - "filename": "0013.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 68, - "h": 63 - }, - "frame": { - "x": 0, - "y": 0, - "w": 68, - "h": 63 - } - }, - { - "filename": "0014.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 68, - "h": 63 - }, - "frame": { - "x": 0, - "y": 0, - "w": 68, - "h": 63 - } - }, - { - "filename": "0035.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 68, - "h": 63 - }, - "frame": { - "x": 0, - "y": 0, - "w": 68, - "h": 63 - } - }, - { - "filename": "0056.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 68, - "h": 63 - }, - "frame": { - "x": 0, - "y": 0, - "w": 68, - "h": 63 - } - }, - { - "filename": "0079.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 5, - "y": 0, - "w": 65, - "h": 66 - }, - "frame": { - "x": 0, - "y": 63, - "w": 65, - "h": 66 - } - }, - { - "filename": "0077.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 7, - "y": 3, - "w": 67, - "h": 63 - }, - "frame": { - "x": 68, - "y": 0, - "w": 67, - "h": 63 - } - }, - { - "filename": "0078.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 7, - "y": 3, - "w": 67, - "h": 63 - }, - "frame": { - "x": 68, - "y": 0, - "w": 67, - "h": 63 - } - }, - { - "filename": "0012.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 5, - "w": 68, - "h": 61 - }, - "frame": { - "x": 65, - "y": 63, - "w": 68, - "h": 61 - } - }, - { - "filename": "0033.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 5, - "w": 68, - "h": 61 - }, - "frame": { - "x": 65, - "y": 63, - "w": 68, - "h": 61 - } - }, - { - "filename": "0034.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 5, - "w": 68, - "h": 61 - }, - "frame": { - "x": 65, - "y": 63, - "w": 68, - "h": 61 - } - }, - { - "filename": "0055.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 5, - "w": 68, - "h": 61 - }, - "frame": { - "x": 65, - "y": 63, - "w": 68, - "h": 61 - } - }, - { - "filename": "0015.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 64, - "h": 63 - }, - "frame": { - "x": 0, - "y": 129, - "w": 64, - "h": 63 - } - }, - { - "filename": "0036.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 64, - "h": 63 - }, - "frame": { - "x": 0, - "y": 129, - "w": 64, - "h": 63 - } - }, - { - "filename": "0057.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 64, - "h": 63 - }, - "frame": { - "x": 0, - "y": 129, - "w": 64, - "h": 63 - } - }, - { - "filename": "0058.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 64, - "h": 63 - }, - "frame": { - "x": 0, - "y": 129, - "w": 64, - "h": 63 - } - }, - { - "filename": "0080.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 3, - "y": 0, - "w": 61, - "h": 66 - }, - "frame": { - "x": 0, - "y": 192, - "w": 61, - "h": 66 - } - }, - { - "filename": "0085.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 0, - "w": 61, - "h": 66 - }, - "frame": { - "x": 0, - "y": 258, - "w": 61, - "h": 66 - } - }, - { - "filename": "0086.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 0, - "w": 61, - "h": 66 - }, - "frame": { - "x": 0, - "y": 258, - "w": 61, - "h": 66 - } - }, - { - "filename": "0069.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 0, - "y": 4, - "w": 64, - "h": 62 - }, - "frame": { - "x": 135, - "y": 0, - "w": 64, - "h": 62 - } - }, - { - "filename": "0070.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 0, - "y": 4, - "w": 64, - "h": 62 - }, - "frame": { - "x": 135, - "y": 0, - "w": 64, - "h": 62 - } - }, - { - "filename": "0076.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 8, - "y": 5, - "w": 65, - "h": 61 - }, - "frame": { - "x": 199, - "y": 0, - "w": 65, - "h": 61 - } - }, - { - "filename": "0011.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 1, - "y": 8, - "w": 67, - "h": 58 - }, - "frame": { - "x": 264, - "y": 0, - "w": 67, - "h": 58 - } - }, - { - "filename": "0032.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 1, - "y": 8, - "w": 67, - "h": 58 - }, - "frame": { - "x": 264, - "y": 0, - "w": 67, - "h": 58 - } - }, - { - "filename": "0053.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 1, - "y": 8, - "w": 67, - "h": 58 - }, - "frame": { - "x": 264, - "y": 0, - "w": 67, - "h": 58 - } - }, - { - "filename": "0054.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 1, - "y": 8, - "w": 67, - "h": 58 - }, - "frame": { - "x": 264, - "y": 0, - "w": 67, - "h": 58 - } - }, - { - "filename": "0009.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 7, - "w": 64, - "h": 59 - }, - "frame": { - "x": 65, - "y": 124, - "w": 64, - "h": 59 - } - }, - { - "filename": "0010.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 7, - "w": 64, - "h": 59 - }, - "frame": { - "x": 65, - "y": 124, - "w": 64, - "h": 59 - } - }, - { - "filename": "0031.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 7, - "w": 64, - "h": 59 - }, - "frame": { - "x": 65, - "y": 124, - "w": 64, - "h": 59 - } - }, - { - "filename": "0052.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 7, - "w": 64, - "h": 59 - }, - "frame": { - "x": 65, - "y": 124, - "w": 64, - "h": 59 - } - }, - { - "filename": "0071.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 0, - "y": 5, - "w": 64, - "h": 61 - }, - "frame": { - "x": 64, - "y": 183, - "w": 64, - "h": 61 - } - }, - { - "filename": "0087.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 1, - "w": 62, - "h": 65 - }, - "frame": { - "x": 61, - "y": 244, - "w": 62, - "h": 65 - } - }, - { - "filename": "0075.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 6, - "y": 9, - "w": 63, - "h": 57 - }, - "frame": { - "x": 61, - "y": 309, - "w": 63, - "h": 57 - } - }, - { - "filename": "0088.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 2, - "w": 61, - "h": 64 - }, - "frame": { - "x": 123, - "y": 244, - "w": 61, - "h": 64 - } - }, - { - "filename": "0072.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 1, - "y": 8, - "w": 63, - "h": 58 - }, - "frame": { - "x": 124, - "y": 308, - "w": 63, - "h": 58 - } - }, - { - "filename": "0008.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 3, - "y": 5, - "w": 61, - "h": 61 - }, - "frame": { - "x": 128, - "y": 183, - "w": 61, - "h": 61 - } - }, - { - "filename": "0029.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 3, - "y": 5, - "w": 61, - "h": 61 - }, - "frame": { - "x": 128, - "y": 183, - "w": 61, - "h": 61 - } - }, - { - "filename": "0030.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 3, - "y": 5, - "w": 61, - "h": 61 - }, - "frame": { - "x": 128, - "y": 183, - "w": 61, - "h": 61 - } - }, - { - "filename": "0051.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 3, - "y": 5, - "w": 61, - "h": 61 - }, - "frame": { - "x": 128, - "y": 183, - "w": 61, - "h": 61 - } - }, - { - "filename": "0001.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0002.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0003.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0004.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0005.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0006.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0007.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0016.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0021.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0022.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0023.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0024.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0025.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0026.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0027.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0028.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0037.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0038.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0043.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0044.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0045.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0046.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0047.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0048.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0049.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0050.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0059.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0064.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 184, - "y": 244, - "w": 60, - "h": 63 - } - }, - { - "filename": "0073.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 9, - "w": 62, - "h": 57 - }, - "frame": { - "x": 187, - "y": 307, - "w": 62, - "h": 57 - } - }, - { - "filename": "0074.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 9, - "w": 62, - "h": 57 - }, - "frame": { - "x": 187, - "y": 307, - "w": 62, - "h": 57 - } - }, - { - "filename": "0068.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 2, - "y": 3, - "w": 62, - "h": 63 - }, - "frame": { - "x": 264, - "y": 58, - "w": 62, - "h": 63 - } - }, - { - "filename": "0017.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 55, - "h": 63 - }, - "frame": { - "x": 133, - "y": 63, - "w": 55, - "h": 63 - } - }, - { - "filename": "0018.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 55, - "h": 63 - }, - "frame": { - "x": 133, - "y": 63, - "w": 55, - "h": 63 - } - }, - { - "filename": "0039.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 55, - "h": 63 - }, - "frame": { - "x": 133, - "y": 63, - "w": 55, - "h": 63 - } - }, - { - "filename": "0060.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 55, - "h": 63 - }, - "frame": { - "x": 133, - "y": 63, - "w": 55, - "h": 63 - } - }, - { - "filename": "0081.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 0, - "w": 55, - "h": 66 - }, - "frame": { - "x": 188, - "y": 62, - "w": 55, - "h": 66 - } - }, - { - "filename": "0082.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 0, - "w": 55, - "h": 66 - }, - "frame": { - "x": 188, - "y": 62, - "w": 55, - "h": 66 - } - }, - { - "filename": "0083.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 0, - "w": 58, - "h": 66 - }, - "frame": { - "x": 189, - "y": 128, - "w": 58, - "h": 66 - } - }, - { - "filename": "0084.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 0, - "w": 60, - "h": 66 - }, - "frame": { - "x": 247, - "y": 121, - "w": 60, - "h": 66 - } - }, - { - "filename": "0020.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 59, - "h": 63 - }, - "frame": { - "x": 307, - "y": 121, - "w": 59, - "h": 63 - } - }, - { - "filename": "0041.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 59, - "h": 63 - }, - "frame": { - "x": 307, - "y": 121, - "w": 59, - "h": 63 - } - }, - { - "filename": "0042.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 59, - "h": 63 - }, - "frame": { - "x": 307, - "y": 121, - "w": 59, - "h": 63 - } - }, - { - "filename": "0063.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 59, - "h": 63 - }, - "frame": { - "x": 307, - "y": 121, - "w": 59, - "h": 63 - } - }, - { - "filename": "0065.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 247, - "y": 187, - "w": 60, - "h": 63 - } - }, - { - "filename": "0066.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 247, - "y": 187, - "w": 60, - "h": 63 - } - }, - { - "filename": "0089.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 247, - "y": 187, - "w": 60, - "h": 63 - } - }, - { - "filename": "0090.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 247, - "y": 187, - "w": 60, - "h": 63 - } - }, - { - "filename": "0019.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 58, - "h": 63 - }, - "frame": { - "x": 307, - "y": 184, - "w": 58, - "h": 63 - } - }, - { - "filename": "0040.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 58, - "h": 63 - }, - "frame": { - "x": 307, - "y": 184, - "w": 58, - "h": 63 - } - }, - { - "filename": "0061.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 58, - "h": 63 - }, - "frame": { - "x": 307, - "y": 184, - "w": 58, - "h": 63 - } - }, - { - "filename": "0062.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 58, - "h": 63 - }, - "frame": { - "x": 307, - "y": 184, - "w": 58, - "h": 63 - } - }, - { - "filename": "0067.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 74, - "h": 66 - }, - "spriteSourceSize": { - "x": 4, - "y": 3, - "w": 60, - "h": 63 - }, - "frame": { - "x": 249, - "y": 250, - "w": 60, - "h": 63 - } - } - ] - } - ], - "meta": { - "app": "https://www.codeandweb.com/texturepacker", - "version": "3.0", - "smartupdate": "$TexturePacker:SmartUpdate:0df67af080306e793f3e63687a642a63:bd66cef8682173381b002070c3411214:40bb9f4809624b12bf79bbfe664bea73$" - } +{ "frames": [ + { + "filename": "0001.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0002.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0003.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0004.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0005.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0006.png", + "frame": { "x": 0, "y": 185, "w": 59, "h": 59 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 4, "y": 7, "w": 59, "h": 59 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0007.png", + "frame": { "x": 119, "y": 182, "w": 62, "h": 57 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 9, "w": 62, "h": 57 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0008.png", + "frame": { "x": 119, "y": 125, "w": 64, "h": 57 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 10, "w": 64, "h": 57 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0009.png", + "frame": { "x": 195, "y": 0, "w": 66, "h": 60 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 7, "w": 66, "h": 60 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0010.png", + "frame": { "x": 129, "y": 0, "w": 66, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 66, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0011.png", + "frame": { "x": 320, "y": 0, "w": 62, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 62, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0012.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0013.png", + "frame": { "x": 0, "y": 244, "w": 53, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 53, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0014.png", + "frame": { "x": 59, "y": 188, "w": 56, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 56, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0015.png", + "frame": { "x": 306, "y": 187, "w": 57, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 57, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0016.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0017.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0018.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0019.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0020.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0021.png", + "frame": { "x": 0, "y": 185, "w": 59, "h": 59 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 4, "y": 7, "w": 59, "h": 59 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0022.png", + "frame": { "x": 119, "y": 182, "w": 62, "h": 57 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 9, "w": 62, "h": 57 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0023.png", + "frame": { "x": 119, "y": 125, "w": 64, "h": 57 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 10, "w": 64, "h": 57 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0024.png", + "frame": { "x": 195, "y": 0, "w": 66, "h": 60 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 7, "w": 66, "h": 60 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0025.png", + "frame": { "x": 129, "y": 0, "w": 66, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 66, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0026.png", + "frame": { "x": 320, "y": 0, "w": 62, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 62, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0027.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0028.png", + "frame": { "x": 0, "y": 244, "w": 53, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 53, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0029.png", + "frame": { "x": 59, "y": 188, "w": 56, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 56, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0030.png", + "frame": { "x": 306, "y": 187, "w": 57, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 57, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0031.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0032.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0033.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0034.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0035.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0036.png", + "frame": { "x": 0, "y": 185, "w": 59, "h": 59 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 4, "y": 7, "w": 59, "h": 59 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0037.png", + "frame": { "x": 119, "y": 182, "w": 62, "h": 57 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 9, "w": 62, "h": 57 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0038.png", + "frame": { "x": 119, "y": 125, "w": 64, "h": 57 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 10, "w": 64, "h": 57 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0039.png", + "frame": { "x": 195, "y": 0, "w": 66, "h": 60 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 7, "w": 66, "h": 60 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0040.png", + "frame": { "x": 129, "y": 0, "w": 66, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 66, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0041.png", + "frame": { "x": 320, "y": 0, "w": 62, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 62, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0042.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0043.png", + "frame": { "x": 0, "y": 244, "w": 53, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 53, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0044.png", + "frame": { "x": 59, "y": 188, "w": 56, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 56, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0045.png", + "frame": { "x": 306, "y": 187, "w": 57, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 57, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0046.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0047.png", + "frame": { "x": 314, "y": 126, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0048.png", + "frame": { "x": 248, "y": 129, "w": 58, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 5, "w": 58, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0049.png", + "frame": { "x": 188, "y": 123, "w": 60, "h": 61 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 3, "y": 5, "w": 60, "h": 61 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0050.png", + "frame": { "x": 0, "y": 125, "w": 61, "h": 60 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 2, "y": 6, "w": 61, "h": 60 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0051.png", + "frame": { "x": 0, "y": 66, "w": 63, "h": 59 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 0, "y": 7, "w": 63, "h": 59 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0052.png", + "frame": { "x": 234, "y": 190, "w": 61, "h": 56 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 2, "y": 10, "w": 61, "h": 56 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0053.png", + "frame": { "x": 234, "y": 246, "w": 60, "h": 55 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 11, "w": 60, "h": 55 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0054.png", + "frame": { "x": 115, "y": 239, "w": 61, "h": 56 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 7, "y": 11, "w": 61, "h": 56 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0055.png", + "frame": { "x": 63, "y": 62, "w": 62, "h": 60 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 10, "y": 7, "w": 62, "h": 60 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0056.png", + "frame": { "x": 63, "y": 0, "w": 66, "h": 62 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 8, "y": 4, "w": 66, "h": 62 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0057.png", + "frame": { "x": 0, "y": 0, "w": 63, "h": 66 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 6, "y": 0, "w": 63, "h": 66 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0058.png", + "frame": { "x": 261, "y": 0, "w": 59, "h": 66 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 4, "y": 0, "w": 59, "h": 66 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0059.png", + "frame": { "x": 181, "y": 184, "w": 53, "h": 66 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 0, "w": 53, "h": 66 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0060.png", + "frame": { "x": 63, "y": 122, "w": 56, "h": 66 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 0, "w": 56, "h": 66 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0061.png", + "frame": { "x": 320, "y": 61, "w": 58, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 1, "w": 58, "h": 65 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0062.png", + "frame": { "x": 129, "y": 61, "w": 59, "h": 64 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 2, "w": 59, "h": 64 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0063.png", + "frame": { "x": 195, "y": 60, "w": 60, "h": 63 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 3, "w": 60, "h": 63 }, + "sourceSize": { "w": 74, "h": 67 } + }, + { + "filename": "0064.png", + "frame": { "x": 255, "y": 66, "w": 59, "h": 63 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 5, "y": 3, "w": 59, "h": 63 }, + "sourceSize": { "w": 74, "h": 67 } + } + ], + "meta": { + "app": "https://www.aseprite.org/", + "version": "1.3.9.2-x64", + "image": "335.png", + "format": "I8", + "size": { "w": 382, "h": 305 }, + "scale": "1" + } } diff --git a/public/images/pokemon/shiny/335.png b/public/images/pokemon/shiny/335.png index 765344af6fd..fc7c325a469 100644 Binary files a/public/images/pokemon/shiny/335.png and b/public/images/pokemon/shiny/335.png differ diff --git a/public/locales b/public/locales index 5f6fa82c17d..bfcd7f91c39 160000 --- a/public/locales +++ b/public/locales @@ -1 +1 @@ -Subproject commit 5f6fa82c17d5981eaec15f105880ac2b4c99cc8d +Subproject commit bfcd7f91c39630f155839872c8f66fd0a89e12ac diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 9bfa153ef60..3f285c274af 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -1401,8 +1401,8 @@ export default class BattleScene extends SceneBase { return this.currentBattle; } - newArena(biome: Biome): Arena { - this.arena = new Arena(biome, Biome[biome].toLowerCase()); + newArena(biome: Biome, playerFaints?: number): Arena { + this.arena = new Arena(biome, Biome[biome].toLowerCase(), playerFaints); this.eventTarget.dispatchEvent(new NewArenaEvent()); this.arenaBg.pipelineData = { terrainColorRatio: this.arena.getBgTerrainColorRatioForBiome() }; @@ -2353,14 +2353,14 @@ export default class BattleScene extends SceneBase { } /** - * Adds Phase to the end of phaseQueuePrepend, or at phaseQueuePrependSpliceIndex - * @param phase {@linkcode Phase} the phase to add + * Adds Phase(s) to the end of phaseQueuePrepend, or at phaseQueuePrependSpliceIndex + * @param phases {@linkcode Phase} the phase(s) to add */ - unshiftPhase(phase: Phase): void { + unshiftPhase(...phases: Phase[]): void { if (this.phaseQueuePrependSpliceIndex === -1) { - this.phaseQueuePrepend.push(phase); + this.phaseQueuePrepend.push(...phases); } else { - this.phaseQueuePrepend.splice(this.phaseQueuePrependSpliceIndex, 0, phase); + this.phaseQueuePrepend.splice(this.phaseQueuePrependSpliceIndex, 0, ...phases); } } @@ -2498,32 +2498,38 @@ export default class BattleScene extends SceneBase { * @param targetPhase {@linkcode Phase} the type of phase to search for in phaseQueue * @returns boolean if a targetPhase was found and added */ - prependToPhase(phase: Phase, targetPhase: Constructor): boolean { + prependToPhase(phase: Phase | Phase [], targetPhase: Constructor): boolean { + if (!Array.isArray(phase)) { + phase = [ phase ]; + } const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof targetPhase); if (targetIndex !== -1) { - this.phaseQueue.splice(targetIndex, 0, phase); + this.phaseQueue.splice(targetIndex, 0, ...phase); return true; } else { - this.unshiftPhase(phase); + this.unshiftPhase(...phase); return false; } } /** - * Tries to add the input phase to index after target phase in the {@linkcode phaseQueue}, else simply calls {@linkcode unshiftPhase()} - * @param phase {@linkcode Phase} the phase to be added + * Tries to add the input phase(s) to index after target phase in the {@linkcode phaseQueue}, else simply calls {@linkcode unshiftPhase()} + * @param phase {@linkcode Phase} the phase(s) to be added * @param targetPhase {@linkcode Phase} the type of phase to search for in {@linkcode phaseQueue} * @returns `true` if a `targetPhase` was found to append to */ - appendToPhase(phase: Phase, targetPhase: Constructor): boolean { + appendToPhase(phase: Phase | Phase[], targetPhase: Constructor): boolean { + if (!Array.isArray(phase)) { + phase = [ phase ]; + } const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof targetPhase); if (targetIndex !== -1 && this.phaseQueue.length > targetIndex) { - this.phaseQueue.splice(targetIndex + 1, 0, phase); + this.phaseQueue.splice(targetIndex + 1, 0, ...phase); return true; } else { - this.unshiftPhase(phase); + this.unshiftPhase(...phase); return false; } } diff --git a/src/battle.ts b/src/battle.ts index fa333040c22..7ede7b2982e 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -101,10 +101,15 @@ export default class Battle { public battleSeed: string = Utils.randomString(16, true); private battleSeedState: string | null = null; public moneyScattered: number = 0; + /** Primarily for double battles, keeps track of last enemy and player pokemon that triggered its ability or used a move */ + public lastEnemyInvolved: number; + public lastPlayerInvolved: number; public lastUsedPokeball: PokeballType | null = null; - /** The number of times a Pokemon on the player's side has fainted this battle */ - public playerFaints: number = 0; - /** The number of times a Pokemon on the enemy's side has fainted this battle */ + /** + * Saves the number of times a Pokemon on the enemy's side has fainted during this battle. + * This is saved here since we encounter a new enemy every wave. + * {@linkcode globalScene.arena.playerFaints} is the corresponding faint counter for the player and needs to be save across waves (reset every arena encounter). + */ public enemyFaints: number = 0; public playerFaintsHistory: FaintLogEntry[] = []; public enemyFaintsHistory: FaintLogEntry[] = []; @@ -115,7 +120,7 @@ export default class Battle { private rngCounter: number = 0; - constructor(gameMode: GameMode, waveIndex: number, battleType: BattleType, trainer?: Trainer, double?: boolean) { + constructor(gameMode: GameMode, waveIndex: number, battleType: BattleType, trainer?: Trainer, double: boolean = false) { this.gameMode = gameMode; this.waveIndex = waveIndex; this.battleType = battleType; @@ -124,7 +129,7 @@ export default class Battle { this.enemyLevels = battleType !== BattleType.TRAINER ? new Array(double ? 2 : 1).fill(null).map(() => this.getLevelForWave()) : trainer?.getPartyLevels(this.waveIndex); - this.double = double ?? false; + this.double = double; } private initBattleSpec(): void { diff --git a/src/data/ability.ts b/src/data/ability.ts index c19b6fe9ba4..a6d00b29fbc 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -2756,6 +2756,44 @@ export class PreStatStageChangeAbAttr extends AbAttr { } } +/** + * Reflect all {@linkcode BattleStat} reductions caused by other Pokémon's moves and Abilities. + * Currently only applies to Mirror Armor. + */ +export class ReflectStatStageChangeAbAttr extends PreStatStageChangeAbAttr { + /** {@linkcode BattleStat} to reflect */ + private reflectedStat? : BattleStat; + + /** + * Apply the {@linkcode ReflectStatStageChangeAbAttr} to an interaction + * @param _pokemon The user pokemon + * @param _passive N/A + * @param simulated `true` if the ability is being simulated by the AI + * @param stat the {@linkcode BattleStat} being affected + * @param cancelled The {@linkcode Utils.BooleanHolder} that will be set to true due to reflection + * @param args + * @returns true because it reflects any stat being lowered + */ + applyPreStatStageChange(_pokemon: Pokemon, _passive: boolean, simulated: boolean, stat: BattleStat, cancelled: Utils.BooleanHolder, args: any[]): boolean { + const attacker: Pokemon = args[0]; + const stages = args[1]; + this.reflectedStat = stat; + if (!simulated) { + globalScene.unshiftPhase(new StatStageChangePhase(attacker.getBattlerIndex(), false, [ stat ], stages, true, false, true, null, true)); + } + cancelled.value = true; + return true; + } + + getTriggerMessage(pokemon: Pokemon, abilityName: string, ..._args: any[]): string { + return i18next.t("abilityTriggers:protectStat", { + pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), + abilityName, + statName: this.reflectedStat ? i18next.t(getStatKey(this.reflectedStat)) : i18next.t("battle:stats") + }); + } +} + /** * Protect one or all {@linkcode BattleStat} from reductions caused by other Pokémon's moves and Abilities */ @@ -4446,6 +4484,13 @@ export class InfiltratorAbAttr extends AbAttr { } } +/** + * Attribute implementing the effects of {@link https://bulbapedia.bulbagarden.net/wiki/Magic_Bounce_(ability) | Magic Bounce}. + * Allows the source to bounce back {@linkcode MoveFlags.REFLECTABLE | Reflectable} + * moves as if the user had used {@linkcode Moves.MAGIC_COAT | Magic Coat}. + */ +export class ReflectStatusMoveAbAttr extends AbAttr { } + export class UncopiableAbilityAbAttr extends AbAttr { constructor() { super(false); @@ -5767,8 +5812,11 @@ export function initAbilities() { }, Stat.SPD, 1) .attr(PostIntimidateStatStageChangeAbAttr, [ Stat.SPD ], 1), new Ability(Abilities.MAGIC_BOUNCE, 5) + .attr(ReflectStatusMoveAbAttr) .ignorable() - .unimplemented(), + // Interactions with stomping tantrum, instruct, encore, and probably other moves that + // rely on move history + .edgeCase(), new Ability(Abilities.SAP_SIPPER, 5) .attr(TypeImmunityStatStageChangeAbAttr, Type.GRASS, Stat.ATK, 1) .ignorable(), @@ -6065,8 +6113,8 @@ export function initAbilities() { new Ability(Abilities.PROPELLER_TAIL, 8) .attr(BlockRedirectAbAttr), new Ability(Abilities.MIRROR_ARMOR, 8) - .ignorable() - .unimplemented(), + .attr(ReflectStatStageChangeAbAttr) + .ignorable(), /** * Right now, the logic is attached to Surf and Dive moves. Ideally, the post-defend/hit should be an * ability attribute but the current implementation of move effects for BattlerTag does not support this- in the case @@ -6275,8 +6323,8 @@ export function initAbilities() { new Ability(Abilities.SHARPNESS, 9) .attr(MovePowerBoostAbAttr, (user, target, move) => move.hasFlag(MoveFlags.SLICING_MOVE), 1.5), new Ability(Abilities.SUPREME_OVERLORD, 9) - .attr(VariableMovePowerBoostAbAttr, (user, target, move) => 1 + 0.1 * Math.min(user.isPlayer() ? globalScene.currentBattle.playerFaints : globalScene.currentBattle.enemyFaints, 5)) - .partial(), // Counter resets every wave instead of on arena reset + .attr(VariableMovePowerBoostAbAttr, (user, target, move) => 1 + 0.1 * Math.min(user.isPlayer() ? globalScene.arena.playerFaints : globalScene.currentBattle.enemyFaints, 5)) + .partial(), // Should only boost once, on summon new Ability(Abilities.COSTAR, 9) .attr(PostSummonCopyAllyStatsAbAttr), new Ability(Abilities.TOXIC_DEBRIS, 9) diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index 816de3e824c..2fa4593fd6c 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -910,7 +910,7 @@ class StickyWebTag extends ArenaTrapTag { if (!cancelled.value) { globalScene.queueMessage(i18next.t("arenaTag:stickyWebActivateTrap", { pokemonName: pokemon.getNameToRender() })); const stages = new NumberHolder(-1); - globalScene.unshiftPhase(new StatStageChangePhase(pokemon.getBattlerIndex(), false, [ Stat.SPD ], stages.value)); + globalScene.unshiftPhase(new StatStageChangePhase(pokemon.getBattlerIndex(), false, [ Stat.SPD ], stages.value, true, false, true, null, false, true)); return true; } } diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index c399a9bb595..91ab10aecfa 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1752,7 +1752,7 @@ export class HighestStatBoostTag extends AbilityBattlerTag { super.onAdd(pokemon); let highestStat: EffectiveStat; - EFFECTIVE_STATS.map(s => pokemon.getEffectiveStat(s)).reduce((highestValue: number, value: number, i: number) => { + EFFECTIVE_STATS.map(s => pokemon.getEffectiveStat(s, undefined, undefined, undefined, undefined, undefined, undefined, true)).reduce((highestValue: number, value: number, i: number) => { if (value > highestValue) { highestStat = EFFECTIVE_STATS[i]; return value; @@ -1763,15 +1763,7 @@ export class HighestStatBoostTag extends AbilityBattlerTag { highestStat = highestStat!; // tell TS compiler it's defined! this.stat = highestStat; - switch (this.stat) { - case Stat.SPD: - this.multiplier = 1.5; - break; - default: - this.multiplier = 1.3; - break; - } - + this.multiplier = this.stat === Stat.SPD ? 1.5 : 1.3; globalScene.queueMessage(i18next.t("battlerTags:highestStatBoostOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), statName: i18next.t(getStatKey(highestStat)) }), null, false, null, true); } @@ -2983,6 +2975,24 @@ export class PsychoShiftTag extends BattlerTag { } } +/** + * Tag associated with the move Magic Coat. + */ +export class MagicCoatTag extends BattlerTag { + constructor() { + super(BattlerTagType.MAGIC_COAT, BattlerTagLapseType.TURN_END, 1, Moves.MAGIC_COAT); + } + + /** + * Queues the "[PokemonName] shrouded itself with Magic Coat" message when the tag is added. + * @param pokemon - The target {@linkcode Pokemon} + */ + override onAdd(pokemon: Pokemon) { + // "{pokemonNameWithAffix} shrouded itself with Magic Coat!" + globalScene.queueMessage(i18next.t("battlerTags:magicCoatOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })); + } +} + /** * Retrieves a {@linkcode BattlerTag} based on the provided tag type, turn count, source move, and source ID. * @param sourceId - The ID of the pokemon adding the tag @@ -3172,6 +3182,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source return new GrudgeTag(); case BattlerTagType.PSYCHO_SHIFT: return new PsychoShiftTag(); + case BattlerTagType.MAGIC_COAT: + return new MagicCoatTag(); case BattlerTagType.NONE: default: return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId); diff --git a/src/data/move.ts b/src/data/move.ts index 48f90297115..1c768f20bb0 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -125,7 +125,9 @@ export enum MoveFlags { /** Indicates a move is able to bypass its target's Substitute (if the target has one) */ IGNORE_SUBSTITUTE = 1 << 17, /** Indicates a move is able to be redirected to allies in a double battle if the attacker faints */ - REDIRECT_COUNTER = 1 << 18, + REDIRECT_COUNTER = 1 << 18, + /** Indicates a move is able to be reflected by {@linkcode Abilities.MAGIC_BOUNCE} and {@linkcode Moves.MAGIC_COAT} */ + REFLECTABLE = 1 << 19, } type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean; @@ -610,6 +612,16 @@ export default class Move implements Localizable { return this; } + /** + * Sets the {@linkcode MoveFlags.REFLECTABLE} flag for the calling Move + * @see {@linkcode Moves.ATTRACT} + * @returns The {@linkcode Move} that called this function + */ + reflectable(): this { + this.setFlag(MoveFlags.REFLECTABLE, true); + return this; + } + /** * Checks if the move flag applies to the pokemon(s) using/receiving the move * @param flag {@linkcode MoveFlags} MoveFlag to check on user and/or target @@ -4368,6 +4380,69 @@ export class CueNextRoundAttr extends MoveEffectAttr { } } +/** + * Attribute that changes stat stages before the damage is calculated + */ +export class StatChangeBeforeDmgCalcAttr extends MoveAttr { + /** + * Applies Stat Changes before damage is calculated + * + * @param user {@linkcode Pokemon} that called {@linkcode move} + * @param target {@linkcode Pokemon} that is the target of {@linkcode move} + * @param move {@linkcode Move} called by {@linkcode user} + * @param args N/A + * + * @returns true if stat stages where correctly applied + */ + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + return false; + } +} + +/** + * Steals the postitive Stat stages of the target before damage calculation so stat changes + * apply to damage calculation (e.g. {@linkcode Moves.SPECTRAL_THIEF}) + * {@link https://bulbapedia.bulbagarden.net/wiki/Spectral_Thief_(move) | Spectral Thief} + */ +export class SpectralThiefAttr extends StatChangeBeforeDmgCalcAttr { + /** + * steals max amount of positive stats of the target while not exceeding the limit of max 6 stat stages + * + * @param user {@linkcode Pokemon} that called {@linkcode move} + * @param target {@linkcode Pokemon} that is the target of {@linkcode move} + * @param move {@linkcode Move} called by {@linkcode user} + * @param args N/A + * + * @returns true if stat stages where correctly stolen + */ + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + /** + * Copy all positive stat stages to user and reduce copied stat stages on target. + */ + for (const s of BATTLE_STATS) { + const statStageValueTarget = target.getStatStage(s); + const statStageValueUser = user.getStatStage(s); + + if (statStageValueTarget > 0) { + /** + * Only value of up to 6 can be stolen (stat stages don't exceed 6) + */ + const availableToSteal = Math.min(statStageValueTarget, 6 - statStageValueUser); + + globalScene.unshiftPhase(new StatStageChangePhase(user.getBattlerIndex(), this.selfTarget, [ s ], availableToSteal)); + target.setStatStage(s, statStageValueTarget - availableToSteal); + } + } + + target.updateInfo(); + user.updateInfo(); + globalScene.queueMessage(i18next.t("moveTriggers:stealPositiveStats", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target) })); + + return true; + } + +} + export class VariableAtkAttr extends MoveAttr { constructor() { super(); @@ -4559,7 +4634,8 @@ export class TeraMoveCategoryAttr extends VariableMoveCategoryAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const category = (args[0] as Utils.NumberHolder); - if (user.isTerastallized() && user.getEffectiveStat(Stat.ATK, target, move) > user.getEffectiveStat(Stat.SPATK, target, move)) { + if (user.isTerastallized() && user.getEffectiveStat(Stat.ATK, target, move, true, true, false, false, true) > + user.getEffectiveStat(Stat.SPATK, target, move, true, true, false, false, true)) { category.value = MoveCategory.PHYSICAL; return true; } @@ -5331,6 +5407,7 @@ export class AddBattlerTagAttr extends MoveEffectAttr { case BattlerTagType.INGRAIN: case BattlerTagType.IGNORE_ACCURACY: case BattlerTagType.AQUA_RING: + case BattlerTagType.MAGIC_COAT: return 3; case BattlerTagType.PROTECTED: case BattlerTagType.FLYING: @@ -8333,7 +8410,8 @@ export function initMoves() { .attr(ForceSwitchOutAttr, false, SwitchType.FORCE_SWITCH) .ignoresSubstitute() .hidesTarget() - .windMove(), + .windMove() + .reflectable(), 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) @@ -8357,7 +8435,8 @@ export function initMoves() { new AttackMove(Moves.ROLLING_KICK, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 85, 15, 30, 0, 1) .attr(FlinchAttr), new StatusMove(Moves.SAND_ATTACK, Type.GROUND, 100, 15, -1, 0, 1) - .attr(StatStageChangeAttr, [ Stat.ACC ], -1), + .attr(StatStageChangeAttr, [ Stat.ACC ], -1) + .reflectable(), new AttackMove(Moves.HEADBUTT, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 15, 30, 0, 1) .attr(FlinchAttr), new AttackMove(Moves.HORN_ATTACK, Type.NORMAL, MoveCategory.PHYSICAL, 65, 100, 25, -1, 0, 1), @@ -8386,7 +8465,8 @@ export function initMoves() { .recklessMove(), new StatusMove(Moves.TAIL_WHIP, Type.NORMAL, 100, 30, -1, 0, 1) .attr(StatStageChangeAttr, [ Stat.DEF ], -1) - .target(MoveTarget.ALL_NEAR_ENEMIES), + .target(MoveTarget.ALL_NEAR_ENEMIES) + .reflectable(), new AttackMove(Moves.POISON_STING, Type.POISON, MoveCategory.PHYSICAL, 15, 100, 35, 30, 0, 1) .attr(StatusEffectAttr, StatusEffect.POISON) .makesContact(false), @@ -8399,30 +8479,36 @@ export function initMoves() { .makesContact(false), new StatusMove(Moves.LEER, Type.NORMAL, 100, 30, -1, 0, 1) .attr(StatStageChangeAttr, [ Stat.DEF ], -1) - .target(MoveTarget.ALL_NEAR_ENEMIES), + .target(MoveTarget.ALL_NEAR_ENEMIES) + .reflectable(), new AttackMove(Moves.BITE, Type.DARK, MoveCategory.PHYSICAL, 60, 100, 25, 30, 0, 1) .attr(FlinchAttr) .bitingMove(), new StatusMove(Moves.GROWL, Type.NORMAL, 100, 40, -1, 0, 1) .attr(StatStageChangeAttr, [ Stat.ATK ], -1) .soundBased() - .target(MoveTarget.ALL_NEAR_ENEMIES), + .target(MoveTarget.ALL_NEAR_ENEMIES) + .reflectable(), new StatusMove(Moves.ROAR, Type.NORMAL, -1, 20, -1, -6, 1) .attr(ForceSwitchOutAttr, false, SwitchType.FORCE_SWITCH) .soundBased() - .hidesTarget(), + .hidesTarget() + .reflectable(), new StatusMove(Moves.SING, Type.NORMAL, 55, 15, -1, 0, 1) .attr(StatusEffectAttr, StatusEffect.SLEEP) - .soundBased(), + .soundBased() + .reflectable(), new StatusMove(Moves.SUPERSONIC, Type.NORMAL, 55, 20, -1, 0, 1) .attr(ConfuseAttr) - .soundBased(), + .soundBased() + .reflectable(), new AttackMove(Moves.SONIC_BOOM, Type.NORMAL, MoveCategory.SPECIAL, -1, 90, 20, -1, 0, 1) .attr(FixedDamageAttr, 20), new StatusMove(Moves.DISABLE, Type.NORMAL, 100, 20, -1, 0, 1) .attr(AddBattlerTagAttr, BattlerTagType.DISABLED, false, true) .condition((user, target, move) => target.getMoveHistory().reverse().find(m => m.move !== Moves.NONE && m.move !== Moves.STRUGGLE && !m.virtual) !== undefined) - .ignoresSubstitute(), + .ignoresSubstitute() + .reflectable(), new AttackMove(Moves.ACID, Type.POISON, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1) .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1) .target(MoveTarget.ALL_NEAR_ENEMIES), @@ -8475,7 +8561,8 @@ export function initMoves() { .triageMove(), new StatusMove(Moves.LEECH_SEED, Type.GRASS, 90, 10, -1, 0, 1) .attr(LeechSeedAttr) - .condition((user, target, move) => !target.getTag(BattlerTagType.SEEDED) && !target.isOfType(Type.GRASS)), + .condition((user, target, move) => !target.getTag(BattlerTagType.SEEDED) && !target.isOfType(Type.GRASS)) + .reflectable(), new SelfStatusMove(Moves.GROWTH, Type.NORMAL, -1, 20, -1, 0, 1) .attr(GrowthStatStageChangeAttr), new AttackMove(Moves.RAZOR_LEAF, Type.GRASS, MoveCategory.PHYSICAL, 55, 95, 25, -1, 0, 1) @@ -8489,13 +8576,16 @@ export function initMoves() { .attr(AntiSunlightPowerDecreaseAttr), new StatusMove(Moves.POISON_POWDER, Type.POISON, 75, 35, -1, 0, 1) .attr(StatusEffectAttr, StatusEffect.POISON) - .powderMove(), + .powderMove() + .reflectable(), new StatusMove(Moves.STUN_SPORE, Type.GRASS, 75, 30, -1, 0, 1) .attr(StatusEffectAttr, StatusEffect.PARALYSIS) - .powderMove(), + .powderMove() + .reflectable(), new StatusMove(Moves.SLEEP_POWDER, Type.GRASS, 75, 15, -1, 0, 1) .attr(StatusEffectAttr, StatusEffect.SLEEP) - .powderMove(), + .powderMove() + .reflectable(), new AttackMove(Moves.PETAL_DANCE, Type.GRASS, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 1) .attr(FrenzyAttr) .attr(MissEffectAttr, frenzyMissFunc) @@ -8505,7 +8595,8 @@ export function initMoves() { .target(MoveTarget.RANDOM_NEAR_ENEMY), new StatusMove(Moves.STRING_SHOT, Type.BUG, 95, 40, -1, 0, 1) .attr(StatStageChangeAttr, [ Stat.SPD ], -2) - .target(MoveTarget.ALL_NEAR_ENEMIES), + .target(MoveTarget.ALL_NEAR_ENEMIES) + .reflectable(), new AttackMove(Moves.DRAGON_RAGE, Type.DRAGON, MoveCategory.SPECIAL, -1, 100, 10, -1, 0, 1) .attr(FixedDamageAttr, 40), new AttackMove(Moves.FIRE_SPIN, Type.FIRE, MoveCategory.SPECIAL, 35, 85, 15, -1, 0, 1) @@ -8516,7 +8607,8 @@ export function initMoves() { .attr(StatusEffectAttr, StatusEffect.PARALYSIS), new StatusMove(Moves.THUNDER_WAVE, Type.ELECTRIC, 90, 20, -1, 0, 1) .attr(StatusEffectAttr, StatusEffect.PARALYSIS) - .attr(RespectAttackTypeImmunityAttr), + .attr(RespectAttackTypeImmunityAttr) + .reflectable(), new AttackMove(Moves.THUNDER, Type.ELECTRIC, MoveCategory.SPECIAL, 110, 70, 10, 30, 0, 1) .attr(StatusEffectAttr, StatusEffect.PARALYSIS) .attr(ThunderAccuracyAttr) @@ -8538,13 +8630,15 @@ export function initMoves() { .chargeAttr(SemiInvulnerableAttr, BattlerTagType.UNDERGROUND), new StatusMove(Moves.TOXIC, Type.POISON, 90, 10, -1, 0, 1) .attr(StatusEffectAttr, StatusEffect.TOXIC) - .attr(ToxicAccuracyAttr), + .attr(ToxicAccuracyAttr) + .reflectable(), new AttackMove(Moves.CONFUSION, Type.PSYCHIC, MoveCategory.SPECIAL, 50, 100, 25, 10, 0, 1) .attr(ConfuseAttr), new AttackMove(Moves.PSYCHIC, Type.PSYCHIC, MoveCategory.SPECIAL, 90, 100, 10, 10, 0, 1) .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1), new StatusMove(Moves.HYPNOSIS, Type.PSYCHIC, 60, 20, -1, 0, 1) - .attr(StatusEffectAttr, StatusEffect.SLEEP), + .attr(StatusEffectAttr, StatusEffect.SLEEP) + .reflectable(), new SelfStatusMove(Moves.MEDITATE, Type.PSYCHIC, -1, 40, -1, 0, 1) .attr(StatStageChangeAttr, [ Stat.ATK ], 1, true), new SelfStatusMove(Moves.AGILITY, Type.PSYCHIC, -1, 30, -1, 0, 1) @@ -8562,7 +8656,8 @@ export function initMoves() { .ignoresSubstitute(), new StatusMove(Moves.SCREECH, Type.NORMAL, 85, 40, -1, 0, 1) .attr(StatStageChangeAttr, [ Stat.DEF ], -2) - .soundBased(), + .soundBased() + .reflectable(), new SelfStatusMove(Moves.DOUBLE_TEAM, Type.NORMAL, -1, 15, -1, 0, 1) .attr(StatStageChangeAttr, [ Stat.EVA ], 1, true), new SelfStatusMove(Moves.RECOVER, Type.NORMAL, -1, 5, -1, 0, 1) @@ -8574,9 +8669,11 @@ export function initMoves() { .attr(AddBattlerTagAttr, BattlerTagType.MINIMIZED, true, false) .attr(StatStageChangeAttr, [ Stat.EVA ], 2, true), new StatusMove(Moves.SMOKESCREEN, Type.NORMAL, 100, 20, -1, 0, 1) - .attr(StatStageChangeAttr, [ Stat.ACC ], -1), + .attr(StatStageChangeAttr, [ Stat.ACC ], -1) + .reflectable(), new StatusMove(Moves.CONFUSE_RAY, Type.GHOST, 100, 10, -1, 0, 1) - .attr(ConfuseAttr), + .attr(ConfuseAttr) + .reflectable(), new SelfStatusMove(Moves.WITHDRAW, Type.WATER, -1, 40, -1, 0, 1) .attr(StatStageChangeAttr, [ Stat.DEF ], 1, true), new SelfStatusMove(Moves.DEFENSE_CURL, Type.NORMAL, -1, 40, -1, 0, 1) @@ -8637,7 +8734,8 @@ export function initMoves() { new SelfStatusMove(Moves.AMNESIA, Type.PSYCHIC, -1, 20, -1, 0, 1) .attr(StatStageChangeAttr, [ Stat.SPDEF ], 2, true), new StatusMove(Moves.KINESIS, Type.PSYCHIC, 80, 15, -1, 0, 1) - .attr(StatStageChangeAttr, [ Stat.ACC ], -1), + .attr(StatStageChangeAttr, [ Stat.ACC ], -1) + .reflectable(), new SelfStatusMove(Moves.SOFT_BOILED, Type.NORMAL, -1, 5, -1, 0, 1) .attr(HealAttr, 0.5) .triageMove(), @@ -8647,14 +8745,16 @@ export function initMoves() { .condition(failOnGravityCondition) .recklessMove(), new StatusMove(Moves.GLARE, Type.NORMAL, 100, 30, -1, 0, 1) - .attr(StatusEffectAttr, StatusEffect.PARALYSIS), + .attr(StatusEffectAttr, StatusEffect.PARALYSIS) + .reflectable(), new AttackMove(Moves.DREAM_EATER, Type.PSYCHIC, MoveCategory.SPECIAL, 100, 100, 15, -1, 0, 1) .attr(HitHealAttr) .condition(targetSleptOrComatoseCondition) .triageMove(), new StatusMove(Moves.POISON_GAS, Type.POISON, 90, 40, -1, 0, 1) .attr(StatusEffectAttr, StatusEffect.POISON) - .target(MoveTarget.ALL_NEAR_ENEMIES), + .target(MoveTarget.ALL_NEAR_ENEMIES) + .reflectable(), new AttackMove(Moves.BARRAGE, Type.NORMAL, MoveCategory.PHYSICAL, 15, 85, 20, -1, 0, 1) .attr(MultiHitAttr) .makesContact(false) @@ -8663,7 +8763,8 @@ export function initMoves() { .attr(HitHealAttr) .triageMove(), new StatusMove(Moves.LOVELY_KISS, Type.NORMAL, 75, 10, -1, 0, 1) - .attr(StatusEffectAttr, StatusEffect.SLEEP), + .attr(StatusEffectAttr, StatusEffect.SLEEP) + .reflectable(), new ChargingAttackMove(Moves.SKY_ATTACK, Type.FLYING, MoveCategory.PHYSICAL, 140, 90, 5, 30, 0, 1) .chargeText(i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" })) .attr(HighCritAttr) @@ -8682,9 +8783,11 @@ export function initMoves() { .punchingMove(), new StatusMove(Moves.SPORE, Type.GRASS, 100, 15, -1, 0, 1) .attr(StatusEffectAttr, StatusEffect.SLEEP) - .powderMove(), + .powderMove() + .reflectable(), new StatusMove(Moves.FLASH, Type.NORMAL, 100, 20, -1, 0, 1) - .attr(StatStageChangeAttr, [ Stat.ACC ], -1), + .attr(StatStageChangeAttr, [ Stat.ACC ], -1) + .reflectable(), new AttackMove(Moves.PSYWAVE, Type.PSYCHIC, MoveCategory.SPECIAL, -1, 100, 15, -1, 0, 1) .attr(RandomLevelDamageAttr), new SelfStatusMove(Moves.SPLASH, Type.NORMAL, -1, 40, -1, 0, 1) @@ -8743,7 +8846,8 @@ export function initMoves() { .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), + .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1) + .reflectable(), new StatusMove(Moves.MIND_READER, Type.NORMAL, -1, 5, -1, 0, 2) .attr(IgnoreAccuracyAttr), new StatusMove(Moves.NIGHTMARE, Type.GHOST, 100, 15, -1, 0, 2) @@ -8774,12 +8878,14 @@ export function initMoves() { new StatusMove(Moves.COTTON_SPORE, Type.GRASS, 100, 40, -1, 0, 2) .attr(StatStageChangeAttr, [ Stat.SPD ], -2) .powderMove() - .target(MoveTarget.ALL_NEAR_ENEMIES), + .target(MoveTarget.ALL_NEAR_ENEMIES) + .reflectable(), new AttackMove(Moves.REVERSAL, Type.FIGHTING, MoveCategory.PHYSICAL, -1, 100, 15, -1, 0, 2) .attr(LowHpPowerAttr), new StatusMove(Moves.SPITE, Type.GHOST, 100, 10, -1, 0, 2) .ignoresSubstitute() - .attr(ReducePpMoveAttr, 4), + .attr(ReducePpMoveAttr, 4) + .reflectable(), new AttackMove(Moves.POWDER_SNOW, Type.ICE, MoveCategory.SPECIAL, 40, 100, 25, 10, 0, 2) .attr(StatusEffectAttr, StatusEffect.FREEZE) .target(MoveTarget.ALL_NEAR_ENEMIES), @@ -8789,10 +8895,12 @@ export function initMoves() { new AttackMove(Moves.MACH_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 40, 100, 30, -1, 1, 2) .punchingMove(), new StatusMove(Moves.SCARY_FACE, Type.NORMAL, 100, 10, -1, 0, 2) - .attr(StatStageChangeAttr, [ Stat.SPD ], -2), + .attr(StatStageChangeAttr, [ Stat.SPD ], -2) + .reflectable(), new AttackMove(Moves.FEINT_ATTACK, Type.DARK, MoveCategory.PHYSICAL, 60, -1, 20, -1, 0, 2), new StatusMove(Moves.SWEET_KISS, Type.FAIRY, 75, 10, -1, 0, 2) - .attr(ConfuseAttr), + .attr(ConfuseAttr) + .reflectable(), new SelfStatusMove(Moves.BELLY_DRUM, Type.NORMAL, -1, 10, -1, 0, 2) .attr(CutHpStatStageBoostAttr, [ Stat.ATK ], 12, 2, (user) => { globalScene.queueMessage(i18next.t("moveTriggers:cutOwnHpAndMaximizedStat", { pokemonName: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.ATK)) })); @@ -8807,13 +8915,15 @@ export function initMoves() { .ballBombMove(), new StatusMove(Moves.SPIKES, Type.GROUND, -1, 20, -1, 0, 2) .attr(AddArenaTrapTagAttr, ArenaTagType.SPIKES) - .target(MoveTarget.ENEMY_SIDE), + .target(MoveTarget.ENEMY_SIDE) + .reflectable(), new AttackMove(Moves.ZAP_CANNON, Type.ELECTRIC, MoveCategory.SPECIAL, 120, 50, 5, 100, 0, 2) .attr(StatusEffectAttr, StatusEffect.PARALYSIS) .ballBombMove(), new StatusMove(Moves.FORESIGHT, Type.NORMAL, -1, 40, -1, 0, 2) .attr(ExposedMoveAttr, BattlerTagType.IGNORE_GHOST) - .ignoresSubstitute(), + .ignoresSubstitute() + .reflectable(), new SelfStatusMove(Moves.DESTINY_BOND, Type.GHOST, -1, 5, -1, 0, 2) .ignoresProtect() .attr(DestinyBondAttr) @@ -8859,7 +8969,8 @@ export function initMoves() { .attr(ProtectAttr, BattlerTagType.ENDURING) .condition(failIfLastCondition), new StatusMove(Moves.CHARM, Type.FAIRY, 100, 20, -1, 0, 2) - .attr(StatStageChangeAttr, [ Stat.ATK ], -2), + .attr(StatStageChangeAttr, [ Stat.ATK ], -2) + .reflectable(), new AttackMove(Moves.ROLLOUT, Type.ROCK, MoveCategory.PHYSICAL, 30, 90, 20, -1, 0, 2) .partial() // Does not lock the user, also does not increase damage properly .attr(ConsecutiveUseDoublePowerAttr, 5, true, true, Moves.DEFENSE_CURL), @@ -8867,7 +8978,8 @@ export function initMoves() { .attr(SurviveDamageAttr), new StatusMove(Moves.SWAGGER, Type.NORMAL, 85, 15, -1, 0, 2) .attr(StatStageChangeAttr, [ Stat.ATK ], 2) - .attr(ConfuseAttr), + .attr(ConfuseAttr) + .reflectable(), new SelfStatusMove(Moves.MILK_DRINK, Type.NORMAL, -1, 5, -1, 0, 2) .attr(HealAttr, 0.5) .triageMove(), @@ -8880,11 +8992,13 @@ export function initMoves() { .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), + .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1) + .reflectable(), new StatusMove(Moves.ATTRACT, Type.NORMAL, 100, 15, -1, 0, 2) .attr(AddBattlerTagAttr, BattlerTagType.INFATUATED) .ignoresSubstitute() - .condition((user, target, move) => user.isOppositeGender(target)), + .condition((user, target, move) => user.isOppositeGender(target)) + .reflectable(), new SelfStatusMove(Moves.SLEEP_TALK, Type.NORMAL, -1, 10, -1, 0, 2) .attr(BypassSleepAttr) .attr(RandomMovesetMoveAttr, invalidSleepTalkMoves, false) @@ -8931,7 +9045,8 @@ export function initMoves() { new StatusMove(Moves.ENCORE, Type.NORMAL, 100, 5, -1, 0, 2) .attr(AddBattlerTagAttr, BattlerTagType.ENCORE, false, true) .ignoresSubstitute() - .condition((user, target, move) => new EncoreTag(user.id).canAdd(target)), + .condition((user, target, move) => new EncoreTag(user.id).canAdd(target)) + .reflectable(), new AttackMove(Moves.PURSUIT, Type.DARK, MoveCategory.PHYSICAL, 40, 100, 20, -1, 0, 2) .partial(), // No effect implemented new AttackMove(Moves.RAPID_SPIN, Type.NORMAL, MoveCategory.PHYSICAL, 50, 100, 40, 100, 0, 2) @@ -8952,7 +9067,8 @@ export function initMoves() { .attr(RemoveArenaTrapAttr), new StatusMove(Moves.SWEET_SCENT, Type.NORMAL, 100, 20, -1, 0, 2) .attr(StatStageChangeAttr, [ Stat.EVA ], -2) - .target(MoveTarget.ALL_NEAR_ENEMIES), + .target(MoveTarget.ALL_NEAR_ENEMIES) + .reflectable(), new AttackMove(Moves.IRON_TAIL, Type.STEEL, MoveCategory.PHYSICAL, 100, 75, 15, 30, 0, 2) .attr(StatStageChangeAttr, [ Stat.DEF ], -1), new AttackMove(Moves.METAL_CLAW, Type.STEEL, MoveCategory.PHYSICAL, 50, 95, 35, 10, 0, 2) @@ -9040,12 +9156,15 @@ export function initMoves() { new StatusMove(Moves.TORMENT, Type.DARK, 100, 15, -1, 0, 3) .ignoresSubstitute() .edgeCase() // Incomplete implementation because of Uproar's partial implementation - .attr(AddBattlerTagAttr, BattlerTagType.TORMENT, false, true, 1), + .attr(AddBattlerTagAttr, BattlerTagType.TORMENT, false, true, 1) + .reflectable(), new StatusMove(Moves.FLATTER, Type.DARK, 100, 15, -1, 0, 3) .attr(StatStageChangeAttr, [ Stat.SPATK ], 1) - .attr(ConfuseAttr), + .attr(ConfuseAttr) + .reflectable(), new StatusMove(Moves.WILL_O_WISP, Type.FIRE, 85, 15, -1, 0, 3) - .attr(StatusEffectAttr, StatusEffect.BURN), + .attr(StatusEffectAttr, StatusEffect.BURN) + .reflectable(), new StatusMove(Moves.MEMENTO, Type.DARK, 100, 10, -1, 0, 3) .attr(SacrificialAttrOnHit) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -2), @@ -9069,7 +9188,8 @@ export function initMoves() { .attr(AddBattlerTagAttr, BattlerTagType.CHARGED, true, false), new StatusMove(Moves.TAUNT, Type.DARK, 100, 20, -1, 0, 3) .ignoresSubstitute() - .attr(AddBattlerTagAttr, BattlerTagType.TAUNT, false, true, 4), + .attr(AddBattlerTagAttr, BattlerTagType.TAUNT, false, true, 4) + .reflectable(), new StatusMove(Moves.HELPING_HAND, Type.NORMAL, -1, 20, -1, 5, 3) .attr(AddBattlerTagAttr, BattlerTagType.HELPING_HAND) .ignoresSubstitute() @@ -9092,7 +9212,12 @@ export function initMoves() { new AttackMove(Moves.SUPERPOWER, Type.FIGHTING, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 3) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], -1, true), new SelfStatusMove(Moves.MAGIC_COAT, Type.PSYCHIC, -1, 15, -1, 4, 3) - .unimplemented(), + .attr(AddBattlerTagAttr, BattlerTagType.MAGIC_COAT, true, true, 0) + .condition(failIfLastCondition) + // Interactions with stomping tantrum, instruct, and other moves that + // rely on move history + // Also will not reflect roar / whirlwind if the target has ForceSwitchOutImmunityAbAttr + .edgeCase(), new SelfStatusMove(Moves.RECYCLE, Type.NORMAL, -1, 10, -1, 0, 3) .unimplemented(), new AttackMove(Moves.REVENGE, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 100, 10, -1, -4, 3) @@ -9101,7 +9226,8 @@ export function initMoves() { .attr(RemoveScreensAttr), new StatusMove(Moves.YAWN, Type.NORMAL, -1, 10, -1, 0, 3) .attr(AddBattlerTagAttr, BattlerTagType.DROWSY, false, true) - .condition((user, target, move) => !target.status && !target.isSafeguarded(user)), + .condition((user, target, move) => !target.status && !target.isSafeguarded(user)) + .reflectable(), new AttackMove(Moves.KNOCK_OFF, Type.DARK, MoveCategory.PHYSICAL, 65, 100, 20, -1, 0, 3) .attr(MovePowerMultiplierAttr, (user, target, move) => target.getHeldItems().filter(i => i.isTransferable).length > 0 ? 1.5 : 1) .attr(RemoveHeldItemAttr, false), @@ -9145,7 +9271,8 @@ export function initMoves() { .ballBombMove(), new StatusMove(Moves.FEATHER_DANCE, Type.FLYING, 100, 15, -1, 0, 3) .attr(StatStageChangeAttr, [ Stat.ATK ], -2) - .danceMove(), + .danceMove() + .reflectable(), new StatusMove(Moves.TEETER_DANCE, Type.NORMAL, 100, 20, -1, 0, 3) .attr(ConfuseAttr) .danceMove() @@ -9191,7 +9318,8 @@ export function initMoves() { .attr(PartyStatusCureAttr, i18next.t("moveTriggers:soothingAromaWaftedThroughArea"), Abilities.SAP_SIPPER) .target(MoveTarget.PARTY), new StatusMove(Moves.FAKE_TEARS, Type.DARK, 100, 20, -1, 0, 3) - .attr(StatStageChangeAttr, [ Stat.SPDEF ], -2), + .attr(StatStageChangeAttr, [ Stat.SPDEF ], -2) + .reflectable(), new AttackMove(Moves.AIR_CUTTER, Type.FLYING, MoveCategory.SPECIAL, 60, 95, 25, -1, 0, 3) .attr(HighCritAttr) .slicingMove() @@ -9202,7 +9330,8 @@ export function initMoves() { .attr(HealStatusEffectAttr, true, StatusEffect.FREEZE), new StatusMove(Moves.ODOR_SLEUTH, Type.NORMAL, -1, 40, -1, 0, 3) .attr(ExposedMoveAttr, BattlerTagType.IGNORE_GHOST) - .ignoresSubstitute(), + .ignoresSubstitute() + .reflectable(), new AttackMove(Moves.ROCK_TOMB, Type.ROCK, MoveCategory.PHYSICAL, 60, 95, 15, 100, 0, 3) .attr(StatStageChangeAttr, [ Stat.SPD ], -1) .makesContact(false), @@ -9211,12 +9340,15 @@ export function initMoves() { .windMove(), new StatusMove(Moves.METAL_SOUND, Type.STEEL, 85, 40, -1, 0, 3) .attr(StatStageChangeAttr, [ Stat.SPDEF ], -2) - .soundBased(), + .soundBased() + .reflectable(), new StatusMove(Moves.GRASS_WHISTLE, Type.GRASS, 55, 15, -1, 0, 3) .attr(StatusEffectAttr, StatusEffect.SLEEP) - .soundBased(), + .soundBased() + .reflectable(), new StatusMove(Moves.TICKLE, Type.NORMAL, 100, 20, -1, 0, 3) - .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], -1), + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], -1) + .reflectable(), new SelfStatusMove(Moves.COSMIC_POWER, Type.PSYCHIC, -1, 20, -1, 0, 3) .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], 1, true), new AttackMove(Moves.WATER_SPOUT, Type.WATER, MoveCategory.SPECIAL, 150, 100, 5, -1, 0, 3) @@ -9254,7 +9386,8 @@ export function initMoves() { .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), + .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1) + .reflectable(), new StatusMove(Moves.HOWL, Type.NORMAL, -1, 40, -1, 0, 3) .attr(StatStageChangeAttr, [ Stat.ATK ], 1) .soundBased() @@ -9317,7 +9450,8 @@ export function initMoves() { .target(MoveTarget.BOTH_SIDES), new StatusMove(Moves.MIRACLE_EYE, Type.PSYCHIC, -1, 40, -1, 0, 4) .attr(ExposedMoveAttr, BattlerTagType.IGNORE_DARK) - .ignoresSubstitute(), + .ignoresSubstitute() + .reflectable(), new AttackMove(Moves.WAKE_UP_SLAP, Type.FIGHTING, MoveCategory.PHYSICAL, 70, 100, 10, -1, 0, 4) .attr(MovePowerMultiplierAttr, (user, target, move) => targetSleptOrComatoseCondition(user, target, move) ? 2 : 1) .attr(HealStatusEffectAttr, false, StatusEffect.SLEEP), @@ -9363,6 +9497,7 @@ export function initMoves() { new AttackMove(Moves.ASSURANCE, Type.DARK, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 4) .attr(MovePowerMultiplierAttr, (user, target, move) => target.turnData.damageTaken > 0 ? 2 : 1), new StatusMove(Moves.EMBARGO, Type.DARK, 100, 15, -1, 0, 4) + .reflectable() .unimplemented(), new AttackMove(Moves.FLING, Type.DARK, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 4) .makesContact(false) @@ -9382,14 +9517,16 @@ export function initMoves() { .attr(LessPPMorePowerAttr), new StatusMove(Moves.HEAL_BLOCK, Type.PSYCHIC, 100, 15, -1, 0, 4) .attr(AddBattlerTagAttr, BattlerTagType.HEAL_BLOCK, false, true, 5) - .target(MoveTarget.ALL_NEAR_ENEMIES), + .target(MoveTarget.ALL_NEAR_ENEMIES) + .reflectable(), new AttackMove(Moves.WRING_OUT, Type.NORMAL, MoveCategory.SPECIAL, -1, 100, 5, -1, 0, 4) .attr(OpponentHighHpPowerAttr, 120) .makesContact(), new SelfStatusMove(Moves.POWER_TRICK, Type.PSYCHIC, -1, 10, -1, 0, 4) .attr(AddBattlerTagAttr, BattlerTagType.POWER_TRICK, true), new StatusMove(Moves.GASTRO_ACID, Type.POISON, 100, 10, -1, 0, 4) - .attr(SuppressAbilitiesAttr), + .attr(SuppressAbilitiesAttr) + .reflectable(), new StatusMove(Moves.LUCKY_CHANT, Type.NORMAL, -1, 30, -1, 0, 4) .attr(AddArenaTagAttr, ArenaTagType.NO_CRIT, 5, true, true) .target(MoveTarget.USER_SIDE), @@ -9411,12 +9548,14 @@ export function initMoves() { new AttackMove(Moves.LAST_RESORT, Type.NORMAL, MoveCategory.PHYSICAL, 140, 100, 5, -1, 0, 4) .attr(LastResortAttr), new StatusMove(Moves.WORRY_SEED, Type.GRASS, 100, 10, -1, 0, 4) - .attr(AbilityChangeAttr, Abilities.INSOMNIA), + .attr(AbilityChangeAttr, Abilities.INSOMNIA) + .reflectable(), new AttackMove(Moves.SUCKER_PUNCH, Type.DARK, MoveCategory.PHYSICAL, 70, 100, 5, -1, 1, 4) .condition((user, target, move) => globalScene.currentBattle.turnCommands[target.getBattlerIndex()]?.command === Command.FIGHT && !target.turnData.acted && allMoves[globalScene.currentBattle.turnCommands[target.getBattlerIndex()]?.move?.move!].category !== MoveCategory.STATUS), // TODO: is this bang correct? new StatusMove(Moves.TOXIC_SPIKES, Type.POISON, -1, 20, -1, 0, 4) .attr(AddArenaTrapTagAttr, ArenaTagType.TOXIC_SPIKES) - .target(MoveTarget.ENEMY_SIDE), + .target(MoveTarget.ENEMY_SIDE) + .reflectable(), new StatusMove(Moves.HEART_SWAP, Type.PSYCHIC, -1, 10, -1, 0, 4) .attr(SwapStatStagesAttr, BATTLE_STATS) .ignoresSubstitute(), @@ -9528,7 +9667,8 @@ export function initMoves() { .attr(ClearTerrainAttr) .attr(RemoveScreensAttr, false) .attr(RemoveArenaTrapAttr, true) - .attr(RemoveArenaTagsAttr, [ ArenaTagType.MIST, ArenaTagType.SAFEGUARD ], false), + .attr(RemoveArenaTagsAttr, [ ArenaTagType.MIST, ArenaTagType.SAFEGUARD ], false) + .reflectable(), new StatusMove(Moves.TRICK_ROOM, Type.PSYCHIC, -1, 5, -1, -7, 4) .attr(AddArenaTagAttr, ArenaTagType.TRICK_ROOM, 5) .ignoresProtect() @@ -9566,10 +9706,12 @@ export function initMoves() { new StatusMove(Moves.CAPTIVATE, Type.NORMAL, 100, 20, -1, 0, 4) .attr(StatStageChangeAttr, [ Stat.SPATK ], -2) .condition((user, target, move) => target.isOppositeGender(user)) - .target(MoveTarget.ALL_NEAR_ENEMIES), + .target(MoveTarget.ALL_NEAR_ENEMIES) + .reflectable(), new StatusMove(Moves.STEALTH_ROCK, Type.ROCK, -1, 20, -1, 0, 4) .attr(AddArenaTrapTagAttr, ArenaTagType.STEALTH_ROCK) - .target(MoveTarget.ENEMY_SIDE), + .target(MoveTarget.ENEMY_SIDE) + .reflectable(), new AttackMove(Moves.GRASS_KNOT, Type.GRASS, MoveCategory.SPECIAL, -1, 100, 20, -1, 0, 4) .attr(WeightPowerAttr) .makesContact(), @@ -9613,7 +9755,8 @@ export function initMoves() { .attr(TrapAttr, BattlerTagType.MAGMA_STORM), new StatusMove(Moves.DARK_VOID, Type.DARK, 80, 10, -1, 0, 4) //Accuracy from Generations 4-6 .attr(StatusEffectAttr, StatusEffect.SLEEP) - .target(MoveTarget.ALL_NEAR_ENEMIES), + .target(MoveTarget.ALL_NEAR_ENEMIES) + .reflectable(), new AttackMove(Moves.SEED_FLARE, Type.GRASS, MoveCategory.SPECIAL, 120, 85, 5, 40, 0, 4) .attr(StatStageChangeAttr, [ Stat.SPDEF ], -2), new AttackMove(Moves.OMINOUS_WIND, Type.GHOST, MoveCategory.SPECIAL, 60, 100, 5, 10, 0, 4) @@ -9653,7 +9796,8 @@ export function initMoves() { .condition((_user, target, _move) => !(target.species.speciesId === Species.GENGAR && target.getFormKey() === "mega")) .condition((_user, target, _move) => Utils.isNullOrUndefined(target.getTag(BattlerTagType.INGRAIN)) && Utils.isNullOrUndefined(target.getTag(BattlerTagType.IGNORE_FLYING))) .attr(AddBattlerTagAttr, BattlerTagType.TELEKINESIS, false, true, 3) - .attr(AddBattlerTagAttr, BattlerTagType.FLOATING, false, true, 3), + .attr(AddBattlerTagAttr, BattlerTagType.FLOATING, false, true, 3) + .reflectable(), new StatusMove(Moves.MAGIC_ROOM, Type.PSYCHIC, -1, 10, -1, 0, 5) .ignoresProtect() .target(MoveTarget.BOTH_SIDES) @@ -9686,7 +9830,8 @@ export function initMoves() { .attr(ElectroBallPowerAttr) .ballBombMove(), new StatusMove(Moves.SOAK, Type.WATER, 100, 20, -1, 0, 5) - .attr(ChangeTypeAttr, Type.WATER), + .attr(ChangeTypeAttr, Type.WATER) + .reflectable(), new AttackMove(Moves.FLAME_CHARGE, Type.FIRE, MoveCategory.PHYSICAL, 50, 100, 20, 100, 0, 5) .attr(StatStageChangeAttr, [ Stat.SPD ], 1, true), new SelfStatusMove(Moves.COIL, Type.POISON, -1, 20, -1, 0, 5) @@ -9699,9 +9844,11 @@ export function initMoves() { new AttackMove(Moves.FOUL_PLAY, Type.DARK, MoveCategory.PHYSICAL, 95, 100, 15, -1, 0, 5) .attr(TargetAtkUserAtkAttr), new StatusMove(Moves.SIMPLE_BEAM, Type.NORMAL, 100, 15, -1, 0, 5) - .attr(AbilityChangeAttr, Abilities.SIMPLE), + .attr(AbilityChangeAttr, Abilities.SIMPLE) + .reflectable(), new StatusMove(Moves.ENTRAINMENT, Type.NORMAL, 100, 15, -1, 0, 5) - .attr(AbilityGiveAttr), + .attr(AbilityGiveAttr) + .reflectable(), new StatusMove(Moves.AFTER_YOU, Type.NORMAL, -1, 15, -1, 0, 5) .ignoresProtect() .ignoresSubstitute() @@ -9739,7 +9886,8 @@ export function initMoves() { new StatusMove(Moves.HEAL_PULSE, Type.PSYCHIC, -1, 10, -1, 0, 5) .attr(HealAttr, 0.5, false, false) .pulseMove() - .triageMove(), + .triageMove() + .reflectable(), new AttackMove(Moves.HEX, Type.GHOST, MoveCategory.SPECIAL, 65, 100, 10, -1, 0, 5) .attr( MovePowerMultiplierAttr, @@ -9942,7 +10090,8 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, { condition: (user, target, move) => target.isOfType(Type.GRASS) && target.isGrounded() }), new StatusMove(Moves.STICKY_WEB, Type.BUG, -1, 20, -1, 0, 6) .attr(AddArenaTrapTagAttr, ArenaTagType.STICKY_WEB) - .target(MoveTarget.ENEMY_SIDE), + .target(MoveTarget.ENEMY_SIDE) + .reflectable(), new AttackMove(Moves.FELL_STINGER, Type.BUG, MoveCategory.PHYSICAL, 50, 100, 25, -1, 0, 6) .attr(PostVictoryStatStageChangeAttr, [ Stat.ATK ], 3, true ), new ChargingAttackMove(Moves.PHANTOM_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6) @@ -9950,10 +10099,12 @@ export function initMoves() { .chargeAttr(SemiInvulnerableAttr, BattlerTagType.HIDDEN) .ignoresProtect(), new StatusMove(Moves.TRICK_OR_TREAT, Type.GHOST, 100, 20, -1, 0, 6) - .attr(AddTypeAttr, Type.GHOST), + .attr(AddTypeAttr, Type.GHOST) + .reflectable(), new StatusMove(Moves.NOBLE_ROAR, Type.NORMAL, 100, 30, -1, 0, 6) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1) - .soundBased(), + .soundBased() + .reflectable(), new StatusMove(Moves.ION_DELUGE, Type.ELECTRIC, -1, 25, -1, 1, 6) .attr(AddArenaTagAttr, ArenaTagType.ION_DELUGE) .target(MoveTarget.BOTH_SIDES), @@ -9962,7 +10113,8 @@ export function initMoves() { .target(MoveTarget.ALL_NEAR_OTHERS) .triageMove(), new StatusMove(Moves.FORESTS_CURSE, Type.GRASS, 100, 20, -1, 0, 6) - .attr(AddTypeAttr, Type.GRASS), + .attr(AddTypeAttr, Type.GRASS) + .reflectable(), new AttackMove(Moves.PETAL_BLIZZARD, Type.GRASS, MoveCategory.PHYSICAL, 90, 100, 15, -1, 0, 6) .windMove() .makesContact(false) @@ -9976,9 +10128,11 @@ export function initMoves() { new StatusMove(Moves.PARTING_SHOT, Type.DARK, 100, 20, -1, 0, 6) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, false, { trigger: MoveEffectTrigger.PRE_APPLY }) .attr(ForceSwitchOutAttr, true) - .soundBased(), + .soundBased() + .reflectable(), new StatusMove(Moves.TOPSY_TURVY, Type.DARK, -1, 20, -1, 0, 6) - .attr(InvertStatsAttr), + .attr(InvertStatsAttr) + .reflectable(), new AttackMove(Moves.DRAINING_KISS, Type.FAIRY, MoveCategory.SPECIAL, 50, 100, 10, -1, 0, 6) .attr(HitHealAttr, 0.75) .makesContact() @@ -10017,10 +10171,12 @@ export function initMoves() { .condition(failIfLastCondition), new StatusMove(Moves.PLAY_NICE, Type.NORMAL, -1, 20, -1, 0, 6) .attr(StatStageChangeAttr, [ Stat.ATK ], -1) - .ignoresSubstitute(), + .ignoresSubstitute() + .reflectable(), new StatusMove(Moves.CONFIDE, Type.NORMAL, -1, 20, -1, 0, 6) .attr(StatStageChangeAttr, [ Stat.SPATK ], -1) - .soundBased(), + .soundBased() + .reflectable(), new AttackMove(Moves.DIAMOND_STORM, Type.ROCK, MoveCategory.PHYSICAL, 100, 95, 5, 50, 0, 6) .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true, { firstTargetOnly: true }) .makesContact(false) @@ -10047,14 +10203,17 @@ export function initMoves() { .condition(failIfSingleBattle) .target(MoveTarget.NEAR_ALLY), new StatusMove(Moves.EERIE_IMPULSE, Type.ELECTRIC, 100, 15, -1, 0, 6) - .attr(StatStageChangeAttr, [ Stat.SPATK ], -2), + .attr(StatStageChangeAttr, [ Stat.SPATK ], -2) + .reflectable(), new StatusMove(Moves.VENOM_DRENCH, Type.POISON, 100, 20, -1, 0, 6) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK, Stat.SPD ], -1, false, { condition: (user, target, move) => target.status?.effect === StatusEffect.POISON || target.status?.effect === StatusEffect.TOXIC }) - .target(MoveTarget.ALL_NEAR_ENEMIES), + .target(MoveTarget.ALL_NEAR_ENEMIES) + .reflectable(), new StatusMove(Moves.POWDER, Type.BUG, 100, 20, -1, 1, 6) .attr(AddBattlerTagAttr, BattlerTagType.POWDER, false, true) .ignoresSubstitute() - .powderMove(), + .powderMove() + .reflectable(), new ChargingSelfStatusMove(Moves.GEOMANCY, Type.FAIRY, -1, 10, -1, 0, 6) .chargeText(i18next.t("moveTriggers:isChargingPower", { pokemonName: "{USER}" })) .attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2, true), @@ -10076,7 +10235,8 @@ export function initMoves() { .ignoresSubstitute() .target(MoveTarget.NEAR_ALLY), new StatusMove(Moves.BABY_DOLL_EYES, Type.FAIRY, 100, 30, -1, 1, 6) - .attr(StatStageChangeAttr, [ Stat.ATK ], -1), + .attr(StatStageChangeAttr, [ Stat.ATK ], -1) + .reflectable(), new AttackMove(Moves.NUZZLE, Type.ELECTRIC, MoveCategory.PHYSICAL, 20, 100, 20, 100, 0, 6) .attr(StatusEffectAttr, StatusEffect.PARALYSIS), new AttackMove(Moves.HOLD_BACK, Type.NORMAL, MoveCategory.PHYSICAL, 40, 100, 40, -1, 0, 6) @@ -10220,13 +10380,15 @@ export function initMoves() { .punchingMove(), new StatusMove(Moves.FLORAL_HEALING, Type.FAIRY, -1, 10, -1, 0, 7) .attr(BoostHealAttr, 0.5, 2 / 3, true, false, (user, target, move) => globalScene.arena.terrain?.terrainType === TerrainType.GRASSY) - .triageMove(), + .triageMove() + .reflectable(), new AttackMove(Moves.HIGH_HORSEPOWER, Type.GROUND, MoveCategory.PHYSICAL, 95, 95, 10, -1, 0, 7), new StatusMove(Moves.STRENGTH_SAP, Type.GRASS, 100, 10, -1, 0, 7) .attr(HitHealAttr, null, Stat.ATK) .attr(StatStageChangeAttr, [ Stat.ATK ], -1) .condition((user, target, move) => target.getStatStage(Stat.ATK) > -6) - .triageMove(), + .triageMove() + .reflectable(), new ChargingAttackMove(Moves.SOLAR_BLADE, Type.GRASS, MoveCategory.PHYSICAL, 125, 100, 10, -1, 0, 7) .chargeText(i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" })) .chargeAttr(WeatherInstantChargeAttr, [ WeatherType.SUNNY, WeatherType.HARSH_SUN ]) @@ -10236,10 +10398,12 @@ export function initMoves() { .makesContact(false), new StatusMove(Moves.SPOTLIGHT, Type.NORMAL, -1, 15, -1, 3, 7) .attr(AddBattlerTagAttr, BattlerTagType.CENTER_OF_ATTENTION, false) - .condition(failIfSingleBattle), + .condition(failIfSingleBattle) + .reflectable(), new StatusMove(Moves.TOXIC_THREAD, Type.POISON, 100, 20, -1, 0, 7) .attr(StatusEffectAttr, StatusEffect.POISON) - .attr(StatStageChangeAttr, [ Stat.SPD ], -1), + .attr(StatStageChangeAttr, [ Stat.SPD ], -1) + .reflectable(), new SelfStatusMove(Moves.LASER_FOCUS, Type.NORMAL, -1, 30, -1, 0, 7) .attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_CRIT, true, false), new StatusMove(Moves.GEAR_UP, Type.STEEL, -1, 20, -1, 0, 7) @@ -10283,7 +10447,8 @@ export function initMoves() { (user: Pokemon, target: Pokemon, move: Move) => isNonVolatileStatusEffect(target.status?.effect!)) // TODO: is this bang correct? .attr(HealAttr, 0.5) .attr(HealStatusEffectAttr, false, getNonVolatileStatusEffects()) - .triageMove(), + .triageMove() + .reflectable(), new AttackMove(Moves.REVELATION_DANCE, Type.NORMAL, MoveCategory.SPECIAL, 90, 100, 15, -1, 0, 7) .danceMove() .attr(MatchUserTypeAttr), @@ -10365,14 +10530,15 @@ export function initMoves() { new AttackMove(Moves.PRISMATIC_LASER, Type.PSYCHIC, MoveCategory.SPECIAL, 160, 100, 10, -1, 0, 7) .attr(RechargeAttr), new AttackMove(Moves.SPECTRAL_THIEF, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 7) - .ignoresSubstitute() - .partial(), // Does not steal stats + .attr(SpectralThiefAttr) + .ignoresSubstitute(), new AttackMove(Moves.SUNSTEEL_STRIKE, Type.STEEL, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 7) .ignoresAbilities(), new AttackMove(Moves.MOONGEIST_BEAM, Type.GHOST, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 7) .ignoresAbilities(), new StatusMove(Moves.TEARFUL_LOOK, Type.NORMAL, -1, 20, -1, 0, 7) - .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1), + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1) + .reflectable(), new AttackMove(Moves.ZING_ZAP, Type.ELECTRIC, MoveCategory.PHYSICAL, 80, 100, 10, 30, 0, 7) .attr(FlinchAttr), new AttackMove(Moves.NATURES_MADNESS, Type.FAIRY, MoveCategory.SPECIAL, -1, 90, 10, -1, 0, 7) @@ -10491,10 +10657,12 @@ export function initMoves() { .condition((user, target, move) => user.getTag(TrappedTag)?.sourceMove !== Moves.NO_RETREAT), // fails if the user is currently trapped by No Retreat new StatusMove(Moves.TAR_SHOT, Type.ROCK, 100, 15, -1, 0, 8) .attr(StatStageChangeAttr, [ Stat.SPD ], -1) - .attr(AddBattlerTagAttr, BattlerTagType.TAR_SHOT, false), + .attr(AddBattlerTagAttr, BattlerTagType.TAR_SHOT, false) + .reflectable(), new StatusMove(Moves.MAGIC_POWDER, Type.PSYCHIC, 100, 20, -1, 0, 8) .attr(ChangeTypeAttr, Type.PSYCHIC) - .powderMove(), + .powderMove() + .reflectable(), new AttackMove(Moves.DRAGON_DARTS, Type.DRAGON, MoveCategory.PHYSICAL, 50, 100, 10, -1, 0, 8) .attr(MultiHitAttr, MultiHitType._2) .makesContact(false) @@ -10671,6 +10839,7 @@ export function initMoves() { .makesContact(false), new StatusMove(Moves.CORROSIVE_GAS, Type.POISON, 100, 40, -1, 0, 8) .target(MoveTarget.ALL_NEAR_OTHERS) + .reflectable() .unimplemented(), new StatusMove(Moves.COACHING, Type.FIGHTING, -1, 10, -1, 0, 8) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], 1) @@ -10905,8 +11074,7 @@ export function initMoves() { .attr(TeraMoveCategoryAttr) .attr(TeraBlastTypeAttr) .attr(TeraBlastPowerAttr) - .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, true, { condition: (user, target, move) => user.isTerastallized() && user.isOfType(Type.STELLAR) }) - .partial(), /** Does not ignore abilities that affect stats, relevant in determining the move's category {@see TeraMoveCategoryAttr} */ + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, true, { condition: (user, target, move) => user.isTerastallized() && user.isOfType(Type.STELLAR) }), new SelfStatusMove(Moves.SILK_TRAP, Type.BUG, -1, 10, -1, 4, 9) .attr(ProtectAttr, BattlerTagType.SILK_TRAP) .condition(failIfLastCondition), @@ -10916,8 +11084,7 @@ export function initMoves() { .attr(ConfuseAttr) .recklessMove(), new AttackMove(Moves.LAST_RESPECTS, Type.GHOST, MoveCategory.PHYSICAL, 50, 100, 10, -1, 0, 9) - .partial() // Counter resets every wave instead of on arena reset - .attr(MovePowerMultiplierAttr, (user, target, move) => 1 + Math.min(user.isPlayer() ? globalScene.currentBattle.playerFaints : globalScene.currentBattle.enemyFaints, 100)) + .attr(MovePowerMultiplierAttr, (user, target, move) => 1 + Math.min(user.isPlayer() ? globalScene.arena.playerFaints : globalScene.currentBattle.enemyFaints, 100)) .makesContact(false), new AttackMove(Moves.LUMINA_CRASH, Type.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 10, 100, 0, 9) .attr(StatStageChangeAttr, [ Stat.SPDEF ], -2), diff --git a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts index 53f89069491..287376f8bd0 100644 --- a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts +++ b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts @@ -41,8 +41,6 @@ export const FunAndGamesEncounter: MysteryEncounter = .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) .withSceneRequirement(new MoneyRequirement(0, 1.5)) // Cost equal to 1 Max Potion to play .withAutoHideIntroVisuals(false) - // Allows using move without a visible enemy pokemon - .withBattleAnimationsWithoutTargets(true) // The Wobbuffet won't use moves .withSkipEnemyBattleTurns(true) // Will skip COMMAND selection menu and go straight to FIGHT (move select) menu diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index ead0443908b..351b969b1a8 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -887,16 +887,21 @@ export function getRandomEncounterSpecies(level: number, isBoss: boolean = false let bossSpecies: PokemonSpecies; let isEventEncounter = false; const eventEncounters = globalScene.eventManager.getEventEncounters(); + let formIndex; if (eventEncounters.length > 0 && randSeedInt(2) === 1) { const eventEncounter = randSeedItem(eventEncounters); const levelSpecies = getPokemonSpecies(eventEncounter.species).getWildSpeciesForLevel(level, !eventEncounter.blockEvolution, isBoss, globalScene.gameMode); isEventEncounter = true; bossSpecies = getPokemonSpecies(levelSpecies); + formIndex = eventEncounter.formIndex; } else { bossSpecies = globalScene.arena.randomSpecies(globalScene.currentBattle.waveIndex, level, 0, getPartyLuckValue(globalScene.getPlayerParty()), isBoss); } const ret = new EnemyPokemon(bossSpecies, level, TrainerSlot.NONE, isBoss); + if (formIndex) { + ret.formIndex = formIndex; + } //Reroll shiny for event encounters if (isEventEncounter && !ret.shiny) { diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index f28ac37ae27..719b08c5b81 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -94,4 +94,5 @@ export enum BattlerTagType { PSYCHO_SHIFT = "PSYCHO_SHIFT", ENDURE_TOKEN = "ENDURE_TOKEN", POWDER = "POWDER", + MAGIC_COAT = "MAGIC_COAT", } diff --git a/src/field/arena.ts b/src/field/arena.ts index 67b83e9518f..5ee065d71dc 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -44,6 +44,11 @@ export class Arena { public bgm: string; public ignoreAbilities: boolean; public ignoringEffectSource: BattlerIndex | null; + /** + * Saves the number of times a party pokemon faints during a arena encounter. + * {@linkcode globalScene.currentBattle.enemyFaints} is the corresponding faint counter for the enemy (this resets every wave). + */ + public playerFaints: number; private lastTimeOfDay: TimeOfDay; @@ -52,12 +57,13 @@ export class Arena { public readonly eventTarget: EventTarget = new EventTarget(); - constructor(biome: Biome, bgm: string) { + constructor(biome: Biome, bgm: string, playerFaints: number = 0) { this.biomeType = biome; this.tags = []; this.bgm = bgm; this.trainerPool = biomeTrainerPools[biome]; this.updatePoolsForTimeOfDay(); + this.playerFaints = playerFaints; } init() { @@ -688,6 +694,7 @@ export class Arena { this.trySetWeather(WeatherType.NONE, false); } this.trySetTerrain(TerrainType.NONE, false, true); + this.resetPlayerFaintCount(); this.removeAllTags(); } @@ -773,6 +780,10 @@ export class Arena { return 0; } } + + resetPlayerFaintCount(): void { + this.playerFaints = 0; + } } export function getBiomeKey(biome: Biome): string { diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 16af8364502..82674fb8b46 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -7,7 +7,40 @@ import { variantColorCache } from "#app/data/variant"; import { variantData } from "#app/data/variant"; import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "#app/ui/battle-info"; import type Move from "#app/data/move"; -import { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr, MoveTarget, CombinedPledgeStabBoostAttr, VariableMoveTypeChartAttr, HpSplitAttr } from "#app/data/move"; +import { + HighCritAttr, + StatChangeBeforeDmgCalcAttr, + HitsTagAttr, + applyMoveAttrs, + FixedDamageAttr, + VariableAtkAttr, + allMoves, + MoveCategory, + TypelessAttr, + CritOnlyAttr, + getMoveTargets, + OneHitKOAttr, + VariableMoveTypeAttr, + VariableDefAttr, + AttackMove, + ModifiedDamageAttr, + VariableMoveTypeMultiplierAttr, + IgnoreOpponentStatStagesAttr, + SacrificialAttr, + VariableMoveCategoryAttr, + CounterDamageAttr, + StatStageChangeAttr, + RechargeAttr, + IgnoreWeatherTypeDebuffAttr, + BypassBurnDamageReductionAttr, + SacrificialAttrOnHit, + OneHitKOAccuracyAttr, + RespectAttackTypeImmunityAttr, + MoveTarget, + CombinedPledgeStabBoostAttr, + VariableMoveTypeChartAttr, + HpSplitAttr +} from "#app/data/move"; import type { PokemonSpeciesForm } from "#app/data/pokemon-species"; import { default as PokemonSpecies, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species"; import { getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters"; @@ -947,11 +980,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param ignoreOppAbility during an attack, determines whether the opposing Pokemon's abilities should be ignored during the stat calculation. * @param isCritical determines whether a critical hit has occurred or not (`false` by default) * @param simulated if `true`, nullifies any effects that produce any changes to game state from triggering + * @param ignoreHeldItems determines whether this Pokemon's held items should be ignored during the stat calculation, default `false` * @returns the final in-battle value of a stat */ - getEffectiveStat(stat: EffectiveStat, opponent?: Pokemon, move?: Move, ignoreAbility: boolean = false, ignoreOppAbility: boolean = false, isCritical: boolean = false, simulated: boolean = true): number { + getEffectiveStat(stat: EffectiveStat, opponent?: Pokemon, move?: Move, ignoreAbility: boolean = false, ignoreOppAbility: boolean = false, isCritical: boolean = false, simulated: boolean = true, ignoreHeldItems: boolean = false): number { const statValue = new Utils.NumberHolder(this.getStat(stat, false)); - globalScene.applyModifiers(StatBoosterModifier, this.isPlayer(), this, stat, statValue); + if (!ignoreHeldItems) { + globalScene.applyModifiers(StatBoosterModifier, this.isPlayer(), this, stat, statValue); + } // The Ruin abilities here are never ignored, but they reveal themselves on summon anyway const fieldApplied = new Utils.BooleanHolder(false); @@ -965,7 +1001,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { applyStatMultiplierAbAttrs(StatMultiplierAbAttr, this, stat, statValue, simulated); } - let ret = statValue.value * this.getStatStageMultiplier(stat, opponent, move, ignoreOppAbility, isCritical, simulated); + let ret = statValue.value * this.getStatStageMultiplier(stat, opponent, move, ignoreOppAbility, isCritical, simulated, ignoreHeldItems); switch (stat) { case Stat.ATK: @@ -1063,6 +1099,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { globalScene.applyModifiers(PokemonBaseStatFlatModifier, this.isPlayer(), this, baseStats); if (this.isFusion()) { const fusionBaseStats = this.getFusionSpeciesForm(true).baseStats; + applyChallenges(globalScene.gameMode, ChallengeType.FLIP_STAT, this, fusionBaseStats); + for (const s of PERMANENT_STATS) { baseStats[s] = Math.ceil((baseStats[s] + fusionBaseStats[s]) / 2); } @@ -2487,9 +2525,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param ignoreOppAbility determines whether the effects of the opponent's abilities (i.e. Unaware) should be ignored (`false` by default) * @param isCritical determines whether a critical hit has occurred or not (`false` by default) * @param simulated determines whether effects are applied without altering game state (`true` by default) + * @param ignoreHeldItems determines whether this Pokemon's held items should be ignored during the stat calculation, default `false` * @return the stat stage multiplier to be used for effective stat calculation */ - getStatStageMultiplier(stat: EffectiveStat, opponent?: Pokemon, move?: Move, ignoreOppAbility: boolean = false, isCritical: boolean = false, simulated: boolean = true): number { + getStatStageMultiplier(stat: EffectiveStat, opponent?: Pokemon, move?: Move, ignoreOppAbility: boolean = false, isCritical: boolean = false, simulated: boolean = true, ignoreHeldItems: boolean = false): number { const statStage = new Utils.IntegerHolder(this.getStatStage(stat)); const ignoreStatStage = new Utils.BooleanHolder(false); @@ -2516,7 +2555,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (!ignoreStatStage.value) { const statStageMultiplier = new Utils.NumberHolder(Math.max(2, 2 + statStage.value) / Math.max(2, 2 - statStage.value)); - globalScene.applyModifiers(TempStatStageBoosterModifier, this.isPlayer(), stat, statStageMultiplier); + if (!ignoreHeldItems) { + globalScene.applyModifiers(TempStatStageBoosterModifier, this.isPlayer(), stat, statStageMultiplier); + } return Math.min(statStageMultiplier.value, 4); } return 1; @@ -2895,6 +2936,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { isCritical = false; } + /** + * Applies stat changes from {@linkcode move} and gives it to {@linkcode source} + * before damage calculation + */ + applyMoveAttrs(StatChangeBeforeDmgCalcAttr, source, this, move); + const { cancelled, result, damage: dmg } = this.getAttackDamage(source, move, false, false, isCritical, false); const typeBoost = source.findTag(t => t instanceof TypeBoostTag && t.boostedType === source.getMoveType(move)) as TypeBoostTag; @@ -4356,8 +4403,12 @@ export class PlayerPokemon extends Pokemon { ].filter(d => !!d); const amount = new Utils.NumberHolder(friendship); globalScene.applyModifier(PokemonFriendshipBoosterModifier, true, this, amount); - const candyFriendshipMultiplier = globalScene.eventManager.getClassicFriendshipMultiplier(); - const starterAmount = new Utils.NumberHolder(Math.floor(amount.value * (globalScene.gameMode.isClassic ? candyFriendshipMultiplier : 1) / (fusionStarterSpeciesId ? 2 : 1))); + const candyFriendshipMultiplier = globalScene.gameMode.isClassic ? globalScene.eventManager.getClassicFriendshipMultiplier() : 1; + const fusionReduction = fusionStarterSpeciesId + ? globalScene.eventManager.areFusionsBoosted() ? 1.5 // Divide candy gain for fusions by 1.5 during events + : 2 // 2 for fusions outside events + : 1; // 1 for non-fused mons + const starterAmount = new Utils.NumberHolder(Math.floor(amount.value * candyFriendshipMultiplier / fusionReduction)); // Add friendship to this PlayerPokemon this.friendship = Math.min(this.friendship + amount.value, 255); diff --git a/src/loading-scene.ts b/src/loading-scene.ts index 60a0513f608..e8f817c1c39 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -249,9 +249,9 @@ export class LoadingScene extends SceneBase { } const availableLangs = [ "en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN" ]; if (lang && availableLangs.includes(lang)) { - this.loadImage("yearofthesnakeevent-" + lang, "events"); + this.loadImage("valentines2025event-" + lang, "events"); } else { - this.loadImage("yearofthesnakeevent-en", "events"); + this.loadImage("valentines2025event-en", "events"); } this.loadAtlas("statuses", ""); diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index b65f1b53441..b1e8b69df36 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -1720,7 +1720,16 @@ const modifierPool: ModifierPool = { }, 4), new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 3), new WeightedModifierType(modifierTypes.TERA_SHARD, 1), - new WeightedModifierType(modifierTypes.DNA_SPLICERS, (party: Pokemon[]) => globalScene.gameMode.isSplicedOnly && party.filter(p => !p.fusionSpecies).length > 1 ? 4 : 0), + new WeightedModifierType(modifierTypes.DNA_SPLICERS, (party: Pokemon[]) => { + if (party.filter(p => !p.fusionSpecies).length > 1) { + if (globalScene.gameMode.isSplicedOnly) { + return 4; + } else if (globalScene.gameMode.isClassic && globalScene.eventManager.areFusionsBoosted()) { + return 1; + } + } + return 0; + }, 4), new WeightedModifierType(modifierTypes.VOUCHER, (_party: Pokemon[], rerollCount: number) => !globalScene.gameMode.isDaily ? Math.max(1 - rerollCount, 0) : 0, 1), ].map(m => { m.setTier(ModifierTier.GREAT); return m; @@ -1879,7 +1888,7 @@ const modifierPool: ModifierPool = { new WeightedModifierType(modifierTypes.MULTI_LENS, 18), new WeightedModifierType(modifierTypes.VOUCHER_PREMIUM, (_party: Pokemon[], rerollCount: number) => !globalScene.gameMode.isDaily && !globalScene.gameMode.isEndless && !globalScene.gameMode.isSplicedOnly ? Math.max(5 - rerollCount * 2, 0) : 0, 5), - new WeightedModifierType(modifierTypes.DNA_SPLICERS, (party: Pokemon[]) => !globalScene.gameMode.isSplicedOnly && party.filter(p => !p.fusionSpecies).length > 1 ? 24 : 0, 24), + new WeightedModifierType(modifierTypes.DNA_SPLICERS, (party: Pokemon[]) => !(globalScene.gameMode.isClassic && globalScene.eventManager.areFusionsBoosted()) && !globalScene.gameMode.isSplicedOnly && party.filter(p => !p.fusionSpecies).length > 1 ? 24 : 0, 24), new WeightedModifierType(modifierTypes.MINI_BLACK_HOLE, () => (globalScene.gameMode.isDaily || (!globalScene.gameMode.isFreshStartChallenge() && globalScene.gameData.isUnlocked(Unlockables.MINI_BLACK_HOLE))) ? 1 : 0, 1), ].map(m => { m.setTier(ModifierTier.MASTER); return m; @@ -2538,7 +2547,7 @@ export function getPartyLuckValue(party: Pokemon[]): number { return DailyLuck.value; } const eventSpecies = globalScene.eventManager.getEventLuckBoostedSpecies(); - const luck = Phaser.Math.Clamp(party.map(p => p.isAllowedInBattle() ? p.getLuck() + (eventSpecies.includes(p.species.speciesId) ? 1 : 0) : 0) + const luck = Phaser.Math.Clamp(party.map(p => p.isAllowedInBattle() ? p.getLuck() + (eventSpecies.includes(p.species.speciesId) ? 3 : 0) : 0) .reduce((total: number, value: number) => total += value, 0), 0, 14); return Math.min(globalScene.eventManager.getEventLuckBoost() + (luck ?? 0), 14); } diff --git a/src/phases/faint-phase.ts b/src/phases/faint-phase.ts index 414aa84ce6c..340c5362087 100644 --- a/src/phases/faint-phase.ts +++ b/src/phases/faint-phase.ts @@ -96,10 +96,9 @@ export class FaintPhase extends PokemonPhase { doFaint(): void { const pokemon = this.getPokemon(); - - // Track total times pokemon have been KO'd for supreme overlord/last respects + // Track total times pokemon have been KO'd for Last Respects/Supreme Overlord if (pokemon.isPlayer()) { - globalScene.currentBattle.playerFaints += 1; + globalScene.arena.playerFaints += 1; globalScene.currentBattle.playerFaintsHistory.push({ pokemon: pokemon, turn: globalScene.currentBattle.turn }); } else { globalScene.currentBattle.enemyFaints += 1; diff --git a/src/phases/game-over-phase.ts b/src/phases/game-over-phase.ts index 5e4e8e1cdf7..d4b529fe00e 100644 --- a/src/phases/game-over-phase.ts +++ b/src/phases/game-over-phase.ts @@ -249,7 +249,8 @@ export class GameOverPhase extends BattlePhase { timestamp: new Date().getTime(), challenges: globalScene.gameMode.challenges.map(c => new ChallengeData(c)), mysteryEncounterType: globalScene.currentBattle.mysteryEncounter?.encounterType ?? -1, - mysteryEncounterSaveData: globalScene.mysteryEncounterSaveData + mysteryEncounterSaveData: globalScene.mysteryEncounterSaveData, + playerFaints: globalScene.arena.playerFaints } as SessionSaveData; } } diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index fff8caf38b5..35fe446fc43 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -12,6 +12,7 @@ import { PostAttackAbAttr, PostDamageAbAttr, PostDefendAbAttr, + ReflectStatusMoveAbAttr, TypeImmunityAbAttr, } from "#app/data/ability"; import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag"; @@ -31,6 +32,7 @@ import { AttackMove, DelayedAttackAttr, FlinchAttr, + getMoveTargets, HitsTagAttr, MissEffectAttr, MoveCategory, @@ -47,7 +49,7 @@ import { } from "#app/data/move"; import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms"; import { Type } from "#enums/type"; -import type { PokemonMove } from "#app/field/pokemon"; +import { PokemonMove } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; import { HitResult, MoveResult } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; @@ -60,17 +62,27 @@ import { } from "#app/modifier/modifier"; import { PokemonPhase } from "#app/phases/pokemon-phase"; import { BooleanHolder, executeIf, isNullOrUndefined, NumberHolder } from "#app/utils"; +import { type nil } from "#app/utils"; import { BattlerTagType } from "#enums/battler-tag-type"; import type { Moves } from "#enums/moves"; import i18next from "i18next"; +import type { Phase } from "#app/phase"; +import { ShowAbilityPhase } from "./show-ability-phase"; +import { MovePhase } from "./move-phase"; +import { MoveEndPhase } from "./move-end-phase"; export class MoveEffectPhase extends PokemonPhase { public move: PokemonMove; protected targets: BattlerIndex[]; + protected reflected: boolean = false; - constructor(battlerIndex: BattlerIndex, targets: BattlerIndex[], move: PokemonMove) { + /** + * @param reflected Indicates that the move was reflected by the user due to magic coat or magic bounce + */ + constructor(battlerIndex: BattlerIndex, targets: BattlerIndex[], move: PokemonMove, reflected: boolean = false) { super(battlerIndex); this.move = move; + this.reflected = reflected; /** * In double battles, if the right Pokemon selects a spread move and the left Pokemon dies * with no party members available to switch in, then the right Pokemon takes the index @@ -95,6 +107,13 @@ export class MoveEffectPhase extends PokemonPhase { return super.end(); } + /** If an enemy used this move, set this as last enemy that used move or ability */ + if (!user.isPlayer()) { + globalScene.currentBattle.lastEnemyInvolved = this.fieldIndex; + } else { + globalScene.currentBattle.lastPlayerInvolved = this.fieldIndex; + } + const isDelayedAttack = this.move.getMove().hasAttr(DelayedAttackAttr); /** If the user was somehow removed from the field and it's not a delayed attack, end this phase */ if (!user.isOnField()) { @@ -177,12 +196,14 @@ export class MoveEffectPhase extends PokemonPhase { && (targets[0]?.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)) && !targets[0]?.getTag(SemiInvulnerableTag); + const mayBounce = move.hasFlag(MoveFlags.REFLECTABLE) && !this.reflected && targets.some(t => t.hasAbilityWithAttr(ReflectStatusMoveAbAttr) || !!t.getTag(BattlerTagType.MAGIC_COAT)); + /** - * If no targets are left for the move to hit (FAIL), or the invoked move is single-target + * If no targets are left for the move to hit (FAIL), or the invoked move is non-reflectable, single-target * (and not random target) and failed the hit check against its target (MISS), log the move * as FAILed or MISSed (depending on the conditions above) and end this phase. */ - if (!hasActiveTargets || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag) && !isImmune)) { + if (!hasActiveTargets || (!mayBounce && !move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag) && !isImmune)) { this.stopMultiHit(); if (hasActiveTargets) { globalScene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getFirstTarget() ? getPokemonNameWithAffix(this.getFirstTarget()!) : "" })); @@ -204,12 +225,21 @@ export class MoveEffectPhase extends PokemonPhase { new MoveAnim(move.id as Moves, user, this.getFirstTarget()!.getBattlerIndex(), playOnEmptyField).play(move.hitsSubstitute(user, this.getFirstTarget()!), () => { /** Has the move successfully hit a target (for damage) yet? */ let hasHit: boolean = false; - for (const target of targets) { - // Prevent ENEMY_SIDE targeted moves from occurring twice in double battles - if (move.moveTarget === MoveTarget.ENEMY_SIDE && target !== targets[targets.length - 1]) { - continue; - } + // Prevent ENEMY_SIDE targeted moves from occurring twice in double battles + // and check which target will magic bounce. + const trueTargets: Pokemon[] = move.moveTarget !== MoveTarget.ENEMY_SIDE ? targets : (() => { + const magicCoatTargets = targets.filter(t => t.getTag(BattlerTagType.MAGIC_COAT) || t.hasAbilityWithAttr(ReflectStatusMoveAbAttr)); + + // only magic coat effect cares about order + if (!mayBounce || magicCoatTargets.length === 0) { + return [ targets[0] ]; + } + return [ magicCoatTargets[0] ]; + })(); + + const queuedPhases: Phase[] = []; + for (const target of trueTargets) { /** The {@linkcode ArenaTagSide} to which the target belongs */ const targetSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; /** Has the invoked move been cancelled by conditional protection (e.g Quick Guard)? */ @@ -222,7 +252,7 @@ export class MoveEffectPhase extends PokemonPhase { } /** Is the target protected by Protect, etc. or a relevant conditional protection effect? */ - const isProtected = ( + const isProtected = !([ MoveTarget.ENEMY_SIDE, MoveTarget.BOTH_SIDES ].includes(this.move.getMove().moveTarget)) && ( bypassIgnoreProtect.value || !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target)) && (hasConditionalProtectApplied.value @@ -231,13 +261,39 @@ export class MoveEffectPhase extends PokemonPhase { || (this.move.getMove().category !== MoveCategory.STATUS && target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType)))); + /** Is the target hidden by the effects of its Commander ability? */ + const isCommanding = globalScene.currentBattle.double && target.getAlly()?.getTag(BattlerTagType.COMMANDED)?.getSourcePokemon() === target; + + /** Is the target reflecting status moves from the magic coat move? */ + const isReflecting = !!target.getTag(BattlerTagType.MAGIC_COAT); + + /** Is the target's magic bounce ability not ignored and able to reflect this move? */ + const canMagicBounce = !isReflecting && !move.checkFlag(MoveFlags.IGNORE_ABILITIES, user, target) && target.hasAbilityWithAttr(ReflectStatusMoveAbAttr); + + const semiInvulnerableTag = target.getTag(SemiInvulnerableTag); + + /** Is the target reflecting the effect, not protected, and not in an semi-invulnerable state?*/ + const willBounce = (!isProtected && !this.reflected && !isCommanding + && move.hasFlag(MoveFlags.REFLECTABLE) + && (isReflecting || canMagicBounce) + && !semiInvulnerableTag); + + // If the move will bounce, then queue the bounce and move on to the next target + if (!target.switchOutStatus && willBounce) { + const newTargets = move.isMultiTarget() ? getMoveTargets(target, move.id).targets : [ user.getBattlerIndex() ]; + if (!isReflecting) { + queuedPhases.push(new ShowAbilityPhase(target.getBattlerIndex(), target.getPassiveAbility().hasAttr(ReflectStatusMoveAbAttr))); + } + + queuedPhases.push(new MovePhase(target, newTargets, new PokemonMove(move.id, 0, 0, true), true, true, true)); + continue; + } + /** Is the pokemon immune due to an ablility, and also not in a semi invulnerable state? */ const isImmune = target.hasAbilityWithAttr(TypeImmunityAbAttr) && (target.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)) - && !target.getTag(SemiInvulnerableTag); + && !semiInvulnerableTag; - /** Is the target hidden by the effects of its Commander ability? */ - const isCommanding = globalScene.currentBattle.double && target.getAlly()?.getTag(BattlerTagType.COMMANDED)?.getSourcePokemon() === target; /** * If the move missed a target, stop all future hits against that target @@ -364,6 +420,10 @@ export class MoveEffectPhase extends PokemonPhase { applyAttrs.push(k); } + // Apply queued phases + if (queuedPhases.length) { + globalScene.appendToPhase(queuedPhases, MoveEndPhase); + } // Apply the move's POST_TARGET effects on the move's last hit, after all targeted effects have resolved const postTarget = (user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive()) ? applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET, user, null, move) : @@ -579,12 +639,7 @@ export class MoveEffectPhase extends PokemonPhase { } } - if (user.hasAbilityWithAttr(AlwaysHitAbAttr) || target.hasAbilityWithAttr(AlwaysHitAbAttr)) { - return true; - } - - // If the user should ignore accuracy on a target, check who the user targeted last turn and see if they match - if (user.getTag(BattlerTagType.IGNORE_ACCURACY) && (user.getLastXMoves().find(() => true)?.targets || []).indexOf(target.getBattlerIndex()) !== -1) { + if (this.checkBypassAccAndInvuln(target)) { return true; } @@ -592,15 +647,12 @@ export class MoveEffectPhase extends PokemonPhase { return true; } - if (target.getTag(BattlerTagType.TELEKINESIS) && !target.getTag(SemiInvulnerableTag) && !this.move.getMove().hasAttr(OneHitKOAttr)) { + const semiInvulnerableTag = target.getTag(SemiInvulnerableTag); + if (target.getTag(BattlerTagType.TELEKINESIS) && !semiInvulnerableTag && !this.move.getMove().hasAttr(OneHitKOAttr)) { return true; } - const semiInvulnerableTag = target.getTag(SemiInvulnerableTag); - if (semiInvulnerableTag - && !this.move.getMove().getAttrs(HitsTagAttr).some(hta => hta.tagType === semiInvulnerableTag.tagType) - && !(this.move.getMove().hasAttr(ToxicAccuracyAttr) && user.isOfType(Type.POISON)) - ) { + if (semiInvulnerableTag && !this.checkBypassSemiInvuln(semiInvulnerableTag)) { return false; } @@ -616,6 +668,52 @@ export class MoveEffectPhase extends PokemonPhase { return rand < (moveAccuracy * accuracyMultiplier); } + /** + * Check whether the move should bypass *both* the accuracy *and* semi-invulnerable states. + * @param target - The {@linkcode Pokemon} targeted by the invoked move + * @returns `true` if the move should bypass accuracy and semi-invulnerability + * + * Accuracy and semi-invulnerability can be bypassed by: + * - An ability like {@linkcode Abilities.NO_GUARD | No Guard} + * - A poison type using {@linkcode Moves.TOXIC | Toxic} + * - A move like {@linkcode Moves.LOCK_ON | Lock-On} or {@linkcode Moves.MIND_READER | Mind Reader}. + * + * Does *not* check against effects {@linkcode Moves.GLAIVE_RUSH | Glaive Rush} status (which + * should not bypass semi-invulnerability), or interactions like Earthquake hitting against Dig, + * (which should not bypass the accuracy check). + * + * @see {@linkcode hitCheck} + */ + public checkBypassAccAndInvuln(target: Pokemon) { + const user = this.getUserPokemon(); + if (!user) { + return false; + } + if (user.hasAbilityWithAttr(AlwaysHitAbAttr) || target.hasAbilityWithAttr(AlwaysHitAbAttr)) { + return true; + } + if ((this.move.getMove().hasAttr(ToxicAccuracyAttr) && user.isOfType(Type.POISON))) { + return true; + } + // TODO: Fix lock on / mind reader check. + if (user.getTag(BattlerTagType.IGNORE_ACCURACY) && (user.getLastXMoves().find(() => true)?.targets || []).indexOf(target.getBattlerIndex()) !== -1) { + return true; + } + } + + /** + * Check whether the move is able to ignore the given `semiInvulnerableTag` + * @param semiInvulnerableTag - The semiInvulnerbale tag to check against + * @returns `true` if the move can ignore the semi-invulnerable state + */ + public checkBypassSemiInvuln(semiInvulnerableTag: SemiInvulnerableTag | nil): boolean { + if (!semiInvulnerableTag) { + return false; + } + const move = this.move.getMove(); + return move.getAttrs(HitsTagAttr).some(hta => hta.tagType === semiInvulnerableTag.tagType); + } + /** @returns The {@linkcode Pokemon} using this phase's invoked move */ public getUserPokemon(): Pokemon | null { if (this.battlerIndex > BattlerIndex.ENEMY_2) { diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 5330540c8b2..9d32189edb5 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -58,6 +58,7 @@ export class MovePhase extends BattlePhase { protected ignorePp: boolean; protected failed: boolean = false; protected cancelled: boolean = false; + protected reflected: boolean = false; public get pokemon(): Pokemon { return this._pokemon; @@ -84,10 +85,12 @@ export class MovePhase extends BattlePhase { } /** - * @param followUp Indicates that the move being uses is a "follow-up" - for example, a move being used by Metronome or Dancer. + * @param followUp Indicates that the move being used is a "follow-up" - for example, a move being used by Metronome or Dancer. * Follow-ups bypass a few failure conditions, including flinches, sleep/paralysis/freeze and volatile status checks, etc. + * @param reflected Indicates that the move was reflected by Magic Coat or Magic Bounce. + * Reflected moves cannot be reflected again and will not trigger Dancer. */ - constructor(pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, followUp: boolean = false, ignorePp: boolean = false) { + constructor(pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, followUp: boolean = false, ignorePp: boolean = false, reflected: boolean = false) { super(); this.pokemon = pokemon; @@ -95,6 +98,7 @@ export class MovePhase extends BattlePhase { this.move = move; this.followUp = followUp; this.ignorePp = ignorePp; + this.reflected = reflected; } /** @@ -140,7 +144,7 @@ export class MovePhase extends BattlePhase { } // Check move to see if arena.ignoreAbilities should be true. - if (!this.followUp) { + if (!this.followUp || this.reflected) { if (this.move.getMove().checkFlag(MoveFlags.IGNORE_ABILITIES, this.pokemon, null)) { globalScene.arena.setIgnoreAbilities(true, this.pokemon.getBattlerIndex()); } @@ -335,7 +339,7 @@ export class MovePhase extends BattlePhase { */ if (success) { applyPreAttackAbAttrs(PokemonTypeChangeAbAttr, this.pokemon, null, this.move.getMove()); - globalScene.unshiftPhase(new MoveEffectPhase(this.pokemon.getBattlerIndex(), this.targets, this.move)); + globalScene.unshiftPhase(new MoveEffectPhase(this.pokemon.getBattlerIndex(), this.targets, this.move, this.reflected)); } else { if ([ Moves.ROAR, Moves.WHIRLWIND, Moves.TRICK_OR_TREAT, Moves.FORESTS_CURSE ].includes(this.move.moveId)) { @@ -543,7 +547,7 @@ export class MovePhase extends BattlePhase { return; } - globalScene.queueMessage(i18next.t("battle:useMove", { + globalScene.queueMessage(i18next.t(this.reflected ? "battle:magicCoatActivated" : "battle:useMove", { pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon), moveName: this.move.getName() }), 500); diff --git a/src/phases/show-ability-phase.ts b/src/phases/show-ability-phase.ts index a0db660ded5..d759ad833a1 100644 --- a/src/phases/show-ability-phase.ts +++ b/src/phases/show-ability-phase.ts @@ -17,6 +17,14 @@ export class ShowAbilityPhase extends PokemonPhase { const pokemon = this.getPokemon(); if (pokemon) { + + if (!pokemon.isPlayer()) { + /** If its an enemy pokemon, list it as last enemy to use ability or move */ + globalScene.currentBattle.lastEnemyInvolved = pokemon.getBattlerIndex() % 2; + } else { + globalScene.currentBattle.lastPlayerInvolved = pokemon.getBattlerIndex() % 2; + } + globalScene.abilityBar.showAbility(pokemon, this.passive); if (pokemon?.battleData) { diff --git a/src/phases/stat-stage-change-phase.ts b/src/phases/stat-stage-change-phase.ts index 359610b320c..753d1f7cede 100644 --- a/src/phases/stat-stage-change-phase.ts +++ b/src/phases/stat-stage-change-phase.ts @@ -1,7 +1,8 @@ import { globalScene } from "#app/global-scene"; import type { BattlerIndex } from "#app/battle"; -import { applyAbAttrs, applyPostStatStageChangeAbAttrs, applyPreStatStageChangeAbAttrs, PostStatStageChangeAbAttr, ProtectStatAbAttr, StatStageChangeCopyAbAttr, StatStageChangeMultiplierAbAttr } from "#app/data/ability"; +import { applyAbAttrs, applyPostStatStageChangeAbAttrs, applyPreStatStageChangeAbAttrs, PostStatStageChangeAbAttr, ProtectStatAbAttr, ReflectStatStageChangeAbAttr, StatStageChangeCopyAbAttr, StatStageChangeMultiplierAbAttr } from "#app/data/ability"; import { ArenaTagSide, MistTag } from "#app/data/arena-tag"; +import type { ArenaTag } from "#app/data/arena-tag"; import type Pokemon from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { ResetNegativeStatStageModifier } from "#app/modifier/modifier"; @@ -10,6 +11,8 @@ import { NumberHolder, BooleanHolder } from "#app/utils"; import i18next from "i18next"; import { PokemonPhase } from "./pokemon-phase"; import { Stat, type BattleStat, getStatKey, getStatStageChangeDescriptionKey } from "#enums/stat"; +import { OctolockTag } from "#app/data/battler-tags"; +import { ArenaTagType } from "#app/enums/arena-tag-type"; export type StatStageChangeCallback = (target: Pokemon | null, changed: BattleStat[], relativeChanges: number[]) => void; @@ -21,9 +24,11 @@ export class StatStageChangePhase extends PokemonPhase { private ignoreAbilities: boolean; private canBeCopied: boolean; private onChange: StatStageChangeCallback | null; + private comingFromMirrorArmorUser: boolean; + private comingFromStickyWeb: boolean; - constructor(battlerIndex: BattlerIndex, selfTarget: boolean, stats: BattleStat[], stages: number, showMessage: boolean = true, ignoreAbilities: boolean = false, canBeCopied: boolean = true, onChange: StatStageChangeCallback | null = null) { + constructor(battlerIndex: BattlerIndex, selfTarget: boolean, stats: BattleStat[], stages: number, showMessage: boolean = true, ignoreAbilities: boolean = false, canBeCopied: boolean = true, onChange: StatStageChangeCallback | null = null, comingFromMirrorArmorUser: boolean = false, comingFromStickyWeb: boolean = false) { super(battlerIndex); this.selfTarget = selfTarget; @@ -33,6 +38,8 @@ export class StatStageChangePhase extends PokemonPhase { this.ignoreAbilities = ignoreAbilities; this.canBeCopied = canBeCopied; this.onChange = onChange; + this.comingFromMirrorArmorUser = comingFromMirrorArmorUser; + this.comingFromStickyWeb = comingFromStickyWeb; } start() { @@ -41,12 +48,44 @@ export class StatStageChangePhase extends PokemonPhase { if (this.stats.length > 1) { for (let i = 0; i < this.stats.length; i++) { const stat = [ this.stats[i] ]; - globalScene.unshiftPhase(new StatStageChangePhase(this.battlerIndex, this.selfTarget, stat, this.stages, this.showMessage, this.ignoreAbilities, this.canBeCopied, this.onChange)); + globalScene.unshiftPhase(new StatStageChangePhase(this.battlerIndex, this.selfTarget, stat, this.stages, this.showMessage, this.ignoreAbilities, this.canBeCopied, this.onChange, this.comingFromMirrorArmorUser)); } return this.end(); } const pokemon = this.getPokemon(); + let opponentPokemon: Pokemon | undefined; + + /** Gets the position of last enemy or player pokemon that used ability or move, primarily for double battles involving Mirror Armor */ + if (pokemon.isPlayer()) { + /** If this SSCP is not from sticky web, then we find the opponent pokemon that last did something */ + if (!this.comingFromStickyWeb) { + opponentPokemon = globalScene.getEnemyField()[globalScene.currentBattle.lastEnemyInvolved]; + } else { + /** If this SSCP is from sticky web, then check if pokemon that last sucessfully used sticky web is on field */ + const stickyTagID = globalScene.arena.findTagsOnSide( + (t: ArenaTag) => t.tagType === ArenaTagType.STICKY_WEB, + ArenaTagSide.PLAYER)[0].sourceId; + globalScene.getEnemyField().forEach((e) => { + if (e.id === stickyTagID) { + opponentPokemon = e; + } + }); + } + } else { + if (!this.comingFromStickyWeb) { + opponentPokemon = globalScene.getPlayerField()[globalScene.currentBattle.lastPlayerInvolved]; + } else { + const stickyTagID = globalScene.arena.findTagsOnSide( + (t: ArenaTag) => t.tagType === ArenaTagType.STICKY_WEB, + ArenaTagSide.ENEMY)[0].sourceId; + globalScene.getPlayerField().forEach((e) => { + if (e.id === stickyTagID) { + opponentPokemon = e; + } + }); + } + } if (!pokemon.isActive(true)) { return this.end(); @@ -70,6 +109,11 @@ export class StatStageChangePhase extends PokemonPhase { if (!cancelled.value && !this.selfTarget && stages.value < 0) { applyPreStatStageChangeAbAttrs(ProtectStatAbAttr, pokemon, stat, cancelled, simulate); + + /** Potential stat reflection due to Mirror Armor, does not apply to Octolock end of turn effect */ + if (opponentPokemon !== undefined && !pokemon.findTag(t => t instanceof OctolockTag) && !this.comingFromMirrorArmorUser) { + applyPreStatStageChangeAbAttrs(ReflectStatStageChangeAbAttr, pokemon, stat, cancelled, simulate, opponentPokemon, this.stages); + } } // If one stat stage decrease is cancelled, simulate the rest of the applications diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 7282d2730a4..c16fab9db04 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -141,6 +141,10 @@ export interface SessionSaveData { challenges: ChallengeData[]; mysteryEncounterType: MysteryEncounterType | -1; // Only defined when current wave is ME, mysteryEncounterSaveData: MysteryEncounterSaveData; + /** + * Counts the amount of pokemon fainted in your party during the current arena encounter. + */ + playerFaints: number; } interface Unlocks { @@ -964,7 +968,8 @@ export class GameData { timestamp: new Date().getTime(), challenges: globalScene.gameMode.challenges.map(c => new ChallengeData(c)), mysteryEncounterType: globalScene.currentBattle.mysteryEncounter?.encounterType ?? -1, - mysteryEncounterSaveData: globalScene.mysteryEncounterSaveData + mysteryEncounterSaveData: globalScene.mysteryEncounterSaveData, + playerFaints: globalScene.arena.playerFaints } as SessionSaveData; } @@ -1056,7 +1061,7 @@ export class GameData { globalScene.mysteryEncounterSaveData = new MysteryEncounterSaveData(sessionData.mysteryEncounterSaveData); - globalScene.newArena(sessionData.arena.biome); + globalScene.newArena(sessionData.arena.biome, sessionData.playerFaints); const battleType = sessionData.battleType || 0; const trainerConfig = sessionData.trainer ? trainerConfigs[sessionData.trainer.trainerType] : null; diff --git a/src/test/abilities/magic_bounce.test.ts b/src/test/abilities/magic_bounce.test.ts new file mode 100644 index 00000000000..2fc460662ca --- /dev/null +++ b/src/test/abilities/magic_bounce.test.ts @@ -0,0 +1,351 @@ +import { BattlerIndex } from "#app/battle"; +import { allAbilities } from "#app/data/ability"; +import { ArenaTagSide } from "#app/data/arena-tag"; +import { allMoves } from "#app/data/move"; +import { ArenaTagType } from "#app/enums/arena-tag-type"; +import { BattlerTagType } from "#app/enums/battler-tag-type"; +import { Stat } from "#app/enums/stat"; +import { StatusEffect } from "#app/enums/status-effect"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Abilities - Magic Bounce", () => { + 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 + .ability(Abilities.BALL_FETCH) + .battleType("single") + .moveset( [ Moves.GROWL, Moves.SPLASH ]) + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.MAGIC_BOUNCE) + .enemyMoveset(Moves.SPLASH); + }); + + it("should reflect basic status moves", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + game.move.select(Moves.GROWL); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(-1); + }); + + it("should not bounce moves while the target is in the semi-invulnerable state", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + game.override.moveset([ Moves.GROWL ]); + game.override.enemyMoveset( [ Moves.FLY ]); + + game.move.select(Moves.GROWL); + await game.forceEnemyMove(Moves.FLY); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(0); + }); + + it("should individually bounce back multi-target moves", async () => { + game.override.battleType("double"); + game.override.moveset([ Moves.GROWL, Moves.SPLASH ]); + await game.classicMode.startBattle([ Species.MAGIKARP, Species.MAGIKARP ]); + + game.move.select(Moves.GROWL, 0); + game.move.select(Moves.SPLASH, 1); + await game.phaseInterceptor.to("BerryPhase"); + + const user = game.scene.getPlayerField()[0]; + expect(user.getStatStage(Stat.ATK)).toBe(-2); + }); + + it("should still bounce back a move that would otherwise fail", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + game.scene.getEnemyPokemon()?.setStatStage(Stat.ATK, -6); + game.override.moveset([ Moves.GROWL ]); + + game.move.select(Moves.GROWL); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(-1); + }); + + it("should not bounce back a move that was just bounced", async () => { + game.override.ability(Abilities.MAGIC_BOUNCE); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + game.move.select(Moves.GROWL); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(-1); + }); + + it("should receive the stat change after reflecting a move back to a mirror armor user", async () => { + game.override.ability(Abilities.MIRROR_ARMOR); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + game.move.select(Moves.GROWL); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(-1); + }); + + it("should not bounce back a move from a mold breaker user", async () => { + game.override.ability(Abilities.MOLD_BREAKER); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + game.move.select(Moves.GROWL); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(-1); + }); + + it("should bounce back a spread status move against both pokemon", async () => { + game.override.battleType("double"); + game.override.moveset([ Moves.GROWL, Moves.SPLASH ]); + game.override.enemyMoveset([ Moves.SPLASH ]); + await game.classicMode.startBattle([ Species.MAGIKARP, Species.MAGIKARP ]); + + game.move.select(Moves.GROWL, 0); + game.move.select(Moves.SPLASH, 1); + + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getPlayerField().every(p => p.getStatStage(Stat.ATK) === -2)).toBeTruthy(); + }); + + it("should only bounce spikes back once in doubles when both targets have magic bounce", async () => { + game.override.battleType("double"); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + game.override.moveset([ Moves.SPIKES ]); + + game.move.select(Moves.SPIKES); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.PLAYER)!["layers"]).toBe(1); + expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY)).toBeUndefined(); + }); + + it("should bounce spikes even when the target is protected", async () => { + game.override.moveset([ Moves.SPIKES ]); + game.override.enemyMoveset([ Moves.PROTECT ]); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + game.move.select(Moves.SPIKES); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.PLAYER)!["layers"]).toBe(1); + }); + + it("should not bounce spikes when the target is in the semi-invulnerable state", async () => { + game.override.moveset([ Moves.SPIKES ]); + game.override.enemyMoveset([ Moves.FLY ]); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + game.move.select(Moves.SPIKES); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY)!["layers"]).toBe(1); + }); + + it("should not bounce back curse", async() => { + game.override.starterSpecies(Species.GASTLY); + await game.classicMode.startBattle([ Species.GASTLY ]); + game.override.moveset([ Moves.CURSE ]); + + game.move.select(Moves.CURSE); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyPokemon()!.getTag(BattlerTagType.CURSED)).toBeDefined(); + }); + + it("should not cause encore to be interrupted after bouncing", async () => { + game.override.moveset([ Moves.SPLASH, Moves.GROWL, Moves.ENCORE ]); + game.override.enemyMoveset([ Moves.TACKLE, Moves.GROWL ]); + // game.override.ability(Abilities.MOLD_BREAKER); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + // Give the player MOLD_BREAKER for this turn to bypass Magic Bounce. + vi.spyOn(playerPokemon, "getAbility").mockReturnValue(allAbilities[Abilities.MOLD_BREAKER]); + + // turn 1 + game.move.select(Moves.ENCORE); + await game.forceEnemyMove(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextTurn(); + expect(enemyPokemon.getTag(BattlerTagType.ENCORE)!["moveId"]).toBe(Moves.TACKLE); + + // turn 2 + vi.spyOn(playerPokemon, "getAbility").mockRestore(); + game.move.select(Moves.GROWL); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("BerryPhase"); + expect(enemyPokemon.getTag(BattlerTagType.ENCORE)!["moveId"]).toBe(Moves.TACKLE); + expect(enemyPokemon.getLastXMoves()[0].move).toBe(Moves.TACKLE); + + }); + + // TODO: encore is failing if the last move was virtual. + it.todo("should not cause the bounced move to count for encore", async () => { + game.override.moveset([ Moves.SPLASH, Moves.GROWL, Moves.ENCORE ]); + game.override.enemyMoveset([ Moves.GROWL, Moves.TACKLE ]); + game.override.enemyAbility(Abilities.MAGIC_BOUNCE); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + // turn 1 + game.move.select(Moves.GROWL); + await game.forceEnemyMove(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextTurn(); + + // Give the player MOLD_BREAKER for this turn to bypass Magic Bounce. + vi.spyOn(playerPokemon, "getAbility").mockReturnValue(allAbilities[Abilities.MOLD_BREAKER]); + + // turn 2 + game.move.select(Moves.ENCORE); + await game.forceEnemyMove(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("BerryPhase"); + expect(enemyPokemon.getTag(BattlerTagType.ENCORE)!["moveId"]).toBe(Moves.TACKLE); + expect(enemyPokemon.getLastXMoves()[0].move).toBe(Moves.TACKLE); + }); + + // TODO: stomping tantrum should consider moves that were bounced. + it.todo("should cause stomping tantrum to double in power when the last move was bounced", async () => { + game.override.battleType("single"); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + game.override.moveset([ Moves.STOMPING_TANTRUM, Moves.CHARM ]); + + const stomping_tantrum = allMoves[Moves.STOMPING_TANTRUM]; + vi.spyOn(stomping_tantrum, "calculateBattlePower"); + + game.move.select(Moves.CHARM); + await game.toNextTurn(); + + game.move.select(Moves.STOMPING_TANTRUM); + await game.phaseInterceptor.to("BerryPhase"); + expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(150); + }); + + // TODO: stomping tantrum should consider moves that were bounced. + it.todo("should properly cause the enemy's stomping tantrum to be doubled in power after bouncing and failing", async () => { + game.override.enemyMoveset([ Moves.STOMPING_TANTRUM, Moves.SPLASH, Moves.CHARM ]); + await game.classicMode.startBattle([ Species.BULBASAUR ]); + + const stomping_tantrum = allMoves[Moves.STOMPING_TANTRUM]; + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(stomping_tantrum, "calculateBattlePower"); + + game.move.select(Moves.SPORE); + await game.forceEnemyMove(Moves.CHARM); + await game.phaseInterceptor.to("TurnEndPhase"); + expect(enemy.getLastXMoves(1)[0].result).toBe("success"); + + await game.phaseInterceptor.to("BerryPhase"); + expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(75); + + await game.toNextTurn(); + game.move.select(Moves.GROWL); + await game.phaseInterceptor.to("BerryPhase"); + expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(75); + }); + + it("should respect immunities when bouncing a move", async () => { + vi.spyOn(allMoves[Moves.THUNDER_WAVE], "accuracy", "get").mockReturnValue(100); + game.override.moveset([ Moves.THUNDER_WAVE, Moves.GROWL ]); + game.override.ability(Abilities.SOUNDPROOF); + await game.classicMode.startBattle([ Species.PHANPY ]); + + // Turn 1 - thunder wave immunity test + game.move.select(Moves.THUNDER_WAVE); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getPlayerPokemon()!.status).toBeUndefined(); + + // Turn 2 - soundproof immunity test + game.move.select(Moves.GROWL); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(0); + }); + + it("should bounce back a move before the accuracy check", async () => { + game.override.moveset([ Moves.SPORE ]); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const attacker = game.scene.getPlayerPokemon()!; + + vi.spyOn(attacker, "getAccuracyMultiplier").mockReturnValue(0.0); + game.move.select(Moves.SPORE); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getPlayerPokemon()!.status?.effect).toBe(StatusEffect.SLEEP); + }); + + it("should take the accuracy of the magic bounce user into account", async () => { + game.override.moveset([ Moves.SPORE ]); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + const opponent = game.scene.getEnemyPokemon()!; + + vi.spyOn(opponent, "getAccuracyMultiplier").mockReturnValue(0); + game.move.select(Moves.SPORE); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getPlayerPokemon()!.status).toBeUndefined(); + }); + + it("should always apply the leftmost available target's magic bounce when bouncing moves like sticky webs in doubles", async () => { + game.override.battleType("double"); + game.override.moveset([ Moves.STICKY_WEB, Moves.SPLASH, Moves.TRICK_ROOM ]); + + await game.classicMode.startBattle([ Species.MAGIKARP, Species.MAGIKARP ]); + const [ enemy_1, enemy_2 ] = game.scene.getEnemyField(); + // set speed just incase logic erroneously checks for speed order + enemy_1.setStat(Stat.SPD, enemy_2.getStat(Stat.SPD) + 1); + + // turn 1 + game.move.select(Moves.STICKY_WEB, 0); + game.move.select(Moves.TRICK_ROOM, 1); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(game.scene.arena.getTagOnSide(ArenaTagType.STICKY_WEB, ArenaTagSide.PLAYER)?.getSourcePokemon()?.getBattlerIndex()).toBe(BattlerIndex.ENEMY); + game.scene.arena.removeTagOnSide(ArenaTagType.STICKY_WEB, ArenaTagSide.PLAYER, true); + + // turn 2 + game.move.select(Moves.STICKY_WEB, 0); + game.move.select(Moves.TRICK_ROOM, 1); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.arena.getTagOnSide(ArenaTagType.STICKY_WEB, ArenaTagSide.PLAYER)?.getSourcePokemon()?.getBattlerIndex()).toBe(BattlerIndex.ENEMY); + }); + + it("should not bounce back status moves that hit through semi-invulnerable states", async () => { + game.override.moveset([ Moves.TOXIC, Moves.CHARM ]); + await game.classicMode.startBattle([ Species.BULBASAUR ]); + game.move.select(Moves.TOXIC); + await game.forceEnemyMove(Moves.FLY); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getEnemyPokemon()!.status?.effect).toBe(StatusEffect.TOXIC); + expect(game.scene.getPlayerPokemon()!.status).toBeUndefined(); + + game.override.ability(Abilities.NO_GUARD); + game.move.select(Moves.CHARM); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(-2); + expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(0); + }); +}); + diff --git a/src/test/abilities/mirror_armor.test.ts b/src/test/abilities/mirror_armor.test.ts new file mode 100644 index 00000000000..070428a8ee7 --- /dev/null +++ b/src/test/abilities/mirror_armor.test.ts @@ -0,0 +1,315 @@ +import { Stat } from "#enums/stat"; +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 { BattlerIndex } from "#app/battle"; + +// TODO: When Magic Bounce is implemented, make a test for its interaction with mirror guard, use screech + +describe("Ability - Mirror Armor", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + + game.override.battleType("single") + .enemySpecies(Species.RATTATA) + .enemyMoveset([ Moves.SPLASH, Moves.STICKY_WEB, Moves.TICKLE, Moves.OCTOLOCK ]) + .enemyAbility(Abilities.BALL_FETCH) + .startingLevel(2000) + .moveset([ Moves.SPLASH, Moves.STICKY_WEB, Moves.TICKLE, Moves.OCTOLOCK ]) + .ability(Abilities.BALL_FETCH); + }); + + it("Player side + single battle Intimidate - opponent loses stats", async () => { + game.override.ability(Abilities.MIRROR_ARMOR); + game.override.enemyAbility(Abilities.INTIMIDATE); + await game.classicMode.startBattle([ Species.BULBASAUR ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const userPokemon = game.scene.getPlayerPokemon()!; + + // Enemy has intimidate, enemy should lose -1 atk + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER); + await game.toNextTurn(); + + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1); + expect(userPokemon.getStatStage(Stat.ATK)).toBe(0); + }); + + it("Enemy side + single battle Intimidate - player loses stats", async () => { + game.override.enemyAbility(Abilities.MIRROR_ARMOR); + game.override.ability(Abilities.INTIMIDATE); + await game.classicMode.startBattle([ Species.BULBASAUR ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const userPokemon = game.scene.getPlayerPokemon()!; + + // Enemy has intimidate, enemy should lose -1 atk + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER); + await game.toNextTurn(); + + expect(userPokemon.getStatStage(Stat.ATK)).toBe(-1); + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); + }); + + it("Player side + double battle Intimidate - opponents each lose -2 atk", async () => { + game.override.battleType("double"); + game.override.ability(Abilities.MIRROR_ARMOR); + game.override.enemyAbility(Abilities.INTIMIDATE); + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER ]); + + const [ enemy1, enemy2 ] = game.scene.getEnemyField(); + const [ player1, player2 ] = game.scene.getPlayerField(); + + // Enemy has intimidate, enemy should lose -2 atk each + game.move.select(Moves.SPLASH); + game.move.select(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER_2); + await game.toNextTurn(); + + expect(enemy1.getStatStage(Stat.ATK)).toBe(-2); + expect(enemy2.getStatStage(Stat.ATK)).toBe(-2); + expect(player1.getStatStage(Stat.ATK)).toBe(0); + expect(player2.getStatStage(Stat.ATK)).toBe(0); + }); + + it("Enemy side + double battle Intimidate - players each lose -2 atk", async () => { + game.override.battleType("double"); + game.override.enemyAbility(Abilities.MIRROR_ARMOR); + game.override.ability(Abilities.INTIMIDATE); + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER ]); + + const [ enemy1, enemy2 ] = game.scene.getEnemyField(); + const [ player1, player2 ] = game.scene.getPlayerField(); + + // Enemy has intimidate, enemy should lose -1 atk + game.move.select(Moves.SPLASH); + game.move.select(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER_2); + await game.toNextTurn(); + + expect(enemy1.getStatStage(Stat.ATK)).toBe(0); + expect(enemy2.getStatStage(Stat.ATK)).toBe(0); + expect(player1.getStatStage(Stat.ATK)).toBe(-2); + expect(player2.getStatStage(Stat.ATK)).toBe(-2); + }); + + it("Player side + single battle Intimidate + Tickle - opponent loses stats", async () => { + game.override.ability(Abilities.MIRROR_ARMOR); + game.override.enemyAbility(Abilities.INTIMIDATE); + await game.classicMode.startBattle([ Species.BULBASAUR ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const userPokemon = game.scene.getPlayerPokemon()!; + + // Enemy has intimidate and uses tickle, enemy receives -2 atk and -1 defense + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.TICKLE, BattlerIndex.PLAYER); + await game.toNextTurn(); + + expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(-1); + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-2); + expect(userPokemon.getStatStage(Stat.ATK)).toBe(0); + expect(userPokemon.getStatStage(Stat.DEF)).toBe(0); + }); + + it("Player side + double battle Intimidate + Tickle - opponents each lose -3 atk, -1 def", async () => { + game.override.battleType("double"); + game.override.ability(Abilities.MIRROR_ARMOR); + game.override.enemyAbility(Abilities.INTIMIDATE); + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER ]); + + const [ enemy1, enemy2 ] = game.scene.getEnemyField(); + const [ player1, player2 ] = game.scene.getPlayerField(); + + game.move.select(Moves.SPLASH); + game.move.select(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.TICKLE, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.TICKLE, BattlerIndex.PLAYER_2); + await game.toNextTurn(); + + expect(player1.getStatStage(Stat.ATK)).toBe(0); + expect(player1.getStatStage(Stat.DEF)).toBe(0); + expect(player2.getStatStage(Stat.ATK)).toBe(0); + expect(player2.getStatStage(Stat.DEF)).toBe(0); + expect(enemy1.getStatStage(Stat.ATK)).toBe(-3); + expect(enemy1.getStatStage(Stat.DEF)).toBe(-1); + expect(enemy2.getStatStage(Stat.ATK)).toBe(-3); + expect(enemy2.getStatStage(Stat.DEF)).toBe(-1); + + }); + + it("Enemy side + single battle Intimidate + Tickle - player loses stats", async () => { + game.override.enemyAbility(Abilities.MIRROR_ARMOR); + game.override.ability(Abilities.INTIMIDATE); + await game.classicMode.startBattle([ Species.BULBASAUR ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const userPokemon = game.scene.getPlayerPokemon()!; + + // Enemy has intimidate and uses tickle, enemy receives -2 atk and -1 defense + game.move.select(Moves.TICKLE); + await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER); + await game.toNextTurn(); + + expect(userPokemon.getStatStage(Stat.DEF)).toBe(-1); + expect(userPokemon.getStatStage(Stat.ATK)).toBe(-2); + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); + expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0); + }); + + it("Player side + single battle Intimidate + oppoenent has white smoke - no one loses stats", async () => { + game.override.enemyAbility(Abilities.WHITE_SMOKE); + game.override.ability(Abilities.MIRROR_ARMOR); + await game.classicMode.startBattle([ Species.BULBASAUR ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const userPokemon = game.scene.getPlayerPokemon()!; + + // Enemy has intimidate and uses tickle, enemy has white smoke, no one loses stats + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.TICKLE, BattlerIndex.PLAYER); + await game.toNextTurn(); + + expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0); + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); + expect(userPokemon.getStatStage(Stat.ATK)).toBe(0); + expect(userPokemon.getStatStage(Stat.DEF)).toBe(0); + }); + + it("Enemy side + single battle Intimidate + player has white smoke - no one loses stats", async () => { + game.override.ability(Abilities.WHITE_SMOKE); + game.override.enemyAbility(Abilities.MIRROR_ARMOR); + await game.classicMode.startBattle([ Species.BULBASAUR ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const userPokemon = game.scene.getPlayerPokemon()!; + + // Enemy has intimidate and uses tickle, enemy has white smoke, no one loses stats + game.move.select(Moves.TICKLE); + await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER); + await game.toNextTurn(); + + expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0); + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); + expect(userPokemon.getStatStage(Stat.ATK)).toBe(0); + expect(userPokemon.getStatStage(Stat.DEF)).toBe(0); + }); + + it("Player side + single battle + opponent uses octolock - does not interact with mirror armor, player loses stats", async () => { + game.override.ability(Abilities.MIRROR_ARMOR); + await game.classicMode.startBattle([ Species.BULBASAUR ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const userPokemon = game.scene.getPlayerPokemon()!; + + // Enemy uses octolock, player loses stats at end of turn + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.OCTOLOCK, BattlerIndex.PLAYER); + await game.toNextTurn(); + + expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0); + expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(0); + expect(userPokemon.getStatStage(Stat.DEF)).toBe(-1); + expect(userPokemon.getStatStage(Stat.SPDEF)).toBe(-1); + }); + + it("Enemy side + single battle + player uses octolock - does not interact with mirror armor, opponent loses stats", async () => { + game.override.enemyAbility(Abilities.MIRROR_ARMOR); + await game.classicMode.startBattle([ Species.BULBASAUR ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const userPokemon = game.scene.getPlayerPokemon()!; + + // Player uses octolock, enemy loses stats at end of turn + game.move.select(Moves.OCTOLOCK); + await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER); + await game.toNextTurn(); + + expect(userPokemon.getStatStage(Stat.DEF)).toBe(0); + expect(userPokemon.getStatStage(Stat.SPDEF)).toBe(0); + expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(-1); + expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(-1); + }); + + it("Both sides have mirror armor - does not loop, player loses attack", async () => { + game.override.enemyAbility(Abilities.MIRROR_ARMOR); + game.override.ability(Abilities.MIRROR_ARMOR); + game.override.ability(Abilities.INTIMIDATE); + await game.classicMode.startBattle([ Species.BULBASAUR ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const userPokemon = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER); + await game.toNextTurn(); + + expect(userPokemon.getStatStage(Stat.ATK)).toBe(-1); + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(0); + }); + + it("Single battle + sticky web applied player side - player switches out and enemy should lose -1 speed", async () => { + game.override.ability(Abilities.MIRROR_ARMOR); + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const userPokemon = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.STICKY_WEB, BattlerIndex.PLAYER); + await game.toNextTurn(); + + game.doSwitchPokemon(1); + await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER); + await game.toNextTurn(); + + expect(userPokemon.getStatStage(Stat.SPD)).toBe(0); + expect(enemyPokemon.getStatStage(Stat.SPD)).toBe(-1); + }); + + it("Double battle + sticky web applied player side - player switches out and enemy 1 should lose -1 speed", async () => { + game.override.battleType("double"); + game.override.ability(Abilities.MIRROR_ARMOR); + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]); + + const [ enemy1, enemy2 ] = game.scene.getEnemyField(); + const [ player1, player2 ] = game.scene.getPlayerField(); + + game.move.select(Moves.SPLASH); + game.move.select(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.STICKY_WEB, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER_2); + await game.toNextTurn(); + + game.doSwitchPokemon(2); + game.move.select(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.SPLASH, BattlerIndex.PLAYER_2); + await game.toNextTurn(); + + expect(enemy1.getStatStage(Stat.SPD)).toBe(-1); + expect(enemy2.getStatStage(Stat.SPD)).toBe(0); + expect(player1.getStatStage(Stat.SPD)).toBe(0); + expect(player2.getStatStage(Stat.SPD)).toBe(0); + }); +}); diff --git a/src/test/abilities/protosynthesis.test.ts b/src/test/abilities/protosynthesis.test.ts new file mode 100644 index 00000000000..67786c3ae9e --- /dev/null +++ b/src/test/abilities/protosynthesis.test.ts @@ -0,0 +1,66 @@ +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Nature } from "#enums/nature"; +import { Species } from "#enums/species"; +import { Stat } from "#enums/stat"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { BattlerIndex } from "#app/battle"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Abilities - Protosynthesis", () => { + 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.PROTOSYNTHESIS) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should not consider temporary items when determining which stat to boost", async() => { + // Mew has uniform base stats + game.override.startingModifier([{ name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.DEF }]) + .enemyMoveset(Moves.SUNNY_DAY) + .startingLevel(100) + .enemyLevel(100); + await game.classicMode.startBattle([ Species.MEW ]); + const mew = game.scene.getPlayerPokemon()!; + // Nature of starting mon is randomized. We need to fix it to a neutral nature for the automated test. + mew.setNature(Nature.HARDY); + const enemy = game.scene.getEnemyPokemon()!; + const def_before_boost = mew.getEffectiveStat(Stat.DEF, undefined, undefined, false, undefined, false, false, true); + const atk_before_boost = mew.getEffectiveStat(Stat.ATK, undefined, undefined, false, undefined, false, false, true); + const initialHp = enemy.hp; + game.move.select(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.toNextTurn(); + const unboosted_dmg = initialHp - enemy.hp; + enemy.hp = initialHp; + const def_after_boost = mew.getEffectiveStat(Stat.DEF, undefined, undefined, false, undefined, false, false, true); + const atk_after_boost = mew.getEffectiveStat(Stat.ATK, undefined, undefined, false, undefined, false, false, true); + game.move.select(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.toNextTurn(); + const boosted_dmg = initialHp - enemy.hp; + expect(boosted_dmg).toBeGreaterThan(unboosted_dmg); + expect(def_after_boost).toEqual(def_before_boost); + expect(atk_after_boost).toBeGreaterThan(atk_before_boost); + }); +}); diff --git a/src/test/abilities/shield_dust.test.ts b/src/test/abilities/shield_dust.test.ts index 9f1e6aeb11d..329f52cc4c6 100644 --- a/src/test/abilities/shield_dust.test.ts +++ b/src/test/abilities/shield_dust.test.ts @@ -53,11 +53,11 @@ describe("Abilities - Shield Dust", () => { expect(move.id).toBe(Moves.AIR_SLASH); const chance = new NumberHolder(move.chance); - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); - applyPreDefendAbAttrs(IgnoreMoveEffectsAbAttr, phase.getFirstTarget()!, phase.getUserPokemon()!, null, null, false, chance); + await applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); + await applyPreDefendAbAttrs(IgnoreMoveEffectsAbAttr, phase.getFirstTarget()!, phase.getUserPokemon()!, null, null, false, chance); expect(chance.value).toBe(0); - }, 20000); + }); //TODO King's Rock Interaction Unit Test }); diff --git a/src/test/abilities/supreme_overlord.test.ts b/src/test/abilities/supreme_overlord.test.ts new file mode 100644 index 00000000000..ecd595cb6bb --- /dev/null +++ b/src/test/abilities/supreme_overlord.test.ts @@ -0,0 +1,178 @@ +import { Moves } from "#app/enums/moves"; +import { Abilities } from "#enums/abilities"; +import { Species } from "#enums/species"; +import { BattlerIndex } from "#app/battle"; +import { MoveEffectPhase } from "#app/phases/move-effect-phase"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { allMoves } from "#app/data/move"; + +describe("Abilities - Supreme Overlord", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + const move = allMoves[Moves.TACKLE]; + const basePower = move.power; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .battleType("single") + .enemySpecies(Species.MAGIKARP) + .enemyLevel(100) + .startingLevel(1) + .enemyAbility(Abilities.BALL_FETCH) + .ability(Abilities.SUPREME_OVERLORD) + .enemyMoveset([ Moves.SPLASH ]) + .moveset([ Moves.TACKLE, Moves.EXPLOSION, Moves.LUNAR_DANCE ]); + + vi.spyOn(move, "calculateBattlePower"); + }); + + it("should increase Power by 20% if 2 Pokemon are fainted in the party", async() => { + await game.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]); + + game.move.select(Moves.EXPLOSION); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + game.move.select(Moves.EXPLOSION); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + game.doSelectPartyPokemon(2); + await game.toNextTurn(); + + game.move.select(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to(MoveEffectPhase); + + expect(move.calculateBattlePower).toHaveReturnedWith(basePower * 1.2); + }); + + it("should increase Power by 30% if an ally fainted twice and another one once", async () => { + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]); + + /** + * Bulbasur faints once + */ + game.move.select(Moves.EXPLOSION); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + /** + * Charmander faints once + */ + game.doRevivePokemon(1); + game.move.select(Moves.EXPLOSION); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + /** + * Bulbasur faints twice + */ + game.move.select(Moves.EXPLOSION); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + game.doSelectPartyPokemon(2); + await game.toNextTurn(); + + game.move.select(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to(MoveEffectPhase); + + expect(move.calculateBattlePower).toHaveReturnedWith(basePower * 1.3); + }); + + it("should maintain its power during next battle if it is within the same arena encounter", async () => { + game.override + .enemySpecies(Species.MAGIKARP) + .startingWave(1) + .enemyLevel(1) + .startingLevel(100); + + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]); + + /** + * The first Pokemon faints and another Pokemon in the party is selected. + */ + game.move.select(Moves.LUNAR_DANCE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + /** + * Enemy Pokemon faints and new wave is entered. + */ + game.move.select(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextWave(); + + game.move.select(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(move.calculateBattlePower).toHaveLastReturnedWith(basePower * 1.1); + }); + + it("should reset playerFaints count if we enter new trainer battle", async () => { + game.override + .enemySpecies(Species.MAGIKARP) + .startingWave(4) + .enemyLevel(1) + .startingLevel(100); + + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]); + + game.move.select(Moves.LUNAR_DANCE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + game.move.select(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextWave(); + + game.move.select(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("BerryPhase", false); + + expect(move.calculateBattlePower).toHaveLastReturnedWith(basePower); + }); + + it("should reset playerFaints count if we enter new biome", async () => { + game.override + .enemySpecies(Species.MAGIKARP) + .startingWave(10) + .enemyLevel(1) + .startingLevel(100); + + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]); + + game.move.select(Moves.LUNAR_DANCE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + game.move.select(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextWave(); + + game.move.select(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("BerryPhase", false); + + expect(move.calculateBattlePower).toHaveLastReturnedWith(basePower); + }); +}); diff --git a/src/test/abilities/unseen_fist.test.ts b/src/test/abilities/unseen_fist.test.ts index f8fa8a723fe..584f997aa55 100644 --- a/src/test/abilities/unseen_fist.test.ts +++ b/src/test/abilities/unseen_fist.test.ts @@ -45,9 +45,9 @@ describe("Abilities - Unseen Fist", () => { it( "should not apply if the source has Long Reach", - () => { + async () => { game.override.passiveAbility(Abilities.LONG_REACH); - testUnseenFistHitResult(game, Moves.QUICK_ATTACK, Moves.PROTECT, false); + await testUnseenFistHitResult(game, Moves.QUICK_ATTACK, Moves.PROTECT, false); } ); @@ -67,7 +67,7 @@ describe("Abilities - Unseen Fist", () => { game.override.enemyLevel(1); game.override.moveset([ Moves.TACKLE ]); - await game.startBattle(); + await game.classicMode.startBattle(); const enemyPokemon = game.scene.getEnemyPokemon()!; enemyPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, enemyPokemon.id); @@ -86,7 +86,7 @@ async function testUnseenFistHitResult(game: GameManager, attackMove: Moves, pro game.override.moveset([ attackMove ]); game.override.enemyMoveset([ protectMove, protectMove, protectMove, protectMove ]); - await game.startBattle(); + await game.classicMode.startBattle(); const leadPokemon = game.scene.getPlayerPokemon()!; expect(leadPokemon).not.toBe(undefined); diff --git a/src/test/data/status_effect.test.ts b/src/test/data/status_effect.test.ts index 7948549b8e8..071dea989a9 100644 --- a/src/test/data/status_effect.test.ts +++ b/src/test/data/status_effect.test.ts @@ -20,8 +20,8 @@ const pokemonName = "PKM"; const sourceText = "SOURCE"; describe("Status Effect Messages", () => { - beforeAll(() => { - i18next.init(); + beforeAll(async () => { + await i18next.init(); }); describe("NONE", () => { diff --git a/src/test/evolution.test.ts b/src/test/evolution.test.ts index d198049801c..8dc19a548ca 100644 --- a/src/test/evolution.test.ts +++ b/src/test/evolution.test.ts @@ -40,10 +40,10 @@ describe("Evolution", () => { eevee.abilityIndex = 2; trapinch.abilityIndex = 2; - eevee.evolve(pokemonEvolutions[Species.EEVEE][6], eevee.getSpeciesForm()); + await eevee.evolve(pokemonEvolutions[Species.EEVEE][6], eevee.getSpeciesForm()); expect(eevee.abilityIndex).toBe(2); - trapinch.evolve(pokemonEvolutions[Species.TRAPINCH][0], trapinch.getSpeciesForm()); + await trapinch.evolve(pokemonEvolutions[Species.TRAPINCH][0], trapinch.getSpeciesForm()); expect(trapinch.abilityIndex).toBe(1); }); @@ -55,10 +55,10 @@ describe("Evolution", () => { bulbasaur.abilityIndex = 0; charmander.abilityIndex = 1; - bulbasaur.evolve(pokemonEvolutions[Species.BULBASAUR][0], bulbasaur.getSpeciesForm()); + await bulbasaur.evolve(pokemonEvolutions[Species.BULBASAUR][0], bulbasaur.getSpeciesForm()); expect(bulbasaur.abilityIndex).toBe(0); - charmander.evolve(pokemonEvolutions[Species.CHARMANDER][0], charmander.getSpeciesForm()); + await charmander.evolve(pokemonEvolutions[Species.CHARMANDER][0], charmander.getSpeciesForm()); expect(charmander.abilityIndex).toBe(1); }); @@ -68,7 +68,7 @@ describe("Evolution", () => { const squirtle = game.scene.getPlayerPokemon()!; squirtle.abilityIndex = 5; - squirtle.evolve(pokemonEvolutions[Species.SQUIRTLE][0], squirtle.getSpeciesForm()); + await squirtle.evolve(pokemonEvolutions[Species.SQUIRTLE][0], squirtle.getSpeciesForm()); expect(squirtle.abilityIndex).toBe(0); }); @@ -80,7 +80,7 @@ describe("Evolution", () => { nincada.metBiome = -1; nincada.gender = 1; - nincada.evolve(pokemonEvolutions[Species.NINCADA][0], nincada.getSpeciesForm()); + await nincada.evolve(pokemonEvolutions[Species.NINCADA][0], nincada.getSpeciesForm()); const ninjask = game.scene.getPlayerParty()[0]; const shedinja = game.scene.getPlayerParty()[1]; expect(ninjask.abilityIndex).toBe(2); diff --git a/src/test/items/light_ball.test.ts b/src/test/items/light_ball.test.ts index 987a5ab8b0c..aae1d806a28 100644 --- a/src/test/items/light_ball.test.ts +++ b/src/test/items/light_ball.test.ts @@ -31,7 +31,7 @@ describe("Items - Light Ball", () => { it("LIGHT_BALL activates in battle correctly", async() => { game.override.startingHeldItems([{ name: "SPECIES_STAT_BOOSTER", type: "LIGHT_BALL" }]); const consoleSpy = vi.spyOn(console, "log"); - await game.startBattle([ + await game.classicMode.startBattle([ Species.PIKACHU ]); @@ -64,7 +64,7 @@ describe("Items - Light Ball", () => { }); it("LIGHT_BALL held by PIKACHU", async() => { - await game.startBattle([ + await game.classicMode.startBattle([ Species.PIKACHU ]); @@ -83,7 +83,7 @@ describe("Items - Light Ball", () => { expect(spAtkValue.value / spAtkStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "LIGHT_BALL" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "LIGHT_BALL" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPATK, spAtkValue); @@ -92,7 +92,7 @@ describe("Items - Light Ball", () => { }, 20000); it("LIGHT_BALL held by fused PIKACHU (base)", async() => { - await game.startBattle([ + await game.classicMode.startBattle([ Species.PIKACHU, Species.MAROWAK ]); @@ -122,7 +122,7 @@ describe("Items - Light Ball", () => { expect(spAtkValue.value / spAtkStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "LIGHT_BALL" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "LIGHT_BALL" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPATK, spAtkValue); @@ -161,7 +161,7 @@ describe("Items - Light Ball", () => { expect(spAtkValue.value / spAtkStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "LIGHT_BALL" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "LIGHT_BALL" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPATK, spAtkValue); @@ -189,7 +189,7 @@ describe("Items - Light Ball", () => { expect(spAtkValue.value / spAtkStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "LIGHT_BALL" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "LIGHT_BALL" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPATK, spAtkValue); diff --git a/src/test/items/metal_powder.test.ts b/src/test/items/metal_powder.test.ts index 42ef9c1bb16..68c3107af08 100644 --- a/src/test/items/metal_powder.test.ts +++ b/src/test/items/metal_powder.test.ts @@ -31,7 +31,7 @@ describe("Items - Metal Powder", () => { it("METAL_POWDER activates in battle correctly", async() => { game.override.startingHeldItems([{ name: "SPECIES_STAT_BOOSTER", type: "METAL_POWDER" }]); const consoleSpy = vi.spyOn(console, "log"); - await game.startBattle([ + await game.classicMode.startBattle([ Species.DITTO ]); @@ -79,7 +79,7 @@ describe("Items - Metal Powder", () => { expect(defValue.value / defStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "METAL_POWDER" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "METAL_POWDER" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue); expect(defValue.value / defStat).toBe(2); @@ -112,7 +112,7 @@ describe("Items - Metal Powder", () => { expect(defValue.value / defStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "METAL_POWDER" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "METAL_POWDER" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue); expect(defValue.value / defStat).toBe(2); @@ -145,7 +145,7 @@ describe("Items - Metal Powder", () => { expect(defValue.value / defStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "METAL_POWDER" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "METAL_POWDER" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue); expect(defValue.value / defStat).toBe(2); @@ -167,7 +167,7 @@ describe("Items - Metal Powder", () => { expect(defValue.value / defStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "METAL_POWDER" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "METAL_POWDER" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.DEF, defValue); expect(defValue.value / defStat).toBe(1); diff --git a/src/test/items/quick_powder.test.ts b/src/test/items/quick_powder.test.ts index d30111cbd6a..ae16daf17ff 100644 --- a/src/test/items/quick_powder.test.ts +++ b/src/test/items/quick_powder.test.ts @@ -31,7 +31,7 @@ describe("Items - Quick Powder", () => { it("QUICK_POWDER activates in battle correctly", async() => { game.override.startingHeldItems([{ name: "SPECIES_STAT_BOOSTER", type: "QUICK_POWDER" }]); const consoleSpy = vi.spyOn(console, "log"); - await game.startBattle([ + await game.classicMode.startBattle([ Species.DITTO ]); @@ -64,7 +64,7 @@ describe("Items - Quick Powder", () => { }); it("QUICK_POWDER held by DITTO", async() => { - await game.startBattle([ + await game.classicMode.startBattle([ Species.DITTO ]); @@ -79,14 +79,14 @@ describe("Items - Quick Powder", () => { expect(spdValue.value / spdStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "QUICK_POWDER" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "QUICK_POWDER" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue); expect(spdValue.value / spdStat).toBe(2); - }, 20000); + }); it("QUICK_POWDER held by fused DITTO (base)", async() => { - await game.startBattle([ + await game.classicMode.startBattle([ Species.DITTO, Species.MAROWAK ]); @@ -112,14 +112,14 @@ describe("Items - Quick Powder", () => { expect(spdValue.value / spdStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "QUICK_POWDER" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "QUICK_POWDER" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue); expect(spdValue.value / spdStat).toBe(2); - }, 20000); + }); it("QUICK_POWDER held by fused DITTO (part)", async() => { - await game.startBattle([ + await game.classicMode.startBattle([ Species.MAROWAK, Species.DITTO ]); @@ -145,14 +145,14 @@ describe("Items - Quick Powder", () => { expect(spdValue.value / spdStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "QUICK_POWDER" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "QUICK_POWDER" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue); expect(spdValue.value / spdStat).toBe(2); - }, 20000); + }); it("QUICK_POWDER not held by DITTO", async() => { - await game.startBattle([ + await game.classicMode.startBattle([ Species.MAROWAK ]); @@ -167,9 +167,9 @@ describe("Items - Quick Powder", () => { expect(spdValue.value / spdStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "QUICK_POWDER" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "QUICK_POWDER" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.SPD, spdValue); expect(spdValue.value / spdStat).toBe(1); - }, 20000); + }); }); diff --git a/src/test/items/thick_club.test.ts b/src/test/items/thick_club.test.ts index 08b19250ea7..d32c213e506 100644 --- a/src/test/items/thick_club.test.ts +++ b/src/test/items/thick_club.test.ts @@ -31,7 +31,7 @@ describe("Items - Thick Club", () => { it("THICK_CLUB activates in battle correctly", async() => { game.override.startingHeldItems([{ name: "SPECIES_STAT_BOOSTER", type: "THICK_CLUB" }]); const consoleSpy = vi.spyOn(console, "log"); - await game.startBattle([ + await game.classicMode.startBattle([ Species.CUBONE ]); @@ -64,7 +64,7 @@ describe("Items - Thick Club", () => { }); it("THICK_CLUB held by CUBONE", async() => { - await game.startBattle([ + await game.classicMode.startBattle([ Species.CUBONE ]); @@ -79,14 +79,14 @@ describe("Items - Thick Club", () => { expect(atkValue.value / atkStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); expect(atkValue.value / atkStat).toBe(2); - }, 20000); + }); it("THICK_CLUB held by MAROWAK", async() => { - await game.startBattle([ + await game.classicMode.startBattle([ Species.MAROWAK ]); @@ -101,14 +101,14 @@ describe("Items - Thick Club", () => { expect(atkValue.value / atkStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); expect(atkValue.value / atkStat).toBe(2); - }, 20000); + }); it("THICK_CLUB held by ALOLA_MAROWAK", async() => { - await game.startBattle([ + await game.classicMode.startBattle([ Species.ALOLA_MAROWAK ]); @@ -123,18 +123,18 @@ describe("Items - Thick Club", () => { expect(atkValue.value / atkStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); expect(atkValue.value / atkStat).toBe(2); - }, 20000); + }); it("THICK_CLUB held by fused CUBONE line (base)", async() => { // Randomly choose from the Cubone line const species = [ Species.CUBONE, Species.MAROWAK, Species.ALOLA_MAROWAK ]; const randSpecies = Utils.randInt(species.length); - await game.startBattle([ + await game.classicMode.startBattle([ species[randSpecies], Species.PIKACHU ]); @@ -160,18 +160,18 @@ describe("Items - Thick Club", () => { expect(atkValue.value / atkStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); expect(atkValue.value / atkStat).toBe(2); - }, 20000); + }); it("THICK_CLUB held by fused CUBONE line (part)", async() => { // Randomly choose from the Cubone line const species = [ Species.CUBONE, Species.MAROWAK, Species.ALOLA_MAROWAK ]; const randSpecies = Utils.randInt(species.length); - await game.startBattle([ + await game.classicMode.startBattle([ Species.PIKACHU, species[randSpecies] ]); @@ -197,14 +197,14 @@ describe("Items - Thick Club", () => { expect(atkValue.value / atkStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); expect(atkValue.value / atkStat).toBe(2); - }, 20000); + }); it("THICK_CLUB not held by CUBONE", async() => { - await game.startBattle([ + await game.classicMode.startBattle([ Species.PIKACHU ]); @@ -219,9 +219,9 @@ describe("Items - Thick Club", () => { expect(atkValue.value / atkStat).toBe(1); // Giving Eviolite to party member and testing if it applies - game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true); + await game.scene.addModifier(modifierTypes.SPECIES_STAT_BOOSTER().generateType([], [ "THICK_CLUB" ])!.newModifier(partyMember), true); game.scene.applyModifiers(SpeciesStatBoosterModifier, true, partyMember, Stat.ATK, atkValue); expect(atkValue.value / atkStat).toBe(1); - }, 20000); + }); }); diff --git a/src/test/moves/dragon_rage.test.ts b/src/test/moves/dragon_rage.test.ts index a2350960546..61630ede326 100644 --- a/src/test/moves/dragon_rage.test.ts +++ b/src/test/moves/dragon_rage.test.ts @@ -45,14 +45,10 @@ describe("Moves - Dragon Rage", () => { game.override.enemyPassiveAbility(Abilities.BALL_FETCH); game.override.enemyLevel(100); - await game.startBattle(); + await game.classicMode.startBattle(); partyPokemon = game.scene.getPlayerParty()[0]; enemyPokemon = game.scene.getEnemyPokemon()!; - - // remove berries - game.scene.removePartyMemberModifiers(0); - game.scene.clearEnemyHeldItemModifiers(); }); it("ignores weaknesses", async () => { diff --git a/src/test/moves/fissure.test.ts b/src/test/moves/fissure.test.ts index 0975a87b2b1..65719df0205 100644 --- a/src/test/moves/fissure.test.ts +++ b/src/test/moves/fissure.test.ts @@ -41,14 +41,10 @@ describe("Moves - Fissure", () => { game.override.enemyPassiveAbility(Abilities.BALL_FETCH); game.override.enemyLevel(100); - await game.startBattle(); + await game.classicMode.startBattle(); partyPokemon = game.scene.getPlayerParty()[0]; enemyPokemon = game.scene.getEnemyPokemon()!; - - // remove berries - game.scene.removePartyMemberModifiers(0); - game.scene.clearEnemyHeldItemModifiers(); }); it("ignores damage modification from abilities, for example FUR_COAT", async () => { diff --git a/src/test/moves/last_respects.test.ts b/src/test/moves/last_respects.test.ts new file mode 100644 index 00000000000..71a76e3fa1a --- /dev/null +++ b/src/test/moves/last_respects.test.ts @@ -0,0 +1,219 @@ +import { Moves } from "#enums/moves"; +import { BattlerIndex } from "#app/battle"; +import { Species } from "#enums/species"; +import { Abilities } from "#enums/abilities"; +import GameManager from "#test/utils/gameManager"; +import { allMoves } from "#app/data/move"; +import { MoveEffectPhase } from "#app/phases/move-effect-phase"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Moves - Last Respects", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + const move = allMoves[Moves.LAST_RESPECTS]; + const basePower = move.power; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .battleType("single") + .disableCrits() + .moveset([ Moves.LAST_RESPECTS, Moves.EXPLOSION, Moves.LUNAR_DANCE ]) + .ability(Abilities.BALL_FETCH) + .enemyAbility(Abilities.BALL_FETCH) + .enemySpecies(Species.MAGIKARP) + .enemyMoveset(Moves.SPLASH) + .startingLevel(1) + .enemyLevel(100); + + vi.spyOn(move, "calculateBattlePower"); + }); + + it("should have 150 power if 2 allies faint before using move", async () => { + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]); + + /** + * Bulbasur faints once + */ + game.move.select(Moves.EXPLOSION); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + /** + * Charmander faints once + */ + game.move.select(Moves.EXPLOSION); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + game.doSelectPartyPokemon(2); + await game.toNextTurn(); + + game.move.select(Moves.LAST_RESPECTS); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to(MoveEffectPhase); + + expect(move.calculateBattlePower).toHaveReturnedWith(basePower + (2 * 50)); + }); + + it("should have 200 power if an ally fainted twice and another one once", async () => { + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]); + + /** + * Bulbasur faints once + */ + game.move.select(Moves.EXPLOSION); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + /** + * Charmander faints once + */ + game.doRevivePokemon(1); + game.move.select(Moves.EXPLOSION); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + /** + * Bulbasur faints twice + */ + game.move.select(Moves.EXPLOSION); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + game.doSelectPartyPokemon(2); + await game.toNextTurn(); + + game.move.select(Moves.LAST_RESPECTS); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to(MoveEffectPhase); + + expect(move.calculateBattlePower).toHaveReturnedWith(basePower + (3 * 50)); + }); + + it("should maintain its power for the player during the next battle if it is within the same arena encounter", async () => { + game.override + .enemySpecies(Species.MAGIKARP) + .startingWave(1) + .enemyLevel(1) + .startingLevel(100) + .enemyMoveset(Moves.SPLASH); + + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]); + + /** + * The first Pokemon faints and another Pokemon in the party is selected. + */ + game.move.select(Moves.LUNAR_DANCE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + /** + * Enemy Pokemon faints and new wave is entered. + */ + game.move.select(Moves.LAST_RESPECTS); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextWave(); + expect(game.scene.arena.playerFaints).toBe(1); + + game.move.select(Moves.LAST_RESPECTS); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("MoveEndPhase"); + expect(move.calculateBattlePower).toHaveLastReturnedWith(basePower + (1 * 50)); + }); + + it("should reset enemyFaints count on progressing to the next wave.", async () => { + game.override + .enemySpecies(Species.MAGIKARP) + .startingWave(1) + .enemyLevel(1) + .startingLevel(100) + .enemyMoveset(Moves.LAST_RESPECTS) + .moveset([ Moves.LUNAR_DANCE, Moves.LAST_RESPECTS, Moves.SPLASH ]); + + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]); + + /** + * The first Pokemon faints and another Pokemon in the party is selected. + */ + game.move.select(Moves.LUNAR_DANCE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + /** + * Enemy Pokemon faints and new wave is entered. + */ + game.move.select(Moves.LAST_RESPECTS); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextWave(); + expect(game.scene.currentBattle.enemyFaints).toBe(0); + + game.move.select(Moves.LAST_RESPECTS); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("MoveEndPhase"); + expect(move.calculateBattlePower).toHaveLastReturnedWith(basePower); + }); + + it("should reset playerFaints count if we enter new trainer battle", async () => { + game.override + .enemySpecies(Species.MAGIKARP) + .startingWave(4) + .enemyLevel(1) + .startingLevel(100); + + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]); + + game.move.select(Moves.LUNAR_DANCE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + game.move.select(Moves.LAST_RESPECTS); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextWave(); + + game.move.select(Moves.LAST_RESPECTS); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("BerryPhase", false); + + expect(move.calculateBattlePower).toHaveLastReturnedWith(basePower); + }); + + it("should reset playerFaints count if we enter new biome", async () => { + game.override + .enemySpecies(Species.MAGIKARP) + .startingWave(10) + .enemyLevel(1) + .startingLevel(100); + + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]); + + game.move.select(Moves.LUNAR_DANCE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + game.move.select(Moves.LAST_RESPECTS); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextWave(); + + game.move.select(Moves.LAST_RESPECTS); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("BerryPhase", false); + + expect(move.calculateBattlePower).toHaveLastReturnedWith(basePower); + }); +}); diff --git a/src/test/moves/magic_coat.test.ts b/src/test/moves/magic_coat.test.ts new file mode 100644 index 00000000000..7371c89d4ac --- /dev/null +++ b/src/test/moves/magic_coat.test.ts @@ -0,0 +1,286 @@ +import { BattlerIndex } from "#app/battle"; +import { ArenaTagSide } from "#app/data/arena-tag"; +import { allMoves } from "#app/data/move"; +import { ArenaTagType } from "#app/enums/arena-tag-type"; +import { BattlerTagType } from "#app/enums/battler-tag-type"; +import { Stat } from "#app/enums/stat"; +import { StatusEffect } from "#app/enums/status-effect"; +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 { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Moves - Magic Coat", () => { + 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 + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.MAGIC_COAT); + }); + + it("should fail if the user goes last in the turn", async () => { + game.override.moveset([ Moves.PROTECT ]); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + game.move.select(Moves.PROTECT); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getEnemyPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + }); + + it("should fail if called again in the same turn due to moves like instruct", async () => { + game.override.moveset([ Moves.INSTRUCT ]); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + game.move.select(Moves.INSTRUCT); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getEnemyPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + }); + + it("should not reflect moves used on the next turn", async () => { + game.override.moveset([ Moves.GROWL, Moves.SPLASH ]); + game.override.enemyMoveset([ Moves.MAGIC_COAT, Moves.SPLASH ]); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + // turn 1 + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.MAGIC_COAT); + await game.toNextTurn(); + + // turn 2 + game.move.select(Moves.GROWL); + await game.forceEnemyMove(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(-1); + }); + + it("should reflect basic status moves", async () => { + game.override.moveset([ Moves.GROWL ]); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + game.move.select(Moves.GROWL); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(-1); + }); + + it("should individually bounce back multi-target moves when used by both targets in doubles", async () => { + game.override.battleType("double"); + game.override.moveset([ Moves.GROWL, Moves.SPLASH ]); + await game.classicMode.startBattle([ Species.MAGIKARP, Species.MAGIKARP ]); + + game.move.select(Moves.GROWL, 0); + game.move.select(Moves.SPLASH, 1); + await game.phaseInterceptor.to("BerryPhase"); + + const user = game.scene.getPlayerField()[0]; + expect(user.getStatStage(Stat.ATK)).toBe(-2); + }); + + it("should bounce back a spread status move against both pokemon", async () => { + game.override.battleType("double"); + game.override.moveset([ Moves.GROWL, Moves.SPLASH ]); + game.override.enemyMoveset([ Moves.SPLASH, Moves.MAGIC_COAT ]); + await game.classicMode.startBattle([ Species.MAGIKARP, Species.MAGIKARP ]); + + game.move.select(Moves.GROWL, 0); + game.move.select(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.SPLASH); + await game.forceEnemyMove(Moves.MAGIC_COAT); + + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getPlayerField().every(p => p.getStatStage(Stat.ATK) === -1)).toBeTruthy(); + }); + + it("should still bounce back a move that would otherwise fail", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + game.scene.getEnemyPokemon()?.setStatStage(Stat.ATK, -6); + game.override.moveset([ Moves.GROWL ]); + + game.move.select(Moves.GROWL); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(-1); + }); + + it("should not bounce back a move that was just bounced", async () => { + game.override.battleType("double"); + game.override.ability(Abilities.MAGIC_BOUNCE); + game.override.moveset([ Moves.GROWL, Moves.MAGIC_COAT ]); + game.override.enemyMoveset([ Moves.SPLASH, Moves.MAGIC_COAT ]); + await game.classicMode.startBattle([ Species.MAGIKARP, Species.MAGIKARP ]); + + game.move.select(Moves.MAGIC_COAT, 0); + game.move.select(Moves.GROWL, 1); + await game.forceEnemyMove(Moves.MAGIC_COAT); + await game.forceEnemyMove(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyField()[0].getStatStage(Stat.ATK)).toBe(0); + }); + + // todo while Mirror Armor is not implemented + it.todo("should receive the stat change after reflecting a move back to a mirror armor user", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + game.move.select(Moves.GROWL); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(-1); + }); + + it("should still bounce back a move from a mold breaker user", async () => { + game.override.ability(Abilities.MOLD_BREAKER); + game.override.moveset([ Moves.GROWL ]); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + game.move.select(Moves.GROWL); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(0); + expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(-1); + }); + + it("should only bounce spikes back once when both targets use magic coat in doubles", async () => { + game.override.battleType("double"); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + game.override.moveset([ Moves.SPIKES ]); + + game.move.select(Moves.SPIKES); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.PLAYER)!["layers"]).toBe(1); + expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY)).toBeUndefined(); + }); + + it("should not bounce back curse", async() => { + game.override.starterSpecies(Species.GASTLY); + await game.classicMode.startBattle([ Species.GASTLY ]); + game.override.moveset([ Moves.CURSE ]); + + game.move.select(Moves.CURSE); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyPokemon()!.getTag(BattlerTagType.CURSED)).toBeDefined(); + }); + + // TODO: encore is failing if the last move was virtual. + it.todo("should not cause the bounced move to count for encore", async () => { + game.override.moveset([ Moves.GROWL, Moves.ENCORE ]); + game.override.enemyMoveset([ Moves.MAGIC_COAT, Moves.TACKLE ]); + game.override.enemyAbility(Abilities.MAGIC_BOUNCE); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + const enemyPokemon = game.scene.getEnemyPokemon()!; + + // turn 1 + game.move.select(Moves.GROWL); + await game.forceEnemyMove(Moves.MAGIC_COAT); + await game.toNextTurn(); + + // turn 2 + game.move.select(Moves.ENCORE); + await game.forceEnemyMove(Moves.TACKLE); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("BerryPhase"); + expect(enemyPokemon.getTag(BattlerTagType.ENCORE)!["moveId"]).toBe(Moves.TACKLE); + expect(enemyPokemon.getLastXMoves()[0].move).toBe(Moves.TACKLE); + }); + + // TODO: stomping tantrum should consider moves that were bounced. + it.todo("should cause stomping tantrum to double in power when the last move was bounced", async () => { + game.override.battleType("single"); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + game.override.moveset([ Moves.STOMPING_TANTRUM, Moves.CHARM ]); + + const stomping_tantrum = allMoves[Moves.STOMPING_TANTRUM]; + vi.spyOn(stomping_tantrum, "calculateBattlePower"); + + game.move.select(Moves.CHARM); + await game.toNextTurn(); + + game.move.select(Moves.STOMPING_TANTRUM); + await game.phaseInterceptor.to("BerryPhase"); + expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(150); + }); + + // TODO: stomping tantrum should consider moves that were bounced. + it.todo("should properly cause the enemy's stomping tantrum to be doubled in power after bouncing and failing", async () => { + game.override.enemyMoveset([ Moves.STOMPING_TANTRUM, Moves.SPLASH, Moves.CHARM ]); + await game.classicMode.startBattle([ Species.BULBASAUR ]); + + const stomping_tantrum = allMoves[Moves.STOMPING_TANTRUM]; + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(stomping_tantrum, "calculateBattlePower"); + + game.move.select(Moves.SPORE); + await game.forceEnemyMove(Moves.CHARM); + await game.phaseInterceptor.to("TurnEndPhase"); + expect(enemy.getLastXMoves(1)[0].result).toBe("success"); + + await game.phaseInterceptor.to("BerryPhase"); + expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(75); + + await game.toNextTurn(); + game.move.select(Moves.GROWL); + await game.phaseInterceptor.to("BerryPhase"); + expect(stomping_tantrum.calculateBattlePower).toHaveReturnedWith(75); + }); + + it("should respect immunities when bouncing a move", async () => { + vi.spyOn(allMoves[Moves.THUNDER_WAVE], "accuracy", "get").mockReturnValue(100); + game.override.moveset([ Moves.THUNDER_WAVE, Moves.GROWL ]); + game.override.ability(Abilities.SOUNDPROOF); + await game.classicMode.startBattle([ Species.PHANPY ]); + + // Turn 1 - thunder wave immunity test + game.move.select(Moves.THUNDER_WAVE); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getPlayerPokemon()!.status).toBeUndefined(); + + // Turn 2 - soundproof immunity test + game.move.select(Moves.GROWL); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(0); + }); + + it("should bounce back a move before the accuracy check", async () => { + game.override.moveset([ Moves.SPORE ]); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const attacker = game.scene.getPlayerPokemon()!; + + vi.spyOn(attacker, "getAccuracyMultiplier").mockReturnValue(0.0); + game.move.select(Moves.SPORE); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getPlayerPokemon()!.status?.effect).toBe(StatusEffect.SLEEP); + }); + + it("should take the accuracy of the magic bounce user into account", async () => { + game.override.moveset([ Moves.SPORE ]); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + const opponent = game.scene.getEnemyPokemon()!; + + vi.spyOn(opponent, "getAccuracyMultiplier").mockReturnValue(0); + game.move.select(Moves.SPORE); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getPlayerPokemon()!.status).toBeUndefined(); + }); +}); diff --git a/src/test/moves/spectral_thief.test.ts b/src/test/moves/spectral_thief.test.ts new file mode 100644 index 00000000000..8913b7f3683 --- /dev/null +++ b/src/test/moves/spectral_thief.test.ts @@ -0,0 +1,224 @@ +import { Abilities } from "#enums/abilities"; +import { BattlerIndex } from "#app/battle"; +import { Stat } from "#enums/stat"; +import { allMoves } from "#app/data/move"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { TurnEndPhase } from "#app/phases/turn-end-phase"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Spectral Thief", () => { + 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 + .enemySpecies(Species.SHUCKLE) + .enemyLevel(100) + .enemyMoveset(Moves.SPLASH) + .enemyAbility(Abilities.BALL_FETCH) + .moveset([ Moves.SPECTRAL_THIEF, Moves.SPLASH ]) + .ability(Abilities.BALL_FETCH) + .disableCrits; + }); + + it("should steal max possible positive stat changes and ignore negative ones.", async () => { + await game.classicMode.startBattle(); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + enemy.setStatStage(Stat.ATK, 6); + enemy.setStatStage(Stat.DEF, -6); + enemy.setStatStage(Stat.SPATK, 6); + enemy.setStatStage(Stat.SPDEF, -6); + enemy.setStatStage(Stat.SPD, 3); + + player.setStatStage(Stat.ATK, 4); + player.setStatStage(Stat.DEF, 1); + player.setStatStage(Stat.SPATK, 0); + player.setStatStage(Stat.SPDEF, 0); + player.setStatStage(Stat.SPD, -2); + + game.move.select(Moves.SPECTRAL_THIEF); + await game.phaseInterceptor.to(TurnEndPhase); + + /** + * enemy has +6 ATK and player +4 => player only steals +2 + * enemy has -6 DEF and player 1 => player should not steal + * enemy has +6 SPATK and player 0 => player only steals +6 + * enemy has -6 SPDEF and player 0 => player should not steal + * enemy has +3 SPD and player -2 => player only steals +3 + */ + expect(player.getStatStages()).toEqual([ 6, 1, 6, 0, 1, 0, 0 ]); + expect(enemy.getStatStages()).toEqual([ 4, -6, 0, -6, 0, 0, 0 ]); + }); + + it("should steal stat stages before dmg calculation", async () => { + game.override + .enemySpecies(Species.MAGIKARP) + .enemyLevel(50); + await game.classicMode.startBattle(); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + const moveToCheck = allMoves[Moves.SPECTRAL_THIEF]; + const dmgBefore = enemy.getAttackDamage(player, moveToCheck, false, false, false, false).damage; + + enemy.setStatStage(Stat.ATK, 6); + + player.setStatStage(Stat.ATK, 0); + + game.move.select(Moves.SPECTRAL_THIEF); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(dmgBefore).toBeLessThan(enemy.getAttackDamage(player, moveToCheck, false, false, false, false).damage); + }); + + it("should steal stat stages as a negative value with Contrary.", async () => { + game.override + .ability(Abilities.CONTRARY); + await game.classicMode.startBattle(); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + enemy.setStatStage(Stat.ATK, 6); + + player.setStatStage(Stat.ATK, 0); + + game.move.select(Moves.SPECTRAL_THIEF); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(player.getStatStage(Stat.ATK)).toEqual(-6); + expect(enemy.getStatStage(Stat.ATK)).toEqual(0); + }); + + it("should steal double the stat stages with Simple.", async () => { + game.override + .ability(Abilities.SIMPLE); + await game.classicMode.startBattle(); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + enemy.setStatStage(Stat.ATK, 3); + + player.setStatStage(Stat.ATK, 0); + + game.move.select(Moves.SPECTRAL_THIEF); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(player.getStatStage(Stat.ATK)).toEqual(6); + expect(enemy.getStatStage(Stat.ATK)).toEqual(0); + }); + + it("should steal the stat stages through Clear Body.", async () => { + game.override + .enemyAbility(Abilities.CLEAR_BODY); + await game.classicMode.startBattle(); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + enemy.setStatStage(Stat.ATK, 3); + + player.setStatStage(Stat.ATK, 0); + + game.move.select(Moves.SPECTRAL_THIEF); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(player.getStatStage(Stat.ATK)).toEqual(3); + expect(enemy.getStatStage(Stat.ATK)).toEqual(0); + }); + + it("should steal the stat stages through White Smoke.", async () => { + game.override + .enemyAbility(Abilities.WHITE_SMOKE); + await game.classicMode.startBattle(); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + enemy.setStatStage(Stat.ATK, 3); + + player.setStatStage(Stat.ATK, 0); + + game.move.select(Moves.SPECTRAL_THIEF); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(player.getStatStage(Stat.ATK)).toEqual(3); + expect(enemy.getStatStage(Stat.ATK)).toEqual(0); + }); + + it("should steal the stat stages through Hyper Cutter.", async () => { + game.override + .enemyAbility(Abilities.HYPER_CUTTER); + await game.classicMode.startBattle(); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + enemy.setStatStage(Stat.ATK, 3); + + player.setStatStage(Stat.ATK, 0); + + game.move.select(Moves.SPECTRAL_THIEF); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(player.getStatStage(Stat.ATK)).toEqual(3); + expect(enemy.getStatStage(Stat.ATK)).toEqual(0); + }); + + it("should bypass Substitute.", async () => { + game.override + .enemyMoveset(Moves.SUBSTITUTE); + await game.classicMode.startBattle(); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + enemy.setStatStage(Stat.ATK, 3); + + player.setStatStage(Stat.ATK, 0); + + game.move.select(Moves.SPECTRAL_THIEF); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(player.getStatStage(Stat.ATK)).toEqual(3); + expect(enemy.getStatStage(Stat.ATK)).toEqual(0); + expect(enemy.hp).toBeLessThan(enemy.getMaxHp() - 1); + }); + + it("should get blocked by protect.", async () => { + game.override + .enemyMoveset(Moves.PROTECT); + await game.classicMode.startBattle(); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + enemy.setStatStage(Stat.ATK, 3); + + player.setStatStage(Stat.ATK, 0); + + game.move.select(Moves.SPECTRAL_THIEF); + await game.phaseInterceptor.to(TurnEndPhase); + + expect(player.getStatStage(Stat.ATK)).toEqual(0); + expect(enemy.getStatStage(Stat.ATK)).toEqual(3); + expect(enemy.hp).toBe(enemy.getMaxHp()); + }); +}); diff --git a/src/test/moves/tera_blast.test.ts b/src/test/moves/tera_blast.test.ts index 44dc29f68b5..34d171b47bb 100644 --- a/src/test/moves/tera_blast.test.ts +++ b/src/test/moves/tera_blast.test.ts @@ -1,6 +1,6 @@ import { BattlerIndex } from "#app/battle"; import { Stat } from "#enums/stat"; -import { allMoves } from "#app/data/move"; +import { allMoves, TeraMoveCategoryAttr } from "#app/data/move"; import { Type } from "#enums/type"; import { Abilities } from "#app/enums/abilities"; import { HitResult } from "#app/field/pokemon"; @@ -14,6 +14,7 @@ describe("Moves - Tera Blast", () => { let phaserGame: Phaser.Game; let game: GameManager; const moveToCheck = allMoves[Moves.TERA_BLAST]; + const teraBlastAttr = moveToCheck.getAttrs(TeraMoveCategoryAttr)[0]; beforeAll(() => { phaserGame = new Phaser.Game({ @@ -37,8 +38,8 @@ describe("Moves - Tera Blast", () => { .startingHeldItems([{ name: "TERA_SHARD", type: Type.FIRE }]) .enemySpecies(Species.MAGIKARP) .enemyMoveset(Moves.SPLASH) - .enemyAbility(Abilities.BALL_FETCH) - .enemyLevel(20); + .enemyAbility(Abilities.STURDY) + .enemyLevel(50); vi.spyOn(moveToCheck, "calculateBattlePower"); }); @@ -86,19 +87,86 @@ describe("Moves - Tera Blast", () => { expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.SUPER_EFFECTIVE); }); - // Currently abilities are bugged and can't see when a move's category is changed - it.todo("uses the higher stat of the user's Atk and SpAtk for damage calculation", async () => { - game.override.enemyAbility(Abilities.TOXIC_DEBRIS); + it("uses the higher ATK for damage calculation", async () => { await game.startBattle(); const playerPokemon = game.scene.getPlayerPokemon()!; playerPokemon.stats[Stat.ATK] = 100; playerPokemon.stats[Stat.SPATK] = 1; + vi.spyOn(teraBlastAttr, "apply"); + game.move.select(Moves.TERA_BLAST); - await game.phaseInterceptor.to("TurnEndPhase"); - expect(game.scene.getEnemyPokemon()!.battleData.abilityRevealed).toBe(true); - }, 20000); + await game.toNextTurn(); + expect(teraBlastAttr.apply).toHaveLastReturnedWith(true); + }); + + it("uses the higher SPATK for damage calculation", async () => { + await game.startBattle(); + + const playerPokemon = game.scene.getPlayerPokemon()!; + playerPokemon.stats[Stat.ATK] = 1; + playerPokemon.stats[Stat.SPATK] = 100; + + vi.spyOn(teraBlastAttr, "apply"); + + game.move.select(Moves.TERA_BLAST); + await game.toNextTurn(); + expect(teraBlastAttr.apply).toHaveLastReturnedWith(false); + }); + + it("should stay as a special move if ATK turns lower than SPATK mid-turn", async () => { + game.override.enemyMoveset([ Moves.CHARM ]); + await game.startBattle(); + + const playerPokemon = game.scene.getPlayerPokemon()!; + playerPokemon.stats[Stat.ATK] = 51; + playerPokemon.stats[Stat.SPATK] = 50; + + vi.spyOn(teraBlastAttr, "apply"); + + game.move.select(Moves.TERA_BLAST); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextTurn(); + expect(teraBlastAttr.apply).toHaveLastReturnedWith(false); + }); + + it("does not change its move category from stat changes due to held items", async () => { + game.override + .startingHeldItems([{ name: "SPECIES_STAT_BOOSTER", type: "THICK_CLUB" }]) + .starterSpecies(Species.CUBONE); + await game.startBattle(); + + const playerPokemon = game.scene.getPlayerPokemon()!; + + playerPokemon.stats[Stat.ATK] = 50; + playerPokemon.stats[Stat.SPATK] = 51; + + vi.spyOn(teraBlastAttr, "apply"); + + game.move.select(Moves.TERA_BLAST); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextTurn(); + + expect(teraBlastAttr.apply).toHaveLastReturnedWith(false); + }); + + it("does not change its move category from stat changes due to abilities", async () => { + game.override.ability(Abilities.HUGE_POWER); + await game.startBattle(); + + const playerPokemon = game.scene.getPlayerPokemon()!; + playerPokemon.stats[Stat.ATK] = 50; + playerPokemon.stats[Stat.SPATK] = 51; + + vi.spyOn(teraBlastAttr, "apply"); + + game.move.select(Moves.TERA_BLAST); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextTurn(); + expect(teraBlastAttr.apply).toHaveLastReturnedWith(false); + }); + it("causes stat drops if user is Stellar tera type", async () => { game.override.startingHeldItems([{ name: "TERA_SHARD", type: Type.STELLAR }]); diff --git a/src/test/moves/toxic_spikes.test.ts b/src/test/moves/toxic_spikes.test.ts index c2d1c5aaee8..8969289c2f2 100644 --- a/src/test/moves/toxic_spikes.test.ts +++ b/src/test/moves/toxic_spikes.test.ts @@ -132,7 +132,7 @@ describe("Moves - Toxic Spikes", () => { const sessionData : SessionSaveData = gameData["getSessionSaveData"](); localStorage.setItem("sessionTestData", encrypt(JSON.stringify(sessionData), true)); const recoveredData : SessionSaveData = gameData.parseSessionData(decrypt(localStorage.getItem("sessionTestData")!, true)); - gameData.loadSession(0, recoveredData); + await gameData.loadSession(0, recoveredData); expect(sessionData.arena.tags).toEqual(recoveredData.arena.tags); localStorage.removeItem("sessionTestData"); diff --git a/src/test/mystery-encounter/mystery-encounter-utils.test.ts b/src/test/mystery-encounter/mystery-encounter-utils.test.ts index f0057fea7f0..7c924b86e0d 100644 --- a/src/test/mystery-encounter/mystery-encounter-utils.test.ts +++ b/src/test/mystery-encounter/mystery-encounter-utils.test.ts @@ -48,12 +48,12 @@ describe("Mystery Encounter Utils", () => { expect(result.species.speciesId).toBe(Species.ARCEUS); }); - it("gets a fainted pokemon from player party if isAllowedInBattle is false", () => { + it("gets a fainted pokemon from player party if isAllowedInBattle is false", async () => { // Both pokemon fainted scene.getPlayerParty().forEach(p => { p.hp = 0; p.trySetStatus(StatusEffect.FAINT); - p.updateInfo(); + void p.updateInfo(); }); // Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal) @@ -68,12 +68,12 @@ describe("Mystery Encounter Utils", () => { expect(result.species.speciesId).toBe(Species.ARCEUS); }); - it("gets an unfainted legal pokemon from player party if isAllowed is true and isFainted is false", () => { + it("gets an unfainted legal pokemon from player party if isAllowed is true and isFainted is false", async () => { // Only faint 1st pokemon const party = scene.getPlayerParty(); party[0].hp = 0; party[0].trySetStatus(StatusEffect.FAINT); - party[0].updateInfo(); + await party[0].updateInfo(); // Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal) game.override.seed("random"); @@ -87,12 +87,12 @@ describe("Mystery Encounter Utils", () => { expect(result.species.speciesId).toBe(Species.MANAPHY); }); - it("returns last unfainted pokemon if doNotReturnLastAbleMon is false", () => { + it("returns last unfainted pokemon if doNotReturnLastAbleMon is false", async () => { // Only faint 1st pokemon const party = scene.getPlayerParty(); party[0].hp = 0; party[0].trySetStatus(StatusEffect.FAINT); - party[0].updateInfo(); + await party[0].updateInfo(); // Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal) game.override.seed("random"); @@ -106,12 +106,12 @@ describe("Mystery Encounter Utils", () => { expect(result.species.speciesId).toBe(Species.MANAPHY); }); - it("never returns last unfainted pokemon if doNotReturnLastAbleMon is true", () => { + it("never returns last unfainted pokemon if doNotReturnLastAbleMon is true", async () => { // Only faint 1st pokemon const party = scene.getPlayerParty(); party[0].hp = 0; party[0].trySetStatus(StatusEffect.FAINT); - party[0].updateInfo(); + await party[0].updateInfo(); // Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal) game.override.seed("random"); @@ -152,12 +152,12 @@ describe("Mystery Encounter Utils", () => { expect(result.species.speciesId).toBe(Species.ARCEUS); }); - it("returns highest level unfainted if unfainted is true", () => { + it("returns highest level unfainted if unfainted is true", async () => { const party = scene.getPlayerParty(); party[0].level = 100; party[0].hp = 0; party[0].trySetStatus(StatusEffect.FAINT); - party[0].updateInfo(); + await party[0].updateInfo(); party[1].level = 10; const result = getHighestLevelPlayerPokemon(true); @@ -191,12 +191,12 @@ describe("Mystery Encounter Utils", () => { expect(result.species.speciesId).toBe(Species.ARCEUS); }); - it("returns lowest level unfainted if unfainted is true", () => { + it("returns lowest level unfainted if unfainted is true", async () => { const party = scene.getPlayerParty(); party[0].level = 10; party[0].hp = 0; party[0].trySetStatus(StatusEffect.FAINT); - party[0].updateInfo(); + await party[0].updateInfo(); party[1].level = 100; const result = getLowestLevelPlayerPokemon(true); diff --git a/src/test/ui/transfer-item.test.ts b/src/test/ui/transfer-item.test.ts index 762db7fc7ce..b08b056f60e 100644 --- a/src/test/ui/transfer-item.test.ts +++ b/src/test/ui/transfer-item.test.ts @@ -2,8 +2,6 @@ import { BerryType } from "#app/enums/berry-type"; import { Button } from "#app/enums/buttons"; import { Moves } from "#app/enums/moves"; import { Species } from "#app/enums/species"; -import { BattleEndPhase } from "#app/phases/battle-end-phase"; -import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; import PartyUiHandler, { PartyUiMode } from "#app/ui/party-ui-handler"; import { Mode } from "#app/ui/ui"; @@ -12,7 +10,6 @@ import Phaser from "phaser"; import type BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; - describe("UI - Transfer Items", () => { let phaserGame: Phaser.Game; let game: GameManager; @@ -41,7 +38,7 @@ describe("UI - Transfer Items", () => { game.override.enemySpecies(Species.MAGIKARP); game.override.enemyMoveset([ Moves.SPLASH ]); - await game.startBattle([ Species.RAYQUAZA, Species.RAYQUAZA, Species.RAYQUAZA ]); + await game.classicMode.startBattle([ Species.RAYQUAZA, Species.RAYQUAZA, Species.RAYQUAZA ]); game.move.select(Moves.DRAGON_CLAW); @@ -52,10 +49,10 @@ describe("UI - Transfer Items", () => { handler.setCursor(1); handler.processInput(Button.ACTION); - game.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.MODIFIER_TRANSFER); + void game.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.MODIFIER_TRANSFER); }); - await game.phaseInterceptor.to(BattleEndPhase); + await game.phaseInterceptor.to("BattleEndPhase"); }); it("check red tint for held item limit in transfer menu", async () => { @@ -72,7 +69,7 @@ describe("UI - Transfer Items", () => { game.phaseInterceptor.unlock(); }); - await game.phaseInterceptor.to(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); }, 20000); it("check transfer option for pokemon to transfer to", async () => { @@ -91,6 +88,6 @@ describe("UI - Transfer Items", () => { game.phaseInterceptor.unlock(); }); - await game.phaseInterceptor.to(SelectModifierPhase); + await game.phaseInterceptor.to("SelectModifierPhase"); }, 20000); }); diff --git a/src/timed-event-manager.ts b/src/timed-event-manager.ts index 7a9f0e59993..bebacf87ebc 100644 --- a/src/timed-event-manager.ts +++ b/src/timed-event-manager.ts @@ -27,6 +27,7 @@ interface EventBanner { interface EventEncounter { species: Species; blockEvolution?: boolean; + formIndex?: number; } interface EventMysteryEncounterTier { @@ -49,6 +50,7 @@ interface TimedEvent extends EventBanner { weather?: WeatherPoolEntry[]; mysteryEncounterTierChanges?: EventMysteryEncounterTier[]; luckBoostedSpecies?: Species[]; + boostFusions?: boolean; //MODIFIER REWORK PLEASE } const timedEvents: TimedEvent[] = [ @@ -144,6 +146,40 @@ const timedEvents: TimedEvent[] = [ Species.ROARING_MOON, Species.BLOODMOON_URSALUNA ] + }, + { + name: "Valentine", + eventType: EventType.SHINY, + startDate: new Date(Date.UTC(2025, 1, 10)), + endDate: new Date(Date.UTC(2025, 1, 21)), + boostFusions: true, + shinyMultiplier: 2, + bannerKey: "valentines2025event-", + scale: 0.21, + availableLangs: [ "en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN" ], + eventEncounters: [ + { species: Species.NIDORAN_F }, + { species: Species.NIDORAN_M }, + { species: Species.IGGLYBUFF }, + { species: Species.SMOOCHUM }, + { species: Species.VOLBEAT }, + { species: Species.ILLUMISE }, + { species: Species.ROSELIA }, + { species: Species.LUVDISC }, + { species: Species.WOOBAT }, + { species: Species.FRILLISH }, + { species: Species.ALOMOMOLA }, + { species: Species.FURFROU, formIndex: 1 }, // Heart trim + { species: Species.ESPURR }, + { species: Species.SPRITZEE }, + { species: Species.SWIRLIX }, + { species: Species.APPLIN }, + { species: Species.MILCERY }, + { species: Species.INDEEDEE }, + { species: Species.TANDEMAUS }, + { species: Species.ENAMORUS } + ], + luckBoostedSpecies: [ Species.LUVDISC ] } ]; @@ -297,6 +333,10 @@ export class TimedEventManager { }); return ret; } + + areFusionsBoosted(): boolean { + return timedEvents.some((te) => this.isActive(te) && te.boostFusions); + } } export class TimedEventDisplay extends Phaser.GameObjects.Container { diff --git a/src/ui/abstact-option-select-ui-handler.ts b/src/ui/abstact-option-select-ui-handler.ts index 1840792e667..10dbedd7b2f 100644 --- a/src/ui/abstact-option-select-ui-handler.ts +++ b/src/ui/abstact-option-select-ui-handler.ts @@ -6,7 +6,7 @@ import { addWindow } from "./ui-theme"; import * as Utils from "../utils"; import { argbFromRgba } from "@material/material-color-utilities"; import { Button } from "#enums/buttons"; -import BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext"; +import BBCodeText from "phaser3-rex-plugins/plugins/gameobjects/tagtext/bbcodetext/BBCodeText"; export interface OptionSelectConfig { xOffset?: number; diff --git a/src/ui/pokedex-mon-container.ts b/src/ui/pokedex-mon-container.ts index f3932aa90c8..31a98c30d1c 100644 --- a/src/ui/pokedex-mon-container.ts +++ b/src/ui/pokedex-mon-container.ts @@ -1,7 +1,17 @@ +import type { Variant } from "#app/data/variant"; import { globalScene } from "#app/global-scene"; +import { isNullOrUndefined } from "#app/utils"; import type PokemonSpecies from "../data/pokemon-species"; import { addTextObject, TextStyle } from "./text"; + +interface SpeciesDetails { + shiny?: boolean, + formIndex?: number + female?: boolean, + variant?: Variant +} + export class PokedexMonContainer extends Phaser.GameObjects.Container { public species: PokemonSpecies; public icon: Phaser.GameObjects.Sprite; @@ -19,16 +29,34 @@ export class PokedexMonContainer extends Phaser.GameObjects.Container { public tmMove2Icon: Phaser.GameObjects.Image; public passive1Icon: Phaser.GameObjects.Image; public passive2Icon: Phaser.GameObjects.Image; + public passive1OverlayIcon: Phaser.GameObjects.Image; + public passive2OverlayIcon: Phaser.GameObjects.Image; public cost: number = 0; - constructor(species: PokemonSpecies) { + constructor(species: PokemonSpecies, options: SpeciesDetails = {}) { super(globalScene, 0, 0); this.species = species; + const { shiny, formIndex, female, variant } = options; + const defaultDexAttr = globalScene.gameData.getSpeciesDefaultDexAttr(species, false, true); const defaultProps = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr); + if (!isNullOrUndefined(formIndex)) { + defaultProps.formIndex = formIndex; + } + if (!isNullOrUndefined(shiny)) { + defaultProps.shiny = shiny; + } + if (!isNullOrUndefined(variant)) { + defaultProps.variant = variant; + } + if (!isNullOrUndefined(female)) { + defaultProps.female = female; + } + + // starter passive bg const starterPassiveBg = globalScene.add.image(2, 5, "passive_bg"); starterPassiveBg.setOrigin(0, 0); @@ -137,7 +165,7 @@ export class PokedexMonContainer extends Phaser.GameObjects.Container { this.tmMove2Icon = tmMove2Icon; - // move icons + // passive icons const passive1Icon = globalScene.add.image(3, 3, "candy"); passive1Icon.setOrigin(0, 0); passive1Icon.setScale(0.25); @@ -145,13 +173,27 @@ export class PokedexMonContainer extends Phaser.GameObjects.Container { this.add(passive1Icon); this.passive1Icon = passive1Icon; - // move icons + const passive1OverlayIcon = globalScene.add.image(12, 12, "candy_overlay"); + passive1OverlayIcon.setOrigin(0, 0); + passive1OverlayIcon.setScale(0.25); + passive1OverlayIcon.setVisible(false); + this.add(passive1OverlayIcon); + this.passive1OverlayIcon = passive1OverlayIcon; + + // passive icons const passive2Icon = globalScene.add.image(12, 3, "candy"); passive2Icon.setOrigin(0, 0); passive2Icon.setScale(0.25); passive2Icon.setVisible(false); this.add(passive2Icon); this.passive2Icon = passive2Icon; + + const passive2OverlayIcon = globalScene.add.image(12, 12, "candy_overlay"); + passive2OverlayIcon.setOrigin(0, 0); + passive2OverlayIcon.setScale(0.25); + passive2OverlayIcon.setVisible(false); + this.add(passive2OverlayIcon); + this.passive2OverlayIcon = passive2OverlayIcon; } checkIconId(female, formIndex, shiny, variant) { diff --git a/src/ui/pokedex-page-ui-handler.ts b/src/ui/pokedex-page-ui-handler.ts index be7d19aff27..683045d7814 100644 --- a/src/ui/pokedex-page-ui-handler.ts +++ b/src/ui/pokedex-page-ui-handler.ts @@ -42,7 +42,6 @@ import type { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import { Button } from "#enums/buttons"; import { EggSourceType } from "#enums/egg-source-types"; -import { StarterContainer } from "#app/ui/starter-container"; import { getPassiveCandyCount, getValueReductionCandyCounts, getSameSpeciesEggCandyCounts } from "#app/data/balance/starters"; import { BooleanHolder, getLocalizedSpriteKey, isNullOrUndefined, NumberHolder, padInt, rgbHexToRgba, toReadableString } from "#app/utils"; import type { Nature } from "#enums/nature"; @@ -127,7 +126,6 @@ interface SpeciesDetails { formIndex?: number female?: boolean, variant?: number, - forSeen?: boolean, // default = false } enum MenuOptions { @@ -146,8 +144,6 @@ enum MenuOptions { export default class PokedexPageUiHandler extends MessageUiHandler { private starterSelectContainer: Phaser.GameObjects.Container; private shinyOverlay: Phaser.GameObjects.Image; - private starterContainers: StarterContainer[] = []; - private filteredStarterContainers: StarterContainer[] = []; private pokemonNumberText: Phaser.GameObjects.Text; private pokemonSprite: Phaser.GameObjects.Sprite; private pokemonNameText: Phaser.GameObjects.Text; @@ -198,6 +194,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler { private allSpecies: PokemonSpecies[] = []; private species: PokemonSpecies; + private starterId: number; private formIndex: number; private speciesLoaded: Map = new Map(); private levelMoves: LevelMoves; @@ -311,10 +308,6 @@ export default class PokedexPageUiHandler extends MessageUiHandler { this.speciesLoaded.set(species.speciesId, false); this.allSpecies.push(species); - - const starterContainer = new StarterContainer(species).setVisible(false); - this.starterContainers.push(starterContainer); - starterBoxContainer.add(starterContainer); } this.starterSelectContainer.add(starterBoxContainer); @@ -512,7 +505,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler { this.scale = getTextStyleOptions(TextStyle.WINDOW, globalScene.uiTheme).scale; this.menuBg = addWindow( - (globalScene.game.canvas.width / 6) - (this.optionSelectText.displayWidth + 25), + (globalScene.game.canvas.width / 6 - 83), 0, this.optionSelectText.displayWidth + 19 + 24 * this.scale, (globalScene.game.canvas.height / 6) - 2 @@ -554,8 +547,6 @@ export default class PokedexPageUiHandler extends MessageUiHandler { // Filter bar sits above everything, except the message box this.starterSelectContainer.bringToTop(this.starterSelectMessageBoxContainer); - - this.updateInstructions(); } show(args: any[]): boolean { @@ -602,6 +593,8 @@ export default class PokedexPageUiHandler extends MessageUiHandler { const species = this.species; const formIndex = this.formIndex ?? 0; + this.starterId = this.getStarterSpeciesId(this.species.speciesId); + const allEvolutions = pokemonEvolutions.hasOwnProperty(species.speciesId) ? pokemonEvolutions[species.speciesId] : []; if (species.forms.length > 0) { @@ -628,17 +621,19 @@ export default class PokedexPageUiHandler extends MessageUiHandler { this.baseTotal = species.baseTotal; } - this.eggMoves = speciesEggMoves[this.getStarterSpeciesId(species.speciesId)] ?? []; - this.hasEggMoves = Array.from({ length: 4 }, (_, em) => (globalScene.gameData.starterData[this.getStarterSpeciesId(species.speciesId)].eggMoves & (1 << em)) !== 0); + this.eggMoves = speciesEggMoves[this.starterId] ?? []; + this.hasEggMoves = Array.from({ length: 4 }, (_, em) => (globalScene.gameData.starterData[this.starterId].eggMoves & (1 << em)) !== 0); const formKey = this.species?.forms.length > 0 ? this.species.forms[this.formIndex].formKey : ""; this.tmMoves = speciesTmMoves[species.speciesId]?.filter(m => Array.isArray(m) ? (m[0] === formKey ? true : false ) : true) .map(m => Array.isArray(m) ? m[1] : m).sort((a, b) => allMoves[a].name > allMoves[b].name ? 1 : -1) ?? []; - const passives = starterPassiveAbilities[this.getStarterSpeciesId(species.speciesId)]; + const passiveId = starterPassiveAbilities.hasOwnProperty(species.speciesId) ? species.speciesId : + starterPassiveAbilities.hasOwnProperty(this.starterId) ? this.starterId : pokemonPrevolutions[this.starterId]; + const passives = starterPassiveAbilities[passiveId]; this.passive = (this.formIndex in passives) ? passives[formIndex] : passives[0]; - const starterData = globalScene.gameData.starterData[this.getStarterSpeciesId(species.speciesId)]; + const starterData = globalScene.gameData.starterData[this.starterId]; const abilityAttr = starterData.abilityAttr; this.hasPassive = starterData.passiveAttr > 0; @@ -654,9 +649,9 @@ export default class PokedexPageUiHandler extends MessageUiHandler { const allBiomes = catchableSpecies[species.speciesId] ?? []; this.preBiomes = this.sanitizeBiomes( - (catchableSpecies[this.getStarterSpeciesId(species.speciesId)] ?? []) + (catchableSpecies[this.starterId] ?? []) .filter(b => !allBiomes.some(bm => (b.biome === bm.biome && b.tier === bm.tier)) && !(b.biome === Biome.TOWN)), - this.getStarterSpeciesId(species.speciesId)); + this.starterId); this.biomes = this.sanitizeBiomes(allBiomes, species.speciesId); const allFormChanges = pokemonFormChanges.hasOwnProperty(species.speciesId) ? pokemonFormChanges[species.speciesId] : []; @@ -798,39 +793,43 @@ export default class PokedexPageUiHandler extends MessageUiHandler { const hasShiny = caughtAttr & DexAttr.SHINY; const hasNonShiny = caughtAttr & DexAttr.NON_SHINY; - if (starterAttributes.shiny && !hasShiny) { + if (!hasShiny || (starterAttributes.shiny === undefined && hasNonShiny)) { // shiny form wasn't unlocked, purging shiny and variant setting starterAttributes.shiny = false; starterAttributes.variant = 0; - } else if (starterAttributes.shiny === false && !hasNonShiny) { - // non shiny form wasn't unlocked, purging shiny setting - starterAttributes.shiny = false; + } else if (!hasNonShiny || (starterAttributes.shiny === undefined && hasShiny)) { + starterAttributes.shiny = true; + starterAttributes.variant = 0; } - if (starterAttributes.variant !== undefined) { - const unlockedVariants = [ - hasShiny && caughtAttr & DexAttr.DEFAULT_VARIANT, - hasShiny && caughtAttr & DexAttr.VARIANT_2, - hasShiny && caughtAttr & DexAttr.VARIANT_3 - ]; - if (isNaN(starterAttributes.variant) || starterAttributes.variant < 0) { - starterAttributes.variant = 0; - } else if (!unlockedVariants[starterAttributes.variant]) { - let highestValidIndex = -1; - for (let i = 0; i <= starterAttributes.variant && i < unlockedVariants.length; i++) { - if (unlockedVariants[i] !== 0n) { - highestValidIndex = i; - } + const unlockedVariants = [ + hasShiny && caughtAttr & DexAttr.DEFAULT_VARIANT, + hasShiny && caughtAttr & DexAttr.VARIANT_2, + hasShiny && caughtAttr & DexAttr.VARIANT_3 + ]; + if (starterAttributes.variant === undefined || isNaN(starterAttributes.variant) || starterAttributes.variant < 0) { + starterAttributes.variant = 0; + } else if (!unlockedVariants[starterAttributes.variant]) { + let highestValidIndex = -1; + for (let i = 0; i <= starterAttributes.variant && i < unlockedVariants.length; i++) { + if (unlockedVariants[i] !== 0n) { + highestValidIndex = i; } - // Set to the highest valid index found or default to 0 - starterAttributes.variant = highestValidIndex !== -1 ? highestValidIndex : 0; } + // Set to the highest valid index found or default to 0 + starterAttributes.variant = highestValidIndex !== -1 ? highestValidIndex : 0; } if (starterAttributes.female !== undefined) { if ((starterAttributes.female && !(caughtAttr & DexAttr.FEMALE)) || (!starterAttributes.female && !(caughtAttr & DexAttr.MALE))) { starterAttributes.female = !starterAttributes.female; } + } else { + if (caughtAttr & DexAttr.FEMALE) { + starterAttributes.female = true; + } else if (caughtAttr & DexAttr.MALE) { + starterAttributes.female = false; + } } return starterAttributes; @@ -877,7 +876,14 @@ export default class PokedexPageUiHandler extends MessageUiHandler { * @returns the id of the corresponding starter */ getStarterSpeciesId(speciesId): number { - if (globalScene.gameData.starterData.hasOwnProperty(speciesId)) { + if (speciesId === Species.PIKACHU) { + if ([ 0, 1, 8 ].includes(this.formIndex)) { + return Species.PICHU; + } else { + return Species.PIKACHU; + } + } + if (speciesStarterCosts.hasOwnProperty(speciesId)) { return speciesId; } else { return pokemonStarters[speciesId]; @@ -885,7 +891,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler { } getStarterSpecies(species): PokemonSpecies { - if (globalScene.gameData.starterData.hasOwnProperty(species.speciesId)) { + if (speciesStarterCosts.hasOwnProperty(species.speciesId)) { return species; } else { return allSpecies.find(sp => sp.speciesId === pokemonStarters[species.speciesId]) ?? species; @@ -932,7 +938,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler { } } else { - const starterData = globalScene.gameData.starterData[this.getStarterSpeciesId(this.species.speciesId)]; + const starterData = globalScene.gameData.starterData[this.starterId]; // prepare persistent starter data to store changes const starterAttributes = this.starterAttributes; @@ -1088,6 +1094,9 @@ export default class PokedexPageUiHandler extends MessageUiHandler { if (!isCaught || !isFormCaught) { error = true; + } else if (this.tmMoves.length < 1) { + ui.showText(i18next.t("pokedexUiHandler:noTmMoves")); + error = true; } else { this.blockInput = true; @@ -1596,90 +1605,55 @@ export default class PokedexPageUiHandler extends MessageUiHandler { error = true; } else { const ui = this.getUi(); + ui.showText(""); const options: any[] = []; // TODO: add proper type const passiveAttr = starterData.passiveAttr; const candyCount = starterData.candyCount; - if (!pokemonPrevolutions.hasOwnProperty(this.species.speciesId)) { - if (!(passiveAttr & PassiveAttr.UNLOCKED)) { - const passiveCost = getPassiveCandyCount(speciesStarterCosts[this.getStarterSpeciesId(this.species.speciesId)]); - options.push({ - label: `x${passiveCost} ${i18next.t("pokedexUiHandler:unlockPassive")} (${allAbilities[this.passive].name})`, - handler: () => { - if (Overrides.FREE_CANDY_UPGRADE_OVERRIDE || candyCount >= passiveCost) { - starterData.passiveAttr |= PassiveAttr.UNLOCKED | PassiveAttr.ENABLED; - if (!Overrides.FREE_CANDY_UPGRADE_OVERRIDE) { - starterData.candyCount -= passiveCost; - } - this.pokemonCandyCountText.setText(`x${starterData.candyCount}`); - globalScene.gameData.saveSystem().then(success => { - if (!success) { - return globalScene.reset(true); - } - }); - ui.setMode(Mode.POKEDEX_PAGE, "refresh"); - this.setSpeciesDetails(this.species); - globalScene.playSound("se/buy"); - - return true; - } - return false; - }, - item: "candy", - itemArgs: starterColors[this.getStarterSpeciesId(this.species.speciesId)] - }); - } - - // Reduce cost option - const valueReduction = starterData.valueReduction; - if (valueReduction < valueReductionMax) { - const reductionCost = getValueReductionCandyCounts(speciesStarterCosts[this.getStarterSpeciesId(this.species.speciesId)])[valueReduction]; - options.push({ - label: `x${reductionCost} ${i18next.t("pokedexUiHandler:reduceCost")}`, - handler: () => { - if (Overrides.FREE_CANDY_UPGRADE_OVERRIDE || candyCount >= reductionCost) { - starterData.valueReduction++; - if (!Overrides.FREE_CANDY_UPGRADE_OVERRIDE) { - starterData.candyCount -= reductionCost; - } - this.pokemonCandyCountText.setText(`x${starterData.candyCount}`); - globalScene.gameData.saveSystem().then(success => { - if (!success) { - return globalScene.reset(true); - } - }); - ui.setMode(Mode.POKEDEX_PAGE, "refresh"); - globalScene.playSound("se/buy"); - - return true; - } - return false; - }, - item: "candy", - itemArgs: starterColors[this.getStarterSpeciesId(this.species.speciesId)] - }); - } - - // Same species egg menu option. - const sameSpeciesEggCost = getSameSpeciesEggCandyCounts(speciesStarterCosts[this.getStarterSpeciesId(this.species.speciesId)]); + if (!(passiveAttr & PassiveAttr.UNLOCKED)) { + const passiveCost = getPassiveCandyCount(speciesStarterCosts[this.starterId]); options.push({ - label: `x${sameSpeciesEggCost} ${i18next.t("pokedexUiHandler:sameSpeciesEgg")}`, + label: `x${passiveCost} ${i18next.t("pokedexUiHandler:unlockPassive")} (${allAbilities[this.passive].name})`, handler: () => { - if (Overrides.FREE_CANDY_UPGRADE_OVERRIDE || candyCount >= sameSpeciesEggCost) { - if (globalScene.gameData.eggs.length >= 99 && !Overrides.UNLIMITED_EGG_COUNT_OVERRIDE) { - // Egg list full, show error message at the top of the screen and abort - this.showText(i18next.t("egg:tooManyEggs"), undefined, () => this.showText("", 0, () => this.tutorialActive = false), 2000, false, undefined, true); - return false; - } + if (Overrides.FREE_CANDY_UPGRADE_OVERRIDE || candyCount >= passiveCost) { + starterData.passiveAttr |= PassiveAttr.UNLOCKED | PassiveAttr.ENABLED; if (!Overrides.FREE_CANDY_UPGRADE_OVERRIDE) { - starterData.candyCount -= sameSpeciesEggCost; + starterData.candyCount -= passiveCost; } this.pokemonCandyCountText.setText(`x${starterData.candyCount}`); + globalScene.gameData.saveSystem().then(success => { + if (!success) { + return globalScene.reset(true); + } + }); + this.setSpeciesDetails(this.species); + globalScene.playSound("se/buy"); + ui.setMode(Mode.POKEDEX_PAGE, "refresh"); - const egg = new Egg({ scene: globalScene, species: this.species.speciesId, sourceType: EggSourceType.SAME_SPECIES_EGG }); - egg.addEggToGameData(); + return true; + } + return false; + }, + style: this.isPassiveAvailable() ? TextStyle.WINDOW : TextStyle.SHADOW_TEXT, + item: "candy", + itemArgs: this.isPassiveAvailable() ? starterColors[this.starterId] : [ "808080", "808080" ] + }); + } + // Reduce cost option + const valueReduction = starterData.valueReduction; + if (valueReduction < valueReductionMax) { + const reductionCost = getValueReductionCandyCounts(speciesStarterCosts[this.starterId])[valueReduction]; + options.push({ + label: `x${reductionCost} ${i18next.t("pokedexUiHandler:reduceCost")}`, + handler: () => { + if (Overrides.FREE_CANDY_UPGRADE_OVERRIDE || candyCount >= reductionCost) { + starterData.valueReduction++; + if (!Overrides.FREE_CANDY_UPGRADE_OVERRIDE) { + starterData.candyCount -= reductionCost; + } + this.pokemonCandyCountText.setText(`x${starterData.candyCount}`); globalScene.gameData.saveSystem().then(success => { if (!success) { return globalScene.reset(true); @@ -1692,24 +1666,59 @@ export default class PokedexPageUiHandler extends MessageUiHandler { } return false; }, + style: this.isValueReductionAvailable() ? TextStyle.WINDOW : TextStyle.SHADOW_TEXT, item: "candy", - itemArgs: starterColors[this.getStarterSpeciesId(this.species.speciesId)] + itemArgs: this.isValueReductionAvailable() ? starterColors[this.starterId] : [ "808080", "808080" ] }); - options.push({ - label: i18next.t("menu:cancel"), - handler: () => { + } + + // Same species egg menu option. + const sameSpeciesEggCost = getSameSpeciesEggCandyCounts(speciesStarterCosts[this.starterId]); + options.push({ + label: `x${sameSpeciesEggCost} ${i18next.t("pokedexUiHandler:sameSpeciesEgg")}`, + handler: () => { + if (Overrides.FREE_CANDY_UPGRADE_OVERRIDE || candyCount >= sameSpeciesEggCost) { + if (globalScene.gameData.eggs.length >= 99 && !Overrides.UNLIMITED_EGG_COUNT_OVERRIDE) { + // Egg list full, show error message at the top of the screen and abort + this.showText(i18next.t("egg:tooManyEggs"), undefined, () => this.showText("", 0, () => this.tutorialActive = false), 2000, false, undefined, true); + return false; + } + if (!Overrides.FREE_CANDY_UPGRADE_OVERRIDE) { + starterData.candyCount -= sameSpeciesEggCost; + } + this.pokemonCandyCountText.setText(`x${starterData.candyCount}`); + + const egg = new Egg({ scene: globalScene, species: this.species.speciesId, sourceType: EggSourceType.SAME_SPECIES_EGG }); + egg.addEggToGameData(); + + globalScene.gameData.saveSystem().then(success => { + if (!success) { + return globalScene.reset(true); + } + }); ui.setMode(Mode.POKEDEX_PAGE, "refresh"); + globalScene.playSound("se/buy"); + return true; } - }); - ui.setModeWithoutClear(Mode.OPTION_SELECT, { - options: options, - yOffset: 47 - }); - success = true; - } else { - error = true; - } + return false; + }, + style: this.isSameSpeciesEggAvailable() ? TextStyle.WINDOW : TextStyle.SHADOW_TEXT, + item: "candy", + itemArgs: this.isSameSpeciesEggAvailable() ? starterColors[this.starterId] : [ "808080", "808080" ] + }); + options.push({ + label: i18next.t("menu:cancel"), + handler: () => { + ui.setMode(Mode.POKEDEX_PAGE, "refresh"); + return true; + } + }); + ui.setModeWithoutClear(Mode.OPTION_SELECT, { + options: options, + yOffset: 47 + }); + success = true; } break; case Button.CYCLE_ABILITY: @@ -1840,9 +1849,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler { if (this.isCaught()) { if (isFormCaught) { - if (!pokemonPrevolutions.hasOwnProperty(this.species.speciesId)) { - this.updateButtonIcon(SettingKeyboard.Button_Stats, gamepadType, this.candyUpgradeIconElement, this.candyUpgradeLabel); - } + this.updateButtonIcon(SettingKeyboard.Button_Stats, gamepadType, this.candyUpgradeIconElement, this.candyUpgradeLabel); if (this.canCycleShiny) { this.updateButtonIcon(SettingKeyboard.Button_Cycle_Shiny, gamepadType, this.shinyIconElement, this.shinyLabel); } @@ -1899,16 +1906,51 @@ export default class PokedexPageUiHandler extends MessageUiHandler { } getFriendship(speciesId: number) { - let currentFriendship = globalScene.gameData.starterData[this.getStarterSpeciesId(speciesId)].friendship; + let currentFriendship = globalScene.gameData.starterData[this.starterId].friendship; if (!currentFriendship || currentFriendship === undefined) { currentFriendship = 0; } - const friendshipCap = getStarterValueFriendshipCap(speciesStarterCosts[this.getStarterSpeciesId(speciesId)]); + const friendshipCap = getStarterValueFriendshipCap(speciesStarterCosts[this.starterId]); return { currentFriendship, friendshipCap }; } + /** + * Determines if a passive upgrade is available for the current species + * @returns true if the user has enough candies and a passive has not been unlocked already + */ + isPassiveAvailable(): boolean { + // Get this species ID's starter data + const starterData = globalScene.gameData.starterData[this.starterId]; + + return starterData.candyCount >= getPassiveCandyCount(speciesStarterCosts[this.starterId]) + && !(starterData.passiveAttr & PassiveAttr.UNLOCKED); + } + + /** + * Determines if a value reduction upgrade is available for the current species + * @returns true if the user has enough candies and all value reductions have not been unlocked already + */ + isValueReductionAvailable(): boolean { + // Get this species ID's starter data + const starterData = globalScene.gameData.starterData[this.starterId]; + + return starterData.candyCount >= getValueReductionCandyCounts(speciesStarterCosts[this.starterId])[starterData.valueReduction] + && starterData.valueReduction < valueReductionMax; + } + + /** + * Determines if an same species egg can be bought for the current species + * @returns true if the user has enough candies + */ + isSameSpeciesEggAvailable(): boolean { + // Get this species ID's starter data + const starterData = globalScene.gameData.starterData[this.starterId]; + + return starterData.candyCount >= getSameSpeciesEggCandyCounts(speciesStarterCosts[this.starterId]); + } + setSpecies() { const species = this.species; const starterAttributes : StarterAttributes | null = species ? { ...this.starterAttributes } : null; @@ -1930,88 +1972,10 @@ export default class PokedexPageUiHandler extends MessageUiHandler { if (species && (this.speciesStarterDexEntry?.seenAttr || this.isCaught())) { this.pokemonNumberText.setText(padInt(species.speciesId, 4)); - if (starterAttributes?.nickname) { - const name = decodeURIComponent(escape(atob(starterAttributes.nickname))); - this.pokemonNameText.setText(name); - } else { - this.pokemonNameText.setText(species.name); - } if (this.isCaught()) { - const colorScheme = starterColors[species.speciesId]; - - const luck = globalScene.gameData.getDexAttrLuck(this.isCaught()); - this.pokemonLuckText.setVisible(!!luck); - this.pokemonLuckText.setText(luck.toString()); - this.pokemonLuckText.setTint(getVariantTint(Math.min(luck - 1, 2) as Variant)); - this.pokemonLuckLabelText.setVisible(this.pokemonLuckText.visible); - - //Growth translate - let growthReadable = toReadableString(GrowthRate[species.growthRate]); - const growthAux = growthReadable.replace(" ", "_"); - if (i18next.exists("growth:" + growthAux)) { - growthReadable = i18next.t("growth:" + growthAux as any); - } - this.pokemonGrowthRateText.setText(growthReadable); - - this.pokemonGrowthRateText.setColor(getGrowthRateColor(species.growthRate)); - this.pokemonGrowthRateText.setShadowColor(getGrowthRateColor(species.growthRate, true)); - this.pokemonGrowthRateLabelText.setVisible(true); - this.pokemonUncaughtText.setVisible(false); - this.pokemonCaughtCountText.setText(`${this.speciesStarterDexEntry?.caughtCount}`); - if (species.speciesId === Species.MANAPHY || species.speciesId === Species.PHIONE) { - this.pokemonHatchedIcon.setFrame("manaphy"); - } else { - this.pokemonHatchedIcon.setFrame(getEggTierForSpecies(species)); - } - this.pokemonHatchedCountText.setText(`${this.speciesStarterDexEntry?.hatchedCount}`); const defaultDexAttr = this.getCurrentDexProps(species.speciesId); - const defaultProps = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr); - const variant = defaultProps.variant; - const tint = getVariantTint(variant); - this.pokemonShinyIcon.setFrame(getVariantIcon(variant)); - this.pokemonShinyIcon.setTint(tint); - this.pokemonShinyIcon.setVisible(defaultProps.shiny); - this.pokemonCaughtHatchedContainer.setVisible(true); - this.pokemonFormText.setVisible(true); - - if (pokemonPrevolutions.hasOwnProperty(species.speciesId)) { - this.pokemonCaughtHatchedContainer.setY(16); - this.pokemonShinyIcon.setY(135); - this.pokemonShinyIcon.setFrame(getVariantIcon(variant)); - [ - this.pokemonCandyContainer, - this.pokemonHatchedIcon, - this.pokemonHatchedCountText - ].map(c => c.setVisible(false)); - this.pokemonFormText.setY(25); - } else { - this.pokemonCaughtHatchedContainer.setY(25); - this.pokemonShinyIcon.setY(117); - this.pokemonCandyIcon.setTint(argbFromRgba(rgbHexToRgba(colorScheme[0]))); - this.pokemonCandyOverlayIcon.setTint(argbFromRgba(rgbHexToRgba(colorScheme[1]))); - this.pokemonCandyCountText.setText(`x${globalScene.gameData.starterData[this.getStarterSpeciesId(species.speciesId)].candyCount}`); - this.pokemonCandyContainer.setVisible(true); - this.pokemonFormText.setY(42); - this.pokemonHatchedIcon.setVisible(true); - this.pokemonHatchedCountText.setVisible(true); - - const { currentFriendship, friendshipCap } = this.getFriendship(this.species.speciesId); - const candyCropY = 16 - (16 * (currentFriendship / friendshipCap)); - this.pokemonCandyDarknessOverlay.setCrop(0, 0, 16, candyCropY); - - this.pokemonCandyContainer.on("pointerover", () => { - globalScene.ui.showTooltip("", `${currentFriendship}/${friendshipCap}`, true); - this.activeTooltip = "CANDY"; - }); - this.pokemonCandyContainer.on("pointerout", () => { - globalScene.ui.hideTooltip(); - this.activeTooltip = undefined; - }); - - } - // Set default attributes if for some reason starterAttributes does not exist or attributes missing const props: StarterAttributes = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr); if (starterAttributes?.variant && !isNaN(starterAttributes.variant)) { @@ -2028,12 +1992,6 @@ export default class PokedexPageUiHandler extends MessageUiHandler { female: props.female, variant: props.variant ?? 0, }); - - if (this.isFormCaught(this.species, props.form)) { - const speciesForm = getPokemonSpeciesForm(species.speciesId, props.form ?? 0); - this.setTypeIcons(speciesForm.type1, speciesForm.type2); - this.pokemonSprite.clearTint(); - } } else { this.pokemonGrowthRateText.setText(""); this.pokemonGrowthRateLabelText.setVisible(false); @@ -2055,7 +2013,6 @@ export default class PokedexPageUiHandler extends MessageUiHandler { formIndex: props.formIndex, female: props.female, variant: props.variant, - forSeen: true }); this.pokemonSprite.setTint(0x808080); } @@ -2086,7 +2043,6 @@ export default class PokedexPageUiHandler extends MessageUiHandler { setSpeciesDetails(species: PokemonSpecies, options: SpeciesDetails = {}, forceUpdate?: boolean): void { let { shiny, formIndex, female, variant } = options; - const forSeen: boolean = options.forSeen ?? false; const oldProps = species ? this.starterAttributes : null; // We will only update the sprite if there is a change to form, shiny/variant @@ -2157,12 +2113,12 @@ export default class PokedexPageUiHandler extends MessageUiHandler { } const isFormCaught = this.isFormCaught(); + const isFormSeen = dexEntry ? (dexEntry.seenAttr & globalScene.gameData.getFormAttr(formIndex ?? 0)) > 0n : false; this.shinyOverlay.setVisible(shiny ?? false); // TODO: is false the correct default? this.pokemonNumberText.setColor(this.getTextColor(shiny ? TextStyle.SUMMARY_GOLD : TextStyle.SUMMARY, false)); this.pokemonNumberText.setShadowColor(this.getTextColor(shiny ? TextStyle.SUMMARY_GOLD : TextStyle.SUMMARY, true)); - const assetLoadCancelled = new BooleanHolder(false); this.assetLoadCancelled = assetLoadCancelled; @@ -2184,13 +2140,6 @@ export default class PokedexPageUiHandler extends MessageUiHandler { this.pokemonSprite.setVisible(!this.statsMode); } - const currentFilteredContainer = this.filteredStarterContainers.find(p => p.species.speciesId === species.speciesId); - if (currentFilteredContainer) { - const starterSprite = currentFilteredContainer.icon as Phaser.GameObjects.Sprite; - starterSprite.setTexture(species.getIconAtlasKey(formIndex, shiny, variant), species.getIconId(female!, formIndex, shiny, variant)); - currentFilteredContainer.checkIconId(female, formIndex, shiny, variant); - } - const isNonShinyCaught = !!(caughtAttr & DexAttr.NON_SHINY); const isShinyCaught = !!(caughtAttr & DexAttr.SHINY); @@ -2213,27 +2162,129 @@ export default class PokedexPageUiHandler extends MessageUiHandler { this.pokemonGenderText.setText(""); } - if (caughtAttr) { - if (isFormCaught) { - this.species.loadAssets(female!, formIndex, shiny, variant as Variant, true).then(() => { - const crier = (this.species.forms && this.species.forms.length > 0) ? this.species.forms[formIndex ?? this.formIndex] : this.species; - crier.cry(); - }); - - this.pokemonSprite.clearTint(); - } else { - this.pokemonSprite.setTint(0x000000); - } + // Setting the name + if (isFormCaught || isFormSeen) { + this.pokemonNameText.setText(species.name); + } else { + this.pokemonNameText.setText(species ? "???" : ""); } - if (caughtAttr || forSeen) { + // Setting tint of the sprite + if (isFormCaught) { + this.species.loadAssets(female!, formIndex, shiny, variant as Variant, true).then(() => { + const crier = (this.species.forms && this.species.forms.length > 0) ? this.species.forms[formIndex ?? this.formIndex] : this.species; + crier.cry(); + }); + this.pokemonSprite.clearTint(); + } else if (isFormSeen) { + this.pokemonSprite.setTint(0x808080); + } else { + this.pokemonSprite.setTint(0); + } + + // Setting luck text and sparks + if (isFormCaught) { + const luck = globalScene.gameData.getDexAttrLuck(this.isCaught()); + this.pokemonLuckText.setVisible(!!luck); + this.pokemonLuckText.setText(luck.toString()); + this.pokemonLuckText.setTint(getVariantTint(Math.min(luck - 1, 2) as Variant)); + this.pokemonLuckLabelText.setVisible(this.pokemonLuckText.visible); + } else { + this.pokemonLuckText.setVisible(false); + this.pokemonLuckLabelText.setVisible(false); + } + + // Setting growth rate text + if (isFormCaught) { + let growthReadable = toReadableString(GrowthRate[species.growthRate]); + const growthAux = growthReadable.replace(" ", "_"); + if (i18next.exists("growth:" + growthAux)) { + growthReadable = i18next.t("growth:" + growthAux as any); + } + this.pokemonGrowthRateText.setText(growthReadable); + + this.pokemonGrowthRateText.setColor(getGrowthRateColor(species.growthRate)); + this.pokemonGrowthRateText.setShadowColor(getGrowthRateColor(species.growthRate, true)); + this.pokemonGrowthRateLabelText.setVisible(true); + } else { + this.pokemonGrowthRateText.setText(""); + this.pokemonGrowthRateLabelText.setVisible(false); + } + + // Caught and hatched + if (isFormCaught) { + const colorScheme = starterColors[this.starterId]; + + this.pokemonUncaughtText.setVisible(false); + this.pokemonCaughtCountText.setText(`${this.speciesStarterDexEntry?.caughtCount}`); + if (species.speciesId === Species.MANAPHY || species.speciesId === Species.PHIONE) { + this.pokemonHatchedIcon.setFrame("manaphy"); + } else { + this.pokemonHatchedIcon.setFrame(getEggTierForSpecies(species)); + } + this.pokemonHatchedCountText.setText(`${this.speciesStarterDexEntry?.hatchedCount}`); + + const defaultDexAttr = this.getCurrentDexProps(species.speciesId); + const defaultProps = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr); + const variant = defaultProps.variant; + const tint = getVariantTint(variant); + this.pokemonShinyIcon.setFrame(getVariantIcon(variant)); + this.pokemonShinyIcon.setTint(tint); + this.pokemonShinyIcon.setVisible(defaultProps.shiny); + this.pokemonCaughtHatchedContainer.setVisible(true); + + this.pokemonCaughtHatchedContainer.setY(25); + this.pokemonCandyIcon.setTint(argbFromRgba(rgbHexToRgba(colorScheme[0]))); + this.pokemonCandyOverlayIcon.setTint(argbFromRgba(rgbHexToRgba(colorScheme[1]))); + this.pokemonCandyCountText.setText(`x${globalScene.gameData.starterData[this.starterId].candyCount}`); + this.pokemonCandyContainer.setVisible(true); + + if (pokemonPrevolutions.hasOwnProperty(species.speciesId)) { + this.pokemonShinyIcon.setY(135); + this.pokemonShinyIcon.setFrame(getVariantIcon(variant)); + this.pokemonHatchedIcon.setVisible(false); + this.pokemonHatchedCountText.setVisible(false); + this.pokemonFormText.setY(36); + } else { + this.pokemonShinyIcon.setY(117); + this.pokemonHatchedIcon.setVisible(true); + this.pokemonHatchedCountText.setVisible(true); + this.pokemonFormText.setY(42); + + const { currentFriendship, friendshipCap } = this.getFriendship(this.species.speciesId); + const candyCropY = 16 - (16 * (currentFriendship / friendshipCap)); + this.pokemonCandyDarknessOverlay.setCrop(0, 0, 16, candyCropY); + + this.pokemonCandyContainer.on("pointerover", () => { + globalScene.ui.showTooltip("", `${currentFriendship}/${friendshipCap}`, true); + this.activeTooltip = "CANDY"; + }); + this.pokemonCandyContainer.on("pointerout", () => { + globalScene.ui.hideTooltip(); + this.activeTooltip = undefined; + }); + + } + } else { + this.pokemonUncaughtText.setVisible(true); + this.pokemonCaughtHatchedContainer.setVisible(false); + this.pokemonCandyContainer.setVisible(false); + this.pokemonShinyIcon.setVisible(false); + } + + // Setting type icons and form text + if (isFormCaught || isFormSeen) { const speciesForm = getPokemonSpeciesForm(species.speciesId, formIndex!); // TODO: is the bang correct? this.setTypeIcons(speciesForm.type1, speciesForm.type2); this.pokemonFormText.setText(species.getFormNameToDisplay(formIndex)); - + this.pokemonFormText.setVisible(true); + if (!isFormCaught) { + this.pokemonFormText.setY(18); + } } else { this.setTypeIcons(null, null); this.pokemonFormText.setText(""); + this.pokemonFormText.setVisible(false); } } else { this.shinyOverlay.setVisible(false); diff --git a/src/ui/pokedex-ui-handler.ts b/src/ui/pokedex-ui-handler.ts index 410bb53906a..4c920a094c6 100644 --- a/src/ui/pokedex-ui-handler.ts +++ b/src/ui/pokedex-ui-handler.ts @@ -11,7 +11,7 @@ import { allSpecies, getPokemonSpeciesForm, getPokerusStarters } from "#app/data import { getStarterValueFriendshipCap, speciesStarterCosts, POKERUS_STARTER_COUNT } from "#app/data/balance/starters"; import { catchableSpecies } from "#app/data/balance/biomes"; import { Type } from "#enums/type"; -import type { DexAttrProps, DexEntry, StarterMoveset, StarterAttributes, StarterPreferences } from "#app/system/game-data"; +import type { DexAttrProps, DexEntry, StarterAttributes, StarterPreferences } from "#app/system/game-data"; import { AbilityAttr, DexAttr, StarterPrefs } from "#app/system/game-data"; import MessageUiHandler from "#app/ui/message-ui-handler"; import PokemonIconAnimHandler, { PokemonIconAnimMode } from "#app/ui/pokemon-icon-anim-handler"; @@ -19,7 +19,6 @@ import { TextStyle, addTextObject } from "#app/ui/text"; import { Mode } from "#app/ui/ui"; import { SettingKeyboard } from "#app/system/settings/settings-keyboard"; import { Passive as PassiveAttr } from "#enums/passive"; -import type { Moves } from "#enums/moves"; import type { Species } from "#enums/species"; import { Button } from "#enums/buttons"; import { DropDown, DropDownLabel, DropDownOption, DropDownState, DropDownType, SortCriteria } from "#app/ui/dropdown"; @@ -42,7 +41,6 @@ import { pokemonStarters } from "#app/data/balance/pokemon-evolutions"; import { Biome } from "#enums/biome"; import { globalScene } from "#app/global-scene"; - interface LanguageSetting { starterInfoTextSize: string, instructionTextSize: string, @@ -139,7 +137,6 @@ interface SpeciesDetails { variant?: Variant, abilityIndex?: number, natureIndex?: number, - forSeen?: boolean, // default = false } export default class PokedexUiHandler extends MessageUiHandler { @@ -161,7 +158,6 @@ export default class PokedexUiHandler extends MessageUiHandler { private filterMode: boolean; private filterBarCursor: number = 0; - private starterMoveset: StarterMoveset | null; private scrollCursor: number; private allSpecies: PokemonSpecies[] = []; @@ -169,7 +165,6 @@ export default class PokedexUiHandler extends MessageUiHandler { private speciesLoaded: Map = new Map(); private pokerusSpecies: PokemonSpecies[] = []; private speciesStarterDexEntry: DexEntry | null; - private speciesStarterMoves: Moves[]; private assetLoadCancelled: BooleanHolder | null; public cursorObj: Phaser.GameObjects.Image; @@ -206,6 +201,20 @@ export default class PokedexUiHandler extends MessageUiHandler { private toggleDecorationsIconElement: Phaser.GameObjects.Sprite; private toggleDecorationsLabel: Phaser.GameObjects.Text; + private formTrayContainer: Phaser.GameObjects.Container; + private trayBg: Phaser.GameObjects.NineSlice; + private trayForms: PokemonForm[]; + private trayContainers: PokedexMonContainer[] = []; + private trayNumIcons: number; + private trayRows: number; + private trayColumns: number; + private trayCursorObj: Phaser.GameObjects.Image; + private trayCursor: number = 0; + private showingTray: boolean = false; + private showFormTrayIconElement: Phaser.GameObjects.Sprite; + private showFormTrayLabel: Phaser.GameObjects.Text; + private canShowFormTray: boolean; + constructor() { super(Mode.POKEDEX); } @@ -425,7 +434,6 @@ export default class PokedexUiHandler extends MessageUiHandler { this.cursorObj = globalScene.add.image(0, 0, "select_cursor"); this.cursorObj.setOrigin(0, 0); - starterBoxContainer.add(this.cursorObj); for (const species of allSpecies) { @@ -438,6 +446,20 @@ export default class PokedexUiHandler extends MessageUiHandler { starterBoxContainer.add(pokemonContainer); } + // Tray to display forms + this.formTrayContainer = globalScene.add.container(0, 0); + + this.trayBg = addWindow(0, 0, 0, 0); + this.trayBg.setOrigin(0, 0); + this.formTrayContainer.add(this.trayBg); + + this.trayCursorObj = globalScene.add.image(0, 0, "select_cursor"); + this.trayCursorObj.setOrigin(0, 0); + this.formTrayContainer.add(this.trayCursorObj); + starterBoxContainer.add(this.formTrayContainer); + starterBoxContainer.bringToTop(this.formTrayContainer); + this.formTrayContainer.setVisible(false); + this.starterSelectContainer.add(starterBoxContainer); this.pokemonSprite = globalScene.add.sprite(96, 143, "pkmn__sub"); @@ -449,7 +471,7 @@ export default class PokedexUiHandler extends MessageUiHandler { this.type1Icon.setOrigin(0, 0); this.starterSelectContainer.add(this.type1Icon); - this.type2Icon = globalScene.add.sprite(10, 166, getLocalizedSpriteKey("types")); + this.type2Icon = globalScene.add.sprite(28, 158, getLocalizedSpriteKey("types")); this.type2Icon.setScale(0.5); this.type2Icon.setOrigin(0, 0); this.starterSelectContainer.add(this.type2Icon); @@ -488,6 +510,17 @@ export default class PokedexUiHandler extends MessageUiHandler { this.starterSelectContainer.add(this.toggleDecorationsIconElement); this.starterSelectContainer.add(this.toggleDecorationsLabel); + this.showFormTrayIconElement = new Phaser.GameObjects.Sprite(globalScene, 6, 168, "keyboard", "F.png"); + this.showFormTrayIconElement.setName("sprite-showFormTray-icon-element"); + this.showFormTrayIconElement.setScale(0.675); + this.showFormTrayIconElement.setOrigin(0.0, 0.0); + this.showFormTrayLabel = addTextObject(16, 168, i18next.t("pokedexUiHandler:showForms"), TextStyle.PARTY, { fontSize: instructionTextSize }); + this.showFormTrayLabel.setName("text-showFormTray-label"); + this.showFormTrayIconElement.setVisible(false); + this.showFormTrayLabel.setVisible(false); + this.starterSelectContainer.add(this.showFormTrayIconElement); + this.starterSelectContainer.add(this.showFormTrayLabel); + this.message = addTextObject(8, 8, "", TextStyle.WINDOW, { maxLines: 2 }); this.message.setOrigin(0, 0); this.starterSelectMessageBoxContainer.add(this.message); @@ -527,7 +560,7 @@ export default class PokedexUiHandler extends MessageUiHandler { this.starterPreferences[species.speciesId] = this.initStarterPrefs(species); - if (dexEntry.caughtAttr) { + if (dexEntry.caughtAttr || globalScene.dexForDevs) { icon.clearTint(); } else if (dexEntry.seenAttr) { icon.setTint(0x808080); @@ -860,32 +893,42 @@ export default class PokedexUiHandler extends MessageUiHandler { } else if (this.filterTextMode && !(this.filterText.getValue(this.filterTextCursor) === this.filterText.defaultText)) { this.filterText.resetSelection(this.filterTextCursor); success = true; + } else if (this.showingTray) { + success = this.closeFormTray(); } else { this.tryExit(); success = true; } } else if (button === Button.STATS) { - if (!this.filterMode) { + if (!this.filterMode && !this.showingTray) { this.cursorObj.setVisible(false); this.setSpecies(null); this.filterText.cursorObj.setVisible(false); this.filterTextMode = false; this.filterBarCursor = 0; this.setFilterMode(true); + } else { + error = true; } } else if (button === Button.V) { - if (!this.filterTextMode) { + if (!this.filterTextMode && !this.showingTray) { this.cursorObj.setVisible(false); this.setSpecies(null); this.filterBar.cursorObj.setVisible(false); this.filterMode = false; this.filterTextCursor = 0; this.setFilterTextMode(true); + } else { + error = true; } } else if (button === Button.CYCLE_SHINY) { - this.showDecorations = !this.showDecorations; - this.updateScroll(); - success = true; + if (!this.showingTray) { + this.showDecorations = !this.showDecorations; + this.updateScroll(); + success = true; + } else { + error = true; + } } else if (this.filterMode) { switch (button) { case Button.LEFT: @@ -982,8 +1025,55 @@ export default class PokedexUiHandler extends MessageUiHandler { success = true; break; } + } else if (this.showingTray) { + if (button === Button.ACTION) { + const formIndex = this.trayForms[this.trayCursor].formIndex; + ui.setOverlayMode(Mode.POKEDEX_PAGE, this.lastSpecies, formIndex, { form: formIndex }); + success = true; + } else { + const numberOfForms = this.trayContainers.length; + const numOfRows = Math.ceil(numberOfForms / maxColumns); + const currentRow = Math.floor(this.trayCursor / maxColumns); + switch (button) { + case Button.UP: + if (currentRow > 0) { + success = this.setTrayCursor(this.trayCursor - 9); + } else { + const targetCol = this.trayCursor; + if (numberOfForms % 9 > targetCol) { + success = this.setTrayCursor(numberOfForms - (numberOfForms) % 9 + targetCol); + } else { + success = this.setTrayCursor(Math.max(numberOfForms - (numberOfForms) % 9 + targetCol - 9, 0)); + } + } + break; + case Button.DOWN: + if (currentRow < numOfRows - 1) { + success = this.setTrayCursor(this.trayCursor + 9); + } else { + success = this.setTrayCursor(this.trayCursor % 9); + } + break; + case Button.LEFT: + if (this.trayCursor % 9 !== 0) { + success = this.setTrayCursor(this.trayCursor - 1); + } else { + success = this.setTrayCursor(currentRow < numOfRows - 1 ? (currentRow + 1) * maxColumns - 1 : numberOfForms - 1); + } + break; + case Button.RIGHT: + if (this.trayCursor % 9 < (currentRow < numOfRows - 1 ? 8 : (numberOfForms - 1) % 9)) { + success = this.setTrayCursor(this.trayCursor + 1); + } else { + success = this.setTrayCursor(currentRow * 9); + } + break; + case Button.CYCLE_FORM: + success = this.closeFormTray(); + break; + } + } } else { - if (button === Button.ACTION) { ui.setOverlayMode(Mode.POKEDEX_PAGE, this.lastSpecies, 0); success = true; @@ -1042,6 +1132,12 @@ export default class PokedexUiHandler extends MessageUiHandler { success = true; } break; + case Button.CYCLE_FORM: + const species = this.filteredPokemonContainers[this.cursor].species; + if (this.canShowFormTray) { + success = this.openFormTray(species); + } + break; } } } @@ -1068,6 +1164,9 @@ export default class PokedexUiHandler extends MessageUiHandler { case SettingKeyboard.Button_Cycle_Variant: iconPath = "V.png"; break; + case SettingKeyboard.Button_Cycle_Form: + iconPath = "F.png"; + break; case SettingKeyboard.Button_Stats: iconPath = "C.png"; break; @@ -1145,13 +1244,15 @@ export default class PokedexUiHandler extends MessageUiHandler { this.validPokemonContainers.forEach(container => { container.setVisible(false); - container.cost = globalScene.gameData.getSpeciesStarterValue(this.getStarterSpeciesId(container.species.speciesId)); + const starterId = this.getStarterSpeciesId(container.species.speciesId); + + container.cost = globalScene.gameData.getSpeciesStarterValue(starterId); // First, ensure you have the caught attributes for the species else default to bigint 0 // TODO: This might be removed depending on how accessible we want the pokedex function to be const caughtAttr = globalScene.gameData.dexData[container.species.speciesId]?.caughtAttr || BigInt(0); - const starterData = globalScene.gameData.starterData[this.getStarterSpeciesId(container.species.speciesId)]; - const isStarterProgressable = speciesEggMoves.hasOwnProperty(this.getStarterSpeciesId(container.species.speciesId)); + const starterData = globalScene.gameData.starterData[starterId]; + const isStarterProgressable = speciesEggMoves.hasOwnProperty(starterId); // Name filter const selectedName = this.filterText.getValue(FilterTextRow.NAME); @@ -1162,8 +1263,8 @@ export default class PokedexUiHandler extends MessageUiHandler { // On the other hand, in some cases it is possible to switch between different forms and combine (Deoxys) const levelMoves = pokemonSpeciesLevelMoves[container.species.speciesId].map(m => allMoves[m[1]].name); // This always gets egg moves from the starter - const eggMoves = speciesEggMoves[this.getStarterSpeciesId(container.species.speciesId)]?.map(m => allMoves[m].name) ?? []; - const tmMoves = speciesTmMoves[this.getStarterSpeciesId(container.species.speciesId)]?.map(m => allMoves[Array.isArray(m) ? m[1] : m].name) ?? []; + const eggMoves = speciesEggMoves[starterId]?.map(m => allMoves[m].name) ?? []; + const tmMoves = speciesTmMoves[starterId]?.map(m => allMoves[Array.isArray(m) ? m[1] : m].name) ?? []; const selectedMove1 = this.filterText.getValue(FilterTextRow.MOVE_1); const selectedMove2 = this.filterText.getValue(FilterTextRow.MOVE_2); @@ -1185,27 +1286,40 @@ export default class PokedexUiHandler extends MessageUiHandler { container.tmMove2Icon.setVisible(false); if (fitsEggMove1 && !fitsLevelMove1) { container.eggMove1Icon.setVisible(true); + const em1 = eggMoves.findIndex(name => name === selectedMove1); + if ((starterData[starterId].eggMoves & (1 << em1)) === 0) { + container.eggMove1Icon.setTint(0x808080); + } else { + container.eggMove1Icon.clearTint(); + } } else if (fitsTmMove1 && !fitsLevelMove1) { container.tmMove1Icon.setVisible(true); } if (fitsEggMove2 && !fitsLevelMove2) { container.eggMove2Icon.setVisible(true); + const em2 = eggMoves.findIndex(name => name === selectedMove2); + if ((starterData[starterId].eggMoves & (1 << em2)) === 0) { + container.eggMove2Icon.setTint(0x808080); + } else { + container.eggMove2Icon.clearTint(); + } } else if (fitsTmMove2 && !fitsLevelMove2) { container.tmMove2Icon.setVisible(true); } // Ability filter const abilities = [ container.species.ability1, container.species.ability2, container.species.abilityHidden ].map(a => allAbilities[a].name); - const passives = starterPassiveAbilities[this.getStarterSpeciesId(container.species.speciesId)] ?? {} as PassiveAbilities; + const passives = starterPassiveAbilities[starterId] ?? {} as PassiveAbilities; const selectedAbility1 = this.filterText.getValue(FilterTextRow.ABILITY_1); - const fitsFormAbility = container.species.forms.some(form => allAbilities[form.ability1].name === selectedAbility1); - const fitsAbility1 = abilities.includes(selectedAbility1) || fitsFormAbility || selectedAbility1 === this.filterText.defaultText; - const fitsPassive1 = Object.values(passives).some(p => p.name === selectedAbility1); + const fitsFormAbility1 = container.species.forms.some(form => [ form.ability1, form.ability2, form.abilityHidden ].map(a => allAbilities[a].name).includes(selectedAbility1)); + const fitsAbility1 = abilities.includes(selectedAbility1) || fitsFormAbility1 || selectedAbility1 === this.filterText.defaultText; + const fitsPassive1 = Object.values(passives).some(p => allAbilities[p].name === selectedAbility1); const selectedAbility2 = this.filterText.getValue(FilterTextRow.ABILITY_2); - const fitsAbility2 = abilities.includes(selectedAbility2) || fitsFormAbility || selectedAbility2 === this.filterText.defaultText; - const fitsPassive2 = Object.values(passives).some(p => p.name === selectedAbility2); + const fitsFormAbility2 = container.species.forms.some(form => [ form.ability1, form.ability2, form.abilityHidden ].map(a => allAbilities[a].name).includes(selectedAbility2)); + const fitsAbility2 = abilities.includes(selectedAbility2) || fitsFormAbility2 || selectedAbility2 === this.filterText.defaultText; + const fitsPassive2 = Object.values(passives).some(p => allAbilities[p].name === selectedAbility2); // If both fields have been set to the same ability, show both ability and passive const fitsAbilities = (fitsAbility1 && (fitsPassive2 || selectedAbility2 === this.filterText.defaultText)) || @@ -1213,11 +1327,26 @@ export default class PokedexUiHandler extends MessageUiHandler { container.passive1Icon.setVisible(false); container.passive2Icon.setVisible(false); - if (fitsPassive1) { - container.passive1Icon.setVisible(true); - } - if (fitsPassive2) { - container.passive2Icon.setVisible(true); + if (fitsPassive1 || fitsPassive2) { + if (fitsPassive1) { + if (starterData.passiveAttr > 0) { + container.passive1Icon.clearTint(); + container.passive1OverlayIcon.clearTint(); + } else { + container.passive1Icon.setTint(0x808080); + container.passive1OverlayIcon.setTint(0x808080); + } + container.passive1Icon.setVisible(true); + } else { + if (starterData.passiveAttr > 0) { + container.passive2Icon.clearTint(); + container.passive2OverlayIcon.clearTint(); + } else { + container.passive2Icon.setTint(0x808080); + container.passive2OverlayIcon.setTint(0x808080); + } + container.passive2Icon.setVisible(true); + } } // Gen filter @@ -1236,7 +1365,7 @@ export default class PokedexUiHandler extends MessageUiHandler { // We get biomes for both the mon and its starters to ensure that evolutions get the correct filters. // TODO: We might also need to do it the other way around. - const biomes = catchableSpecies[container.species.speciesId].concat(catchableSpecies[this.getStarterSpeciesId(container.species.speciesId)]).map(b => Biome[b.biome]); + const biomes = catchableSpecies[container.species.speciesId].concat(catchableSpecies[starterId]).map(b => Biome[b.biome]); if (biomes.length === 0) { biomes.push("Uncatchable"); } @@ -1530,6 +1659,8 @@ export default class PokedexUiHandler extends MessageUiHandler { this.cursorObj.setVisible(!filterMode); this.filterBar.cursorObj.setVisible(filterMode); this.pokemonSprite.setVisible(false); + this.showFormTrayIconElement.setVisible(false); + this.showFormTrayLabel.setVisible(false); if (filterMode !== this.filterMode) { this.filterMode = filterMode; @@ -1546,6 +1677,8 @@ export default class PokedexUiHandler extends MessageUiHandler { this.cursorObj.setVisible(!filterTextMode); this.filterText.cursorObj.setVisible(filterTextMode); this.pokemonSprite.setVisible(false); + this.showFormTrayIconElement.setVisible(false); + this.showFormTrayLabel.setVisible(false); if (filterTextMode !== this.filterTextMode) { this.filterTextMode = filterTextMode; @@ -1558,6 +1691,101 @@ export default class PokedexUiHandler extends MessageUiHandler { return false; } + openFormTray(species: PokemonSpecies): boolean { + + this.trayForms = species.forms; + + this.trayNumIcons = this.trayForms.length; + this.trayRows = Math.floor(this.trayNumIcons / 9) + (this.trayNumIcons % 9 === 0 ? 0 : 1); + this.trayColumns = Math.min(this.trayNumIcons, 9); + + const maxColumns = 9; + const onScreenFirstIndex = this.scrollCursor * maxColumns; + const boxCursor = this.cursor - onScreenFirstIndex; + const boxCursorY = Math.floor(boxCursor / maxColumns); + const boxCursorX = boxCursor - boxCursorY * 9; + const spaceBelow = 9 - 1 - boxCursorY; + const spaceRight = 9 - boxCursorX; + const boxPos = calcStarterPosition(this.cursor, this.scrollCursor); + const goUp = this.trayRows <= spaceBelow - 1 ? 0 : 1; + const goLeft = this.trayColumns <= spaceRight ? 0 : 1; + + this.trayBg.setSize(13 + this.trayColumns * 17, 8 + this.trayRows * 18); + this.formTrayContainer.setX( + (goLeft ? boxPos.x - 18 * (this.trayColumns - spaceRight) : boxPos.x) - 3 + ); + this.formTrayContainer.setY( + goUp ? boxPos.y - this.trayBg.height : boxPos.y + 17 + ); + + const dexEntry = globalScene.gameData.dexData[species.speciesId]; + const dexAttr = this.getCurrentDexProps(species.speciesId); + const props = this.getSanitizedProps(globalScene.gameData.getSpeciesDexAttrProps(this.lastSpecies, dexAttr)); + + this.trayContainers = []; + this.trayForms.map((f, index) => { + const isFormCaught = dexEntry ? (dexEntry.caughtAttr & globalScene.gameData.getFormAttr(f.formIndex ?? 0)) > 0n : false; + const isFormSeen = dexEntry ? (dexEntry.seenAttr & globalScene.gameData.getFormAttr(f.formIndex ?? 0)) > 0n : false; + const formContainer = new PokedexMonContainer(species, { formIndex: f.formIndex, female: props.female, shiny: props.shiny, variant: props.variant }); + this.iconAnimHandler.addOrUpdate(formContainer.icon, PokemonIconAnimMode.NONE); + // Setting tint, for all saves some caught forms may only show up as seen + if (isFormCaught || globalScene.dexForDevs) { + formContainer.icon.clearTint(); + } else if (isFormSeen) { + formContainer.icon.setTint(0x808080); + } + formContainer.setPosition(5 + (index % 9) * 18, 4 + Math.floor(index / 9) * 17); + this.formTrayContainer.add(formContainer); + this.trayContainers.push(formContainer); + }); + + this.showingTray = true; + + this.setTrayCursor(0); + + this.formTrayContainer.setVisible(true); + + this.showFormTrayIconElement.setVisible(false); + this.showFormTrayLabel.setVisible(false); + + return true; + } + + closeFormTray(): boolean { + + this.trayContainers.forEach(obj => { + this.formTrayContainer.remove(obj, true); // Removes from container and destroys it + }); + + this.trayContainers = []; + this.formTrayContainer.setVisible(false); + this.showingTray = false; + + this.setSpeciesDetails(this.lastSpecies); + return true; + } + + setTrayCursor(cursor: number): boolean { + if (!this.showingTray) { + return false; + } + + cursor = Phaser.Math.Clamp(this.trayContainers.length - 1, cursor, 0); + const changed = this.trayCursor !== cursor; + if (changed) { + this.trayCursor = cursor; + } + + this.trayCursorObj.setPosition(5 + (cursor % 9) * 18, 4 + Math.floor(cursor / 9) * 17); + + const species = this.lastSpecies; + const formIndex = this.trayForms[cursor].formIndex; + + this.setSpeciesDetails(species, { formIndex: formIndex }); + + return changed; + } + getFriendship(speciesId: number) { let currentFriendship = globalScene.gameData.starterData[this.getStarterSpeciesId(speciesId)].friendship; if (!currentFriendship || currentFriendship === undefined) { @@ -1592,13 +1820,13 @@ export default class PokedexUiHandler extends MessageUiHandler { this.lastSpecies = species!; // TODO: is this bang correct? - if (species && (this.speciesStarterDexEntry?.seenAttr || this.speciesStarterDexEntry?.caughtAttr)) { + if (species && (this.speciesStarterDexEntry?.seenAttr || this.speciesStarterDexEntry?.caughtAttr || globalScene.dexForDevs)) { this.pokemonNumberText.setText(i18next.t("pokedexUiHandler:pokemonNumber") + padInt(species.speciesId, 4)); this.pokemonNameText.setText(species.name); - if (this.speciesStarterDexEntry?.caughtAttr) { + if (this.speciesStarterDexEntry?.caughtAttr || globalScene.dexForDevs) { // Pause the animation when the species is selected const speciesIndex = this.allSpecies.indexOf(species); @@ -1627,9 +1855,7 @@ export default class PokedexUiHandler extends MessageUiHandler { this.type1Icon.setVisible(true); this.type2Icon.setVisible(true); - this.setSpeciesDetails(species, { - forSeen: true - }); + this.setSpeciesDetails(species); this.pokemonSprite.setTint(0x808080); } } else { @@ -1646,7 +1872,6 @@ export default class PokedexUiHandler extends MessageUiHandler { setSpeciesDetails(species: PokemonSpecies, options: SpeciesDetails = {}): void { let { shiny, formIndex, female, variant } = options; - const forSeen: boolean = options.forSeen ?? false; // We will only update the sprite if there is a change to form, shiny/variant // or gender for species with gender sprite differences @@ -1667,34 +1892,33 @@ export default class PokedexUiHandler extends MessageUiHandler { this.assetLoadCancelled = null; } - this.starterMoveset = null; - this.speciesStarterMoves = []; - if (species) { const dexEntry = globalScene.gameData.dexData[species.speciesId]; if (!dexEntry.caughtAttr) { const props = this.getSanitizedProps(globalScene.gameData.getSpeciesDexAttrProps(species, this.getCurrentDexProps(species.speciesId))); - if (shiny === undefined || shiny !== props.shiny) { + if (shiny === undefined) { shiny = props.shiny; } - if (formIndex === undefined || formIndex !== props.formIndex) { + if (formIndex === undefined) { formIndex = props.formIndex; } - if (female === undefined || female !== props.female) { + if (female === undefined) { female = props.female; } - if (variant === undefined || variant !== props.variant) { + if (variant === undefined) { variant = props.variant; } } + const isFormCaught = dexEntry ? (dexEntry.caughtAttr & globalScene.gameData.getFormAttr(formIndex ?? 0)) > 0n : false; + const isFormSeen = dexEntry ? (dexEntry.seenAttr & globalScene.gameData.getFormAttr(formIndex ?? 0)) > 0n : false; + const assetLoadCancelled = new BooleanHolder(false); this.assetLoadCancelled = assetLoadCancelled; if (shouldUpdateSprite) { - species.loadAssets(female!, formIndex, shiny, variant, true).then(() => { // TODO: is this bang correct? if (assetLoadCancelled.value) { return; @@ -1711,21 +1935,37 @@ export default class PokedexUiHandler extends MessageUiHandler { this.pokemonSprite.setVisible(!(this.filterMode || this.filterTextMode)); } - if (dexEntry.caughtAttr || forSeen) { + if (isFormCaught || globalScene.dexForDevs) { + this.pokemonSprite.clearTint(); + } else if (isFormSeen) { + this.pokemonSprite.setTint(0x808080); + } else { + this.pokemonSprite.setTint(0); + } + if (isFormCaught || isFormSeen || globalScene.dexForDevs) { const speciesForm = getPokemonSpeciesForm(species.speciesId, 0); // TODO: always selecting the first form - this.setTypeIcons(speciesForm.type1, speciesForm.type2); } else { this.setTypeIcons(null, null); } + + if (species?.forms?.length > 1) { + if (!this.showingTray) { + this.showFormTrayIconElement.setVisible(true); + this.showFormTrayLabel.setVisible(true); + } + this.canShowFormTray = true; + } else { + this.showFormTrayIconElement.setVisible(false); + this.showFormTrayLabel.setVisible(false); + this.canShowFormTray = false; + } + } else { this.setTypeIcons(null, null); } - if (!this.starterMoveset) { - this.starterMoveset = this.speciesStarterMoves.slice(0, 4) as StarterMoveset; - } } setTypeIcons(type1: Type | null, type2: Type | null): void { @@ -1784,7 +2024,6 @@ export default class PokedexUiHandler extends MessageUiHandler { ui.showText(i18next.t("pokedexUiHandler:confirmExit"), null, () => { ui.setModeWithoutClear(Mode.CONFIRM, () => { ui.setMode(Mode.POKEDEX, "refresh"); - globalScene.clearPhaseQueue(); this.clearText(); this.clear(); ui.revertMode(); diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index 4217d7e5205..65c159c62a8 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -1981,8 +1981,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler { female: starterAttributes.female }; ui.setOverlayMode(Mode.POKEDEX_PAGE, this.lastSpecies, starterAttributes.form, attributes); - return true; }); + return true; } }); options.push({ diff --git a/src/ui/summary-ui-handler.ts b/src/ui/summary-ui-handler.ts index 3305b3f7aa2..cf5d40bc006 100644 --- a/src/ui/summary-ui-handler.ts +++ b/src/ui/summary-ui-handler.ts @@ -96,6 +96,9 @@ export default class SummaryUiHandler extends UiHandler { private friendshipText: Phaser.GameObjects.Text; private friendshipIcon: Phaser.GameObjects.Sprite; private friendshipOverlay: Phaser.GameObjects.Sprite; + private permStatsContainer: Phaser.GameObjects.Container; + private ivContainer: Phaser.GameObjects.Container; + private statsContainer: Phaser.GameObjects.Container; private descriptionScrollTween: Phaser.Tweens.Tween | null; private moveCursorBlinkTimer: Phaser.Time.TimerEvent | null; @@ -534,6 +537,10 @@ export default class SummaryUiHandler extends UiHandler { this.passiveContainer.nameText?.setVisible(!this.passiveContainer.descriptionText?.visible); this.passiveContainer.descriptionText?.setVisible(!this.passiveContainer.descriptionText.visible); this.passiveContainer.labelImage.setVisible(!this.passiveContainer.labelImage.visible); + } else if (this.cursor === Page.STATS) { + //Show IVs + this.permStatsContainer.setVisible(!this.permStatsContainer.visible); + this.ivContainer.setVisible(!this.ivContainer.visible); } } else if (button === Button.CANCEL) { if (this.summaryUiMode === SummaryUiMode.LEARN_MOVE) { @@ -877,8 +884,13 @@ export default class SummaryUiHandler extends UiHandler { profileContainer.add(memoText); break; case Page.STATS: - const statsContainer = globalScene.add.container(0, -pageBg.height); - pageContainer.add(statsContainer); + this.statsContainer = globalScene.add.container(0, -pageBg.height); + pageContainer.add(this.statsContainer); + this.permStatsContainer = globalScene.add.container(27, 56); + this.statsContainer.add(this.permStatsContainer); + this.ivContainer = globalScene.add.container(27, 56); + this.statsContainer.add(this.ivContainer); + this.statsContainer.setVisible(true); PERMANENT_STATS.forEach((stat, s) => { const statName = i18next.t(getStatKey(stat)); @@ -887,18 +899,27 @@ export default class SummaryUiHandler extends UiHandler { const natureStatMultiplier = getNatureStatMultiplier(this.pokemon?.getNature()!, s); // TODO: is this bang correct? - const statLabel = addTextObject(27 + 115 * colIndex + (colIndex === 1 ? 5 : 0), 56 + 16 * rowIndex, statName, natureStatMultiplier === 1 ? TextStyle.SUMMARY : natureStatMultiplier > 1 ? TextStyle.SUMMARY_PINK : TextStyle.SUMMARY_BLUE); + const statLabel = addTextObject(115 * colIndex + (colIndex === 1 ? 5 : 0), 16 * rowIndex, statName, natureStatMultiplier === 1 ? TextStyle.SUMMARY : natureStatMultiplier > 1 ? TextStyle.SUMMARY_PINK : TextStyle.SUMMARY_BLUE); + const ivLabel = addTextObject(115 * colIndex + (colIndex === 1 ? 5 : 0), 16 * rowIndex, statName, this.pokemon?.ivs[stat] === 31 ? TextStyle.SUMMARY_GOLD : TextStyle.SUMMARY); + statLabel.setOrigin(0.5, 0); - statsContainer.add(statLabel); + ivLabel.setOrigin(0.5, 0); + this.permStatsContainer.add(statLabel); + this.ivContainer.add(ivLabel); const statValueText = stat !== Stat.HP ? Utils.formatStat(this.pokemon?.getStat(stat)!) // TODO: is this bang correct? : `${Utils.formatStat(this.pokemon?.hp!, true)}/${Utils.formatStat(this.pokemon?.getMaxHp()!, true)}`; // TODO: are those bangs correct? + const ivText = `${this.pokemon?.ivs[stat]}/31`; - const statValue = addTextObject(120 + 88 * colIndex, 56 + 16 * rowIndex, statValueText, TextStyle.WINDOW_ALT); + const statValue = addTextObject(93 + 88 * colIndex, 16 * rowIndex, statValueText, TextStyle.WINDOW_ALT); statValue.setOrigin(1, 0); - statsContainer.add(statValue); + this.permStatsContainer.add(statValue); + const ivValue = addTextObject(93 + 88 * colIndex, 16 * rowIndex, ivText, TextStyle.WINDOW_ALT); + ivValue.setOrigin(1, 0); + this.ivContainer.add(ivValue); }); + this.ivContainer.setVisible(false); const itemModifiers = (globalScene.findModifiers(m => m instanceof PokemonHeldItemModifier && m.pokemonId === this.pokemon?.id, this.playerParty) as PokemonHeldItemModifier[]) @@ -908,7 +929,7 @@ export default class SummaryUiHandler extends UiHandler { const icon = item.getIcon(true); icon.setPosition((i % 17) * 12 + 3, 14 * Math.floor(i / 17) + 15); - statsContainer.add(icon); + this.statsContainer.add(icon); icon.setInteractive(new Phaser.Geom.Rectangle(0, 0, 32, 32), Phaser.Geom.Rectangle.Contains); icon.on("pointerover", () => globalScene.ui.showTooltip(item.type.name, item.type.getDescription(), true)); @@ -924,26 +945,26 @@ export default class SummaryUiHandler extends UiHandler { const expLabel = addTextObject(6, 112, i18next.t("pokemonSummary:expPoints"), TextStyle.SUMMARY); expLabel.setOrigin(0, 0); - statsContainer.add(expLabel); + this.statsContainer.add(expLabel); const nextLvExpLabel = addTextObject(6, 128, i18next.t("pokemonSummary:nextLv"), TextStyle.SUMMARY); nextLvExpLabel.setOrigin(0, 0); - statsContainer.add(nextLvExpLabel); + this.statsContainer.add(nextLvExpLabel); const expText = addTextObject(208, 112, pkmExp.toString(), TextStyle.WINDOW_ALT); expText.setOrigin(1, 0); - statsContainer.add(expText); + this.statsContainer.add(expText); const nextLvExp = pkmLvl < globalScene.getMaxExpLevel() ? getLevelTotalExp(pkmLvl + 1, pkmSpeciesGrowthRate) - pkmExp : 0; const nextLvExpText = addTextObject(208, 128, nextLvExp.toString(), TextStyle.WINDOW_ALT); nextLvExpText.setOrigin(1, 0); - statsContainer.add(nextLvExpText); + this.statsContainer.add(nextLvExpText); const expOverlay = globalScene.add.image(140, 145, "summary_stats_overlay_exp"); expOverlay.setOrigin(0, 0); - statsContainer.add(expOverlay); + this.statsContainer.add(expOverlay); const expMaskRect = globalScene.make.graphics({}); expMaskRect.setScale(6); @@ -954,6 +975,11 @@ export default class SummaryUiHandler extends UiHandler { const expMask = expMaskRect.createGeometryMask(); expOverlay.setMask(expMask); + this.abilityPrompt = globalScene.add.image(0, 0, !globalScene.inputController?.gamepadSupport ? "summary_profile_prompt_z" : "summary_profile_prompt_a"); + this.abilityPrompt.setPosition(8, 47); + this.abilityPrompt.setVisible(true); + this.abilityPrompt.setOrigin(0, 0); + this.statsContainer.add(this.abilityPrompt); break; case Page.MOVES: this.movesContainer = globalScene.add.container(5, -pageBg.height + 26);