diff --git a/public/images/items.json b/public/images/items.json index 05d021b6a061..3c9cff7a35a2 100644 --- a/public/images/items.json +++ b/public/images/items.json @@ -6583,7 +6583,7 @@ } }, { - "filename": "rb", + "filename": "pb_silver", "rotated": false, "trimmed": true, "sourceSize": { @@ -6667,7 +6667,7 @@ } }, { - "filename": "smooth_meteorite", + "filename": "rb", "rotated": false, "trimmed": true, "sourceSize": { @@ -6675,7 +6675,7 @@ "h": 32 }, "spriteSourceSize": { - "x": 7, + "x": 6, "y": 6, "w": 20, "h": 20 @@ -6688,7 +6688,7 @@ } }, { - "filename": "strange_ball", + "filename": "smooth_meteorite", "rotated": false, "trimmed": true, "sourceSize": { @@ -6696,7 +6696,7 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, + "x": 7, "y": 6, "w": 20, "h": 20 @@ -6709,7 +6709,7 @@ } }, { - "filename": "ub", + "filename": "strange_ball", "rotated": false, "trimmed": true, "sourceSize": { @@ -6751,7 +6751,7 @@ } }, { - "filename": "apicot_berry", + "filename": "ub", "rotated": false, "trimmed": true, "sourceSize": { @@ -6761,13 +6761,13 @@ "spriteSourceSize": { "x": 6, "y": 6, - "w": 19, + "w": 20, "h": 20 }, "frame": { "x": 221, "y": 337, - "w": 19, + "w": 20, "h": 20 } }, @@ -6793,7 +6793,7 @@ } }, { - "filename": "big_mushroom", + "filename": "apicot_berry", "rotated": false, "trimmed": true, "sourceSize": { @@ -6804,13 +6804,13 @@ "x": 6, "y": 6, "w": 19, - "h": 19 + "h": 20 }, "frame": { "x": 343, "y": 287, "w": 19, - "h": 19 + "h": 20 } }, { @@ -6835,7 +6835,7 @@ } }, { - "filename": "hard_stone", + "filename": "big_mushroom", "rotated": false, "trimmed": true, "sourceSize": { @@ -6846,17 +6846,17 @@ "x": 6, "y": 6, "w": 19, - "h": 20 + "h": 19 }, "frame": { "x": 318, "y": 306, "w": 19, - "h": 20 + "h": 19 } }, { - "filename": "miracle_seed", + "filename": "hard_stone", "rotated": false, "trimmed": true, "sourceSize": { @@ -6865,15 +6865,15 @@ }, "spriteSourceSize": { "x": 6, - "y": 7, + "y": 6, "w": 19, - "h": 19 + "h": 20 }, "frame": { - "x": 337, - "y": 306, + "x": 314, + "y": 325, "w": 19, - "h": 19 + "h": 20 } }, { @@ -6891,35 +6891,14 @@ "h": 18 }, "frame": { - "x": 314, - "y": 326, - "w": 20, - "h": 18 - } - }, - { - "filename": "wl_antidote", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 18 - }, - "frame": { - "x": 356, + "x": 337, "y": 307, "w": 20, "h": 18 } }, { - "filename": "golden_egg", + "filename": "miracle_seed", "rotated": false, "trimmed": true, "sourceSize": { @@ -6927,20 +6906,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 7, - "y": 6, - "w": 17, - "h": 20 + "x": 6, + "y": 7, + "w": 19, + "h": 19 }, "frame": { - "x": 376, - "y": 307, - "w": 17, - "h": 20 + "x": 333, + "y": 325, + "w": 19, + "h": 19 } }, { - "filename": "wl_awakening", + "filename": "wl_antidote", "rotated": false, "trimmed": true, "sourceSize": { @@ -6954,14 +6933,14 @@ "h": 18 }, "frame": { - "x": 393, - "y": 241, + "x": 357, + "y": 307, "w": 20, "h": 18 } }, { - "filename": "toxic_orb", + "filename": "wl_awakening", "rotated": false, "trimmed": true, "sourceSize": { @@ -6969,15 +6948,15 @@ "h": 32 }, "spriteSourceSize": { - "x": 7, - "y": 7, - "w": 18, + "x": 6, + "y": 8, + "w": 20, "h": 18 }, "frame": { - "x": 413, - "y": 258, - "w": 18, + "x": 352, + "y": 325, + "w": 20, "h": 18 } }, @@ -6996,35 +6975,14 @@ "h": 18 }, "frame": { - "x": 393, - "y": 259, + "x": 241, + "y": 340, "w": 20, "h": 18 } }, { - "filename": "ampharosite", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 8, - "w": 16, - "h": 16 - }, - "frame": { - "x": 377, - "y": 243, - "w": 16, - "h": 16 - } - }, - { - "filename": "audinite", + "filename": "golden_egg", "rotated": false, "trimmed": true, "sourceSize": { @@ -7032,20 +6990,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, - "y": 8, - "w": 16, - "h": 16 + "x": 7, + "y": 6, + "w": 17, + "h": 20 }, "frame": { - "x": 377, - "y": 259, - "w": 16, - "h": 16 + "x": 372, + "y": 325, + "w": 17, + "h": 20 } }, { - "filename": "relic_gold", + "filename": "toxic_orb", "rotated": false, "trimmed": true, "sourceSize": { @@ -7053,16 +7011,16 @@ "h": 32 }, "spriteSourceSize": { - "x": 9, - "y": 11, - "w": 15, - "h": 11 + "x": 7, + "y": 7, + "w": 18, + "h": 18 }, "frame": { "x": 377, - "y": 275, - "w": 15, - "h": 11 + "y": 307, + "w": 18, + "h": 18 } }, { @@ -7080,8 +7038,8 @@ "h": 20 }, "frame": { - "x": 381, - "y": 286, + "x": 389, + "y": 325, "w": 17, "h": 20 } @@ -7101,8 +7059,8 @@ "h": 18 }, "frame": { - "x": 398, - "y": 277, + "x": 352, + "y": 343, "w": 20, "h": 18 } @@ -7122,8 +7080,8 @@ "h": 18 }, "frame": { - "x": 398, - "y": 295, + "x": 372, + "y": 345, "w": 20, "h": 18 } @@ -7143,8 +7101,8 @@ "h": 18 }, "frame": { - "x": 393, - "y": 313, + "x": 392, + "y": 345, "w": 20, "h": 18 } @@ -7164,14 +7122,14 @@ "h": 18 }, "frame": { - "x": 240, - "y": 340, + "x": 378, + "y": 241, "w": 20, "h": 18 } }, { - "filename": "banettite", + "filename": "relic_gold", "rotated": false, "trimmed": true, "sourceSize": { @@ -7179,16 +7137,16 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, - "y": 8, - "w": 16, - "h": 16 + "x": 9, + "y": 11, + "w": 15, + "h": 11 }, "frame": { - "x": 413, - "y": 313, - "w": 16, - "h": 16 + "x": 398, + "y": 241, + "w": 15, + "h": 11 } }, { @@ -7206,8 +7164,8 @@ "h": 18 }, "frame": { - "x": 202, - "y": 358, + "x": 377, + "y": 259, "w": 20, "h": 18 } @@ -7227,8 +7185,8 @@ "h": 18 }, "frame": { - "x": 201, - "y": 376, + "x": 381, + "y": 277, "w": 20, "h": 18 } @@ -7248,8 +7206,8 @@ "h": 18 }, "frame": { - "x": 201, - "y": 394, + "x": 397, + "y": 259, "w": 20, "h": 18 } @@ -7269,14 +7227,14 @@ "h": 18 }, "frame": { - "x": 201, - "y": 412, + "x": 401, + "y": 277, "w": 20, "h": 18 } }, { - "filename": "beedrillite", + "filename": "wl_ice_heal", "rotated": false, "trimmed": true, "sourceSize": { @@ -7284,20 +7242,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, + "x": 6, "y": 8, - "w": 16, - "h": 16 + "w": 20, + "h": 18 }, "frame": { - "x": 222, - "y": 357, - "w": 16, - "h": 16 + "x": 395, + "y": 295, + "w": 20, + "h": 18 } }, { - "filename": "wl_ice_heal", + "filename": "ampharosite", "rotated": false, "trimmed": true, "sourceSize": { @@ -7305,20 +7263,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, + "x": 8, "y": 8, - "w": 20, - "h": 18 + "w": 16, + "h": 16 }, "frame": { - "x": 238, - "y": 358, - "w": 20, - "h": 18 + "x": 415, + "y": 295, + "w": 16, + "h": 16 } }, { - "filename": "blastoisinite", + "filename": "audinite", "rotated": false, "trimmed": true, "sourceSize": { @@ -7332,8 +7290,8 @@ "h": 16 }, "frame": { - "x": 222, - "y": 373, + "x": 415, + "y": 311, "w": 16, "h": 16 } @@ -7353,12 +7311,33 @@ "h": 18 }, "frame": { - "x": 221, - "y": 389, + "x": 406, + "y": 327, "w": 20, "h": 18 } }, + { + "filename": "banettite", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 16, + "h": 16 + }, + "frame": { + "x": 412, + "y": 345, + "w": 16, + "h": 16 + } + }, { "filename": "wl_item_urge", "rotated": false, @@ -7375,7 +7354,7 @@ }, "frame": { "x": 221, - "y": 407, + "y": 357, "w": 20, "h": 18 } @@ -7396,7 +7375,7 @@ }, "frame": { "x": 241, - "y": 376, + "y": 358, "w": 20, "h": 18 } @@ -7416,8 +7395,8 @@ "h": 18 }, "frame": { - "x": 241, - "y": 394, + "x": 201, + "y": 360, "w": 20, "h": 18 } @@ -7437,8 +7416,8 @@ "h": 18 }, "frame": { - "x": 241, - "y": 412, + "x": 201, + "y": 378, "w": 20, "h": 18 } @@ -7458,8 +7437,8 @@ "h": 18 }, "frame": { - "x": 258, - "y": 358, + "x": 221, + "y": 375, "w": 20, "h": 18 } @@ -7479,8 +7458,8 @@ "h": 18 }, "frame": { - "x": 261, - "y": 376, + "x": 201, + "y": 396, "w": 20, "h": 18 } @@ -7500,8 +7479,8 @@ "h": 18 }, "frame": { - "x": 261, - "y": 394, + "x": 221, + "y": 393, "w": 20, "h": 18 } @@ -7521,8 +7500,8 @@ "h": 18 }, "frame": { - "x": 261, - "y": 412, + "x": 241, + "y": 376, "w": 20, "h": 18 } @@ -7542,12 +7521,33 @@ "h": 18 }, "frame": { - "x": 278, - "y": 345, + "x": 241, + "y": 394, "w": 20, "h": 18 } }, + { + "filename": "beedrillite", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 16, + "h": 16 + }, + "frame": { + "x": 201, + "y": 414, + "w": 16, + "h": 16 + } + }, { "filename": "wl_super_potion", "rotated": false, @@ -7563,12 +7563,33 @@ "h": 18 }, "frame": { - "x": 298, + "x": 261, "y": 345, "w": 20, "h": 18 } }, + { + "filename": "blastoisinite", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 16, + "h": 16 + }, + "frame": { + "x": 261, + "y": 363, + "w": 16, + "h": 16 + } + }, { "filename": "blazikenite", "rotated": false, @@ -7584,8 +7605,8 @@ "h": 16 }, "frame": { - "x": 318, - "y": 344, + "x": 281, + "y": 345, "w": 16, "h": 16 } @@ -7605,8 +7626,8 @@ "h": 16 }, "frame": { - "x": 334, - "y": 326, + "x": 261, + "y": 379, "w": 16, "h": 16 } @@ -7626,8 +7647,8 @@ "h": 16 }, "frame": { - "x": 334, - "y": 342, + "x": 297, + "y": 345, "w": 16, "h": 16 } @@ -7647,8 +7668,8 @@ "h": 16 }, "frame": { - "x": 350, - "y": 325, + "x": 261, + "y": 395, "w": 16, "h": 16 } @@ -7668,8 +7689,8 @@ "h": 16 }, "frame": { - "x": 350, - "y": 341, + "x": 313, + "y": 345, "w": 16, "h": 16 } @@ -7689,8 +7710,8 @@ "h": 16 }, "frame": { - "x": 366, - "y": 327, + "x": 217, + "y": 414, "w": 16, "h": 16 } @@ -7710,8 +7731,8 @@ "h": 16 }, "frame": { - "x": 366, - "y": 343, + "x": 233, + "y": 412, "w": 16, "h": 16 } @@ -7731,8 +7752,8 @@ "h": 16 }, "frame": { - "x": 382, - "y": 331, + "x": 249, + "y": 412, "w": 16, "h": 16 } @@ -7752,8 +7773,8 @@ "h": 16 }, "frame": { - "x": 398, - "y": 331, + "x": 265, + "y": 411, "w": 16, "h": 16 } @@ -7773,8 +7794,8 @@ "h": 16 }, "frame": { - "x": 414, - "y": 329, + "x": 329, + "y": 345, "w": 16, "h": 16 } @@ -7794,8 +7815,8 @@ "h": 16 }, "frame": { - "x": 382, - "y": 347, + "x": 277, + "y": 363, "w": 16, "h": 16 } @@ -7815,8 +7836,8 @@ "h": 16 }, "frame": { - "x": 398, - "y": 347, + "x": 277, + "y": 379, "w": 16, "h": 16 } @@ -7836,8 +7857,8 @@ "h": 16 }, "frame": { - "x": 414, - "y": 345, + "x": 277, + "y": 395, "w": 16, "h": 16 } @@ -7857,8 +7878,8 @@ "h": 16 }, "frame": { - "x": 281, - "y": 363, + "x": 293, + "y": 361, "w": 16, "h": 16 } @@ -7878,8 +7899,8 @@ "h": 16 }, "frame": { - "x": 281, - "y": 379, + "x": 309, + "y": 361, "w": 16, "h": 16 } @@ -7899,8 +7920,8 @@ "h": 16 }, "frame": { - "x": 297, - "y": 363, + "x": 293, + "y": 377, "w": 16, "h": 16 } @@ -7920,8 +7941,8 @@ "h": 16 }, "frame": { - "x": 281, - "y": 395, + "x": 325, + "y": 361, "w": 16, "h": 16 } @@ -7941,8 +7962,8 @@ "h": 16 }, "frame": { - "x": 297, - "y": 379, + "x": 293, + "y": 393, "w": 16, "h": 16 } @@ -7962,8 +7983,8 @@ "h": 16 }, "frame": { - "x": 281, - "y": 411, + "x": 309, + "y": 377, "w": 16, "h": 16 } @@ -7983,8 +8004,8 @@ "h": 16 }, "frame": { - "x": 297, - "y": 395, + "x": 309, + "y": 393, "w": 16, "h": 16 } @@ -8004,8 +8025,8 @@ "h": 16 }, "frame": { - "x": 297, - "y": 411, + "x": 325, + "y": 377, "w": 16, "h": 16 } @@ -8025,8 +8046,8 @@ "h": 16 }, "frame": { - "x": 313, - "y": 363, + "x": 325, + "y": 393, "w": 16, "h": 16 } @@ -8046,8 +8067,8 @@ "h": 16 }, "frame": { - "x": 313, - "y": 379, + "x": 281, + "y": 411, "w": 16, "h": 16 } @@ -8067,8 +8088,8 @@ "h": 16 }, "frame": { - "x": 313, - "y": 395, + "x": 297, + "y": 409, "w": 16, "h": 16 } @@ -8089,7 +8110,7 @@ }, "frame": { "x": 313, - "y": 411, + "y": 409, "w": 16, "h": 16 } @@ -8109,8 +8130,8 @@ "h": 16 }, "frame": { - "x": 350, - "y": 357, + "x": 329, + "y": 409, "w": 16, "h": 16 } @@ -8130,8 +8151,8 @@ "h": 16 }, "frame": { - "x": 366, - "y": 359, + "x": 341, + "y": 361, "w": 16, "h": 16 } @@ -8151,8 +8172,8 @@ "h": 16 }, "frame": { - "x": 334, - "y": 358, + "x": 341, + "y": 377, "w": 16, "h": 16 } @@ -8172,8 +8193,8 @@ "h": 16 }, "frame": { - "x": 382, - "y": 363, + "x": 341, + "y": 393, "w": 16, "h": 16 } @@ -8193,8 +8214,8 @@ "h": 16 }, "frame": { - "x": 398, - "y": 363, + "x": 345, + "y": 409, "w": 16, "h": 16 } @@ -8214,7 +8235,7 @@ "h": 16 }, "frame": { - "x": 414, + "x": 412, "y": 361, "w": 16, "h": 16 @@ -8235,8 +8256,8 @@ "h": 16 }, "frame": { - "x": 329, - "y": 374, + "x": 357, + "y": 363, "w": 16, "h": 16 } @@ -8256,8 +8277,8 @@ "h": 16 }, "frame": { - "x": 329, - "y": 390, + "x": 357, + "y": 379, "w": 16, "h": 16 } @@ -8277,8 +8298,8 @@ "h": 16 }, "frame": { - "x": 329, - "y": 406, + "x": 373, + "y": 363, "w": 16, "h": 16 } @@ -8298,8 +8319,8 @@ "h": 16 }, "frame": { - "x": 345, - "y": 374, + "x": 373, + "y": 379, "w": 16, "h": 16 } @@ -8319,8 +8340,8 @@ "h": 16 }, "frame": { - "x": 345, - "y": 390, + "x": 389, + "y": 363, "w": 16, "h": 16 } @@ -8340,8 +8361,8 @@ "h": 16 }, "frame": { - "x": 345, - "y": 406, + "x": 389, + "y": 379, "w": 16, "h": 16 } @@ -8361,8 +8382,8 @@ "h": 16 }, "frame": { - "x": 361, - "y": 375, + "x": 405, + "y": 377, "w": 16, "h": 16 } @@ -8383,7 +8404,7 @@ }, "frame": { "x": 361, - "y": 391, + "y": 395, "w": 16, "h": 16 } @@ -8403,8 +8424,8 @@ "h": 16 }, "frame": { - "x": 361, - "y": 407, + "x": 377, + "y": 395, "w": 16, "h": 16 } @@ -8415,6 +8436,6 @@ "meta": { "app": "https://www.codeandweb.com/texturepacker", "version": "3.0", - "smartupdate": "$TexturePacker:SmartUpdate:9ef21166268f7487fc9ff8d0f9b996e4:82658ac7bdd4c2b417e1f59168179262:110e074689c9edd2c54833ce2e4d9270$" + "smartupdate": "$TexturePacker:SmartUpdate:875c6d67e72590dfc6d319101aa31cfa:dd2bb865ecbc5ac7b975ddf70b993334:110e074689c9edd2c54833ce2e4d9270$" } } diff --git a/public/images/items.png b/public/images/items.png index 8aaa0281c008..1bd7b3af9c33 100644 Binary files a/public/images/items.png and b/public/images/items.png differ diff --git a/public/images/items/pb_silver.png b/public/images/items/pb_silver.png new file mode 100644 index 000000000000..f60a8348a94d Binary files /dev/null and b/public/images/items/pb_silver.png differ diff --git a/public/images/trainer/future_self_f.json b/public/images/trainer/future_self_f.json new file mode 100644 index 000000000000..0831828c2bc8 --- /dev/null +++ b/public/images/trainer/future_self_f.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "future_self_f.png", + "format": "RGBA8888", + "size": { + "w": 29, + "h": 69 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 69, + "h": 69 + }, + "spriteSourceSize": { + "x": 21, + "y": 0, + "w": 29, + "h": 69 + }, + "frame": { + "x": 0, + "y": 0, + "w": 29, + "h": 69 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:4eb16332c2e77886e4e621b62269f05e:26f1bc53c853efdbe228d67604b95b54:d25525a5db42bd57d2afe4b6e3081ee1$" + } +} diff --git a/public/images/trainer/future_self_f.png b/public/images/trainer/future_self_f.png new file mode 100644 index 000000000000..e75c5e5e65b7 Binary files /dev/null and b/public/images/trainer/future_self_f.png differ diff --git a/public/images/trainer/future_self_m.json b/public/images/trainer/future_self_m.json new file mode 100644 index 000000000000..f5d60ff5add0 --- /dev/null +++ b/public/images/trainer/future_self_m.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "future_self_m.png", + "format": "RGBA8888", + "size": { + "w": 36, + "h": 73 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 73, + "h": 73 + }, + "spriteSourceSize": { + "x": 18, + "y": 0, + "w": 36, + "h": 73 + }, + "frame": { + "x": 0, + "y": 0, + "w": 36, + "h": 73 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:0d8d68d06ae75bc93d72b54183a9df02:d3d509801da9ff5c0bd4793a05ece391:83c4b8c2ed25ea7d9795bec5c40c8602$" + } +} diff --git a/public/images/trainer/future_self_m.png b/public/images/trainer/future_self_m.png new file mode 100644 index 000000000000..edd15cb6ea3a Binary files /dev/null and b/public/images/trainer/future_self_m.png differ diff --git a/public/images/ui/legacy/link_icon.png b/public/images/ui/legacy/link_icon.png new file mode 100644 index 000000000000..56081261b9c6 Binary files /dev/null and b/public/images/ui/legacy/link_icon.png differ diff --git a/public/images/ui/legacy/unlink_icon.png b/public/images/ui/legacy/unlink_icon.png new file mode 100644 index 000000000000..f0da5f8e3eda Binary files /dev/null and b/public/images/ui/legacy/unlink_icon.png differ diff --git a/public/images/ui/link_icon.png b/public/images/ui/link_icon.png new file mode 100644 index 000000000000..56081261b9c6 Binary files /dev/null and b/public/images/ui/link_icon.png differ diff --git a/public/images/ui/unlink_icon.png b/public/images/ui/unlink_icon.png new file mode 100644 index 000000000000..f0da5f8e3eda Binary files /dev/null and b/public/images/ui/unlink_icon.png differ diff --git a/public/locales b/public/locales index fc4a1effd517..87615556d8a2 160000 --- a/public/locales +++ b/public/locales @@ -1 +1 @@ -Subproject commit fc4a1effd5170def3c8314208a52cd0d8e6913ef +Subproject commit 87615556d8a2bd7eef7abac818f84423a8a13b03 diff --git a/src/battle-scene.ts b/src/battle-scene.ts index a21e1b093425..3cbf4d7b4226 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -1387,7 +1387,7 @@ export default class BattleScene extends SceneBase { case Species.ZYGARDE: return Utils.randSeedInt(4); case Species.MINIOR: - return Utils.randSeedInt(6); + return Utils.randSeedInt(7); case Species.ALCREMIE: return Utils.randSeedInt(9); case Species.MEOWSTIC: diff --git a/src/data/ability.ts b/src/data/ability.ts index ebdd5105bb4c..d761657f5cd1 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -7,7 +7,7 @@ import { Weather, WeatherType } from "./weather"; import { BattlerTag, GroundedTag } from "./battler-tags"; import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect"; import { Gender } from "./gender"; -import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, ChargeAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move"; +import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move"; import { ArenaTagSide, ArenaTrapTag } from "./arena-tag"; import { BerryModifier, PokemonHeldItemModifier } from "../modifier/modifier"; import { TerrainType } from "./terrain"; @@ -1139,7 +1139,9 @@ export class MoveEffectChanceMultiplierAbAttr extends AbAttr { apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { // Disable showAbility during getTargetBenefitScore this.showAbility = args[4]; - if ((args[0] as Utils.NumberHolder).value <= 0 || (args[1] as Move).id === Moves.ORDER_UP) { + + const exceptMoves = [ Moves.ORDER_UP, Moves.ELECTRO_SHOT ]; + if ((args[0] as Utils.NumberHolder).value <= 0 || exceptMoves.includes((args[1] as Move).id)) { return false; } @@ -1329,7 +1331,6 @@ export class AddSecondStrikeAbAttr extends PreAttackAbAttr { */ const exceptAttrs: Constructor[] = [ MultiHitAttr, - ChargeAttr, SacrificialAttr, SacrificialAttrOnHit ]; @@ -1345,6 +1346,7 @@ export class AddSecondStrikeAbAttr extends PreAttackAbAttr { /** Also check if this move is an Attack move and if it's only targeting one Pokemon */ return numTargets === 1 + && !move.isChargingMove() && !exceptAttrs.some(attr => move.hasAttr(attr)) && !exceptMoves.some(id => move.id === id) && move.category !== MoveCategory.STATUS; @@ -2420,11 +2422,12 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr { super(true); } - applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean { + async applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): Promise { const targets = pokemon.getOpponents(); if (simulated || !targets.length) { return simulated; } + const promises: Promise[] = []; let target: Pokemon; if (targets.length > 1) { @@ -2433,7 +2436,7 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr { target = targets[0]; } - target = target!; // compiler doesn't know its guranteed to be defined + target = target!; pokemon.summonData.speciesForm = target.getSpeciesForm(); pokemon.summonData.fusionSpeciesForm = target.getFusionSpeciesForm(); pokemon.summonData.ability = target.getAbility().id; @@ -2450,18 +2453,23 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr { pokemon.setStatStage(s, target.getStatStage(s)); } - pokemon.summonData.moveset = target.getMoveset().map(m => new PokemonMove(m?.moveId ?? Moves.NONE, m?.ppUsed, m?.ppUp)); + pokemon.summonData.moveset = target.getMoveset().map(m => { + const pp = m?.getMove().pp ?? 0; + // if PP value is less than 5, do nothing. If greater, we need to reduce the value to 5 using a negative ppUp value. + const ppUp = pp <= 5 ? 0 : (5 - pp) / Math.max(Math.floor(pp / 5), 1); + return new PokemonMove(m?.moveId ?? Moves.NONE, 0, ppUp); + }); pokemon.summonData.types = target.getTypes(); + promises.push(pokemon.updateInfo()); - + pokemon.scene.queueMessage(i18next.t("abilityTriggers:postSummonTransform", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), targetName: target!.name, })); pokemon.scene.playSound("battle_anims/PRSFX- Transform"); - - pokemon.loadAssets(false).then(() => { + promises.push(pokemon.loadAssets(false).then(() => { pokemon.playAnim(); pokemon.updateInfo(); - }); + })); - pokemon.scene.queueMessage(i18next.t("abilityTriggers:postSummonTransform", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), targetName: target.name, })); + await Promise.all(promises); return true; } diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index aa6aec6f73a3..d2c95b7ccdf4 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -970,6 +970,9 @@ export class GravityTag extends ArenaTag { if (pokemon !== null) { pokemon.removeTag(BattlerTagType.FLOATING); pokemon.removeTag(BattlerTagType.TELEKINESIS); + if (pokemon.getTag(BattlerTagType.FLYING)) { + pokemon.addTag(BattlerTagType.INTERRUPTED); + } } }); } diff --git a/src/data/balance/pokemon-evolutions.ts b/src/data/balance/pokemon-evolutions.ts index c838f6b2c497..7d0d86fadbf8 100644 --- a/src/data/balance/pokemon-evolutions.ts +++ b/src/data/balance/pokemon-evolutions.ts @@ -10,7 +10,7 @@ import { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import { TimeOfDay } from "#enums/time-of-day"; -import { DamageMoneyRewardModifier, ExtraModifierModifier, MoneyMultiplierModifier } from "#app/modifier/modifier"; +import { DamageMoneyRewardModifier, ExtraModifierModifier, MoneyMultiplierModifier, TempExtraModifierModifier } from "#app/modifier/modifier"; import { SpeciesFormKey } from "#enums/species-form-key"; @@ -1652,11 +1652,11 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesFormEvolution(Species.GHOLDENGO, "chest", "", 1, null, new SpeciesEvolutionCondition(p => p.evoCounter + p.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length + p.scene.findModifiers(m => m instanceof MoneyMultiplierModifier - || m instanceof ExtraModifierModifier).length > 9), SpeciesWildEvolutionDelay.VERY_LONG), + || m instanceof ExtraModifierModifier || m instanceof TempExtraModifierModifier).length > 9), SpeciesWildEvolutionDelay.VERY_LONG), new SpeciesFormEvolution(Species.GHOLDENGO, "roaming", "", 1, null, new SpeciesEvolutionCondition(p => p.evoCounter + p.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length + p.scene.findModifiers(m => m instanceof MoneyMultiplierModifier - || m instanceof ExtraModifierModifier).length > 9), SpeciesWildEvolutionDelay.VERY_LONG) + || m instanceof ExtraModifierModifier || m instanceof TempExtraModifierModifier).length > 9), SpeciesWildEvolutionDelay.VERY_LONG) ] }; diff --git a/src/data/balance/pokemon-level-moves.ts b/src/data/balance/pokemon-level-moves.ts index b5608093df26..53f547c45044 100644 --- a/src/data/balance/pokemon-level-moves.ts +++ b/src/data/balance/pokemon-level-moves.ts @@ -93,6 +93,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.GROWL ], [ 1, Moves.EMBER ], [ 1, Moves.SMOKESCREEN ], + [ 1, Moves.FIRE_SPIN ], // Previous Stage Move [ 12, Moves.DRAGON_BREATH ], [ 19, Moves.FIRE_FANG ], [ 24, Moves.SLASH ], @@ -174,6 +175,9 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.METAPOD]: [ [ EVOLVE_MOVE, Moves.HARDEN ], + [ RELEARN_MOVE, Moves.TACKLE ], // Previous Stage Move + [ RELEARN_MOVE, Moves.STRING_SHOT ], // Previous Stage Move + [ RELEARN_MOVE, Moves.BUG_BITE ], // Previous Stage Move [ 1, Moves.HARDEN ], ], [Species.BUTTERFREE]: [ @@ -203,10 +207,17 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.KAKUNA]: [ [ EVOLVE_MOVE, Moves.HARDEN ], + [ RELEARN_MOVE, Moves.POISON_STING ], // Previous Stage Move + [ RELEARN_MOVE, Moves.STRING_SHOT ], // Previous Stage Move + [ RELEARN_MOVE, Moves.BUG_BITE ], // Previous Stage Move [ 1, Moves.HARDEN ], ], [Species.BEEDRILL]: [ [ EVOLVE_MOVE, Moves.TWINEEDLE ], + [ 1, Moves.POISON_STING ], // Previous Stage Move + [ 1, Moves.STRING_SHOT ], // Previous Stage Move + [ 1, Moves.HARDEN ], // Previous Stage Move + [ 1, Moves.BUG_BITE ], // Previous Stage Move [ 1, Moves.FURY_ATTACK ], [ 11, Moves.FURY_CUTTER ], [ 14, Moves.RAGE ], @@ -454,6 +465,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.POISON_STING ], [ 1, Moves.DEFENSE_CURL ], [ 1, Moves.CRUSH_CLAW ], + [ 1, Moves.AGILITY ], // Previous Stage Move [ 9, Moves.ROLLOUT ], [ 12, Moves.FURY_CUTTER ], [ 15, Moves.RAPID_SPIN ], @@ -961,6 +973,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.SCRATCH ], [ 1, Moves.LEER ], [ 1, Moves.FOCUS_ENERGY ], + [ 1, Moves.COVET ], // Previous Stage Move [ 1, Moves.FLING ], [ 5, Moves.FURY_SWIPES ], [ 8, Moves.LOW_KICK ], @@ -1044,10 +1057,6 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.POLIWRATH]: [ [ EVOLVE_MOVE, Moves.DYNAMIC_PUNCH ], - [ 1, Moves.BUBBLE_BEAM ], - [ 1, Moves.BODY_SLAM ], - [ 1, Moves.HYPNOSIS ], - [ 1, Moves.WATER_SPORT ], [ RELEARN_MOVE, Moves.POUND ], [ RELEARN_MOVE, Moves.DOUBLE_EDGE ], [ RELEARN_MOVE, Moves.WATER_GUN ], @@ -1057,13 +1066,18 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ RELEARN_MOVE, Moves.MUD_SHOT ], [ RELEARN_MOVE, Moves.EARTH_POWER ], [ RELEARN_MOVE, Moves.CIRCLE_THROW ], + [ 1, Moves.BUBBLE_BEAM ], + [ 1, Moves.BODY_SLAM ], + [ 1, Moves.HYPNOSIS ], + [ 1, Moves.WATER_SPORT ], ], [Species.ABRA]: [ [ 1, Moves.TELEPORT ], - [ 1, Moves.CONFUSION ], //Custom + [ 1, Moves.CONFUSION ], // Custom ], [Species.KADABRA]: [ - [ EVOLVE_MOVE, Moves.PSYBEAM ], //LGPE + [ EVOLVE_MOVE, Moves.PSYBEAM ], // LGPE + [ 1, Moves.CONFUSION ], // Previous Stage Move, Custom [ 1, Moves.DISABLE ], [ 1, Moves.TELEPORT ], [ 1, Moves.KINESIS ], @@ -1184,10 +1198,18 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ RELEARN_MOVE, Moves.STOCKPILE ], [ RELEARN_MOVE, Moves.SWALLOW ], [ RELEARN_MOVE, Moves.SPIT_UP ], + [ RELEARN_MOVE, Moves.WRAP ], // Previous Stage Move + [ RELEARN_MOVE, Moves.GROWTH ], // Previous Stage Move + [ RELEARN_MOVE, Moves.ACID ], // Previous Stage Move + [ RELEARN_MOVE, Moves.KNOCK_OFF ], // Previous Stage Move [ RELEARN_MOVE, Moves.GASTRO_ACID ], + [ RELEARN_MOVE, Moves.POISON_JAB ], // Previous Stage Move + [ RELEARN_MOVE, Moves.SLAM ], // Previous Stage Move [ RELEARN_MOVE, Moves.POWER_WHIP ], [ 1, Moves.VINE_WHIP ], [ 1, Moves.SLEEP_POWDER ], + [ 1, Moves.POISON_POWDER ], // Previous Stage Move + [ 1, Moves.STUN_SPORE ], // Previous Stage Move [ 1, Moves.SWEET_SCENT ], [ 1, Moves.RAZOR_LEAF ], [ 44, Moves.LEAF_BLADE ], @@ -1262,6 +1284,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.TACKLE ], [ 1, Moves.DEFENSE_CURL ], [ 1, Moves.ROCK_POLISH ], + [ 1, Moves.ROLLOUT ], // Previous Stage Move [ 1, Moves.HEAVY_SLAM ], [ 16, Moves.ROCK_THROW ], [ 18, Moves.SMACK_DOWN ], @@ -1548,7 +1571,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.GASTLY]: [ [ 1, Moves.CONFUSE_RAY ], [ 1, Moves.LICK ], - [ 1, Moves.ACID ], //Custom + [ 1, Moves.ACID ], // Custom [ 4, Moves.HYPNOSIS ], [ 8, Moves.MEAN_LOOK ], [ 12, Moves.PAYBACK ], @@ -1567,6 +1590,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.HYPNOSIS ], [ 1, Moves.CONFUSE_RAY ], [ 1, Moves.LICK ], + [ 1, Moves.ACID ], // Previous Stage Move, Custom [ 1, Moves.MEAN_LOOK ], [ 12, Moves.PAYBACK ], [ 16, Moves.SPITE ], @@ -1583,6 +1607,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.HYPNOSIS ], [ 1, Moves.CONFUSE_RAY ], [ 1, Moves.LICK ], + [ 1, Moves.ACID ], // Previous Stage Move, Custom [ 1, Moves.PERISH_SONG ], [ 1, Moves.MEAN_LOOK ], [ 1, Moves.SHADOW_PUNCH ], @@ -1609,7 +1634,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 12, Moves.DRAGON_BREATH ], [ 16, Moves.CURSE ], [ 20, Moves.ROCK_SLIDE ], - [ 22, Moves.GYRO_BALL ], //Custom, from USUM + [ 22, Moves.GYRO_BALL ], // Custom, from USUM [ 24, Moves.SCREECH ], [ 28, Moves.SAND_TOMB ], [ 32, Moves.STEALTH_ROCK ], @@ -1849,7 +1874,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.LICKITUNG]: [ [ 1, Moves.DEFENSE_CURL ], [ 1, Moves.LICK ], - [ 1, Moves.TACKLE ], //Custom + [ 1, Moves.TACKLE ], // Custom [ 6, Moves.REST ], [ 12, Moves.SUPERSONIC ], [ 18, Moves.WRAP ], @@ -2090,6 +2115,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.MR_MIME]: [ [ 1, Moves.POUND ], + [ 1, Moves.TICKLE ], // Previous Stage Move [ 1, Moves.BATON_PASS ], [ 1, Moves.ENCORE ], [ 1, Moves.COPYCAT ], @@ -2122,7 +2148,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 20, Moves.DOUBLE_HIT ], [ 24, Moves.SLASH ], [ 28, Moves.FOCUS_ENERGY ], - [ 30, Moves.STEEL_WING ], //Custom + [ 30, Moves.STEEL_WING ], // Custom [ 32, Moves.AGILITY ], [ 36, Moves.AIR_SLASH ], [ 40, Moves.X_SCISSOR ], @@ -2279,6 +2305,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.VAPOREON]: [ [ EVOLVE_MOVE, Moves.BOUNCY_BUBBLE ], + [ RELEARN_MOVE, Moves.VEEVEE_VOLLEY ], // Previous Stage Move [ 1, Moves.TACKLE ], [ 1, Moves.TAKE_DOWN ], [ 1, Moves.DOUBLE_EDGE ], @@ -2306,6 +2333,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.JOLTEON]: [ [ EVOLVE_MOVE, Moves.BUZZY_BUZZ ], + [ RELEARN_MOVE, Moves.VEEVEE_VOLLEY ], // Previous Stage Move [ 1, Moves.TACKLE ], [ 1, Moves.TAKE_DOWN ], [ 1, Moves.DOUBLE_EDGE ], @@ -2333,6 +2361,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.FLAREON]: [ [ EVOLVE_MOVE, Moves.SIZZLY_SLIDE ], + [ RELEARN_MOVE, Moves.VEEVEE_VOLLEY ], // Previous Stage Move [ 1, Moves.TACKLE ], [ 1, Moves.TAKE_DOWN ], [ 1, Moves.DOUBLE_EDGE ], @@ -2463,6 +2492,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.SNORLAX]: [ [ 1, Moves.TACKLE ], [ 1, Moves.SCREECH ], + [ 1, Moves.ODOR_SLEUTH ], // Previous Stage Move [ 1, Moves.DEFENSE_CURL ], [ 1, Moves.METRONOME ], [ 1, Moves.LICK ], @@ -2632,7 +2662,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.CHIKORITA]: [ [ 1, Moves.TACKLE ], [ 1, Moves.GROWL ], - [ 5, Moves.RAZOR_LEAF ], //Custom, moved from 6 to 5 + [ 5, Moves.RAZOR_LEAF ], // Custom, moved from 6 to 5 [ 9, Moves.POISON_POWDER ], [ 12, Moves.SYNTHESIS ], [ 17, Moves.REFLECT ], @@ -2682,8 +2712,8 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.CYNDAQUIL]: [ [ 1, Moves.TACKLE ], [ 1, Moves.LEER ], - [ 5, Moves.EMBER ], //Custom, moved from 10 to 5 - [ 10, Moves.SMOKESCREEN ], //Custom, moved from 6 to 10 + [ 5, Moves.EMBER ], // Custom, moved from 10 to 5 + [ 10, Moves.SMOKESCREEN ], // Custom, moved from 6 to 10 [ 13, Moves.QUICK_ATTACK ], [ 19, Moves.FLAME_WHEEL ], [ 22, Moves.DEFENSE_CURL ], @@ -2737,7 +2767,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.TOTODILE]: [ [ 1, Moves.SCRATCH ], [ 1, Moves.LEER ], - [ 5, Moves.WATER_GUN ], //Custom, moved from 6 to 5 + [ 5, Moves.WATER_GUN ], // Custom, moved from 6 to 5 [ 9, Moves.BITE ], [ 13, Moves.SCARY_FACE ], [ 19, Moves.ICE_FANG ], @@ -3149,6 +3179,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.MOONBLAST ], ], [Species.MARILL]: [ + [ 1, Moves.SPLASH ], // Previous Stage Move [ 1, Moves.TACKLE ], [ 1, Moves.TAIL_WHIP ], [ 1, Moves.WATER_GUN ], @@ -3168,6 +3199,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 36, Moves.SUPERPOWER ], ], [Species.AZUMARILL]: [ + [ 1, Moves.SPLASH ], // Previous Stage Move [ 1, Moves.TACKLE ], [ 1, Moves.TAIL_WHIP ], [ 1, Moves.WATER_GUN ], @@ -3189,6 +3221,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.SUDOWOODO]: [ [ EVOLVE_MOVE, Moves.SLAM ], [ 1, Moves.ROCK_THROW ], + [ 1, Moves.TACKLE ], // Previous Stage Move, Custom [ 1, Moves.FLAIL ], [ 1, Moves.FAKE_TEARS ], [ 1, Moves.HAMMER_ARM ], @@ -3222,6 +3255,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.HYDRO_PUMP ], [ 1, Moves.BELLY_DRUM ], [ 1, Moves.POUND ], + [ 1, Moves.WATER_SPORT ], // Previous Stage Move ], [Species.HOPPIP]: [ [ 1, Moves.TACKLE ], @@ -3315,9 +3349,12 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 39, Moves.SEED_BOMB ], ], [Species.SUNFLORA]: [ + [ RELEARN_MOVE, Moves.SEED_BOMB ], // Previous Stage Move [ 1, Moves.POUND ], [ 1, Moves.TACKLE ], [ 1, Moves.GROWTH ], + [ 1, Moves.ENDEAVOR ], // Previous Stage Move + [ 1, Moves.SYNTHESIS ], // Previous Stage Move [ 4, Moves.INGRAIN ], [ 7, Moves.ABSORB ], [ 10, Moves.MEGA_DRAIN ], @@ -3382,6 +3419,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.ESPEON]: [ [ EVOLVE_MOVE, Moves.GLITZY_GLOW ], + [ RELEARN_MOVE, Moves.VEEVEE_VOLLEY ], // Previous Stage Move [ 1, Moves.TACKLE ], [ 1, Moves.TAKE_DOWN ], [ 1, Moves.DOUBLE_EDGE ], @@ -3408,6 +3446,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.UMBREON]: [ [ EVOLVE_MOVE, Moves.BADDY_BAD ], + [ RELEARN_MOVE, Moves.VEEVEE_VOLLEY ], // Previous Stage Move [ 1, Moves.TACKLE ], [ 1, Moves.TAKE_DOWN ], [ 1, Moves.DOUBLE_EDGE ], @@ -3464,6 +3503,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 15, Moves.DISABLE ], [ 18, Moves.WATER_PULSE ], [ 21, Moves.HEADBUTT ], + [ 24, Moves.ZEN_HEADBUTT ], // Previous Stage Move, Galar Slowking Level [ 27, Moves.AMNESIA ], [ 30, Moves.SURF ], [ 33, Moves.SLACK_OFF ], @@ -3562,7 +3602,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.DUNSPARCE]: [ [ 1, Moves.DEFENSE_CURL ], [ 1, Moves.FLAIL ], - [ 1, Moves.TACKLE ], //Custom + [ 1, Moves.TACKLE ], // Custom [ 4, Moves.MUD_SLAP ], [ 8, Moves.ROLLOUT ], [ 12, Moves.GLARE ], @@ -3609,6 +3649,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 12, Moves.DRAGON_BREATH ], [ 16, Moves.CURSE ], [ 20, Moves.ROCK_SLIDE ], + [ 22, Moves.GYRO_BALL ], // Custom from USUM [ 24, Moves.SCREECH ], [ 28, Moves.SAND_TOMB ], [ 32, Moves.STEALTH_ROCK ], @@ -3688,6 +3729,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 20, Moves.DOUBLE_HIT ], [ 24, Moves.SLASH ], [ 28, Moves.FOCUS_ENERGY ], + [ 30, Moves.STEEL_WING ], // Custom [ 32, Moves.IRON_DEFENSE ], [ 36, Moves.IRON_HEAD ], [ 40, Moves.X_SCISSOR ], @@ -3765,8 +3807,11 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.SCRATCH ], [ 1, Moves.LEER ], [ 1, Moves.LICK ], - [ 1, Moves.FAKE_TEARS ], [ 1, Moves.COVET ], + [ 1, Moves.FLING ], // Previous Stage Move + [ 1, Moves.BABY_DOLL_EYES ], // Previous Stage Move + [ 1, Moves.FAKE_TEARS ], + [ 1, Moves.CHARM ], // Previous Stage Move [ 8, Moves.FURY_SWIPES ], [ 13, Moves.PAYBACK ], [ 17, Moves.SWEET_SCENT ], @@ -3783,7 +3828,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.SLUGMA]: [ [ 1, Moves.SMOG ], [ 1, Moves.YAWN ], - [ 5, Moves.EMBER ], //Custom, Moved from Level 6 to 5 + [ 5, Moves.EMBER ], // Custom, Moved from Level 6 to 5 [ 8, Moves.ROCK_THROW ], [ 13, Moves.HARDEN ], [ 20, Moves.CLEAR_SMOG ], @@ -3898,7 +3943,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 48, Moves.SOAK ], [ 54, Moves.HYPER_BEAM ], ], - [Species.DELIBIRD]: [ //Given a custom level up learnset + [Species.DELIBIRD]: [ // Given a custom level up learnset [ 1, Moves.PRESENT ], [ 1, Moves.METRONOME ], [ 5, Moves.FAKE_OUT ], @@ -3991,6 +4036,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 62, Moves.INFERNO ], ], [Species.KINGDRA]: [ + [ RELEARN_MOVE, Moves.LASER_FOCUS ], // Previous Stage Move [ 1, Moves.LEER ], [ 1, Moves.WATER_GUN ], [ 1, Moves.SMOKESCREEN ], @@ -4025,9 +4071,17 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.DONPHAN]: [ [ EVOLVE_MOVE, Moves.FURY_ATTACK ], - [ 1, Moves.HORN_ATTACK ], + [ 1, Moves.TACKLE ], // Previous Stage Move [ 1, Moves.GROWL ], + [ 1, Moves.HORN_ATTACK ], [ 1, Moves.DEFENSE_CURL ], + [ 1, Moves.ODOR_SLEUTH ], // Previous Stage Move + [ 1, Moves.FLAIL ], // Previous Stage Move + [ 1, Moves.ENDURE ], // Previous Stage Move + [ 1, Moves.TAKE_DOWN ], // Previous Stage Move + [ 1, Moves.CHARM ], // Previous Stage Move + [ 1, Moves.LAST_RESORT ], // Previous Stage Move + [ 1, Moves.DOUBLE_EDGE ], // Previous Stage Move [ 1, Moves.THUNDER_FANG ], [ 1, Moves.FIRE_FANG ], [ 1, Moves.BULLDOZE ], @@ -4047,6 +4101,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.CONVERSION ], [ 1, Moves.RECYCLE ], [ 1, Moves.MAGNET_RISE ], + [ 1, Moves.MAGIC_COAT ], // Previous Stage Move [ 15, Moves.THUNDER_SHOCK ], [ 20, Moves.PSYBEAM ], [ 25, Moves.CONVERSION_2 ], @@ -4513,6 +4568,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.MARSHTOMP]: [ [ EVOLVE_MOVE, Moves.MUD_SHOT ], + [ RELEARN_MOVE, Moves.SURF ], // Previous Stage Move [ RELEARN_MOVE, Moves.ROCK_SMASH ], [ 1, Moves.TACKLE ], [ 1, Moves.GROWL ], @@ -4634,10 +4690,15 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.SILCOON]: [ [ EVOLVE_MOVE, Moves.HARDEN ], + [ RELEARN_MOVE, Moves.TACKLE ], // Previous Stage Move + [ RELEARN_MOVE, Moves.STRING_SHOT ], // Previous Stage Move + [ RELEARN_MOVE, Moves.POISON_STING ], // Previous Stage Move + [ RELEARN_MOVE, Moves.BUG_BITE ], // Previous Stage Move [ 1, Moves.HARDEN ], ], [Species.BEAUTIFLY]: [ [ EVOLVE_MOVE, Moves.GUST ], + [ 1, Moves.TACKLE ], // Previous Stage Move [ 1, Moves.BUG_BITE ], [ 1, Moves.GUST ], [ 1, Moves.HARDEN ], @@ -4658,10 +4719,15 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.CASCOON]: [ [ EVOLVE_MOVE, Moves.HARDEN ], + [ RELEARN_MOVE, Moves.TACKLE ], // Previous Stage Move + [ RELEARN_MOVE, Moves.STRING_SHOT ], // Previous Stage Move + [ RELEARN_MOVE, Moves.POISON_STING ], // Previous Stage Move + [ RELEARN_MOVE, Moves.BUG_BITE ], // Previous Stage Move [ 1, Moves.HARDEN ], ], [Species.DUSTOX]: [ [ EVOLVE_MOVE, Moves.GUST ], + [ 1, Moves.TACKLE ], // Previous Stage Move [ 1, Moves.BUG_BITE ], [ 1, Moves.GUST ], [ 1, Moves.HARDEN ], @@ -4701,6 +4767,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.ABSORB ], [ 1, Moves.FLAIL ], [ 1, Moves.FAKE_OUT ], + [ 1, Moves.RAIN_DANCE ], // Previous Stage Move [ 1, Moves.KNOCK_OFF ], [ 1, Moves.TEETER_DANCE ], [ 1, Moves.ASTONISH ], @@ -4728,6 +4795,8 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ RELEARN_MOVE, Moves.ASTONISH ], [ RELEARN_MOVE, Moves.ENERGY_BALL ], [ RELEARN_MOVE, Moves.ZEN_HEADBUTT ], + [ RELEARN_MOVE, Moves.LEECH_SEED ], // Previous Stage Move + [ RELEARN_MOVE, Moves.GIGA_DRAIN ], // Previous Stage Move [ 1, Moves.FAKE_OUT ], [ 1, Moves.BUBBLE_BEAM ], [ 1, Moves.RAIN_DANCE ], @@ -4757,8 +4826,10 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.EXPLOSION ], [ 1, Moves.TACKLE ], [ 1, Moves.HARDEN ], + [ 1, Moves.BIDE ], // Previous Stage Move [ 1, Moves.ABSORB ], [ 1, Moves.ASTONISH ], + [ 1, Moves.HEADBUTT ], // Previous Stage Move [ 9, Moves.GROWTH ], [ 12, Moves.ROLLOUT ], [ 18, Moves.MEGA_DRAIN ], @@ -4773,11 +4844,13 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ EVOLVE_MOVE, Moves.LEAF_BLADE ], [ RELEARN_MOVE, Moves.WHIRLWIND ], [ RELEARN_MOVE, Moves.TACKLE ], + [ RELEARN_MOVE, Moves.BIDE ], // Previous Stage Move [ RELEARN_MOVE, Moves.ABSORB ], [ RELEARN_MOVE, Moves.MEGA_DRAIN ], [ RELEARN_MOVE, Moves.GROWTH ], [ RELEARN_MOVE, Moves.RAZOR_LEAF ], [ RELEARN_MOVE, Moves.HARDEN ], + [ RELEARN_MOVE, Moves.HEADBUTT ], // Previous Stage Move [ RELEARN_MOVE, Moves.EXPLOSION ], [ RELEARN_MOVE, Moves.ROLLOUT ], [ RELEARN_MOVE, Moves.SWAGGER ], @@ -4930,11 +5003,17 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 38, Moves.STICKY_WEB ], ], [Species.MASQUERAIN]: [ + [ RELEARN_MOVE, Moves.BATON_PASS ], // Previous Stage Move + [ RELEARN_MOVE, Moves.STICKY_WEB ], // Previous Stage Move [ 1, Moves.WHIRLWIND ], [ 1, Moves.WATER_GUN ], [ 1, Moves.QUICK_ATTACK ], [ 1, Moves.SWEET_SCENT ], [ 1, Moves.SOAK ], + [ 1, Moves.BUBBLE_BEAM ], // Previous Stage Move + [ 1, Moves.AGILITY ], // Previous Stage Move + [ 1, Moves.MIST ], // Previous Stage Move + [ 1, Moves.HAZE ], // Previous Stage Move [ 1, Moves.OMINOUS_WIND ], [ 17, Moves.GUST ], [ 22, Moves.SCARY_FACE ], @@ -4963,6 +5042,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ EVOLVE_MOVE, Moves.MACH_PUNCH ], [ RELEARN_MOVE, Moves.SPORE ], [ 1, Moves.POISON_POWDER ], + [ 1, Moves.GIGA_DRAIN ], // Previous Stage Move [ 1, Moves.GROWTH ], [ 1, Moves.TOXIC ], [ 1, Moves.ABSORB ], @@ -4994,9 +5074,16 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 38, Moves.PLAY_ROUGH ], ], [Species.VIGOROTH]: [ + [ RELEARN_MOVE, Moves.PLAY_ROUGH ], // Previous Stage Move [ 1, Moves.SCRATCH ], + [ 1, Moves.YAWN ], // Previous Stage Move [ 1, Moves.FOCUS_ENERGY ], + [ 1, Moves.SLACK_OFF ], // Previous Stage Move [ 1, Moves.ENCORE ], + [ 1, Moves.HEADBUTT ], // Previous Stage Move + [ 1, Moves.AMNESIA ], // Previous Stage Move + [ 1, Moves.COVET ], // Previous Stage Move + [ 1, Moves.FLAIL ], // Previous Stage Move [ 1, Moves.UPROAR ], [ 14, Moves.FURY_SWIPES ], [ 17, Moves.ENDURE ], @@ -5008,11 +5095,20 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.SLAKING]: [ [ EVOLVE_MOVE, Moves.SWAGGER ], + [ RELEARN_MOVE, Moves.PLAY_ROUGH ], // Previous Stage Move + [ RELEARN_MOVE, Moves.FOCUS_PUNCH ], // Previous Stage Move [ 1, Moves.SUCKER_PUNCH ], [ 1, Moves.SCRATCH ], [ 1, Moves.YAWN ], + [ 1, Moves.FOCUS_ENERGY ], // Previous Stage Move [ 1, Moves.ENCORE ], [ 1, Moves.SLACK_OFF ], + [ 1, Moves.UPROAR ], // Previous Stage Move + [ 1, Moves.FURY_SWIPES ], // Previous Stage Move + [ 1, Moves.ENDURE ], // Previous Stage Move + [ 1, Moves.HEADBUTT ], // Previous Stage Move + [ 1, Moves.SLASH ], // Previous Stage Move + [ 1, Moves.REVERSAL ], // Previous Stage Move [ 17, Moves.AMNESIA ], [ 23, Moves.COVET ], [ 27, Moves.THROAT_CHOP ], @@ -5148,6 +5244,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.BRINE ], [ 1, Moves.TACKLE ], [ 1, Moves.FOCUS_ENERGY ], + [ 1, Moves.SAND_ATTACK ], // Previous Stage Move [ 1, Moves.ARM_THRUST ], [ 10, Moves.FAKE_OUT ], [ 13, Moves.FORCE_PALM ], @@ -5351,6 +5448,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.DETECT ], [ 1, Moves.WORK_UP ], [ 1, Moves.BIDE ], + [ 1, Moves.REVERSAL ], // Previous Stage Move [ 12, Moves.ENDURE ], [ 15, Moves.FEINT ], [ 17, Moves.FORCE_PALM ], @@ -5520,6 +5618,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.POISON_GAS ], [ 1, Moves.WRING_OUT ], [ 1, Moves.SLUDGE ], + [ 1, Moves.PAIN_SPLIT ], // Previous Stage Move [ 12, Moves.AMNESIA ], [ 17, Moves.ACID_SPRAY ], [ 20, Moves.ENCORE ], @@ -5565,7 +5664,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.WAILMER]: [ [ 1, Moves.SPLASH ], - [ 1, Moves.TACKLE ], //Custom + [ 1, Moves.TACKLE ], // Custom [ 3, Moves.GROWL ], [ 6, Moves.ASTONISH ], [ 12, Moves.WATER_GUN ], @@ -5586,6 +5685,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.SOAK ], [ 1, Moves.NOBLE_ROAR ], [ 1, Moves.SPLASH ], + [ 1, Moves.TACKLE ], // Previous Stage Move, Custom [ 1, Moves.GROWL ], [ 1, Moves.ASTONISH ], [ 1, Moves.WATER_GUN ], @@ -5620,6 +5720,8 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.CAMERUPT]: [ [ EVOLVE_MOVE, Moves.ROCK_SLIDE ], + [ RELEARN_MOVE, Moves.FLAMETHROWER ], // Previous Stage Move + [ RELEARN_MOVE, Moves.DOUBLE_EDGE ], // Previous Stage Move [ 1, Moves.FISSURE ], [ 1, Moves.ERUPTION ], [ 1, Moves.GROWL ], @@ -5658,7 +5760,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.SPOINK]: [ [ 1, Moves.SPLASH ], - [ 5, Moves.CONFUSION ], //Custom, Moved from Level 7 to 5 + [ 5, Moves.CONFUSION ], // Custom, Moved from Level 7 to 5 [ 10, Moves.GROWL ], [ 14, Moves.PSYBEAM ], [ 18, Moves.PSYCH_UP ], @@ -5676,6 +5778,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.BELCH ], [ 1, Moves.SPLASH ], [ 1, Moves.CONFUSION ], + [ 1, Moves.GROWL ], // Previous Stage Move [ 1, Moves.PSYBEAM ], [ 18, Moves.PSYCH_UP ], [ 22, Moves.CONFUSE_RAY ], @@ -6167,7 +6270,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.SHUPPET]: [ [ 1, Moves.ASTONISH ], - [ 1, Moves.PURSUIT ], //Custom + [ 1, Moves.PURSUIT ], // Custom [ 4, Moves.SCREECH ], [ 7, Moves.NIGHT_SHADE ], [ 10, Moves.SPITE ], @@ -6183,6 +6286,8 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.BANETTE]: [ [ EVOLVE_MOVE, Moves.KNOCK_OFF ], + [ 1, Moves.ASTONISH ], // Previous Stage Move + [ 1, Moves.PURSUIT ], // Previous Stage Move, Custom [ 1, Moves.SCREECH ], [ 1, Moves.NIGHT_SHADE ], [ 1, Moves.SPITE ], @@ -6199,7 +6304,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.DUSKULL]: [ [ 1, Moves.ASTONISH ], [ 1, Moves.LEER ], - [ 1, Moves.PURSUIT ], //Custom + [ 1, Moves.PURSUIT ], // Custom [ 4, Moves.DISABLE ], [ 8, Moves.SHADOW_SNEAK ], [ 12, Moves.CONFUSE_RAY ], @@ -6221,6 +6326,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.BIND ], [ 1, Moves.ASTONISH ], [ 1, Moves.LEER ], + [ 1, Moves.PURSUIT ], // Previous Stage Move, Custom [ 1, Moves.DISABLE ], [ 1, Moves.SHADOW_SNEAK ], [ 12, Moves.CONFUSE_RAY ], @@ -6252,7 +6358,10 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.CHIMECHO]: [ [ 1, Moves.HEALING_WISH ], + [ 1, Moves.LAST_RESORT ], // Previous Stage Move + [ 1, Moves.ENTRAINMENT ], // Previous Stage Move [ 1, Moves.WRAP ], + [ 1, Moves.PSYWAVE ], // Previous Stage Move, Custom [ 1, Moves.GROWL ], [ 1, Moves.ASTONISH ], [ 1, Moves.CONFUSION ], @@ -6392,6 +6501,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 50, Moves.SHELL_SMASH ], ], [Species.HUNTAIL]: [ + [ 1, Moves.CLAMP ], // Previous Stage Move [ 1, Moves.WATER_GUN ], [ 1, Moves.IRON_DEFENSE ], [ 1, Moves.SHELL_SMASH ], @@ -6412,6 +6522,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 50, Moves.HYDRO_PUMP ], ], [Species.GOREBYSS]: [ + [ 1, Moves.CLAMP ], // Previous Stage Move [ 1, Moves.WATER_GUN ], [ 1, Moves.IRON_DEFENSE ], [ 1, Moves.SHELL_SMASH ], @@ -6497,6 +6608,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.SALAMENCE]: [ [ EVOLVE_MOVE, Moves.FLY ], + [ RELEARN_MOVE, Moves.OUTRAGE ], // Previous Stage Move [ 1, Moves.PROTECT ], [ 1, Moves.DRAGON_TAIL ], [ 1, Moves.DUAL_WINGBEAT ], @@ -6712,7 +6824,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 98, Moves.DOOM_DESIRE ], ], [Species.DEOXYS]: [ - [ 1, Moves.CONFUSION ], //Custom + [ 1, Moves.CONFUSION ], // Custom [ 1, Moves.LEER ], [ 1, Moves.WRAP ], [ 7, Moves.NIGHT_SHADE ], @@ -6731,8 +6843,8 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.TURTWIG]: [ [ 1, Moves.TACKLE ], [ 5, Moves.WITHDRAW ], - [ 5, Moves.LEAFAGE ], //Custom, moved from 10 to 5, BDSP - [ 9, Moves.GROWTH ], //Fill empty moveslot, from BDSP level 6 + [ 5, Moves.LEAFAGE ], // Custom, moved from 10 to 5, BDSP + [ 9, Moves.GROWTH ], // Fill empty moveslot, from BDSP level 6 [ 13, Moves.RAZOR_LEAF ], [ 17, Moves.CURSE ], [ 21, Moves.BITE ], @@ -6748,6 +6860,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.ABSORB ], [ 1, Moves.WITHDRAW ], [ 1, Moves.LEAFAGE ], + [ 1, Moves.GROWTH ], // Previous Stage Move [ 13, Moves.RAZOR_LEAF ], [ 17, Moves.CURSE ], [ 22, Moves.BITE ], @@ -6763,6 +6876,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.TACKLE ], [ 1, Moves.ABSORB ], [ 1, Moves.LEAFAGE ], + [ 1, Moves.GROWTH ], // Previous Stage Move [ 1, Moves.RAZOR_LEAF ], [ 1, Moves.WITHDRAW ], [ 1, Moves.WOOD_HAMMER ], @@ -6779,7 +6893,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.CHIMCHAR]: [ [ 1, Moves.SCRATCH ], [ 1, Moves.LEER ], - [ 5, Moves.EMBER ], //Custom, moved from 7 to 5 + [ 5, Moves.EMBER ], // Custom, moved from 7 to 5 [ 9, Moves.TAUNT ], [ 15, Moves.FURY_SWIPES ], [ 17, Moves.FLAME_WHEEL ], @@ -6793,6 +6907,9 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.MONFERNO]: [ [ EVOLVE_MOVE, Moves.MACH_PUNCH ], + [ RELEARN_MOVE, Moves.NASTY_PLOT ], // Previous Stage Move + [ RELEARN_MOVE, Moves.FACADE ], // Previous Stage Move + [ RELEARN_MOVE, Moves.FLAMETHROWER ], // Previous Stage Move [ 1, Moves.SCRATCH ], [ 1, Moves.LEER ], [ 1, Moves.EMBER ], @@ -6810,7 +6927,10 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.INFERNAPE]: [ [ EVOLVE_MOVE, Moves.CLOSE_COMBAT ], [ RELEARN_MOVE, Moves.TAUNT ], + [ RELEARN_MOVE, Moves.NASTY_PLOT ], // Previous Stage Move + [ RELEARN_MOVE, Moves.FACADE ], // Previous Stage Move [ RELEARN_MOVE, Moves.SLACK_OFF ], + [ RELEARN_MOVE, Moves.FLAMETHROWER ], // Previous Stage Move [ 1, Moves.SCRATCH ], [ 1, Moves.LEER ], [ 1, Moves.EMBER ], @@ -6828,7 +6948,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.PIPLUP]: [ [ 1, Moves.POUND ], [ 4, Moves.GROWL ], - [ 5, Moves.WATER_GUN ], //Custom, moved from 8 to 5 + [ 5, Moves.WATER_GUN ], // Custom, moved from 8 to 5 [ 11, Moves.CHARM ], [ 15, Moves.PECK ], [ 18, Moves.BUBBLE_BEAM ], @@ -6845,6 +6965,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.TACKLE ], [ 1, Moves.GROWL ], [ 1, Moves.WATER_GUN ], + [ 1, Moves.CHARM ], // Previous Stage Move [ 15, Moves.PECK ], [ 19, Moves.BUBBLE_BEAM ], [ 24, Moves.SWAGGER ], @@ -6860,6 +6981,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.TACKLE ], [ 1, Moves.GROWL ], [ 1, Moves.WATER_GUN ], + [ 1, Moves.CHARM ], // Previous Stage Move [ 1, Moves.METAL_CLAW ], [ 11, Moves.SWORDS_DANCE ], [ 15, Moves.PECK ], @@ -6963,6 +7085,8 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.TACKLE ], [ 1, Moves.GROWL ], [ 1, Moves.BIDE ], + [ 1, Moves.STRUGGLE_BUG ], // Previous Stage Move + [ 1, Moves.BUG_BITE ], // Previous Stage Move [ 14, Moves.ABSORB ], [ 18, Moves.SING ], [ 22, Moves.FOCUS_ENERGY ], @@ -7113,13 +7237,14 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.BURMY]: [ [ 1, Moves.PROTECT ], - [ 1, Moves.STRUGGLE_BUG ], //Custom + [ 1, Moves.STRUGGLE_BUG ], // Custom [ 10, Moves.TACKLE ], [ 15, Moves.BUG_BITE ], [ 20, Moves.STRING_SHOT ], ], [Species.WORMADAM]: [ [ EVOLVE_MOVE, Moves.QUIVER_DANCE ], + [ 1, Moves.STRUGGLE_BUG ], // Previous Stage Move, Custom [ 1, Moves.TACKLE ], [ 1, Moves.PROTECT ], [ 1, Moves.SUCKER_PUNCH ], @@ -7140,6 +7265,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.MOTHIM]: [ [ EVOLVE_MOVE, Moves.QUIVER_DANCE ], + [ 1, Moves.STRUGGLE_BUG ], // Previous Stage Move [ 1, Moves.TACKLE ], [ 1, Moves.PROTECT ], [ 1, Moves.BUG_BITE ], @@ -7221,6 +7347,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 49, Moves.WAVE_CRASH ], ], [Species.FLOATZEL]: [ + [ 1, Moves.TACKLE ], // Previous Stage Move [ 1, Moves.GROWL ], [ 1, Moves.QUICK_ATTACK ], [ 1, Moves.CRUNCH ], @@ -7368,6 +7495,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.LOPUNNY]: [ [ EVOLVE_MOVE, Moves.RETURN ], + [ 1, Moves.FRUSTRATION ], // Previous Stage Move [ 1, Moves.POUND ], [ 1, Moves.DEFENSE_CURL ], [ 1, Moves.SPLASH ], @@ -7389,6 +7517,16 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 56, Moves.HIGH_JUMP_KICK ], ], [Species.MISMAGIUS]: [ + // Previous Stage Relearn Learnset + [ RELEARN_MOVE, Moves.CONFUSION ], + [ RELEARN_MOVE, Moves.CONFUSE_RAY ], + [ RELEARN_MOVE, Moves.MEAN_LOOK ], + [ RELEARN_MOVE, Moves.HEX ], + [ RELEARN_MOVE, Moves.PSYBEAM ], + [ RELEARN_MOVE, Moves.PAIN_SPLIT ], + [ RELEARN_MOVE, Moves.PAYBACK ], + [ RELEARN_MOVE, Moves.SHADOW_BALL ], + [ RELEARN_MOVE, Moves.PERISH_SONG ], [ 1, Moves.GROWL ], [ 1, Moves.SPITE ], [ 1, Moves.PSYWAVE ], @@ -7400,11 +7538,18 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.MYSTICAL_FIRE ], ], [Species.HONCHKROW]: [ - [ 1, Moves.WING_ATTACK ], - [ 1, Moves.HAZE ], + [ 1, Moves.PECK ], // Previous Stage Move [ 1, Moves.ASTONISH ], + [ 1, Moves.GUST ], // Previous Stage Move + [ 1, Moves.HAZE ], + [ 1, Moves.WING_ATTACK ], + [ 1, Moves.NIGHT_SHADE ], // Previous Stage Move + [ 1, Moves.ASSURANCE ], // Previous Stage Move + [ 1, Moves.TAUNT ], // Previous Stage Move + [ 1, Moves.MEAN_LOOK ], // Previous Stage Move [ 1, Moves.SUCKER_PUNCH ], [ 1, Moves.NIGHT_SLASH ], + [ 1, Moves.TORMENT ], // Previous Stage Move [ 1, Moves.QUASH ], [ 1, Moves.PURSUIT ], [ 25, Moves.SWAGGER ], @@ -7449,7 +7594,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.CHINGLING]: [ [ 1, Moves.WRAP ], - [ 1, Moves.PSYWAVE ], //Custom + [ 1, Moves.PSYWAVE ], // Custom [ 4, Moves.GROWL ], [ 7, Moves.ASTONISH ], [ 10, Moves.CONFUSION ], @@ -7482,6 +7627,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.SMOKESCREEN ], [ 1, Moves.POISON_GAS ], [ 1, Moves.FEINT ], + [ 1, Moves.ACID_SPRAY ], // Previous Stage Move [ 12, Moves.FURY_SWIPES ], [ 15, Moves.FOCUS_ENERGY ], [ 18, Moves.BITE ], @@ -7533,7 +7679,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.BONSLY]: [ [ 1, Moves.FAKE_TEARS ], [ 1, Moves.COPYCAT ], - [ 1, Moves.TACKLE ], //Custom + [ 1, Moves.TACKLE ], // Custom [ 4, Moves.FLAIL ], [ 8, Moves.ROCK_THROW ], [ 12, Moves.BLOCK ], @@ -7554,11 +7700,11 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 4, Moves.BATON_PASS ], [ 8, Moves.ENCORE ], [ 12, Moves.CONFUSION ], - [ 16, Moves.MIMIC ], //Custom, swapped with Role Play to be closer to USUM + [ 16, Moves.MIMIC ], // Custom, swapped with Role Play to be closer to USUM [ 20, Moves.PROTECT ], [ 24, Moves.RECYCLE ], [ 28, Moves.PSYBEAM ], - [ 32, Moves.ROLE_PLAY ], //Custom, swapped with Mimic + [ 32, Moves.ROLE_PLAY ], // Custom, swapped with Mimic [ 36, Moves.LIGHT_SCREEN ], [ 36, Moves.REFLECT ], [ 36, Moves.SAFEGUARD ], @@ -7696,6 +7842,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.LUCARIO]: [ [ EVOLVE_MOVE, Moves.AURA_SPHERE ], [ 1, Moves.QUICK_ATTACK ], + [ 1, Moves.ENDURE ], // Previous Stage Move [ 1, Moves.SCREECH ], [ 1, Moves.REVERSAL ], [ 1, Moves.DETECT ], @@ -7836,7 +7983,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.CARNIVINE]: [ [ 1, Moves.BIND ], [ 1, Moves.GROWTH ], - [ 1, Moves.LEAFAGE ], //Custom + [ 1, Moves.LEAFAGE ], // Custom [ 7, Moves.BITE ], [ 11, Moves.VINE_WHIP ], [ 17, Moves.SWEET_SCENT ], @@ -7973,6 +8120,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.SUPERSONIC ], [ 1, Moves.DEFENSE_CURL ], [ 1, Moves.LICK ], + [ 1, Moves.TACKLE ], // Previous Stage Move, Custom [ 1, Moves.ROLLOUT ], [ 1, Moves.WRING_OUT ], [ 6, Moves.REST ], @@ -8086,7 +8234,9 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ RELEARN_MOVE, Moves.HYPNOSIS ], [ 1, Moves.TACKLE ], [ 1, Moves.DOUBLE_TEAM ], + [ 1, Moves.AIR_CUTTER ], // Previous Stage Move [ 1, Moves.NIGHT_SLASH ], + [ 1, Moves.WING_ATTACK ], // Previous Stage Move [ 1, Moves.AIR_SLASH ], [ 1, Moves.BUG_BUZZ ], [ 14, Moves.QUICK_ATTACK ], @@ -8102,6 +8252,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.LEAFEON]: [ [ EVOLVE_MOVE, Moves.SAPPY_SEED ], + [ RELEARN_MOVE, Moves.VEEVEE_VOLLEY ], // Previous Stage Move [ 1, Moves.TACKLE ], [ 1, Moves.TAKE_DOWN ], [ 1, Moves.DOUBLE_EDGE ], @@ -8129,6 +8280,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.GLACEON]: [ [ EVOLVE_MOVE, Moves.FREEZY_FROST ], + [ RELEARN_MOVE, Moves.VEEVEE_VOLLEY ], // Previous Stage Move [ 1, Moves.TACKLE ], [ 1, Moves.TAKE_DOWN ], [ 1, Moves.DOUBLE_EDGE ], @@ -8154,8 +8306,11 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 60, Moves.LAST_RESORT ], ], [Species.GLISCOR]: [ + [ 1, Moves.POISON_STING ], // Previous Stage Move [ 1, Moves.SAND_ATTACK ], [ 1, Moves.HARDEN ], + [ 1, Moves.POISON_TAIL ], // Previous Stage Move + [ 1, Moves.SLASH ], // Previous Stage Move [ 1, Moves.POISON_JAB ], [ 1, Moves.THUNDER_FANG ], [ 1, Moves.ICE_FANG ], @@ -8248,8 +8403,10 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.PROBOPASS]: [ [ EVOLVE_MOVE, Moves.TRI_ATTACK ], [ 1, Moves.TACKLE ], + [ 1, Moves.HARDEN ], // Previous Stage Move [ 1, Moves.IRON_DEFENSE ], [ 1, Moves.BLOCK ], + [ 1, Moves.ROCK_THROW ], // Previous Stage Move [ 1, Moves.GRAVITY ], [ 1, Moves.MAGNET_RISE ], [ 1, Moves.WIDE_GUARD ], @@ -8275,6 +8432,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.LEER ], [ 1, Moves.DISABLE ], [ 1, Moves.ASTONISH ], + [ 1, Moves.PURSUIT ], // Previous Stage Move, Custom [ 1, Moves.SHADOW_PUNCH ], [ 1, Moves.GRAVITY ], [ 1, Moves.SHADOW_SNEAK ], @@ -8298,6 +8456,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.POWDER_SNOW ], [ 1, Moves.PROTECT ], [ 1, Moves.DESTINY_BOND ], + [ 1, Moves.WEATHER_BALL ], // Previous Stage Move [ 1, Moves.CRUNCH ], [ 1, Moves.ASTONISH ], [ 1, Moves.ICE_FANG ], @@ -8538,7 +8697,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.DARKRAI]: [ [ 1, Moves.DISABLE ], [ 1, Moves.OMINOUS_WIND ], - [ 1, Moves.PURSUIT ], //Custom + [ 1, Moves.PURSUIT ], // Custom [ 11, Moves.QUICK_ATTACK ], [ 20, Moves.HYPNOSIS ], [ 29, Moves.SUCKER_PUNCH ], @@ -8551,7 +8710,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 93, Moves.DARK_PULSE ], ], [Species.SHAYMIN]: [ - [ 1, Moves.LEAFAGE ], //Custom + [ 1, Moves.LEAFAGE ], // Custom [ 1, Moves.GROWTH ], [ 10, Moves.MAGICAL_LEAF ], [ 19, Moves.LEECH_SEED ], @@ -8603,7 +8762,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.SNIVY]: [ [ 1, Moves.TACKLE ], [ 4, Moves.LEER ], - [ 5, Moves.VINE_WHIP ], //Custom, moved from 7 to 5 + [ 5, Moves.VINE_WHIP ], // Custom, moved from 7 to 5 [ 10, Moves.WRAP ], [ 13, Moves.GROWTH ], [ 16, Moves.MAGICAL_LEAF ], @@ -8651,7 +8810,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.TEPIG]: [ [ 1, Moves.TACKLE ], [ 3, Moves.TAIL_WHIP ], - [ 5, Moves.EMBER ], //Custom, moved from 7 to 5 + [ 5, Moves.EMBER ], // Custom, moved from 7 to 5 [ 9, Moves.ENDURE ], [ 13, Moves.DEFENSE_CURL ], [ 15, Moves.FLAME_CHARGE ], @@ -8705,7 +8864,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.OSHAWOTT]: [ [ 1, Moves.TACKLE ], [ 5, Moves.TAIL_WHIP ], - [ 5, Moves.WATER_GUN ], //Custom, moved from 7 to 5 + [ 5, Moves.WATER_GUN ], // Custom, moved from 7 to 5 [ 11, Moves.SOAK ], [ 13, Moves.FOCUS_ENERGY ], [ 17, Moves.RAZOR_SHELL ], @@ -8776,6 +8935,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.WATCHOG]: [ [ EVOLVE_MOVE, Moves.CONFUSE_RAY ], + [ RELEARN_MOVE, Moves.WORK_UP ], // Previous Stage Move [ 1, Moves.TACKLE ], [ 1, Moves.LEER ], [ 1, Moves.BITE ], @@ -8895,6 +9055,19 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 43, Moves.CRUNCH ], ], [Species.SIMISAGE]: [ + // Previous Stage Relearn Learnset + [ RELEARN_MOVE, Moves.SCRATCH ], + [ RELEARN_MOVE, Moves.PLAY_NICE ], + [ RELEARN_MOVE, Moves.VINE_WHIP ], + [ RELEARN_MOVE, Moves.LEECH_SEED ], + [ RELEARN_MOVE, Moves.BITE ], + [ RELEARN_MOVE, Moves.TORMENT ], + [ RELEARN_MOVE, Moves.FLING ], + [ RELEARN_MOVE, Moves.ACROBATICS ], + [ RELEARN_MOVE, Moves.GRASS_KNOT ], + [ RELEARN_MOVE, Moves.RECYCLE ], + [ RELEARN_MOVE, Moves.NATURAL_GIFT ], + [ RELEARN_MOVE, Moves.CRUNCH ], [ 1, Moves.LEER ], [ 1, Moves.LICK ], [ 1, Moves.FURY_SWIPES ], @@ -8919,6 +9092,19 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 43, Moves.CRUNCH ], ], [Species.SIMISEAR]: [ + // Previous Stage Relearn Learnset + [ RELEARN_MOVE, Moves.SCRATCH ], + [ RELEARN_MOVE, Moves.PLAY_NICE ], + [ RELEARN_MOVE, Moves.INCINERATE ], + [ RELEARN_MOVE, Moves.YAWN ], + [ RELEARN_MOVE, Moves.BITE ], + [ RELEARN_MOVE, Moves.AMNESIA ], + [ RELEARN_MOVE, Moves.FLING ], + [ RELEARN_MOVE, Moves.ACROBATICS ], + [ RELEARN_MOVE, Moves.FIRE_BLAST ], + [ RELEARN_MOVE, Moves.RECYCLE ], + [ RELEARN_MOVE, Moves.NATURAL_GIFT ], + [ RELEARN_MOVE, Moves.CRUNCH ], [ 1, Moves.LEER ], [ 1, Moves.LICK ], [ 1, Moves.FURY_SWIPES ], @@ -8943,6 +9129,19 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 43, Moves.CRUNCH ], ], [Species.SIMIPOUR]: [ + // Previous Stage Relearn Learnset + [ RELEARN_MOVE, Moves.SCRATCH ], + [ RELEARN_MOVE, Moves.PLAY_NICE ], + [ RELEARN_MOVE, Moves.WATER_GUN ], + [ RELEARN_MOVE, Moves.WATER_SPORT ], + [ RELEARN_MOVE, Moves.BITE ], + [ RELEARN_MOVE, Moves.TAUNT ], + [ RELEARN_MOVE, Moves.FLING ], + [ RELEARN_MOVE, Moves.ACROBATICS ], + [ RELEARN_MOVE, Moves.BRINE ], + [ RELEARN_MOVE, Moves.RECYCLE ], + [ RELEARN_MOVE, Moves.NATURAL_GIFT ], + [ RELEARN_MOVE, Moves.CRUNCH ], [ 1, Moves.LEER ], [ 1, Moves.LICK ], [ 1, Moves.FURY_SWIPES ], @@ -8967,6 +9166,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 52, Moves.WONDER_ROOM ], ], [Species.MUSHARNA]: [ + [ 1, Moves.PSYWAVE ], // Previous Stage Move [ 1, Moves.PSYBEAM ], [ 1, Moves.PSYCHIC ], [ 1, Moves.HYPNOSIS ], @@ -9295,7 +9495,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 70, Moves.HYDRO_PUMP ], ], [Species.THROH]: [ - [ 1, Moves.ROCK_SMASH ], //Custom + [ 1, Moves.ROCK_SMASH ], // Custom [ 1, Moves.LEER ], [ 1, Moves.BIDE ], [ 1, Moves.MAT_BLOCK ], @@ -9355,9 +9555,15 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.LEAVANNY]: [ [ EVOLVE_MOVE, Moves.SLASH ], [ RELEARN_MOVE, Moves.BUG_BITE ], + [ RELEARN_MOVE, Moves.STICKY_WEB ], // Previous Stage Move + [ RELEARN_MOVE, Moves.BUG_BUZZ ], // Previous Stage Move + [ 1, Moves.PROTECT ], // Previous Stage Move [ 1, Moves.TACKLE ], [ 1, Moves.RAZOR_LEAF ], [ 1, Moves.STRING_SHOT ], + [ 1, Moves.GRASS_WHISTLE ], // Previous Stage Move + [ 1, Moves.ENDURE ], // Previous Stage Move + [ 1, Moves.FLAIL ], // Previous Stage Move [ 1, Moves.FALSE_SWIPE ], [ 22, Moves.STRUGGLE_BUG ], [ 29, Moves.FELL_STINGER ], @@ -9890,6 +10096,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.TORMENT ], [ 1, Moves.U_TURN ], [ 1, Moves.HONE_CLAWS ], + [ 1, Moves.SCARY_FACE ], // Previous Stage Move [ 1, Moves.PURSUIT ], [ 12, Moves.FURY_SWIPES ], [ 20, Moves.TAUNT ], @@ -9964,6 +10171,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 28, Moves.FAKE_TEARS ], [ 34, Moves.HEAL_BLOCK ], [ 35, Moves.PSYCH_UP ], + [ 40, Moves.PSYCHIC ], // Previous Stage Move, Gothitelle Level [ 46, Moves.FLATTER ], [ 52, Moves.FUTURE_SIGHT ], [ 58, Moves.MAGIC_ROOM ], @@ -10081,7 +10289,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.VANILLITE]: [ [ 1, Moves.HARDEN ], [ 1, Moves.ASTONISH ], - [ 1, Moves.POWDER_SNOW ], //Custom + [ 1, Moves.POWDER_SNOW ], // Custom [ 4, Moves.TAUNT ], [ 8, Moves.MIST ], [ 12, Moves.ICY_WIND ], @@ -10100,6 +10308,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.HARDEN ], [ 1, Moves.TAUNT ], [ 1, Moves.ASTONISH ], + [ 1, Moves.POWDER_SNOW ], // Previous Stage Move, Custom [ 12, Moves.ICY_WIND ], [ 16, Moves.AVALANCHE ], [ 20, Moves.HAIL ], @@ -10116,6 +10325,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.HARDEN ], [ 1, Moves.TAUNT ], [ 1, Moves.ASTONISH ], + [ 1, Moves.POWDER_SNOW ], // Previous Stage Move, Custom [ 1, Moves.WEATHER_BALL ], [ 1, Moves.ICICLE_CRASH ], [ 1, Moves.FREEZE_DRY ], @@ -10428,6 +10638,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.EELEKTRIK]: [ [ EVOLVE_MOVE, Moves.CRUNCH ], + [ 1, Moves.TACKLE ], // Previous Stage Move [ 1, Moves.HEADBUTT ], [ 1, Moves.THUNDER_WAVE ], [ 1, Moves.SPARK ], @@ -10445,7 +10656,15 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 74, Moves.THRASH ], ], [Species.EELEKTROSS]: [ + [ RELEARN_MOVE, Moves.THUNDERBOLT ], // Previous Stage Move + [ RELEARN_MOVE, Moves.ACID_SPRAY ], // Previous Stage Move + [ 1, Moves.TACKLE ], // Previous Stage Move [ 1, Moves.HEADBUTT ], + [ 1, Moves.THUNDER_WAVE ], // Previous Stage Move + [ 1, Moves.SPARK ], // Previous Stage Move + [ 1, Moves.CHARGE_BEAM ], // Previous Stage Move + [ 1, Moves.ION_DELUGE ], // Previous Stage Move + [ 1, Moves.BIND ], // Previous Stage Move [ 1, Moves.THRASH ], [ 1, Moves.ACID ], [ 1, Moves.ZAP_CANNON ], @@ -10688,6 +10907,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.BODY_SLAM ], [ 1, Moves.ACID ], [ 1, Moves.ABSORB ], + [ 1, Moves.PROTECT ], // Previous Stage Move [ 1, Moves.QUICK_ATTACK ], [ 1, Moves.DOUBLE_TEAM ], [ 1, Moves.ACID_ARMOR ], @@ -10881,6 +11101,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.BRAVIARY]: [ [ EVOLVE_MOVE, Moves.SUPERPOWER ], + [ RELEARN_MOVE, Moves.BRAVE_BIRD ], // Previous Stage Move [ 1, Moves.WING_ATTACK ], [ 1, Moves.LEER ], [ 1, Moves.PECK ], @@ -11422,6 +11643,10 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.GROWL ], [ 1, Moves.WATER_GUN ], [ 1, Moves.QUICK_ATTACK ], + [ 1, Moves.ROUND ], // Previous Stage Move + [ 1, Moves.FLING ], // Previous Stage Move + [ 1, Moves.SMACK_DOWN ], // Previous Stage Move + [ 1, Moves.BOUNCE ], // Previous Stage Move [ 1, Moves.HAZE ], [ 1, Moves.MAT_BLOCK ], [ 1, Moves.ROLE_PLAY ], @@ -11529,10 +11754,19 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.SPEWPA]: [ [ EVOLVE_MOVE, Moves.PROTECT ], + [ RELEARN_MOVE, Moves.TACKLE ], // Previous Stage Move + [ RELEARN_MOVE, Moves.STRING_SHOT ], // Previous Stage Move + [ RELEARN_MOVE, Moves.STUN_SPORE ], // Previous Stage Move + [ RELEARN_MOVE, Moves.BUG_BITE ], // Previous Stage Move [ 1, Moves.HARDEN ], ], [Species.VIVILLON]: [ [ EVOLVE_MOVE, Moves.GUST ], + [ 1, Moves.PROTECT ], // Previous Stage Move + [ 1, Moves.TACKLE ], // Previous Stage Move + [ 1, Moves.STRING_SHOT ], // Previous Stage Move + [ 1, Moves.HARDEN ], // Previous Stage Move + [ 1, Moves.BUG_BITE ], // Previous Stage Move [ 1, Moves.POISON_POWDER ], [ 1, Moves.STUN_SPORE ], [ 1, Moves.SLEEP_POWDER ], @@ -11615,6 +11849,10 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 58, Moves.SOLAR_BEAM ], ], [Species.FLORGES]: [ + [ 1, Moves.VINE_WHIP ], // Previous Stage Move + [ 1, Moves.TACKLE ], // Previous Stage Move + [ 1, Moves.FAIRY_WIND ], // Previous Stage Move + [ 1, Moves.RAZOR_LEAF ], // Previous Stage Move [ 1, Moves.SOLAR_BEAM ], [ 1, Moves.PETAL_DANCE ], [ 1, Moves.SAFEGUARD ], @@ -12106,6 +12344,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.SYLVEON]: [ [ EVOLVE_MOVE, Moves.SPARKLY_SWIRL ], + [ RELEARN_MOVE, Moves.VEEVEE_VOLLEY ], // Previous Stage Move [ 1, Moves.TACKLE ], [ 1, Moves.TAKE_DOWN ], [ 1, Moves.DOUBLE_EDGE ], @@ -12216,6 +12455,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.TACKLE ], [ 1, Moves.WATER_GUN ], [ 1, Moves.ABSORB ], + [ 1, Moves.ACID_ARMOR ], // Previous Stage Move [ 1, Moves.DRAGON_BREATH ], [ 1, Moves.POISON_TAIL ], [ 1, Moves.FEINT ], @@ -12225,6 +12465,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 20, Moves.FLAIL ], [ 25, Moves.WATER_PULSE ], [ 30, Moves.RAIN_DANCE ], + [ 35, Moves.DRAGON_PULSE ], // Previous Stage Move, NatDex / Hisui Goodra Level [ 43, Moves.CURSE ], [ 49, Moves.BODY_SLAM ], [ 58, Moves.MUDDY_WATER ], @@ -12285,7 +12526,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.PUMPKABOO]: [ [ 1, Moves.ASTONISH ], [ 1, Moves.TRICK_OR_TREAT ], - [ 1, Moves.LEAFAGE ], //Custom + [ 1, Moves.LEAFAGE ], // Custom [ 4, Moves.SHADOW_SNEAK ], [ 8, Moves.CONFUSE_RAY ], [ 12, Moves.RAZOR_LEAF ], @@ -12302,6 +12543,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.CONFUSE_RAY ], [ 1, Moves.EXPLOSION ], [ 1, Moves.ASTONISH ], + [ 1, Moves.LEAFAGE ], // Previous Stage Move, Custom [ 1, Moves.SHADOW_SNEAK ], [ 1, Moves.TRICK_OR_TREAT ], [ 1, Moves.MOONBLAST ], @@ -12813,11 +13055,14 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.CRABOMINABLE]: [ [ EVOLVE_MOVE, Moves.ICE_PUNCH ], + [ RELEARN_MOVE, Moves.CRABHAMMER ], // Previous Stage Move + [ 1, Moves.VISE_GRIP ], // Previous Stage Move [ 1, Moves.LEER ], [ 1, Moves.PROTECT ], [ 1, Moves.ROCK_SMASH ], [ 1, Moves.BUBBLE ], [ 1, Moves.PURSUIT ], + [ 1, Moves.PAYBACK ], // Previous Stage Move [ 17, Moves.BUBBLE_BEAM ], [ 22, Moves.BRICK_BREAK ], [ 25, Moves.SLAM ], @@ -13006,6 +13251,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.BUG_BITE ], [ 1, Moves.WIDE_GUARD ], [ 1, Moves.INFESTATION ], + [ 1, Moves.WATER_SPORT ], // Previous Stage Move [ 1, Moves.SPIDER_WEB ], [ 12, Moves.BUBBLE_BEAM ], [ 16, Moves.AQUA_RING ], @@ -13154,7 +13400,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.BOUNSWEET]: [ [ 1, Moves.SPLASH ], - [ 1, Moves.LEAFAGE ], //Custom + [ 1, Moves.LEAFAGE ], // Custom [ 4, Moves.PLAY_NICE ], [ 8, Moves.RAPID_SPIN ], [ 12, Moves.RAZOR_LEAF ], @@ -13165,6 +13411,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 32, Moves.AROMATIC_MIST ], ], [Species.STEENEE]: [ + [ 1, Moves.LEAFAGE ], // Previous Stage Move, Custom [ 1, Moves.RAZOR_LEAF ], [ 1, Moves.SPLASH ], [ 1, Moves.FLAIL ], @@ -13179,6 +13426,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.TSAREENA]: [ [ EVOLVE_MOVE, Moves.TROP_KICK ], + [ 1, Moves.LEAFAGE ], // Previous Stage Move, Custom [ 1, Moves.RAZOR_LEAF ], [ 1, Moves.SPLASH ], [ 1, Moves.FLAIL ], @@ -13303,7 +13551,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 68, Moves.SANDSTORM ], ], [Species.PYUKUMUKU]: [ - [ 1, Moves.COUNTER ], //Custom, Moved from Level 20 to 1 + [ 1, Moves.COUNTER ], // Custom, Moved from Level 20 to 1 [ 1, Moves.HARDEN ], [ 1, Moves.BATON_PASS ], [ 1, Moves.BIDE ], @@ -13312,7 +13560,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 5, Moves.HELPING_HAND ], [ 10, Moves.TAUNT ], [ 15, Moves.SAFEGUARD ], - [ 20, Moves.MIRROR_COAT ], //Custom + [ 20, Moves.MIRROR_COAT ], // Custom [ 25, Moves.PURIFY ], [ 30, Moves.CURSE ], [ 35, Moves.GASTRO_ACID ], @@ -13629,15 +13877,19 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.COSMOG]: [ [ 1, Moves.TELEPORT ], [ 1, Moves.SPLASH ], - [ 1, Moves.STORED_POWER ], //Custom + [ 1, Moves.STORED_POWER ], // Custom ], [Species.COSMOEM]: [ [ EVOLVE_MOVE, Moves.COSMIC_POWER ], [ 1, Moves.TELEPORT ], + [ 1, Moves.SPLASH ], // Previous Stage Move + [ 1, Moves.STORED_POWER ], // Previous Stage Move, Custom ], [Species.SOLGALEO]: [ [ EVOLVE_MOVE, Moves.SUNSTEEL_STRIKE ], [ 1, Moves.TELEPORT ], + [ 1, Moves.SPLASH ], // Previous Stage Move + [ 1, Moves.STORED_POWER ], // Previous Stage Move, Custom [ 1, Moves.METAL_CLAW ], [ 1, Moves.COSMIC_POWER ], [ 1, Moves.NOBLE_ROAR ], @@ -13660,6 +13912,8 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.CONFUSION ], [ 1, Moves.HYPNOSIS ], [ 1, Moves.TELEPORT ], + [ 1, Moves.SPLASH ], // Previous Stage Move + [ 1, Moves.STORED_POWER ], // Previous Stage Move, Custom [ 1, Moves.COSMIC_POWER ], [ 7, Moves.NIGHT_SHADE ], [ 14, Moves.CONFUSE_RAY ], @@ -13826,7 +14080,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.MAGEARNA]: [ [ 1, Moves.HELPING_HAND ], [ 1, Moves.GYRO_BALL ], - [ 1, Moves.DISARMING_VOICE ], //Custom + [ 1, Moves.DISARMING_VOICE ], // Custom [ 1, Moves.CRAFTY_SHIELD ], [ 1, Moves.GEAR_UP ], [ 6, Moves.DEFENSE_CURL ], @@ -13867,7 +14121,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 99, Moves.CLOSE_COMBAT ], ], [Species.POIPOLE]: [ - [ RELEARN_MOVE, Moves.DRAGON_PULSE ], //Custom, made relearn + [ RELEARN_MOVE, Moves.DRAGON_PULSE ], // Custom, made relearn [ 1, Moves.GROWL ], [ 1, Moves.ACID ], [ 1, Moves.PECK ], @@ -13986,7 +14240,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.GROOKEY]: [ [ 1, Moves.SCRATCH ], [ 1, Moves.GROWL ], - [ 5, Moves.BRANCH_POKE ], //Custom, moved from 6 to 5 + [ 5, Moves.BRANCH_POKE ], // Custom, moved from 6 to 5 [ 8, Moves.TAUNT ], [ 12, Moves.RAZOR_LEAF ], [ 17, Moves.SCREECH ], @@ -14031,7 +14285,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.SCORBUNNY]: [ [ 1, Moves.TACKLE ], [ 1, Moves.GROWL ], - [ 5, Moves.EMBER ], //Custom, moved from 6 to 5 + [ 5, Moves.EMBER ], // Custom, moved from 6 to 5 [ 8, Moves.QUICK_ATTACK ], [ 12, Moves.DOUBLE_KICK ], [ 17, Moves.FLAME_CHARGE ], @@ -14073,7 +14327,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.SOBBLE]: [ [ 1, Moves.POUND ], [ 1, Moves.GROWL ], - [ 5, Moves.WATER_GUN ], //Custom, moved from 6 to 5 + [ 5, Moves.WATER_GUN ], // Custom, moved from 6 to 5 [ 8, Moves.BIND ], [ 12, Moves.WATER_PULSE ], [ 17, Moves.TEARFUL_LOOK ], @@ -14400,10 +14654,11 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.APPLIN]: [ [ 1, Moves.WITHDRAW ], [ 1, Moves.ASTONISH ], - [ 1, Moves.LEAFAGE ], //Custom + [ 1, Moves.LEAFAGE ], // Custom ], [Species.FLAPPLE]: [ [ EVOLVE_MOVE, Moves.WING_ATTACK ], + [ 1, Moves.LEAFAGE ], // Previous Stage Move, Custom [ 1, Moves.GROWTH ], [ 1, Moves.WITHDRAW ], [ 1, Moves.TWISTER ], @@ -14423,6 +14678,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.APPLETUN]: [ [ EVOLVE_MOVE, Moves.HEADBUTT ], + [ 1, Moves.LEAFAGE ], // Previous Stage Move, Custom [ 1, Moves.GROWTH ], [ 1, Moves.WITHDRAW ], [ 1, Moves.SWEET_SCENT ], @@ -14443,7 +14699,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.SILICOBRA]: [ [ 1, Moves.SAND_ATTACK ], [ 1, Moves.WRAP ], - [ 1, Moves.MUD_SLAP ], //Custom + [ 1, Moves.MUD_SLAP ], // Custom [ 5, Moves.MINIMIZE ], [ 10, Moves.BRUTAL_SWING ], [ 15, Moves.BULLDOZE ], @@ -14458,6 +14714,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.SANDACONDA]: [ [ 1, Moves.SAND_ATTACK ], [ 1, Moves.WRAP ], + [ 1, Moves.MUD_SLAP ], // Previous Stage Move, Custom [ 1, Moves.MINIMIZE ], [ 1, Moves.BRUTAL_SWING ], [ 15, Moves.BULLDOZE ], @@ -14605,7 +14862,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.SINISTEA]: [ [ 1, Moves.WITHDRAW ], [ 1, Moves.ASTONISH ], - [ 1, Moves.ABSORB ], //Custom + [ 1, Moves.ABSORB ], // Custom [ 6, Moves.AROMATIC_MIST ], [ 12, Moves.MEGA_DRAIN ], [ 24, Moves.SUCKER_PUNCH ], @@ -14618,6 +14875,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.POLTEAGEIST]: [ [ EVOLVE_MOVE, Moves.TEATIME ], + [ 1, Moves.ABSORB ], // Previous Stage Move, Custom [ 1, Moves.MEGA_DRAIN ], [ 1, Moves.WITHDRAW ], [ 1, Moves.ASTONISH ], @@ -14805,6 +15063,8 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.MR_RIME]: [ [ 1, Moves.POUND ], + [ 1, Moves.BARRIER ], // Previous Stage Move + [ 1, Moves.TICKLE ], // Previous Stage Move [ 1, Moves.MIMIC ], [ 1, Moves.LIGHT_SCREEN ], [ 1, Moves.REFLECT ], @@ -15133,6 +15393,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.DRAGAPULT]: [ [ EVOLVE_MOVE, Moves.DRAGON_DARTS ], + [ RELEARN_MOVE, Moves.DRAGON_PULSE ], // Previous Stage Move [ 1, Moves.BITE ], [ 1, Moves.QUICK_ATTACK ], [ 1, Moves.DRAGON_BREATH ], @@ -15339,6 +15600,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.WYRDEER]: [ [ EVOLVE_MOVE, Moves.PSYSHIELD_BASH ], [ 1, Moves.TACKLE ], + [ 1, Moves.ME_FIRST ], // Previous Stage Move [ 3, Moves.LEER ], [ 7, Moves.ASTONISH ], [ 10, Moves.HYPNOSIS ], @@ -15355,6 +15617,8 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.KLEAVOR]: [ [ EVOLVE_MOVE, Moves.STONE_AXE ], + [ 1, Moves.WING_ATTACK ], // Previous Stage Move + [ 1, Moves.AIR_SLASH ], // Previous Stage Move [ 1, Moves.LEER ], [ 1, Moves.QUICK_ATTACK ], [ 4, Moves.FURY_CUTTER ], @@ -15364,6 +15628,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 20, Moves.DOUBLE_HIT ], [ 24, Moves.SLASH ], [ 28, Moves.FOCUS_ENERGY ], + [ 30, Moves.STEEL_WING ], // Custom [ 32, Moves.AGILITY ], [ 36, Moves.ROCK_SLIDE ], [ 40, Moves.X_SCISSOR ], @@ -15374,8 +15639,11 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.SCRATCH ], [ 1, Moves.LEER ], [ 1, Moves.LICK ], - [ 1, Moves.FAKE_TEARS ], [ 1, Moves.COVET ], + [ 1, Moves.FLING ], // Previous Stage Move + [ 1, Moves.BABY_DOLL_EYES ], // Previous Stage Move + [ 1, Moves.FAKE_TEARS ], + [ 1, Moves.CHARM ], // Previous Stage Moves [ 8, Moves.FURY_SWIPES ], [ 13, Moves.PAYBACK ], [ 17, Moves.SWEET_SCENT ], @@ -15390,6 +15658,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 64, Moves.HAMMER_ARM ], ], [Species.BASCULEGION]: [ + [ RELEARN_MOVE, Moves.FINAL_GAMBIT ], // Previous Stage Move, White Stripe currently shares moveset with other forms [ 1, Moves.TAIL_WHIP ], [ 1, Moves.WATER_GUN ], [ 1, Moves.SHADOW_BALL ], @@ -15949,10 +16218,12 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.GARGANACL]: [ [ EVOLVE_MOVE, Moves.HAMMER_ARM ], + [ RELEARN_MOVE, Moves.IRON_DEFENSE ], // Previous Stage Move [ 1, Moves.TACKLE ], [ 1, Moves.HARDEN ], [ 1, Moves.BLOCK ], [ 1, Moves.ROCK_BLAST ], + [ 1, Moves.SMACK_DOWN ], // Previous Stage Move [ 1, Moves.WIDE_GUARD ], [ 5, Moves.ROCK_THROW ], [ 7, Moves.MUD_SHOT ], @@ -16140,6 +16411,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ EVOLVE_MOVE, Moves.DOODLE ], [ 1, Moves.SCRATCH ], [ 1, Moves.LEER ], + [ 1, Moves.BITE ], // Previous Stage Move [ 5, Moves.ACID_SPRAY ], [ 8, Moves.FURY_SWIPES ], [ 11, Moves.SWITCHEROO ], @@ -16294,6 +16566,8 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.TACKLE ], [ 1, Moves.CONFUSION ], [ 1, Moves.DEFENSE_CURL ], + [ 1, Moves.MUD_SHOT ], // Previous Stage Move + [ 1, Moves.DIG ], // Previous Stage Move [ 4, Moves.SAND_ATTACK ], [ 7, Moves.STRUGGLE_BUG ], [ 11, Moves.ROLLOUT ], @@ -16717,6 +16991,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.LEER ], [ 1, Moves.COUNTER ], [ 1, Moves.FOCUS_ENERGY ], + [ 1, Moves.COVET ], // Previous Stage Move [ 1, Moves.FLING ], [ 5, Moves.FURY_SWIPES ], [ 8, Moves.LOW_KICK ], @@ -16734,6 +17009,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.CLODSIRE]: [ [ EVOLVE_MOVE, Moves.AMNESIA ], + [ 1, Moves.TACKLE ], // Previous Stage Move [ 1, Moves.TAIL_WHIP ], [ 1, Moves.POISON_STING ], [ 4, Moves.TOXIC_SPIKES ], @@ -16768,6 +17044,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.DUDUNSPARCE]: [ [ 1, Moves.DEFENSE_CURL ], [ 1, Moves.FLAIL ], + [ 1, Moves.TACKLE ], // Previous Stage Move, Custom [ 4, Moves.MUD_SLAP ], [ 8, Moves.ROLLOUT ], [ 12, Moves.GLARE ], @@ -16864,7 +17141,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.CONFUSE_RAY ], [ 1, Moves.SPITE ], [ 1, Moves.ASTONISH ], - [ 1, Moves.PSYBEAM ], //Custom, moved from 7 to 1 + [ 1, Moves.PSYBEAM ], // Custom, moved from 7 to 1 [ 14, Moves.MEAN_LOOK ], [ 21, Moves.MEMENTO ], [ 28, Moves.WISH ], @@ -16939,7 +17216,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.IRON_BUNDLE]: [ [ RELEARN_MOVE, Moves.ELECTRIC_TERRAIN ], [ 1, Moves.PRESENT ], - [ 1, Moves.WATER_GUN ], //Custom + [ 1, Moves.WATER_GUN ], // Custom [ 7, Moves.POWDER_SNOW ], [ 14, Moves.WHIRLPOOL ], [ 21, Moves.TAKE_DOWN ], @@ -17058,6 +17335,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 18, Moves.FOCUS_ENERGY ], [ 24, Moves.BITE ], [ 29, Moves.ICE_FANG ], + [ 32, Moves.DRAGON_CLAW ], // Previous Stage Move, Frigibax Level [ 40, Moves.TAKE_DOWN ], [ 45, Moves.ICE_BEAM ], [ 50, Moves.CRUNCH ], @@ -17305,6 +17583,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [Species.DIPPLIN]: [ [ EVOLVE_MOVE, Moves.DOUBLE_HIT ], [ RELEARN_MOVE, Moves.DRAGON_CHEER ], // Custom + [ 1, Moves.LEAFAGE ], [ 1, Moves.WITHDRAW ], [ 1, Moves.SWEET_SCENT ], [ 1, Moves.RECYCLE ], @@ -17324,7 +17603,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.STUN_SPORE ], [ 1, Moves.WITHDRAW ], [ 1, Moves.ASTONISH ], - [ 5, Moves.ABSORB ], //Custom, Moved from Level 6 to 5 + [ 5, Moves.ABSORB ], // Custom, Moved from Level 6 to 5 [ 12, Moves.LIFE_DEW ], [ 18, Moves.FOUL_PLAY ], [ 24, Moves.MEGA_DRAIN ], @@ -17337,6 +17616,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.SINISTCHA]: [ [ EVOLVE_MOVE, Moves.MATCHA_GOTCHA ], + [ RELEARN_MOVE, Moves.GIGA_DRAIN ], // Previous Stage Move [ 1, Moves.STUN_SPORE ], [ 1, Moves.WITHDRAW ], [ 1, Moves.ASTONISH ], @@ -17419,6 +17699,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.ARCHALUDON]: [ [ EVOLVE_MOVE, Moves.ELECTRO_SHOT ], + [ RELEARN_MOVE, Moves.LASER_FOCUS ], // Previous Stage Move [ 1, Moves.LEER ], [ 1, Moves.METAL_CLAW ], [ 6, Moves.ROCK_SMASH ], @@ -17438,6 +17719,8 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ RELEARN_MOVE, Moves.YAWN ], [ RELEARN_MOVE, Moves.DOUBLE_HIT ], [ RELEARN_MOVE, Moves.INFESTATION ], + [ RELEARN_MOVE, Moves.DRAGON_CHEER ], // Previous Stage Move, Custom + [ 1, Moves.LEAFAGE ], // Previous Stage Move, Custom [ 1, Moves.WITHDRAW ], [ 1, Moves.SWEET_SCENT ], [ 1, Moves.RECYCLE ], @@ -17809,6 +18092,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.DEFENSE_CURL ], [ 1, Moves.CHARGE ], [ 1, Moves.ROCK_POLISH ], + [ 1, Moves.ROLLOUT ], // Previous Stage Move [ 1, Moves.HEAVY_SLAM ], [ 12, Moves.SPARK ], [ 16, Moves.ROCK_THROW ], @@ -18051,6 +18335,8 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.GALAR_MR_MIME]: [ [ 1, Moves.POUND ], + [ 1, Moves.BARRIER ], // Previous Stage Move + [ 1, Moves.TICKLE ], // Previous Stage Move [ 1, Moves.MIMIC ], [ 1, Moves.LIGHT_SCREEN ], [ 1, Moves.REFLECT ], @@ -18411,6 +18697,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.TACKLE ], [ 1, Moves.TAIL_WHIP ], [ 1, Moves.WATER_GUN ], + [ 1, Moves.SOAK ], // Previous Stage Move [ 1, Moves.SLASH ], [ 1, Moves.MEGAHORN ], [ 1, Moves.SUCKER_PUNCH ], @@ -18436,6 +18723,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.STUN_SPORE ], [ 1, Moves.SLEEP_POWDER ], [ 1, Moves.GIGA_DRAIN ], + [ 1, Moves.CHARM ], // Previous Stage Move [ 1, Moves.SYNTHESIS ], [ 1, Moves.SUNNY_DAY ], [ 1, Moves.HELPING_HAND ], @@ -18487,6 +18775,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.HISUI_BRAVIARY]: [ [ EVOLVE_MOVE, Moves.ESPER_WING ], + [ RELEARN_MOVE, Moves.BRAVE_BIRD ], // Previous Stage Move [ 1, Moves.WING_ATTACK ], [ 1, Moves.LEER ], [ 1, Moves.PECK ], @@ -18511,6 +18800,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.ABSORB ], [ 1, Moves.ACID_ARMOR ], [ 1, Moves.DRAGON_BREATH ], + [ 1, Moves.BODY_SLAM ], // Previous Stage Move [ 15, Moves.PROTECT ], [ 20, Moves.FLAIL ], [ 25, Moves.WATER_PULSE ], @@ -18525,6 +18815,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.TACKLE ], [ 1, Moves.WATER_GUN ], [ 1, Moves.ABSORB ], + [ 1, Moves.ACID_ARMOR ], // Previous Stage Move [ 1, Moves.DRAGON_BREATH ], [ 1, Moves.FEINT ], [ 1, Moves.ACID_SPRAY ], @@ -18565,9 +18856,11 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { ], [Species.HISUI_DECIDUEYE]: [ [ EVOLVE_MOVE, Moves.TRIPLE_ARROWS ], + [ RELEARN_MOVE, Moves.NASTY_PLOT ], // Previous Stage Move [ 1, Moves.TACKLE ], [ 1, Moves.GROWL ], [ 1, Moves.U_TURN ], + [ 1, Moves.ASTONISH ], // Previous Stage Move [ 1, Moves.LEAF_STORM ], [ 1, Moves.LEAFAGE ], [ 9, Moves.PECK ], @@ -18633,7 +18926,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { }; export const pokemonFormLevelMoves: PokemonSpeciesFormLevelMoves = { - [Species.PIKACHU]: { //Custom + [Species.PIKACHU]: { // Custom 1: [ [ 1, Moves.TAIL_WHIP ], [ 1, Moves.GROWL ], @@ -18648,14 +18941,14 @@ export const pokemonFormLevelMoves: PokemonSpeciesFormLevelMoves = { [ 8, Moves.DOUBLE_TEAM ], [ 12, Moves.ELECTRO_BALL ], [ 16, Moves.FEINT ], - [ 20, Moves.ZIPPY_ZAP ], //Custom + [ 20, Moves.ZIPPY_ZAP ], // Custom [ 24, Moves.AGILITY ], [ 28, Moves.IRON_TAIL ], [ 32, Moves.DISCHARGE ], - [ 34, Moves.FLOATY_FALL ], //Custom + [ 34, Moves.FLOATY_FALL ], // Custom [ 36, Moves.THUNDERBOLT ], [ 40, Moves.LIGHT_SCREEN ], - [ 42, Moves.SPLISHY_SPLASH ], //Custom + [ 42, Moves.SPLISHY_SPLASH ], // Custom [ 44, Moves.THUNDER ], [ 48, Moves.PIKA_PAPOW ], ], @@ -18816,19 +19109,19 @@ export const pokemonFormLevelMoves: PokemonSpeciesFormLevelMoves = { [ 8, Moves.DOUBLE_TEAM ], [ 12, Moves.ELECTRO_BALL ], [ 16, Moves.FEINT ], - [ 20, Moves.ZIPPY_ZAP ], //Custom + [ 20, Moves.ZIPPY_ZAP ], // Custom [ 24, Moves.AGILITY ], [ 28, Moves.IRON_TAIL ], [ 32, Moves.DISCHARGE ], - [ 34, Moves.FLOATY_FALL ], //Custom + [ 34, Moves.FLOATY_FALL ], // Custom [ 36, Moves.THUNDERBOLT ], [ 40, Moves.LIGHT_SCREEN ], - [ 42, Moves.SPLISHY_SPLASH ], //Custom + [ 42, Moves.SPLISHY_SPLASH ], // Custom [ 44, Moves.THUNDER ], [ 48, Moves.PIKA_PAPOW ], ], }, - [Species.EEVEE]: { //Custom + [Species.EEVEE]: { // Custom 1: [ [ 1, Moves.TACKLE ], [ 1, Moves.TAIL_WHIP ], @@ -18838,21 +19131,21 @@ export const pokemonFormLevelMoves: PokemonSpeciesFormLevelMoves = { [ 5, Moves.SAND_ATTACK ], [ 10, Moves.QUICK_ATTACK ], [ 15, Moves.BABY_DOLL_EYES ], - [ 18, Moves.BOUNCY_BUBBLE ], //Custom - [ 18, Moves.SIZZLY_SLIDE ], //Custom - [ 18, Moves.BUZZY_BUZZ ], //Custom + [ 18, Moves.BOUNCY_BUBBLE ], // Custom + [ 18, Moves.SIZZLY_SLIDE ], // Custom + [ 18, Moves.BUZZY_BUZZ ], // Custom [ 20, Moves.SWIFT ], [ 25, Moves.BITE ], [ 30, Moves.COPYCAT ], - [ 33, Moves.BADDY_BAD ], //Custom - [ 33, Moves.GLITZY_GLOW ], //Custom + [ 33, Moves.BADDY_BAD ], // Custom + [ 33, Moves.GLITZY_GLOW ], // Custom [ 35, Moves.BATON_PASS ], - [ 40, Moves.VEEVEE_VOLLEY ], //Custom, replaces Take Down - [ 43, Moves.FREEZY_FROST ], //Custom - [ 43, Moves.SAPPY_SEED ], //Custom + [ 40, Moves.VEEVEE_VOLLEY ], // Custom, replaces Take Down + [ 43, Moves.FREEZY_FROST ], // Custom + [ 43, Moves.SAPPY_SEED ], // Custom [ 45, Moves.CHARM ], [ 50, Moves.DOUBLE_EDGE ], - [ 53, Moves.SPARKLY_SWIRL ], //Custom + [ 53, Moves.SPARKLY_SWIRL ], // Custom [ 55, Moves.LAST_RESORT ], ], 2: [ @@ -18864,27 +19157,27 @@ export const pokemonFormLevelMoves: PokemonSpeciesFormLevelMoves = { [ 5, Moves.SAND_ATTACK ], [ 10, Moves.QUICK_ATTACK ], [ 15, Moves.BABY_DOLL_EYES ], - [ 18, Moves.BOUNCY_BUBBLE ], //Custom - [ 18, Moves.SIZZLY_SLIDE ], //Custom - [ 18, Moves.BUZZY_BUZZ ], //Custom + [ 18, Moves.BOUNCY_BUBBLE ], // Custom + [ 18, Moves.SIZZLY_SLIDE ], // Custom + [ 18, Moves.BUZZY_BUZZ ], // Custom [ 20, Moves.SWIFT ], [ 25, Moves.BITE ], [ 30, Moves.COPYCAT ], - [ 33, Moves.BADDY_BAD ], //Custom - [ 33, Moves.GLITZY_GLOW ], //Custom + [ 33, Moves.BADDY_BAD ], // Custom + [ 33, Moves.GLITZY_GLOW ], // Custom [ 35, Moves.BATON_PASS ], - [ 40, Moves.VEEVEE_VOLLEY ], //Custom, replaces Take Down - [ 43, Moves.FREEZY_FROST ], //Custom - [ 43, Moves.SAPPY_SEED ], //Custom + [ 40, Moves.VEEVEE_VOLLEY ], // Custom, replaces Take Down + [ 43, Moves.FREEZY_FROST ], // Custom + [ 43, Moves.SAPPY_SEED ], // Custom [ 45, Moves.CHARM ], [ 50, Moves.DOUBLE_EDGE ], - [ 53, Moves.SPARKLY_SWIRL ], //Custom + [ 53, Moves.SPARKLY_SWIRL ], // Custom [ 55, Moves.LAST_RESORT ], ], }, [Species.DEOXYS]: { 1: [ - [ 1, Moves.CONFUSION ], //Custom + [ 1, Moves.CONFUSION ], // Custom [ 1, Moves.WRAP ], [ 1, Moves.LEER ], [ 7, Moves.NIGHT_SHADE ], @@ -18901,7 +19194,7 @@ export const pokemonFormLevelMoves: PokemonSpeciesFormLevelMoves = { [ 73, Moves.HYPER_BEAM ], ], 2: [ - [ 1, Moves.CONFUSION ], //Custom + [ 1, Moves.CONFUSION ], // Custom [ 1, Moves.WRAP ], [ 1, Moves.LEER ], [ 7, Moves.NIGHT_SHADE ], @@ -18920,7 +19213,7 @@ export const pokemonFormLevelMoves: PokemonSpeciesFormLevelMoves = { [ 73, Moves.MIRROR_COAT ], ], 3: [ - [ 1, Moves.CONFUSION ], //Custom + [ 1, Moves.CONFUSION ], // Custom [ 1, Moves.WRAP ], [ 1, Moves.LEER ], [ 7, Moves.NIGHT_SHADE ], @@ -18940,6 +19233,7 @@ export const pokemonFormLevelMoves: PokemonSpeciesFormLevelMoves = { [Species.WORMADAM]: { 1: [ [ EVOLVE_MOVE, Moves.QUIVER_DANCE ], + [ 1, Moves.STRUGGLE_BUG ], // Previous Stage Move, Custom [ 1, Moves.TACKLE ], [ 1, Moves.PROTECT ], [ 1, Moves.SUCKER_PUNCH ], @@ -18960,6 +19254,7 @@ export const pokemonFormLevelMoves: PokemonSpeciesFormLevelMoves = { ], 2: [ [ EVOLVE_MOVE, Moves.QUIVER_DANCE ], + [ 1, Moves.STRUGGLE_BUG ], // Previous Stage Move, Custom [ 1, Moves.METAL_BURST ], [ 1, Moves.TACKLE ], [ 1, Moves.PROTECT ], @@ -19064,7 +19359,7 @@ export const pokemonFormLevelMoves: PokemonSpeciesFormLevelMoves = { }, [Species.SHAYMIN]: { 1: [ - [ 1, Moves.LEAFAGE ], //Custom + [ 1, Moves.LEAFAGE ], // Custom [ 1, Moves.GROWTH ], [ 10, Moves.MAGICAL_LEAF ], [ 19, Moves.LEECH_SEED ], @@ -19166,6 +19461,10 @@ export const pokemonFormLevelMoves: PokemonSpeciesFormLevelMoves = { [ 1, Moves.GROWL ], [ 1, Moves.WATER_GUN ], [ 1, Moves.QUICK_ATTACK ], + [ 1, Moves.ROUND ], // Previous Stage Move + [ 1, Moves.FLING ], // Previous Stage Move + [ 1, Moves.SMACK_DOWN ], // Previous Stage Move + [ 1, Moves.BOUNCE ], // Previous Stage Move [ 1, Moves.HAZE ], [ 1, Moves.ROLE_PLAY ], [ 1, Moves.NIGHT_SLASH ], diff --git a/src/data/battle-anims.ts b/src/data/battle-anims.ts index 03bf0809fa6f..37900a3ab5ac 100644 --- a/src/data/battle-anims.ts +++ b/src/data/battle-anims.ts @@ -1,6 +1,6 @@ //import { battleAnimRawData } from "./battle-anim-raw-data"; import BattleScene from "../battle-scene"; -import { AttackMove, BeakBlastHeaderAttr, ChargeAttr, DelayedAttackAttr, MoveFlags, SelfStatusMove, allMoves } from "./move"; +import { AttackMove, BeakBlastHeaderAttr, DelayedAttackAttr, MoveFlags, SelfStatusMove, allMoves } from "./move"; import Pokemon from "../field/pokemon"; import * as Utils from "../utils"; import { BattlerIndex } from "../battle"; @@ -476,8 +476,11 @@ export function initMoveAnim(scene: BattleScene, move: Moves): Promise { } else { const loadedCheckTimer = setInterval(() => { if (moveAnims.get(move) !== null) { - const chargeAttr = allMoves[move].getAttrs(ChargeAttr)[0] || allMoves[move].getAttrs(DelayedAttackAttr)[0]; - if (chargeAttr && chargeAnims.get(chargeAttr.chargeAnim) === null) { + const chargeAnimSource = (allMoves[move].isChargingMove()) + ? allMoves[move] + : (allMoves[move].getAttrs(DelayedAttackAttr)[0] + ?? allMoves[move].getAttrs(BeakBlastHeaderAttr)[0]); + if (chargeAnimSource && chargeAnims.get(chargeAnimSource.chargeAnim) === null) { return; } clearInterval(loadedCheckTimer); @@ -507,11 +510,12 @@ export function initMoveAnim(scene: BattleScene, move: Moves): Promise { } else { populateMoveAnim(move, ba); } - const chargeAttr = allMoves[move].getAttrs(ChargeAttr)[0] - || allMoves[move].getAttrs(DelayedAttackAttr)[0] - || allMoves[move].getAttrs(BeakBlastHeaderAttr)[0]; - if (chargeAttr) { - initMoveChargeAnim(scene, chargeAttr.chargeAnim).then(() => resolve()); + const chargeAnimSource = (allMoves[move].isChargingMove()) + ? allMoves[move] + : (allMoves[move].getAttrs(DelayedAttackAttr)[0] + ?? allMoves[move].getAttrs(BeakBlastHeaderAttr)[0]); + if (chargeAnimSource) { + initMoveChargeAnim(scene, chargeAnimSource.chargeAnim).then(() => resolve()); } else { resolve(); } @@ -638,11 +642,12 @@ export function loadMoveAnimAssets(scene: BattleScene, moveIds: Moves[], startLo return new Promise(resolve => { const moveAnimations = moveIds.map(m => moveAnims.get(m) as AnimConfig).flat(); for (const moveId of moveIds) { - const chargeAttr = allMoves[moveId].getAttrs(ChargeAttr)[0] - || allMoves[moveId].getAttrs(DelayedAttackAttr)[0] - || allMoves[moveId].getAttrs(BeakBlastHeaderAttr)[0]; - if (chargeAttr) { - const moveChargeAnims = chargeAnims.get(chargeAttr.chargeAnim); + const chargeAnimSource = (allMoves[moveId].isChargingMove()) + ? allMoves[moveId] + : (allMoves[moveId].getAttrs(DelayedAttackAttr)[0] + ?? allMoves[moveId].getAttrs(BeakBlastHeaderAttr)[0]); + if (chargeAnimSource) { + const moveChargeAnims = chargeAnims.get(chargeAnimSource.chargeAnim); moveAnimations.push(moveChargeAnims instanceof AnimConfig ? moveChargeAnims : moveChargeAnims![0]); // TODO: is the bang correct? if (Array.isArray(moveChargeAnims)) { moveAnimations.push(moveChargeAnims[1]); diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index c3b7765d062a..d671c56ab26c 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -11,7 +11,6 @@ import { ChargeAnim, CommonAnim, CommonBattleAnim, MoveChargeAnim } from "#app/d import Move, { allMoves, applyMoveAttrs, - ChargeAttr, ConsecutiveUseDoublePowerAttr, HealOnAllyAttr, MoveCategory, @@ -949,10 +948,6 @@ export class EncoreTag extends BattlerTag { return false; } - if (allMoves[repeatableMove.move].hasAttr(ChargeAttr) && repeatableMove.result === MoveResult.OTHER) { - return false; - } - this.moveId = repeatableMove.move; return true; @@ -2591,7 +2586,7 @@ export class TormentTag extends MoveRestrictionBattlerTag { // This checks for locking / momentum moves like Rollout and Hydro Cannon + if the user is under the influence of BattlerTagType.FRENZY // Because Uproar's unique behavior is not implemented, it does not check for Uproar. Torment has been marked as partial in moves.ts const moveObj = allMoves[lastMove.move]; - const isUnaffected = moveObj.hasAttr(ConsecutiveUseDoublePowerAttr) || user.getTag(BattlerTagType.FRENZY) || moveObj.hasAttr(ChargeAttr); + const isUnaffected = moveObj.hasAttr(ConsecutiveUseDoublePowerAttr) || user.getTag(BattlerTagType.FRENZY); const validLastMoveResult = (lastMove.result === MoveResult.SUCCESS) || (lastMove.result === MoveResult.MISS); if (lastMove.move === move && validLastMoveResult && lastMove.move !== Moves.STRUGGLE && !isUnaffected) { return true; diff --git a/src/data/move.ts b/src/data/move.ts index 501b7ade28d7..a4819439a55a 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -289,10 +289,9 @@ export default class Move implements Localizable { } /** - * Getter function that returns if the move targets itself or an ally + * Getter function that returns if the move targets the user or its ally * @returns boolean */ - isAllyTarget(): boolean { switch (this.moveTarget) { case MoveTarget.USER: @@ -306,6 +305,10 @@ export default class Move implements Localizable { return false; } + isChargingMove(): this is ChargingMove { + return false; + } + /** * Checks if the move is immune to certain types. * Currently looks at cases of Grass types with powder moves and Dark types with moves affected by Prankster. @@ -893,6 +896,85 @@ export class SelfStatusMove extends Move { } } +type SubMove = new (...args: any[]) => Move; + +function ChargeMove(Base: TBase) { + return class extends Base { + /** The animation to play during the move's charging phase */ + public readonly chargeAnim: ChargeAnim = ChargeAnim[`${Moves[this.id]}_CHARGING`]; + /** The message to show during the move's charging phase */ + private _chargeText: string; + + /** Move attributes that apply during the move's charging phase */ + public chargeAttrs: MoveAttr[] = []; + + override isChargingMove(): this is ChargingMove { + return true; + } + + /** + * Sets the text to be displayed during this move's charging phase. + * References to the user Pokemon should be written as "{USER}", and + * references to the target Pokemon should be written as "{TARGET}". + * @param chargeText the text to set + * @returns this {@linkcode Move} (for chaining API purposes) + */ + chargeText(chargeText: string): this { + this._chargeText = chargeText; + return this; + } + + /** + * Queues the charge text to display to the player + * @param user the {@linkcode Pokemon} using this move + * @param target the {@linkcode Pokemon} targeted by this move (optional) + */ + showChargeText(user: Pokemon, target?: Pokemon): void { + user.scene.queueMessage(this._chargeText + .replace("{USER}", getPokemonNameWithAffix(user)) + .replace("{TARGET}", getPokemonNameWithAffix(target)) + ); + } + + /** + * Gets all charge attributes of the given attribute type. + * @param attrType any attribute that extends {@linkcode MoveAttr} + * @returns Array of attributes that match `attrType`, or an empty array if + * no matches are found. + */ + getChargeAttrs(attrType: Constructor): T[] { + return this.chargeAttrs.filter((attr): attr is T => attr instanceof attrType); + } + + /** + * Checks if this move has an attribute of the given type. + * @param attrType any attribute that extends {@linkcode MoveAttr} + * @returns `true` if a matching attribute is found; `false` otherwise + */ + hasChargeAttr(attrType: Constructor): boolean { + return this.chargeAttrs.some((attr) => attr instanceof attrType); + } + + /** + * Adds an attribute to this move to be applied during the move's charging phase + * @param ChargeAttrType the type of {@linkcode MoveAttr} being added + * @param args the parameters to construct the given {@linkcode MoveAttr} with + * @returns this {@linkcode Move} (for chaining API purposes) + */ + chargeAttr>(ChargeAttrType: T, ...args: ConstructorParameters): this { + const chargeAttr = new ChargeAttrType(...args); + this.chargeAttrs.push(chargeAttr); + + return this; + } + }; +} + +export class ChargingAttackMove extends ChargeMove(AttackMove) {} +export class ChargingSelfStatusMove extends ChargeMove(SelfStatusMove) {} + +export type ChargingMove = ChargingAttackMove | ChargingSelfStatusMove; + /** * Base class defining all {@linkcode Move} Attributes * @abstract @@ -2623,116 +2705,68 @@ export class OneHitKOAttr extends MoveAttr { } } -export class OverrideMoveEffectAttr extends MoveAttr { - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean | Promise { - //const overridden = args[0] as Utils.BooleanHolder; - //const virtual = arg[1] as boolean; - return true; - } -} - -export class ChargeAttr extends OverrideMoveEffectAttr { - public chargeAnim: ChargeAnim; - private chargeText: string; - private tagType: BattlerTagType | null; - private chargeEffect: boolean; - public followUpPriority: integer | null; - - constructor(chargeAnim: ChargeAnim, chargeText: string, tagType?: BattlerTagType | null, chargeEffect: boolean = false) { - super(); - - this.chargeAnim = chargeAnim; - this.chargeText = chargeText; - this.tagType = tagType!; // TODO: is this bang correct? - this.chargeEffect = chargeEffect; - } +/** + * Attribute that allows charge moves to resolve in 1 turn under a given condition. + * Should only be used for {@linkcode ChargingMove | ChargingMoves} as a `chargeAttr`. + * @extends MoveAttr + */ +export class InstantChargeAttr extends MoveAttr { + /** The condition in which the move with this attribute instantly charges */ + protected readonly condition: UserMoveConditionFunc; - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { - return new Promise(resolve => { - const lastMove = user.getLastXMoves().find(() => true); - if (!lastMove || lastMove.move !== move.id || (lastMove.result !== MoveResult.OTHER && lastMove.turn !== user.scene.currentBattle.turn)) { - (args[0] as Utils.BooleanHolder).value = true; - new MoveChargeAnim(this.chargeAnim, move.id, user).play(user.scene, false, () => { - user.scene.queueMessage(this.chargeText.replace("{TARGET}", getPokemonNameWithAffix(target)).replace("{USER}", getPokemonNameWithAffix(user))); - if (this.tagType) { - user.addTag(this.tagType, 1, move.id, user.id); - } - if (this.chargeEffect) { - applyMoveAttrs(MoveEffectAttr, user, target, move); - } - user.pushMoveHistory({ move: move.id, targets: [ target.getBattlerIndex() ], result: MoveResult.OTHER }); - user.getMoveQueue().push({ move: move.id, targets: [ target.getBattlerIndex() ], ignorePP: true }); - user.addTag(BattlerTagType.CHARGING, 1, move.id, user.id); - resolve(true); - }); - } else { - user.lapseTag(BattlerTagType.CHARGING); - resolve(false); - } - }); + constructor(condition: UserMoveConditionFunc) { + super(true); + this.condition = condition; } - usedChargeEffect(user: Pokemon, target: Pokemon | null, move: Move): boolean { - if (!this.chargeEffect) { + /** + * Flags the move with this attribute as instantly charged if this attribute's condition is met. + * @param user the {@linkcode Pokemon} using the move + * @param target n/a + * @param move the {@linkcode Move} associated with this attribute + * @param args + * - `[0]` a {@linkcode Utils.BooleanHolder | BooleanHolder} for the "instant charge" flag + * @returns `true` if the instant charge condition is met; `false` otherwise. + */ + override apply(user: Pokemon, target: Pokemon | null, move: Move, args: any[]): boolean { + const instantCharge = args[0]; + if (!(instantCharge instanceof Utils.BooleanHolder)) { return false; } - // Account for move history being populated when this function is called - const lastMoves = user.getLastXMoves(2); - return lastMoves.length === 2 && lastMoves[1].move === move.id && lastMoves[1].result === MoveResult.OTHER; + + if (this.condition(user, move)) { + instantCharge.value = true; + return true; + } + return false; } } -export class SunlightChargeAttr extends ChargeAttr { - constructor(chargeAnim: ChargeAnim, chargeText: string) { - super(chargeAnim, chargeText); - } +/** + * Attribute that allows charge moves to resolve in 1 turn while specific {@linkcode WeatherType | Weather} + * is active. Should only be used for {@linkcode ChargingMove | ChargingMoves} as a `chargeAttr`. + * @extends InstantChargeAttr + */ +export class WeatherInstantChargeAttr extends InstantChargeAttr { + constructor(weatherTypes: WeatherType[]) { + super((user, move) => { + const currentWeather = user.scene.arena.weather; - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { - return new Promise(resolve => { - const weatherType = user.scene.arena.weather?.weatherType; - if (!user.scene.arena.weather?.isEffectSuppressed(user.scene) && (weatherType === WeatherType.SUNNY || weatherType === WeatherType.HARSH_SUN)) { - resolve(false); + if (Utils.isNullOrUndefined(currentWeather?.weatherType)) { + return false; } else { - super.apply(user, target, move, args).then(result => resolve(result)); + return !currentWeather?.isEffectSuppressed(user.scene) + && weatherTypes.includes(currentWeather?.weatherType); } }); } } -export class ElectroShotChargeAttr extends ChargeAttr { - private statIncreaseApplied: boolean; - constructor() { - super(ChargeAnim.ELECTRO_SHOT_CHARGING, i18next.t("moveTriggers:absorbedElectricity", { pokemonName: "{USER}" }), null, true); - // Add a flag because ChargeAttr skills use themselves twice instead of once over one-to-two turns - this.statIncreaseApplied = false; - } - - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { - return new Promise(resolve => { - const weatherType = user.scene.arena.weather?.weatherType; - if (!user.scene.arena.weather?.isEffectSuppressed(user.scene) && (weatherType === WeatherType.RAIN || weatherType === WeatherType.HEAVY_RAIN)) { - // Apply the SPATK increase every call when used in the rain - const statChangeAttr = new StatStageChangeAttr([ Stat.SPATK ], 1, true); - statChangeAttr.apply(user, target, move, args); - // After the SPATK is raised, execute the move resolution e.g. deal damage - resolve(false); - } else { - if (!this.statIncreaseApplied) { - // Apply the SPATK increase only if it hasn't been applied before e.g. on the first turn charge up animation - const statChangeAttr = new StatStageChangeAttr([ Stat.SPATK ], 1, true); - statChangeAttr.apply(user, target, move, args); - // Set the flag to true so that on the following turn it doesn't raise SPATK a second time - this.statIncreaseApplied = true; - } - super.apply(user, target, move, args).then(result => { - if (!result) { - // On the second turn, reset the statIncreaseApplied flag without applying the SPATK increase - this.statIncreaseApplied = false; - } - resolve(result); - }); - } - }); +export class OverrideMoveEffectAttr extends MoveAttr { + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean | Promise { + //const overridden = args[0] as Utils.BooleanHolder; + //const virtual = arg[1] as boolean; + return true; } } @@ -4943,6 +4977,37 @@ export const frenzyMissFunc: UserMoveConditionFunc = (user: Pokemon, move: Move) return true; }; +/** + * Attribute that grants {@link https://bulbapedia.bulbagarden.net/wiki/Semi-invulnerable_turn | semi-invulnerability} to the user during + * the associated move's charging phase. Should only be used for {@linkcode ChargingMove | ChargingMoves} as a `chargeAttr`. + * @extends MoveEffectAttr + */ +export class SemiInvulnerableAttr extends MoveEffectAttr { + /** The type of {@linkcode SemiInvulnerableTag} to grant to the user */ + public tagType: BattlerTagType; + + constructor(tagType: BattlerTagType) { + super(true); + this.tagType = tagType; + } + + /** + * Grants a {@linkcode SemiInvulnerableTag} to the associated move's user. + * @param user the {@linkcode Pokemon} using the move + * @param target n/a + * @param move the {@linkcode Move} being used + * @param args n/a + * @returns `true` if semi-invulnerability was successfully granted; `false` otherwise. + */ + override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean { + if (!super.apply(user, target, move, args)) { + return false; + } + + return user.addTag(this.tagType, 1, move.id, user.id); + } +} + export class AddBattlerTagAttr extends MoveEffectAttr { public tagType: BattlerTagType; public turnCountMin: integer; @@ -6203,7 +6268,7 @@ const lastMoveCopiableCondition: MoveConditionFunc = (user, target, move) => { return false; } - if (allMoves[copiableMove].hasAttr(ChargeAttr)) { + if (allMoves[copiableMove].isChargingMove()) { return false; } @@ -6351,7 +6416,7 @@ const targetMoveCopiableCondition: MoveConditionFunc = (user, target, move) => { return false; } - if (allMoves[copiableMove.move].hasAttr(ChargeAttr) && copiableMove.result === MoveResult.OTHER) { + if (allMoves[copiableMove.move].isChargingMove() && copiableMove.result === MoveResult.OTHER) { return false; } @@ -6643,42 +6708,49 @@ export class SuppressAbilitiesIfActedAttr extends MoveEffectAttr { } export class TransformAttr extends MoveEffectAttr { - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { - return new Promise(resolve => { - if (!super.apply(user, target, move, args)) { - return resolve(false); - } + async apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { + if (!super.apply(user, target, move, args)) { + return false; + } - user.summonData.speciesForm = target.getSpeciesForm(); - user.summonData.fusionSpeciesForm = target.getFusionSpeciesForm(); - user.summonData.ability = target.getAbility().id; - user.summonData.gender = target.getGender(); - user.summonData.fusionGender = target.getFusionGender(); + const promises: Promise[] = []; + user.summonData.speciesForm = target.getSpeciesForm(); + user.summonData.fusionSpeciesForm = target.getFusionSpeciesForm(); + user.summonData.ability = target.getAbility().id; + user.summonData.gender = target.getGender(); + user.summonData.fusionGender = target.getFusionGender(); - // Power Trick's effect will not preserved after using Transform - user.removeTag(BattlerTagType.POWER_TRICK); + // Power Trick's effect will not preserved after using Transform + user.removeTag(BattlerTagType.POWER_TRICK); - // Copy all stats (except HP) - for (const s of EFFECTIVE_STATS) { - user.setStat(s, target.getStat(s, false), false); - } + // Copy all stats (except HP) + for (const s of EFFECTIVE_STATS) { + user.setStat(s, target.getStat(s, false), false); + } - // Copy all stat stages - for (const s of BATTLE_STATS) { - user.setStatStage(s, target.getStatStage(s)); - } + // Copy all stat stages + for (const s of BATTLE_STATS) { + user.setStatStage(s, target.getStatStage(s)); + } - user.summonData.moveset = target.getMoveset().map(m => new PokemonMove(m?.moveId!, m?.ppUsed, m?.ppUp)); // TODO: is this bang correct? - user.summonData.types = target.getTypes(); + user.summonData.moveset = target.getMoveset().map(m => { + const pp = m?.getMove().pp ?? 0; + // if PP value is less than 5, do nothing. If greater, we need to reduce the value to 5 using a negative ppUp value. + const ppUp = pp <= 5 ? 0 : (5 - pp) / Math.max(Math.floor(pp / 5), 1); + return new PokemonMove(m?.moveId!, 0, ppUp); + }); + user.summonData.types = target.getTypes(); + promises.push(user.updateInfo()); - user.scene.queueMessage(i18next.t("moveTriggers:transformedIntoTarget", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target) })); + user.scene.queueMessage(i18next.t("moveTriggers:transformedIntoTarget", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target) })); - user.loadAssets(false).then(() => { - user.playAnim(); - user.updateInfo(); - resolve(true); - }); - }); + promises.push(user.loadAssets(false).then(() => { + user.playAnim(); + user.updateInfo(); + })); + + await Promise.all(promises); + return true; } } @@ -7050,6 +7122,20 @@ function applyMoveAttrsInternal(attrFilter: MoveAttrFilter, user: Pokemon | null }); } +function applyMoveChargeAttrsInternal(attrFilter: MoveAttrFilter, user: Pokemon | null, target: Pokemon | null, move: ChargingMove, args: any[]): Promise { + return new Promise(resolve => { + const chargeAttrPromises: Promise[] = []; + const chargeMoveAttrs = move.chargeAttrs.filter(a => attrFilter(a)); + for (const attr of chargeMoveAttrs) { + const result = attr.apply(user, target, move, args); + if (result instanceof Promise) { + chargeAttrPromises.push(result); + } + } + Promise.allSettled(chargeAttrPromises).then(() => resolve()); + }); +} + export function applyMoveAttrs(attrType: Constructor, user: Pokemon | null, target: Pokemon | null, move: Move, ...args: any[]): Promise { return applyMoveAttrsInternal((attr: MoveAttr) => attr instanceof attrType, user, target, move, args); } @@ -7058,6 +7144,10 @@ export function applyFilteredMoveAttrs(attrFilter: MoveAttrFilter, user: Pokemon return applyMoveAttrsInternal(attrFilter, user, target, move, args); } +export function applyMoveChargeAttrs(attrType: Constructor, user: Pokemon | null, target: Pokemon | null, move: ChargingMove, ...args: any[]): Promise { + return applyMoveChargeAttrsInternal((attr: MoveAttr) => attr instanceof attrType, user, target, move, args); +} + export class MoveCondition { protected func: MoveConditionFunc; @@ -7302,8 +7392,8 @@ export function initMoves() { new AttackMove(Moves.GUILLOTINE, Type.NORMAL, MoveCategory.PHYSICAL, 200, 30, 5, -1, 0, 1) .attr(OneHitKOAttr) .attr(OneHitKOAccuracyAttr), - new AttackMove(Moves.RAZOR_WIND, Type.NORMAL, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 1) - .attr(ChargeAttr, ChargeAnim.RAZOR_WIND_CHARGING, i18next.t("moveTriggers:whippedUpAWhirlwind", { pokemonName: "{USER}" })) + new ChargingAttackMove(Moves.RAZOR_WIND, Type.NORMAL, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 1) + .chargeText(i18next.t("moveTriggers:whippedUpAWhirlwind", { pokemonName: "{USER}" })) .attr(HighCritAttr) .windMove() .ignoresVirtual() @@ -7323,8 +7413,9 @@ export function initMoves() { .hidesTarget() .windMove() .partial(), // Should force random switches - new AttackMove(Moves.FLY, Type.FLYING, MoveCategory.PHYSICAL, 90, 95, 15, -1, 0, 1) - .attr(ChargeAttr, ChargeAnim.FLY_CHARGING, i18next.t("moveTriggers:flewUpHigh", { pokemonName: "{USER}" }), BattlerTagType.FLYING) + 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) .condition(failOnGravityCondition) .ignoresVirtual(), new AttackMove(Moves.BIND, Type.NORMAL, MoveCategory.PHYSICAL, 15, 85, 20, -1, 0, 1) @@ -7473,8 +7564,9 @@ export function initMoves() { .makesContact(false) .slicingMove() .target(MoveTarget.ALL_NEAR_ENEMIES), - new AttackMove(Moves.SOLAR_BEAM, Type.GRASS, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 1) - .attr(SunlightChargeAttr, ChargeAnim.SOLAR_BEAM_CHARGING, i18next.t("moveTriggers:tookInSunlight", { pokemonName: "{USER}" })) + new ChargingAttackMove(Moves.SOLAR_BEAM, Type.GRASS, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 1) + .chargeText(i18next.t("moveTriggers:tookInSunlight", { pokemonName: "{USER}" })) + .chargeAttr(WeatherInstantChargeAttr, [ WeatherType.SUNNY, WeatherType.HARSH_SUN ]) .attr(AntiSunlightPowerDecreaseAttr) .ignoresVirtual(), new StatusMove(Moves.POISON_POWDER, Type.POISON, 75, 35, -1, 0, 1) @@ -7523,8 +7615,9 @@ export function initMoves() { .attr(OneHitKOAccuracyAttr) .attr(HitsTagAttr, BattlerTagType.UNDERGROUND) .makesContact(false), - new AttackMove(Moves.DIG, Type.GROUND, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 1) - .attr(ChargeAttr, ChargeAnim.DIG_CHARGING, i18next.t("moveTriggers:dugAHole", { pokemonName: "{USER}" }), BattlerTagType.UNDERGROUND) + new ChargingAttackMove(Moves.DIG, Type.GROUND, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 1) + .chargeText(i18next.t("moveTriggers:dugAHole", { pokemonName: "{USER}" })) + .chargeAttr(SemiInvulnerableAttr, BattlerTagType.UNDERGROUND) .ignoresVirtual(), new StatusMove(Moves.TOXIC, Type.POISON, 90, 10, -1, 0, 1) .attr(StatusEffectAttr, StatusEffect.TOXIC) @@ -7620,9 +7713,9 @@ export function initMoves() { .attr(TrapAttr, BattlerTagType.CLAMP), new AttackMove(Moves.SWIFT, Type.NORMAL, MoveCategory.SPECIAL, 60, -1, 20, -1, 0, 1) .target(MoveTarget.ALL_NEAR_ENEMIES), - new AttackMove(Moves.SKULL_BASH, Type.NORMAL, MoveCategory.PHYSICAL, 130, 100, 10, -1, 0, 1) - .attr(ChargeAttr, ChargeAnim.SKULL_BASH_CHARGING, i18next.t("moveTriggers:loweredItsHead", { pokemonName: "{USER}" }), null, true) - .attr(StatStageChangeAttr, [ Stat.DEF ], 1, true) + new ChargingAttackMove(Moves.SKULL_BASH, Type.NORMAL, MoveCategory.PHYSICAL, 130, 100, 10, -1, 0, 1) + .chargeText(i18next.t("moveTriggers:loweredItsHead", { pokemonName: "{USER}" })) + .chargeAttr(StatStageChangeAttr, [ Stat.DEF ], 1, true) .ignoresVirtual(), new AttackMove(Moves.SPIKE_CANNON, Type.NORMAL, MoveCategory.PHYSICAL, 20, 100, 15, -1, 0, 1) .attr(MultiHitAttr) @@ -7659,8 +7752,8 @@ export function initMoves() { .triageMove(), new StatusMove(Moves.LOVELY_KISS, Type.NORMAL, 75, 10, -1, 0, 1) .attr(StatusEffectAttr, StatusEffect.SLEEP), - new AttackMove(Moves.SKY_ATTACK, Type.FLYING, MoveCategory.PHYSICAL, 140, 90, 5, 30, 0, 1) - .attr(ChargeAttr, ChargeAnim.SKY_ATTACK_CHARGING, i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" })) + new ChargingAttackMove(Moves.SKY_ATTACK, Type.FLYING, MoveCategory.PHYSICAL, 140, 90, 5, 30, 0, 1) + .chargeText(i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" })) .attr(HighCritAttr) .attr(FlinchAttr) .makesContact(false) @@ -8125,9 +8218,10 @@ export function initMoves() { new AttackMove(Moves.SECRET_POWER, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 20, 30, 0, 3) .makesContact(false) .attr(SecretPowerAttr), - new AttackMove(Moves.DIVE, Type.WATER, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 3) - .attr(ChargeAttr, ChargeAnim.DIVE_CHARGING, i18next.t("moveTriggers:hidUnderwater", { pokemonName: "{USER}" }), BattlerTagType.UNDERWATER, true) - .attr(GulpMissileTagAttr) + new ChargingAttackMove(Moves.DIVE, Type.WATER, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 3) + .chargeText(i18next.t("moveTriggers:hidUnderwater", { pokemonName: "{USER}" })) + .chargeAttr(SemiInvulnerableAttr, BattlerTagType.UNDERWATER) + .chargeAttr(GulpMissileTagAttr) .ignoresVirtual(), new AttackMove(Moves.ARM_THRUST, Type.FIGHTING, MoveCategory.PHYSICAL, 15, 100, 20, -1, 0, 3) .attr(MultiHitAttr), @@ -8260,8 +8354,9 @@ export function initMoves() { .attr(RechargeAttr), new SelfStatusMove(Moves.BULK_UP, Type.FIGHTING, -1, 20, -1, 0, 3) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], 1, true), - new AttackMove(Moves.BOUNCE, Type.FLYING, MoveCategory.PHYSICAL, 85, 85, 5, 30, 0, 3) - .attr(ChargeAttr, ChargeAnim.BOUNCE_CHARGING, i18next.t("moveTriggers:sprangUp", { pokemonName: "{USER}" }), BattlerTagType.FLYING) + new ChargingAttackMove(Moves.BOUNCE, Type.FLYING, MoveCategory.PHYSICAL, 85, 85, 5, 30, 0, 3) + .chargeText(i18next.t("moveTriggers:sprangUp", { pokemonName: "{USER}" })) + .chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING) .attr(StatusEffectAttr, StatusEffect.PARALYSIS) .condition(failOnGravityCondition) .ignoresVirtual(), @@ -8616,8 +8711,9 @@ export function initMoves() { new AttackMove(Moves.OMINOUS_WIND, Type.GHOST, MoveCategory.SPECIAL, 60, 100, 5, 10, 0, 4) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true) .windMove(), - new AttackMove(Moves.SHADOW_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 4) - .attr(ChargeAttr, ChargeAnim.SHADOW_FORCE_CHARGING, i18next.t("moveTriggers:vanishedInstantly", { pokemonName: "{USER}" }), BattlerTagType.HIDDEN) + new ChargingAttackMove(Moves.SHADOW_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 4) + .chargeText(i18next.t("moveTriggers:vanishedInstantly", { pokemonName: "{USER}" })) + .chargeAttr(SemiInvulnerableAttr, BattlerTagType.HIDDEN) .ignoresProtect() .ignoresVirtual(), new SelfStatusMove(Moves.HONE_CLAWS, Type.DARK, -1, 15, -1, 0, 5) @@ -8740,12 +8836,13 @@ export function initMoves() { .attr( MovePowerMultiplierAttr, (user, target, move) => target.status || target.hasAbility(Abilities.COMATOSE) ? 2 : 1), - new AttackMove(Moves.SKY_DROP, Type.FLYING, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 5) - .partial() // Should immobilize the target, Flying types should take no damage. cf https://bulbapedia.bulbagarden.net/wiki/Sky_Drop_(move) and https://www.smogon.com/dex/sv/moves/sky-drop/ - .attr(ChargeAttr, ChargeAnim.SKY_DROP_CHARGING, i18next.t("moveTriggers:tookTargetIntoSky", { pokemonName: "{USER}", targetName: "{TARGET}" }), BattlerTagType.FLYING) // TODO: Add 2nd turn message + new ChargingAttackMove(Moves.SKY_DROP, Type.FLYING, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 5) + .chargeText(i18next.t("moveTriggers:tookTargetIntoSky", { pokemonName: "{USER}", targetName: "{TARGET}" })) + .chargeAttr(SemiInvulnerableAttr, BattlerTagType.FLYING) .condition(failOnGravityCondition) .condition((user, target, move) => !target.getTag(BattlerTagType.SUBSTITUTE)) - .ignoresVirtual(), + .ignoresVirtual() + .partial(), // Should immobilize the target, Flying types should take no damage. cf https://bulbapedia.bulbagarden.net/wiki/Sky_Drop_(move) and https://www.smogon.com/dex/sv/moves/sky-drop/ new SelfStatusMove(Moves.SHIFT_GEAR, Type.STEEL, -1, 10, -1, 0, 5) .attr(StatStageChangeAttr, [ Stat.ATK ], 1, true) .attr(StatStageChangeAttr, [ Stat.SPD ], 2, true), @@ -8895,12 +8992,12 @@ export function initMoves() { new AttackMove(Moves.FIERY_DANCE, Type.FIRE, MoveCategory.SPECIAL, 80, 100, 10, 50, 0, 5) .attr(StatStageChangeAttr, [ Stat.SPATK ], 1, true) .danceMove(), - new AttackMove(Moves.FREEZE_SHOCK, Type.ICE, MoveCategory.PHYSICAL, 140, 90, 5, 30, 0, 5) - .attr(ChargeAttr, ChargeAnim.FREEZE_SHOCK_CHARGING, i18next.t("moveTriggers:becameCloakedInFreezingLight", { pokemonName: "{USER}" })) + new ChargingAttackMove(Moves.FREEZE_SHOCK, Type.ICE, MoveCategory.PHYSICAL, 140, 90, 5, 30, 0, 5) + .chargeText(i18next.t("moveTriggers:becameCloakedInFreezingLight", { pokemonName: "{USER}" })) .attr(StatusEffectAttr, StatusEffect.PARALYSIS) .makesContact(false), - new AttackMove(Moves.ICE_BURN, Type.ICE, MoveCategory.SPECIAL, 140, 90, 5, 30, 0, 5) - .attr(ChargeAttr, ChargeAnim.ICE_BURN_CHARGING, i18next.t("moveTriggers:becameCloakedInFreezingAir", { pokemonName: "{USER}" })) + new ChargingAttackMove(Moves.ICE_BURN, Type.ICE, MoveCategory.SPECIAL, 140, 90, 5, 30, 0, 5) + .chargeText(i18next.t("moveTriggers:becameCloakedInFreezingAir", { pokemonName: "{USER}" })) .attr(StatusEffectAttr, StatusEffect.BURN) .ignoresVirtual(), new AttackMove(Moves.SNARL, Type.DARK, MoveCategory.SPECIAL, 55, 95, 15, 100, 0, 5) @@ -8942,8 +9039,9 @@ export function initMoves() { .target(MoveTarget.ENEMY_SIDE), new AttackMove(Moves.FELL_STINGER, Type.BUG, MoveCategory.PHYSICAL, 50, 100, 25, -1, 0, 6) .attr(PostVictoryStatStageChangeAttr, [ Stat.ATK ], 3, true ), - new AttackMove(Moves.PHANTOM_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6) - .attr(ChargeAttr, ChargeAnim.PHANTOM_FORCE_CHARGING, i18next.t("moveTriggers:vanishedInstantly", { pokemonName: "{USER}" }), BattlerTagType.HIDDEN) + new ChargingAttackMove(Moves.PHANTOM_FORCE, Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6) + .chargeText(i18next.t("moveTriggers:vanishedInstantly", { pokemonName: "{USER}" })) + .chargeAttr(SemiInvulnerableAttr, BattlerTagType.HIDDEN) .ignoresProtect() .ignoresVirtual(), new StatusMove(Moves.TRICK_OR_TREAT, Type.GHOST, 100, 20, -1, 0, 6) @@ -9053,8 +9151,8 @@ export function initMoves() { .ignoresSubstitute() .powderMove() .unimplemented(), - new SelfStatusMove(Moves.GEOMANCY, Type.FAIRY, -1, 10, -1, 0, 6) - .attr(ChargeAttr, ChargeAnim.GEOMANCY_CHARGING, i18next.t("moveTriggers:isChargingPower", { pokemonName: "{USER}" })) + 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) .ignoresVirtual(), new StatusMove(Moves.MAGNETIC_FLUX, Type.ELECTRIC, -1, 20, -1, 0, 6) @@ -9263,8 +9361,9 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.ATK ], -1) .condition((user, target, move) => target.getStatStage(Stat.ATK) > -6) .triageMove(), - new AttackMove(Moves.SOLAR_BLADE, Type.GRASS, MoveCategory.PHYSICAL, 125, 100, 10, -1, 0, 7) - .attr(SunlightChargeAttr, ChargeAnim.SOLAR_BLADE_CHARGING, i18next.t("moveTriggers:isGlowing", { pokemonName: "{USER}" })) + 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 ]) .attr(AntiSunlightPowerDecreaseAttr) .slicingMove(), new AttackMove(Moves.LEAFAGE, Type.GRASS, MoveCategory.PHYSICAL, 40, 100, 40, -1, 0, 7) @@ -9690,9 +9789,9 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, { lastHitOnly: true }) .attr(MultiHitAttr) .makesContact(false), - new AttackMove(Moves.METEOR_BEAM, Type.ROCK, MoveCategory.SPECIAL, 120, 90, 10, 100, 0, 8) - .attr(ChargeAttr, ChargeAnim.METEOR_BEAM_CHARGING, i18next.t("moveTriggers:isOverflowingWithSpacePower", { pokemonName: "{USER}" }), null, true) - .attr(StatStageChangeAttr, [ Stat.SPATK ], 1, true) + new ChargingAttackMove(Moves.METEOR_BEAM, Type.ROCK, MoveCategory.SPECIAL, 120, 90, 10, -1, 0, 8) + .chargeText(i18next.t("moveTriggers:isOverflowingWithSpacePower", { pokemonName: "{USER}" })) + .chargeAttr(StatStageChangeAttr, [ Stat.SPATK ], 1, true) .ignoresVirtual(), new AttackMove(Moves.SHELL_SIDE_ARM, Type.POISON, MoveCategory.SPECIAL, 90, 100, 10, 20, 0, 8) .attr(ShellSideArmCategoryAttr) @@ -10144,8 +10243,10 @@ export function initMoves() { .attr(IvyCudgelTypeAttr) .attr(HighCritAttr) .makesContact(false), - new AttackMove(Moves.ELECTRO_SHOT, Type.ELECTRIC, MoveCategory.SPECIAL, 130, 100, 10, 100, 0, 9) - .attr(ElectroShotChargeAttr) + new ChargingAttackMove(Moves.ELECTRO_SHOT, Type.ELECTRIC, MoveCategory.SPECIAL, 130, 100, 10, 100, 0, 9) + .chargeText(i18next.t("moveTriggers:absorbedElectricity", { pokemonName: "{USER}" })) + .chargeAttr(StatStageChangeAttr, [ Stat.SPATK ], 1, true) + .chargeAttr(WeatherInstantChargeAttr, [ WeatherType.RAIN, WeatherType.HEAVY_RAIN ]) .ignoresVirtual(), new AttackMove(Moves.TERA_STARSTORM, Type.NORMAL, MoveCategory.SPECIAL, 120, 100, 5, -1, 0, 9) .attr(TeraMoveCategoryAttr) diff --git a/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts b/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts index 8dc4eca25bf6..1e20b73e3516 100644 --- a/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts +++ b/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts @@ -1,4 +1,4 @@ -import { leaveEncounterWithoutBattle, setEncounterExp, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { generateModifierType, leaveEncounterWithoutBattle, setEncounterExp, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { modifierTypes } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Species } from "#enums/species"; @@ -14,6 +14,7 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import i18next from "i18next"; /** the i18n namespace for this encounter */ const namespace = "mysteryEncounters/anOfferYouCantRefuse"; @@ -98,6 +99,8 @@ export const AnOfferYouCantRefuseEncounter: MysteryEncounter = } } + const shinyCharm = generateModifierType(scene, modifierTypes.SHINY_CHARM); + encounter.setDialogueToken("itemName", shinyCharm?.name ?? i18next.t("modifierType:ModifierType.SHINY_CHARM.name")); encounter.setDialogueToken("liepardName", getPokemonSpecies(Species.LIEPARD).getName()); return true; @@ -123,7 +126,7 @@ export const AnOfferYouCantRefuseEncounter: MysteryEncounter = return true; }) .withOptionPhase(async (scene: BattleScene) => { - // Give the player a Shiny charm + // Give the player a Shiny Charm scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.SHINY_CHARM)); leaveEncounterWithoutBattle(scene, true); }) @@ -132,9 +135,11 @@ export const AnOfferYouCantRefuseEncounter: MysteryEncounter = .withOption( MysteryEncounterOptionBuilder .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) - .withPrimaryPokemonRequirement(new CombinationPokemonRequirement( - new MoveRequirement(EXTORTION_MOVES, true), - new AbilityRequirement(EXTORTION_ABILITIES, true)) + .withPrimaryPokemonRequirement( + CombinationPokemonRequirement.Some( + new MoveRequirement(EXTORTION_MOVES, true), + new AbilityRequirement(EXTORTION_ABILITIES, true) + ) ) .withDialogue({ buttonLabel: `${namespace}:option.2.label`, diff --git a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts index d316ab14cde4..e24eadb56c72 100644 --- a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts +++ b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts @@ -193,12 +193,14 @@ const WAVE_LEVEL_BREAKPOINTS = [ 30, 50, 70, 100, 120, 140, 160 ]; export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.BUG_TYPE_SUPERFAN) .withEncounterTier(MysteryEncounterTier.GREAT) - .withPrimaryPokemonRequirement(new CombinationPokemonRequirement( - // Must have at least 1 Bug type on team, OR have a bug item somewhere on the team - new HeldItemRequirement([ "BypassSpeedChanceModifier", "ContactHeldItemTransferChanceModifier" ], 1), - new AttackTypeBoosterHeldItemTypeRequirement(Type.BUG, 1), - new TypeRequirement(Type.BUG, false, 1) - )) + .withPrimaryPokemonRequirement( + CombinationPokemonRequirement.Some( + // Must have at least 1 Bug type on team, OR have a bug item somewhere on the team + new HeldItemRequirement([ "BypassSpeedChanceModifier", "ContactHeldItemTransferChanceModifier" ], 1), + new AttackTypeBoosterHeldItemTypeRequirement(Type.BUG, 1), + new TypeRequirement(Type.BUG, false, 1) + ) + ) .withMaxAllowedEncounters(1) .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) .withIntroSpriteConfigs([]) // These are set in onInit() @@ -405,11 +407,13 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = .build()) .withOption(MysteryEncounterOptionBuilder .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) - .withPrimaryPokemonRequirement(new CombinationPokemonRequirement( - // Meets one or both of the below reqs - new HeldItemRequirement([ "BypassSpeedChanceModifier", "ContactHeldItemTransferChanceModifier" ], 1), - new AttackTypeBoosterHeldItemTypeRequirement(Type.BUG, 1) - )) + .withPrimaryPokemonRequirement( + CombinationPokemonRequirement.Some( + // Meets one or both of the below reqs + new HeldItemRequirement([ "BypassSpeedChanceModifier", "ContactHeldItemTransferChanceModifier" ], 1), + new AttackTypeBoosterHeldItemTypeRequirement(Type.BUG, 1) + ) + ) .withDialogue({ buttonLabel: `${namespace}:option.3.label`, buttonTooltip: `${namespace}:option.3.tooltip`, diff --git a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts index 57c8aa7a561e..c4b03660bde0 100644 --- a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts +++ b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts @@ -11,7 +11,7 @@ import { Species } from "#enums/species"; import { TrainerType } from "#enums/trainer-type"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import { Abilities } from "#enums/abilities"; -import { applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { applyAbilityOverrideToPokemon, applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { Type } from "#app/data/type"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; @@ -425,17 +425,8 @@ function onYesAbilitySwap(scene: BattleScene, resolve) { const onPokemonSelected = (pokemon: PlayerPokemon) => { // Do ability swap const encounter = scene.currentBattle.mysteryEncounter!; - if (pokemon.isFusion()) { - if (!pokemon.fusionCustomPokemonData) { - pokemon.fusionCustomPokemonData = new CustomPokemonData(); - } - pokemon.fusionCustomPokemonData.ability = encounter.misc.ability; - } else { - if (!pokemon.customPokemonData) { - pokemon.customPokemonData = new CustomPokemonData(); - } - pokemon.customPokemonData.ability = encounter.misc.ability; - } + + applyAbilityOverrideToPokemon(pokemon, encounter.misc.ability); encounter.setDialogueToken("chosenPokemon", pokemon.getNameToRender()); scene.ui.setMode(Mode.MESSAGE).then(() => resolve(true)); }; diff --git a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts index 7f199b5487c7..8a814b582489 100644 --- a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts +++ b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts @@ -14,6 +14,7 @@ import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase"; import { PokemonFormChangeItemModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import { Challenges } from "#enums/challenges"; /** i18n namespace for encounter */ const namespace = "mysteryEncounters/darkDeal"; @@ -141,6 +142,7 @@ export const DarkDealEncounter: MysteryEncounter = // Removes random pokemon (including fainted) from party and adds name to dialogue data tokens // Will never return last battle able mon and instead pick fainted/unable to battle const removedPokemon = getRandomPlayerPokemon(scene, true, false, true); + // Get all the pokemon's held items const modifiers = removedPokemon.getHeldItems().filter(m => !(m instanceof PokemonFormChangeItemModifier)); scene.removePokemonFromPlayerParty(removedPokemon); @@ -160,7 +162,13 @@ export const DarkDealEncounter: MysteryEncounter = scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.ROGUE_BALL)); // Start encounter with random legendary (7-10 starter strength) that has level additive - const bossTypes: Type[] = encounter.misc.removedTypes; + // If this is a mono-type challenge, always ensure the required type is filtered for + let bossTypes: Type[] = encounter.misc.removedTypes; + const singleTypeChallenges = scene.gameMode.challenges.filter(c => c.value && c.id === Challenges.SINGLE_TYPE); + if (scene.gameMode.isChallenge && singleTypeChallenges.length > 0) { + bossTypes = singleTypeChallenges.map(c => (c.value - 1) as Type); + } + const bossModifiers: PokemonHeldItemModifier[] = encounter.misc.modifiers; // Starter egg tier, 35/50/10/5 %odds for tiers 6/7/8/9+ const roll = randSeedInt(100); @@ -172,7 +180,8 @@ export const DarkDealEncounter: MysteryEncounter = isBoss: true, modifierConfigs: bossModifiers.map(m => { return { - modifier: m + modifier: m, + stackCount: m.getStackCount(), }; }) }; diff --git a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts index 5686d0f6ce58..d5f9388b56c5 100644 --- a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts +++ b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts @@ -45,10 +45,13 @@ export const DelibirdyEncounter: MysteryEncounter = .withEncounterTier(MysteryEncounterTier.GREAT) .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) .withSceneRequirement(new MoneyRequirement(0, DELIBIRDY_MONEY_PRICE_MULTIPLIER)) // Must have enough money for it to spawn at the very least - .withPrimaryPokemonRequirement(new CombinationPokemonRequirement( // Must also have either option 2 or 3 available to spawn - new HeldItemRequirement(OPTION_2_ALLOWED_MODIFIERS), - new HeldItemRequirement(OPTION_3_DISALLOWED_MODIFIERS, 1, true) - )) + .withPrimaryPokemonRequirement( + CombinationPokemonRequirement.Some( + // Must also have either option 2 or 3 available to spawn + new HeldItemRequirement(OPTION_2_ALLOWED_MODIFIERS), + new HeldItemRequirement(OPTION_3_DISALLOWED_MODIFIERS, 1, true) + ) + ) .withIntroSpriteConfigs([ { spriteKey: "", @@ -196,7 +199,7 @@ export const DelibirdyEncounter: MysteryEncounter = const encounter = scene.currentBattle.mysteryEncounter!; const modifier: BerryModifier | HealingBoosterModifier = encounter.misc.chosenModifier; - // Give the player a Candy Jar if they gave a Berry, and a Healing Charm for Reviver Seed + // Give the player a Candy Jar if they gave a Berry, and a Berry Pouch for Reviver Seed if (modifier instanceof BerryModifier) { // Check if the player has max stacks of that Candy Jar already const existing = scene.findModifier(m => m instanceof LevelIncrementBoosterModifier) as LevelIncrementBoosterModifier; @@ -211,8 +214,8 @@ export const DelibirdyEncounter: MysteryEncounter = scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.CANDY_JAR)); } } else { - // Check if the player has max stacks of that Healing Charm already - const existing = scene.findModifier(m => m instanceof HealingBoosterModifier) as HealingBoosterModifier; + // Check if the player has max stacks of that Berry Pouch already + const existing = scene.findModifier(m => m instanceof PreserveBerryModifier) as PreserveBerryModifier; if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { // At max stacks, give the first party pokemon a Shell Bell instead @@ -221,7 +224,7 @@ export const DelibirdyEncounter: MysteryEncounter = scene.playSound("item_fanfare"); await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true); } else { - scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.HEALING_CHARM)); + scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.BERRY_POUCH)); } } @@ -290,8 +293,8 @@ export const DelibirdyEncounter: MysteryEncounter = const encounter = scene.currentBattle.mysteryEncounter!; const modifier = encounter.misc.chosenModifier; - // Check if the player has max stacks of Berry Pouch already - const existing = scene.findModifier(m => m instanceof PreserveBerryModifier) as PreserveBerryModifier; + // Check if the player has max stacks of Healing Charm already + const existing = scene.findModifier(m => m instanceof HealingBoosterModifier) as HealingBoosterModifier; if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { // At max stacks, give the first party pokemon a Shell Bell instead @@ -300,7 +303,7 @@ export const DelibirdyEncounter: MysteryEncounter = scene.playSound("item_fanfare"); await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true); } else { - scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.BERRY_POUCH)); + scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.HEALING_CHARM)); } // Remove the modifier if its stacks go to 0 diff --git a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts index d306206159a5..5c16e5d8564c 100644 --- a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts +++ b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts @@ -4,24 +4,30 @@ import { AttackTypeBoosterModifierType, modifierTypes, } from "#app/modifier/mod import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; -import { TypeRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; +import { AbilityRequirement, CombinationPokemonRequirement, TypeRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { Species } from "#enums/species"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import { Gender } from "#app/data/gender"; import { Type } from "#app/data/type"; import { BattlerIndex } from "#app/battle"; -import { PokemonMove } from "#app/field/pokemon"; +import Pokemon, { PokemonMove } from "#app/field/pokemon"; import { Moves } from "#enums/moves"; import { EncounterBattleAnim } from "#app/data/battle-anims"; import { WeatherType } from "#app/data/weather"; import { isNullOrUndefined, randSeedInt } from "#app/utils"; import { StatusEffect } from "#app/data/status-effect"; import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; -import { applyDamageToPokemon, applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { applyAbilityOverrideToPokemon, applyDamageToPokemon, applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { EncounterAnim } from "#enums/encounter-anims"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import { Abilities } from "#enums/abilities"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; +import { Stat } from "#enums/stat"; +import { Ability } from "#app/data/ability"; +import { FIRE_RESISTANT_ABILITIES } from "#app/data/mystery-encounters/requirements/requirement-groups"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/fieryFallout"; @@ -62,16 +68,24 @@ export const FieryFalloutEncounter: MysteryEncounter = { species: volcaronaSpecies, isBoss: false, - gender: Gender.MALE + gender: Gender.MALE, + tags: [ BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON ], + mysteryEncounterBattleEffects: (pokemon: Pokemon) => { + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ Stat.SPDEF, Stat.SPD ], 1)); + } }, { species: volcaronaSpecies, isBoss: false, - gender: Gender.FEMALE + gender: Gender.FEMALE, + tags: [ BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON ], + mysteryEncounterBattleEffects: (pokemon: Pokemon) => { + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ Stat.SPDEF, Stat.SPD ], 1)); + } } ], doubleBattle: true, - disableSwitch: true + disableSwitch: true, }; encounter.enemyPartyConfigs = [ config ]; @@ -139,7 +153,7 @@ export const FieryFalloutEncounter: MysteryEncounter = async (scene: BattleScene) => { // Pick battle const encounter = scene.currentBattle.mysteryEncounter!; - setEncounterRewards(scene, { fillRemaining: true }, undefined, () => giveLeadPokemonCharcoal(scene)); + setEncounterRewards(scene, { fillRemaining: true }, undefined, () => giveLeadPokemonAttackTypeBoostItem(scene)); encounter.startOfBattleEffects.push( { @@ -153,18 +167,6 @@ export const FieryFalloutEncounter: MysteryEncounter = targets: [ BattlerIndex.PLAYER_2 ], move: new PokemonMove(Moves.FIRE_SPIN), ignorePp: true - }, - { - sourceBattlerIndex: BattlerIndex.ENEMY, - targets: [ BattlerIndex.ENEMY ], - move: new PokemonMove(Moves.QUIVER_DANCE), - ignorePp: true - }, - { - sourceBattlerIndex: BattlerIndex.ENEMY_2, - targets: [ BattlerIndex.ENEMY_2 ], - move: new PokemonMove(Moves.QUIVER_DANCE), - ignorePp: true }); await initBattleWithEnemyConfig(scene, scene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]); } @@ -180,7 +182,7 @@ export const FieryFalloutEncounter: MysteryEncounter = ], }, async (scene: BattleScene) => { - // Damage non-fire types and burn 1 random non-fire type member + // Damage non-fire types and burn 1 random non-fire type member + give it Heatproof const encounter = scene.currentBattle.mysteryEncounter!; const nonFireTypes = scene.getParty().filter((p) => p.isAllowedInBattle() && !p.getTypes().includes(Type.FIRE)); @@ -198,7 +200,11 @@ export const FieryFalloutEncounter: MysteryEncounter = if (chosenPokemon.trySetStatus(StatusEffect.BURN)) { // Burn applied encounter.setDialogueToken("burnedPokemon", chosenPokemon.getNameToRender()); + encounter.setDialogueToken("abilityName", new Ability(Abilities.HEATPROOF, 3).name); queueEncounterMessage(scene, `${namespace}:option.2.target_burned`); + + // Also permanently change the burned Pokemon's ability to Heatproof + applyAbilityOverrideToPokemon(chosenPokemon, Abilities.HEATPROOF); } } @@ -209,8 +215,12 @@ export const FieryFalloutEncounter: MysteryEncounter = .withOption( MysteryEncounterOptionBuilder .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) - .withPrimaryPokemonRequirement(new TypeRequirement(Type.FIRE, true, 1)) // Will set option3PrimaryName dialogue token automatically - .withSecondaryPokemonRequirement(new TypeRequirement(Type.FIRE, true, 1)) // Will set option3SecondaryName dialogue token automatically + .withPrimaryPokemonRequirement( + CombinationPokemonRequirement.Some( + new TypeRequirement(Type.FIRE, true, 1), + new AbilityRequirement(FIRE_RESISTANT_ABILITIES, true) + ) + ) // Will set option3PrimaryName dialogue token automatically .withDialogue({ buttonLabel: `${namespace}:option.3.label`, buttonTooltip: `${namespace}:option.3.tooltip`, @@ -233,26 +243,32 @@ export const FieryFalloutEncounter: MysteryEncounter = { fillRemaining: true }, undefined, () => { - giveLeadPokemonCharcoal(scene); + giveLeadPokemonAttackTypeBoostItem(scene); }); const primary = encounter.options[2].primaryPokemon!; - const secondary = encounter.options[2].secondaryPokemon![0]; - setEncounterExp(scene, [ primary.id, secondary.id ], getPokemonSpecies(Species.VOLCARONA).baseExp * 2); + setEncounterExp(scene, [ primary.id ], getPokemonSpecies(Species.VOLCARONA).baseExp * 2); leaveEncounterWithoutBattle(scene); }) .build() ) .build(); -function giveLeadPokemonCharcoal(scene: BattleScene) { - // Give first party pokemon Charcoal for free at end of battle +function giveLeadPokemonAttackTypeBoostItem(scene: BattleScene) { + // Give first party pokemon attack type boost item for free at end of battle const leadPokemon = scene.getParty()?.[0]; if (leadPokemon) { - const charcoal = generateModifierType(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [ Type.FIRE ]) as AttackTypeBoosterModifierType; - applyModifierTypeToPlayerPokemon(scene, leadPokemon, charcoal); - scene.currentBattle.mysteryEncounter!.setDialogueToken("leadPokemon", leadPokemon.getNameToRender()); - queueEncounterMessage(scene, `${namespace}:found_charcoal`); + // Generate type booster held item, default to Charcoal if item fails to generate + let boosterModifierType = generateModifierType(scene, modifierTypes.ATTACK_TYPE_BOOSTER) as AttackTypeBoosterModifierType; + if (!boosterModifierType) { + boosterModifierType = generateModifierType(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [ Type.FIRE ]) as AttackTypeBoosterModifierType; + } + applyModifierTypeToPlayerPokemon(scene, leadPokemon, boosterModifierType); + + const encounter = scene.currentBattle.mysteryEncounter!; + encounter.setDialogueToken("itemName", boosterModifierType.name); + encounter.setDialogueToken("leadPokemon", leadPokemon.getNameToRender()); + queueEncounterMessage(scene, `${namespace}:found_item`); } } diff --git a/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts b/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts index f282064bb94d..7fdd29d36a23 100644 --- a/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts +++ b/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts @@ -56,7 +56,13 @@ export const MysteriousChallengersEncounter: MysteryEncounter = // Hard difficulty trainer is another random trainer, but with AVERAGE_BALANCED config // Number of mons is based off wave: 1-20 is 2, 20-40 is 3, etc. capping at 6 after wave 100 - const hardTrainerType = scene.arena.randomTrainerType(scene.currentBattle.waveIndex); + let retries = 0; + let hardTrainerType = scene.arena.randomTrainerType(scene.currentBattle.waveIndex); + while (retries < 5 && hardTrainerType === normalTrainerType) { + // Will try to use a different trainer from the normal trainer type + hardTrainerType = scene.arena.randomTrainerType(scene.currentBattle.waveIndex); + retries++; + } const hardTemplate = new TrainerPartyCompoundTemplate( new TrainerPartyTemplate(1, PartyMemberStrength.STRONGER, false, true), new TrainerPartyTemplate( diff --git a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts index d30c97b27de5..8dd730492b18 100644 --- a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts +++ b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts @@ -21,7 +21,7 @@ import i18next from "i18next"; const namespace = "mysteryEncounters/shadyVitaminDealer"; const VITAMIN_DEALER_CHEAP_PRICE_MULTIPLIER = 1.5; -const VITAMIN_DEALER_EXPENSIVE_PRICE_MULTIPLIER = 3.5; +const VITAMIN_DEALER_EXPENSIVE_PRICE_MULTIPLIER = 5; /** * Shady Vitamin Dealer encounter. diff --git a/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts b/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts index 9c10d33d0192..610209f8aad8 100644 --- a/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts @@ -222,7 +222,10 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter = encounter.misc.chosenPokemon = pokemon1; encounter.setDialogueToken("chosenPokemon", pokemon1.getNameToRender()); const eggOptions = getEggOptions(scene, pokemon1CommonEggs, pokemon1RareEggs); - setEncounterRewards(scene, { fillRemaining: true }, eggOptions, () => doPostEncounterCleanup(scene)); + setEncounterRewards(scene, + { guaranteedModifierTypeFuncs: [ modifierTypes.SOOTHE_BELL ], fillRemaining: true }, + eggOptions, + () => doPostEncounterCleanup(scene)); // Remove all Pokemon from the party except the chosen Pokemon removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon1); @@ -271,7 +274,10 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter = encounter.misc.chosenPokemon = pokemon2; encounter.setDialogueToken("chosenPokemon", pokemon2.getNameToRender()); const eggOptions = getEggOptions(scene, pokemon2CommonEggs, pokemon2RareEggs); - setEncounterRewards(scene, { fillRemaining: true }, eggOptions, () => doPostEncounterCleanup(scene)); + setEncounterRewards(scene, + { guaranteedModifierTypeFuncs: [ modifierTypes.SOOTHE_BELL ], fillRemaining: true }, + eggOptions, + () => doPostEncounterCleanup(scene)); // Remove all Pokemon from the party except the chosen Pokemon removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon2); @@ -320,7 +326,10 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter = encounter.misc.chosenPokemon = pokemon3; encounter.setDialogueToken("chosenPokemon", pokemon3.getNameToRender()); const eggOptions = getEggOptions(scene, pokemon3CommonEggs, pokemon3RareEggs); - setEncounterRewards(scene, { fillRemaining: true }, eggOptions, () => doPostEncounterCleanup(scene)); + setEncounterRewards(scene, + { guaranteedModifierTypeFuncs: [ modifierTypes.SOOTHE_BELL ], fillRemaining: true }, + eggOptions, + () => doPostEncounterCleanup(scene)); // Remove all Pokemon from the party except the chosen Pokemon removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon3); @@ -454,12 +463,16 @@ function calculateEggRewardsForPokemon(pokemon: PlayerPokemon): [number, number] } // Maximum of 30 points - const totalPoints = Math.min(pointsFromStarterTier + pointsFromBst, 30); + let totalPoints = Math.min(pointsFromStarterTier + pointsFromBst, 30); - // 1 Rare egg for every 6 points - const numRares = Math.floor(totalPoints / 6); + // First 5 points go to Common eggs + let numCommons = Math.min(totalPoints, 5); + totalPoints -= numCommons; + + // Then, 1 Rare egg for every 4 points + const numRares = Math.floor(totalPoints / 4); // 1 Common egg for every point leftover - const numCommons = totalPoints % 6; + numCommons += totalPoints % 4; return [ numCommons, numRares ]; } diff --git a/src/data/mystery-encounters/encounters/training-session-encounter.ts b/src/data/mystery-encounters/encounters/training-session-encounter.ts index 9f80bbbffde4..03341a713f29 100644 --- a/src/data/mystery-encounters/encounters/training-session-encounter.ts +++ b/src/data/mystery-encounters/encounters/training-session-encounter.ts @@ -37,6 +37,7 @@ export const TrainingSessionEncounter: MysteryEncounter = .withScenePartySizeRequirement(2, 6, true) // Must have at least 2 unfainted pokemon in party .withFleeAllowed(false) .withHideWildIntroMessage(true) + .withPreventGameStatsUpdates(true) // Do not count the Pokemon as seen or defeated since it is ours .withIntroSpriteConfigs([ { spriteKey: "training_session_gear", diff --git a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts index 2b3b38b21644..d3c16ce2122e 100644 --- a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts +++ b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts @@ -71,7 +71,7 @@ export const TrashToTreasureEncounter: MysteryEncounter = moveSet: [ Moves.PAYBACK, Moves.GUNK_SHOT, Moves.STOMPING_TANTRUM, Moves.DRAIN_PUNCH ] }; const config: EnemyPartyConfig = { - levelAdditiveModifier: 1, + levelAdditiveModifier: 0.5, pokemonConfigs: [ pokemonConfig ], disableSwitch: true }; diff --git a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts index b97a22dbe515..2ecba6ce6580 100644 --- a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts +++ b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts @@ -4,23 +4,30 @@ import { Species } from "#enums/species"; import BattleScene from "#app/battle-scene"; import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; -import { leaveEncounterWithoutBattle, setEncounterRewards, } from "../utils/encounter-phase-utils"; +import { EnemyPartyConfig, EnemyPokemonConfig, generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterRewards, } from "../utils/encounter-phase-utils"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; -import { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; +import Pokemon, { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import { IntegerHolder, isNullOrUndefined, randSeedInt, randSeedShuffle } from "#app/utils"; import PokemonSpecies, { allSpecies, getPokemonSpecies } from "#app/data/pokemon-species"; import { HiddenAbilityRateBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; import { achvs } from "#app/system/achv"; import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import i18next from "#app/plugins/i18n"; import { doPokemonTransformationSequence, TransformationScreenPosition } from "#app/data/mystery-encounters/utils/encounter-transformation-sequence"; import { getLevelTotalExp } from "#app/data/exp"; import { Stat } from "#enums/stat"; -import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import { Challenges } from "#enums/challenges"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import { PlayerGender } from "#enums/player-gender"; +import { TrainerType } from "#enums/trainer-type"; +import PokemonData from "#app/system/pokemon-data"; +import { Nature } from "#enums/nature"; +import HeldModifierConfig from "#app/interfaces/held-modifier-config"; +import { trainerConfigs, TrainerPartyTemplate } from "#app/data/trainer-config"; +import { PartyMemberStrength } from "#enums/party-member-strength"; /** i18n namespace for encounter */ const namespace = "mysteryEncounters/weirdDream"; @@ -80,10 +87,11 @@ const EXCLUDED_TRANSFORMATION_SPECIES = [ const SUPER_LEGENDARY_BST_THRESHOLD = 600; const NON_LEGENDARY_BST_THRESHOLD = 570; -const GAIN_OLD_GATEAU_ITEM_BST_THRESHOLD = 450; + +const OLD_GATEAU_STATS_UP = 20; /** 0-100 */ -const PERCENT_LEVEL_LOSS_ON_REFUSE = 12.5; +const PERCENT_LEVEL_LOSS_ON_REFUSE = 10; /** * Value ranges of the resulting species BST transformations after adding values to original species @@ -105,7 +113,8 @@ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.WEIRD_DREAM) .withEncounterTier(MysteryEncounterTier.ROGUE) .withDisallowedChallenges(Challenges.SINGLE_TYPE, Challenges.SINGLE_GENERATION) - .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + // TODO: should reset minimum wave to 10 when there are more Rogue tiers in pool. Matching Dark Deal minimum for now. + .withSceneWaveRangeRequirement(30, 140) .withIntroSpriteConfigs([ { spriteKey: "weird_dream_woman", @@ -131,6 +140,15 @@ export const WeirdDreamEncounter: MysteryEncounter = .withQuery(`${namespace}:query`) .withOnInit((scene: BattleScene) => { scene.loadBgm("mystery_encounter_weird_dream", "mystery_encounter_weird_dream.mp3"); + + // Calculate all the newly transformed Pokemon and begin asset load + const teamTransformations = getTeamTransformations(scene); + const loadAssets = teamTransformations.map(t => (t.newPokemon as PlayerPokemon).loadAssets()); + scene.currentBattle.mysteryEncounter!.misc = { + teamTransformations, + loadAssets + }; + return true; }) .withOnVisualsStart((scene: BattleScene) => { @@ -156,13 +174,10 @@ export const WeirdDreamEncounter: MysteryEncounter = doShowDreamBackground(scene); }); - // Calculate all the newly transformed Pokemon and begin asset load - const teamTransformations = getTeamTransformations(scene); - const loadAssets = teamTransformations.map(t => (t.newPokemon as PlayerPokemon).loadAssets()); - scene.currentBattle.mysteryEncounter!.misc = { - teamTransformations, - loadAssets - }; + for (const transformation of scene.currentBattle.mysteryEncounter!.misc.teamTransformations) { + scene.removePokemonFromPlayerParty(transformation.previousPokemon, false); + scene.getParty().push(transformation.newPokemon); + } }) .withOptionPhase(async (scene: BattleScene) => { // Starts cutscene dialogue, but does not await so that cutscene plays as player goes through dialogue @@ -193,7 +208,7 @@ export const WeirdDreamEncounter: MysteryEncounter = await showEncounterText(scene, `${namespace}:option.1.dream_complete`); await doNewTeamPostProcess(scene, transformations); - setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [ modifierTypes.MEMORY_MUSHROOM, modifierTypes.ROGUE_BALL, modifierTypes.MINT, modifierTypes.MINT ]}); + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [ modifierTypes.MEMORY_MUSHROOM, modifierTypes.ROGUE_BALL, modifierTypes.MINT, modifierTypes.MINT, modifierTypes.MINT ], fillRemaining: false }); leaveEncounterWithoutBattle(scene, true); }) .build() @@ -209,7 +224,88 @@ export const WeirdDreamEncounter: MysteryEncounter = ], }, async (scene: BattleScene) => { - // Reduce party levels by 20% + // Battle your "future" team for some item rewards + const transformations: PokemonTransformation[] = scene.currentBattle.mysteryEncounter!.misc.teamTransformations; + + // Uses the pokemon that player's party would have transformed into + const enemyPokemonConfigs: EnemyPokemonConfig[] = []; + for (const transformation of transformations) { + const newPokemon = transformation.newPokemon; + const previousPokemon = transformation.previousPokemon; + + await postProcessTransformedPokemon(scene, previousPokemon, newPokemon, newPokemon.species.getRootSpeciesId(), true); + + const dataSource = new PokemonData(newPokemon); + dataSource.player = false; + + // Copy held items to new pokemon + const newPokemonHeldItemConfigs: HeldModifierConfig[] = []; + for (const item of transformation.heldItems) { + newPokemonHeldItemConfigs.push({ + modifier: item.clone() as PokemonHeldItemModifier, + stackCount: item.getStackCount(), + isTransferable: false + }); + } + // Any pokemon that is below 570 BST gets +20 permanent BST to 3 stats + if (shouldGetOldGateau(newPokemon)) { + const stats = getOldGateauBoostedStats(newPokemon); + newPokemonHeldItemConfigs.push({ + modifier: generateModifierType(scene, modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU, [ OLD_GATEAU_STATS_UP, stats ]) as PokemonHeldItemModifierType, + stackCount: 1, + isTransferable: false + }); + } + + const enemyConfig: EnemyPokemonConfig = { + species: transformation.newSpecies, + isBoss: newPokemon.getSpeciesForm().getBaseStatTotal() > NON_LEGENDARY_BST_THRESHOLD, + level: previousPokemon.level, + dataSource: dataSource, + modifierConfigs: newPokemonHeldItemConfigs + }; + + enemyPokemonConfigs.push(enemyConfig); + } + + const genderIndex = scene.gameData.gender ?? PlayerGender.UNSET; + const trainerConfig = trainerConfigs[genderIndex === PlayerGender.FEMALE ? TrainerType.FUTURE_SELF_F : TrainerType.FUTURE_SELF_M].clone(); + trainerConfig.setPartyTemplates(new TrainerPartyTemplate(transformations.length, PartyMemberStrength.STRONG)); + const enemyPartyConfig: EnemyPartyConfig = { + trainerConfig: trainerConfig, + pokemonConfigs: enemyPokemonConfigs, + female: genderIndex === PlayerGender.FEMALE + }; + + const onBeforeRewards = () => { + // Before battle rewards, unlock the passive on a pokemon in the player's team for the rest of the run (not permanently) + // One random pokemon will get its passive unlocked + const passiveDisabledPokemon = scene.getParty().filter(p => !p.passive); + if (passiveDisabledPokemon?.length > 0) { + const enablePassiveMon = passiveDisabledPokemon[randSeedInt(passiveDisabledPokemon.length)]; + enablePassiveMon.passive = true; + enablePassiveMon.updateInfo(true); + } + }; + + setEncounterRewards(scene, { guaranteedModifierTiers: [ ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT ], fillRemaining: false }, undefined, onBeforeRewards); + + await showEncounterText(scene, `${namespace}:option.2.selected_2`, null, undefined, true); + await initBattleWithEnemyConfig(scene, enemyPartyConfig); + } + ) + .withSimpleOption( + { + buttonLabel: `${namespace}:option.3.label`, + buttonTooltip: `${namespace}:option.3.tooltip`, + selected: [ + { + text: `${namespace}:option.3.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Leave, reduce party levels by 10% for (const pokemon of scene.getParty()) { pokemon.level = Math.max(Math.ceil((100 - PERCENT_LEVEL_LOSS_ON_REFUSE) / 100 * pokemon.level), 1); pokemon.exp = getLevelTotalExp(pokemon.level, pokemon.species.growthRate); @@ -235,7 +331,7 @@ interface PokemonTransformation { function getTeamTransformations(scene: BattleScene): PokemonTransformation[] { const party = scene.getParty(); // Removes all pokemon from the party - const alreadyUsedSpecies: PokemonSpecies[] = []; + const alreadyUsedSpecies: PokemonSpecies[] = party.map(p => p.species); const pokemonTransformations: PokemonTransformation[] = party.map(p => { return { previousPokemon: p @@ -250,11 +346,11 @@ function getTeamTransformations(scene: BattleScene): PokemonTransformation[] { // First, roll 2 of the party members to new Pokemon at a +90 to +110 BST difference // Then, roll the remainder of the party members at a +40 to +50 BST difference const numPokemon = party.length; + const removedPokemon = randSeedShuffle(party.slice(0)); for (let i = 0; i < numPokemon; i++) { - const removed = party[randSeedInt(party.length)]; + const removed = removedPokemon[i]; const index = pokemonTransformations.findIndex(p => p.previousPokemon.id === removed.id); pokemonTransformations[index].heldItems = removed.getHeldItems().filter(m => !(m instanceof PokemonFormChangeItemModifier)); - scene.removePokemonFromPlayerParty(removed, false); const bst = removed.calculateBaseStats().reduce((a, b) => a + b, 0); let newBstRange: [number, number]; @@ -276,14 +372,13 @@ function getTeamTransformations(scene: BattleScene): PokemonTransformation[] { pokemonTransformations[index].newSpecies = newSpecies; + console.log("New species: " + JSON.stringify(newSpecies)); alreadyUsedSpecies.push(newSpecies); } for (const transformation of pokemonTransformations) { const newAbilityIndex = randSeedInt(transformation.newSpecies.getAbilityCount()); - const newPlayerPokemon = scene.addPlayerPokemon(transformation.newSpecies, transformation.previousPokemon.level, newAbilityIndex, undefined); - transformation.newPokemon = newPlayerPokemon; - scene.getParty().push(newPlayerPokemon); + transformation.newPokemon = scene.addPlayerPokemon(transformation.newSpecies, transformation.previousPokemon.level, newAbilityIndex, undefined); } return pokemonTransformations; @@ -296,109 +391,20 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon const newPokemon = transformation.newPokemon; const speciesRootForm = newPokemon.species.getRootSpeciesId(); - // Roll HA a second time - if (newPokemon.species.abilityHidden) { - const hiddenIndex = newPokemon.species.ability2 ? 2 : 1; - if (newPokemon.abilityIndex < hiddenIndex) { - const hiddenAbilityChance = new IntegerHolder(256); - scene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); - - const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value); - - if (hasHiddenAbility) { - newPokemon.abilityIndex = hiddenIndex; - } - } - } - - // Roll IVs a second time - newPokemon.ivs = newPokemon.ivs.map(iv => { - const newValue = randSeedInt(31); - return newValue > iv ? newValue : iv; - }); - - // For pokemon at/below 570 BST or any shiny pokemon, unlock it permanently as if you had caught it - if (newPokemon.getSpeciesForm().getBaseStatTotal() <= NON_LEGENDARY_BST_THRESHOLD || newPokemon.isShiny()) { - if (newPokemon.getSpeciesForm().abilityHidden && newPokemon.abilityIndex === newPokemon.getSpeciesForm().getAbilityCount() - 1) { - scene.validateAchv(achvs.HIDDEN_ABILITY); - } - - if (newPokemon.species.subLegendary) { - scene.validateAchv(achvs.CATCH_SUB_LEGENDARY); - } - - if (newPokemon.species.legendary) { - scene.validateAchv(achvs.CATCH_LEGENDARY); - } - - if (newPokemon.species.mythical) { - scene.validateAchv(achvs.CATCH_MYTHICAL); - } - - scene.gameData.updateSpeciesDexIvs(newPokemon.species.getRootSpeciesId(true), newPokemon.ivs); - const newStarterUnlocked = await scene.gameData.setPokemonCaught(newPokemon, true, false, false); - if (newStarterUnlocked) { - atLeastOneNewStarter = true; - await showEncounterText(scene, i18next.t("battle:addedAsAStarter", { pokemonName: getPokemonSpecies(speciesRootForm).getName() })); - } - } - - // If the previous pokemon had pokerus, transfer to new pokemon - newPokemon.pokerus = previousPokemon.pokerus; - - // Transfer previous Pokemon's luck value - newPokemon.luck = previousPokemon.getLuck(); - - // If the previous pokemon had higher IVs, override to those (after updating dex IVs > prevents perfect 31s on a new unlock) - newPokemon.ivs = newPokemon.ivs.map((iv, index) => { - return previousPokemon.ivs[index] > iv ? previousPokemon.ivs[index] : iv; - }); - - // For pokemon that the player owns (including ones just caught), gain a candy - if (!!scene.gameData.dexData[speciesRootForm].caughtAttr) { - scene.gameData.addStarterCandy(getPokemonSpecies(speciesRootForm), 1); + if (await postProcessTransformedPokemon(scene, previousPokemon, newPokemon, speciesRootForm)) { + atLeastOneNewStarter = true; } - // Set the moveset of the new pokemon to be the same as previous, but with 1 egg move and 1 (attempted) STAB move of the new species - newPokemon.generateAndPopulateMoveset(); - // Store a copy of a "standard" generated moveset for the new pokemon, will be used later for finding a favored move - const newPokemonGeneratedMoveset = newPokemon.moveset; - - newPokemon.moveset = previousPokemon.moveset; - - const newEggMoveIndex = await addEggMoveToNewPokemonMoveset(scene, newPokemon, speciesRootForm); - - // Try to add a favored STAB move (might fail if Pokemon already knows a bunch of moves from newPokemonGeneratedMoveset) - addFavoredMoveToNewPokemonMoveset(newPokemon, newPokemonGeneratedMoveset, newEggMoveIndex); - - // Randomize the second type of the pokemon - // If the pokemon does not normally have a second type, it will gain 1 - const newTypes = [ newPokemon.getTypes()[0] ]; - let newType = randSeedInt(18) as Type; - while (newType === newTypes[0]) { - newType = randSeedInt(18) as Type; - } - newTypes.push(newType); - if (!newPokemon.customPokemonData) { - newPokemon.customPokemonData = new CustomPokemonData(); - } - newPokemon.customPokemonData.types = newTypes; - + // Copy old items to new pokemon for (const item of transformation.heldItems) { item.pokemonId = newPokemon.id; await scene.addModifier(item, false, false, false, true); } - - // Any pokemon that is at or below 450 BST gets +20 permanent BST to 3 stats: HP (halved, +10), lowest of Atk/SpAtk, and lowest of Def/SpDef - if (newPokemon.getSpeciesForm().getBaseStatTotal() <= GAIN_OLD_GATEAU_ITEM_BST_THRESHOLD) { - const stats: Stat[] = [ Stat.HP ]; - const baseStats = newPokemon.getSpeciesForm().baseStats.slice(0); - // Attack or SpAtk - stats.push(baseStats[Stat.ATK] < baseStats[Stat.SPATK] ? Stat.ATK : Stat.SPATK); - // Def or SpDef - stats.push(baseStats[Stat.DEF] < baseStats[Stat.SPDEF] ? Stat.DEF : Stat.SPDEF); + // Any pokemon that is below 570 BST gets +20 permanent BST to 3 stats + if (shouldGetOldGateau(newPokemon)) { + const stats = getOldGateauBoostedStats(newPokemon); const modType = modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU() - .generateType(scene.getParty(), [ 20, stats ]) + .generateType(scene.getParty(), [ OLD_GATEAU_STATS_UP, stats ]) ?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU); const modifier = modType?.newModifier(newPokemon); if (modifier) { @@ -406,9 +412,6 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon } } - // Enable passive if previous had it - newPokemon.passive = previousPokemon.passive; - newPokemon.calculateStats(); await newPokemon.updateInfo(); } @@ -427,6 +430,138 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon } } +/** + * Applies special changes to the newly transformed pokemon, such as passing previous moves, gaining egg moves, etc. + * Returns whether the transformed pokemon unlocks a new starter for the player. + * @param scene + * @param previousPokemon + * @param newPokemon + * @param speciesRootForm + * @param forBattle Default `false`. If false, will perform achievements and dex unlocks for the player. + */ +async function postProcessTransformedPokemon(scene: BattleScene, previousPokemon: PlayerPokemon, newPokemon: PlayerPokemon, speciesRootForm: Species, forBattle: boolean = false): Promise { + let isNewStarter = false; + // Roll HA a second time + if (newPokemon.species.abilityHidden) { + const hiddenIndex = newPokemon.species.ability2 ? 2 : 1; + if (newPokemon.abilityIndex < hiddenIndex) { + const hiddenAbilityChance = new IntegerHolder(256); + scene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); + + const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value); + + if (hasHiddenAbility) { + newPokemon.abilityIndex = hiddenIndex; + } + } + } + + // Roll IVs a second time + newPokemon.ivs = newPokemon.ivs.map(iv => { + const newValue = randSeedInt(31); + return newValue > iv ? newValue : iv; + }); + + // Roll a neutral nature + newPokemon.nature = [ Nature.HARDY, Nature.DOCILE, Nature.BASHFUL, Nature.QUIRKY, Nature.SERIOUS ][randSeedInt(5)]; + + // For pokemon at/below 570 BST or any shiny pokemon, unlock it permanently as if you had caught it + if (!forBattle && (newPokemon.getSpeciesForm().getBaseStatTotal() <= NON_LEGENDARY_BST_THRESHOLD || newPokemon.isShiny())) { + if (newPokemon.getSpeciesForm().abilityHidden && newPokemon.abilityIndex === newPokemon.getSpeciesForm().getAbilityCount() - 1) { + scene.validateAchv(achvs.HIDDEN_ABILITY); + } + + if (newPokemon.species.subLegendary) { + scene.validateAchv(achvs.CATCH_SUB_LEGENDARY); + } + + if (newPokemon.species.legendary) { + scene.validateAchv(achvs.CATCH_LEGENDARY); + } + + if (newPokemon.species.mythical) { + scene.validateAchv(achvs.CATCH_MYTHICAL); + } + + scene.gameData.updateSpeciesDexIvs(newPokemon.species.getRootSpeciesId(true), newPokemon.ivs); + const newStarterUnlocked = await scene.gameData.setPokemonCaught(newPokemon, true, false, false); + if (newStarterUnlocked) { + isNewStarter = true; + await showEncounterText(scene, i18next.t("battle:addedAsAStarter", { pokemonName: getPokemonSpecies(speciesRootForm).getName() })); + } + } + + // If the previous pokemon had pokerus, transfer to new pokemon + newPokemon.pokerus = previousPokemon.pokerus; + + // Transfer previous Pokemon's luck value + newPokemon.luck = previousPokemon.getLuck(); + + // If the previous pokemon had higher IVs, override to those (after updating dex IVs > prevents perfect 31s on a new unlock) + newPokemon.ivs = newPokemon.ivs.map((iv, index) => { + return previousPokemon.ivs[index] > iv ? previousPokemon.ivs[index] : iv; + }); + + // For pokemon that the player owns (including ones just caught), gain a candy + if (!forBattle && !!scene.gameData.dexData[speciesRootForm].caughtAttr) { + scene.gameData.addStarterCandy(getPokemonSpecies(speciesRootForm), 1); + } + + // Set the moveset of the new pokemon to be the same as previous, but with 1 egg move and 1 (attempted) STAB move of the new species + newPokemon.generateAndPopulateMoveset(); + // Store a copy of a "standard" generated moveset for the new pokemon, will be used later for finding a favored move + const newPokemonGeneratedMoveset = newPokemon.moveset; + + newPokemon.moveset = previousPokemon.moveset.slice(0); + + const newEggMoveIndex = await addEggMoveToNewPokemonMoveset(scene, newPokemon, speciesRootForm, forBattle); + + // Try to add a favored STAB move (might fail if Pokemon already knows a bunch of moves from newPokemonGeneratedMoveset) + addFavoredMoveToNewPokemonMoveset(newPokemon, newPokemonGeneratedMoveset, newEggMoveIndex); + + // Randomize the second type of the pokemon + // If the pokemon does not normally have a second type, it will gain 1 + const newTypes = [ newPokemon.getTypes()[0] ]; + let newType = randSeedInt(18) as Type; + while (newType === newTypes[0]) { + newType = randSeedInt(18) as Type; + } + newTypes.push(newType); + if (!newPokemon.customPokemonData) { + newPokemon.customPokemonData = new CustomPokemonData(); + } + newPokemon.customPokemonData.types = newTypes; + + // Enable passive if previous had it + newPokemon.passive = previousPokemon.passive; + + return isNewStarter; +} + +/** + * @returns `true` if a given Pokemon has valid BST to be given an Old Gateau + */ +function shouldGetOldGateau(pokemon: Pokemon): boolean { + return pokemon.getSpeciesForm().getBaseStatTotal() < NON_LEGENDARY_BST_THRESHOLD; +} + +/** + * Get the lowest of HP/Spd, lowest of Atk/SpAtk, and lowest of Def/SpDef + * @returns Array of 3 {@linkcode Stat}s to boost + */ +function getOldGateauBoostedStats(pokemon: Pokemon): Stat[] { + const stats: Stat[] = []; + const baseStats = pokemon.getSpeciesForm().baseStats.slice(0); + // HP or Speed + stats.push(baseStats[Stat.HP] < baseStats[Stat.SPD] ? Stat.HP : Stat.SPD); + // Attack or SpAtk + stats.push(baseStats[Stat.ATK] < baseStats[Stat.SPATK] ? Stat.ATK : Stat.SPATK); + // Def or SpDef + stats.push(baseStats[Stat.DEF] < baseStats[Stat.SPDEF] ? Stat.DEF : Stat.SPDEF); + return stats; +} + + function getTransformedSpecies(originalBst: number, bstSearchRange: [number, number], hasPokemonBstHigherThan600: boolean, hasPokemonBstBetween570And600: boolean, alreadyUsedSpecies: PokemonSpecies[]): PokemonSpecies { let newSpecies: PokemonSpecies | undefined; while (isNullOrUndefined(newSpecies)) { @@ -550,7 +685,7 @@ function doSideBySideTransformations(scene: BattleScene, transformations: Pokemo * @param newPokemon * @param speciesRootForm */ -async function addEggMoveToNewPokemonMoveset(scene: BattleScene, newPokemon: PlayerPokemon, speciesRootForm: Species): Promise { +async function addEggMoveToNewPokemonMoveset(scene: BattleScene, newPokemon: PlayerPokemon, speciesRootForm: Species, forBattle: boolean = false): Promise { let eggMoveIndex: null | number = null; const eggMoves = newPokemon.getEggMoves()?.slice(0); if (eggMoves) { @@ -576,7 +711,7 @@ async function addEggMoveToNewPokemonMoveset(scene: BattleScene, newPokemon: Pla } // For pokemon that the player owns (including ones just caught), unlock the egg move - if (!isNullOrUndefined(randomEggMoveIndex) && !!scene.gameData.dexData[speciesRootForm].caughtAttr) { + if (!forBattle && !isNullOrUndefined(randomEggMoveIndex) && !!scene.gameData.dexData[speciesRootForm].caughtAttr) { await scene.gameData.setEggMoveUnlocked(getPokemonSpecies(speciesRootForm), randomEggMoveIndex, true); } } diff --git a/src/data/mystery-encounters/mystery-encounter-requirements.ts b/src/data/mystery-encounters/mystery-encounter-requirements.ts index a57cedc8fa35..91ea0c5be19c 100644 --- a/src/data/mystery-encounters/mystery-encounter-requirements.ts +++ b/src/data/mystery-encounters/mystery-encounter-requirements.ts @@ -37,31 +37,58 @@ export abstract class EncounterSceneRequirement implements EncounterRequirement abstract getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string]; } +/** + * Combination of multiple {@linkcode EncounterSceneRequirement | EncounterSceneRequirements} (OR/AND possible. See {@linkcode isAnd}) + */ export class CombinationSceneRequirement extends EncounterSceneRequirement { - orRequirements: EncounterSceneRequirement[]; + /** If `true`, all requirements must be met (AND). If `false`, any requirement must be met (OR) */ + private isAnd: boolean; + requirements: EncounterSceneRequirement[]; + + public static Some(...requirements: EncounterSceneRequirement[]): CombinationSceneRequirement { + return new CombinationSceneRequirement(false, ...requirements); + } + + public static Every(...requirements: EncounterSceneRequirement[]): CombinationSceneRequirement { + return new CombinationSceneRequirement(true, ...requirements); + } - constructor(... orRequirements: EncounterSceneRequirement[]) { + private constructor(isAnd: boolean, ...requirements: EncounterSceneRequirement[]) { super(); - this.orRequirements = orRequirements; + this.isAnd = isAnd; + this.requirements = requirements; } + /** + * Checks if all/any requirements are met (depends on {@linkcode isAnd}) + * @param scene The {@linkcode BattleScene} to check against + * @returns true if all/any requirements are met (depends on {@linkcode isAnd}) + */ override meetsRequirement(scene: BattleScene): boolean { - for (const req of this.orRequirements) { - if (req.meetsRequirement(scene)) { - return true; - } - } - return false; + return this.isAnd + ? this.requirements.every(req => req.meetsRequirement(scene)) + : this.requirements.some(req => req.meetsRequirement(scene)); } + /** + * Retrieves a dialogue token key/value pair for the given {@linkcode EncounterSceneRequirement | requirements}. + * @param scene The {@linkcode BattleScene} to check against + * @param pokemon The {@linkcode PlayerPokemon} to check against + * @returns A dialogue token key/value pair + * @throws An {@linkcode Error} if {@linkcode isAnd} is `true` (not supported) + */ override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { - for (const req of this.orRequirements) { - if (req.meetsRequirement(scene)) { - return req.getDialogueToken(scene, pokemon); + if (this.isAnd) { + throw new Error("Not implemented (Sorry)"); + } else { + for (const req of this.requirements) { + if (req.meetsRequirement(scene)) { + return req.getDialogueToken(scene, pokemon); + } } - } - return this.orRequirements[0].getDialogueToken(scene, pokemon); + return this.requirements[0].getDialogueToken(scene, pokemon); + } } } @@ -90,44 +117,74 @@ export abstract class EncounterPokemonRequirement implements EncounterRequiremen abstract getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string]; } +/** + * Combination of multiple {@linkcode EncounterPokemonRequirement | EncounterPokemonRequirements} (OR/AND possible. See {@linkcode isAnd}) + */ export class CombinationPokemonRequirement extends EncounterPokemonRequirement { - orRequirements: EncounterPokemonRequirement[]; + /** If `true`, all requirements must be met (AND). If `false`, any requirement must be met (OR) */ + private isAnd: boolean; + private requirements: EncounterPokemonRequirement[]; - constructor(...orRequirements: EncounterPokemonRequirement[]) { + public static Some(...requirements: EncounterPokemonRequirement[]): CombinationPokemonRequirement { + return new CombinationPokemonRequirement(false, ...requirements); + } + + public static Every(...requirements: EncounterPokemonRequirement[]): CombinationPokemonRequirement { + return new CombinationPokemonRequirement(true, ...requirements); + } + + private constructor(isAnd: boolean, ...requirements: EncounterPokemonRequirement[]) { super(); + this.isAnd = isAnd; this.invertQuery = false; this.minNumberOfPokemon = 1; - this.orRequirements = orRequirements; + this.requirements = requirements; } + /** + * Checks if all/any requirements are met (depends on {@linkcode isAnd}) + * @param scene The {@linkcode BattleScene} to check against + * @returns true if all/any requirements are met (depends on {@linkcode isAnd}) + */ override meetsRequirement(scene: BattleScene): boolean { - for (const req of this.orRequirements) { - if (req.meetsRequirement(scene)) { - return true; - } - } - return false; + return this.isAnd + ? this.requirements.every(req => req.meetsRequirement(scene)) + : this.requirements.some(req => req.meetsRequirement(scene)); } + /** + * Queries the players party for all party members that are compatible with all/any requirements (depends on {@linkcode isAnd}) + * @param partyPokemon The party of {@linkcode PlayerPokemon} + * @returns All party members that are compatible with all/any requirements (depends on {@linkcode isAnd}) + */ override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { - for (const req of this.orRequirements) { - const result = req.queryParty(partyPokemon); - if (result?.length > 0) { - return result; - } + if (this.isAnd) { + return this.requirements.reduce((relevantPokemon, req) => req.queryParty(relevantPokemon), partyPokemon); + } else { + const matchingRequirement = this.requirements.find(req => req.queryParty(partyPokemon).length > 0); + return matchingRequirement ? matchingRequirement.queryParty(partyPokemon) : []; } - - return []; } + /** + * Retrieves a dialogue token key/value pair for the given {@linkcode EncounterPokemonRequirement | requirements}. + * @param scene The {@linkcode BattleScene} to check against + * @param pokemon The {@linkcode PlayerPokemon} to check against + * @returns A dialogue token key/value pair + * @throws An {@linkcode Error} if {@linkcode isAnd} is `true` (not supported) + */ override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { - for (const req of this.orRequirements) { - if (req.meetsRequirement(scene)) { - return req.getDialogueToken(scene, pokemon); + if (this.isAnd) { + throw new Error("Not implemented (Sorry)"); + } else { + for (const req of this.requirements) { + if (req.meetsRequirement(scene)) { + return req.getDialogueToken(scene, pokemon); + } } - } - return this.orRequirements[0].getDialogueToken(scene, pokemon); + return this.requirements[0].getDialogueToken(scene, pokemon); + } } } diff --git a/src/data/mystery-encounters/mystery-encounter.ts b/src/data/mystery-encounters/mystery-encounter.ts index ee9eb159e10b..c045ee51bd7c 100644 --- a/src/data/mystery-encounters/mystery-encounter.ts +++ b/src/data/mystery-encounters/mystery-encounter.ts @@ -53,6 +53,7 @@ export interface IMysteryEncounter { hasBattleAnimationsWithoutTargets: boolean; skipEnemyBattleTurns: boolean; skipToFightInput: boolean; + preventGameStatsUpdates: boolean; onInit?: (scene: BattleScene) => boolean; onVisualsStart?: (scene: BattleScene) => boolean; @@ -150,6 +151,10 @@ export default class MysteryEncounter implements IMysteryEncounter { * If true, will skip COMMAND input and go straight to FIGHT (move select) input menu */ skipToFightInput: boolean; + /** + * If true, will prevent updating {@linkcode GameStats} for encountering and/or defeating Pokemon + */ + preventGameStatsUpdates: boolean; // #region Event callback functions @@ -548,6 +553,7 @@ export class MysteryEncounterBuilder implements Partial { hasBattleAnimationsWithoutTargets: boolean = false; skipEnemyBattleTurns: boolean = false; skipToFightInput: boolean = false; + preventGameStatsUpdates: boolean = false; maxAllowedEncounters: number = 3; expMultiplier: number = 1; @@ -735,6 +741,14 @@ export class MysteryEncounterBuilder implements Partial { return Object.assign(this, { skipToFightInput }); } + /** + * If true, will prevent updating {@linkcode GameStats} for encountering and/or defeating Pokemon + * Default `false` + */ + withPreventGameStatsUpdates(preventGameStatsUpdates: boolean): this & Required> { + return Object.assign(this, { preventGameStatsUpdates }); + } + /** * Sets the maximum number of times that an encounter can spawn in a given Classic run * @param maxAllowedEncounters diff --git a/src/data/mystery-encounters/requirements/requirement-groups.ts b/src/data/mystery-encounters/requirements/requirement-groups.ts index 63c899fc5e97..76bbb8f03a77 100644 --- a/src/data/mystery-encounters/requirements/requirement-groups.ts +++ b/src/data/mystery-encounters/requirements/requirement-groups.ts @@ -118,3 +118,20 @@ export const EXTORTION_ABILITIES = [ Abilities.SUCTION_CUPS, Abilities.STICKY_HOLD ]; + +/** + * Abilities that signify resistance to fire + */ +export const FIRE_RESISTANT_ABILITIES = [ + Abilities.FLAME_BODY, + Abilities.FLASH_FIRE, + Abilities.WELL_BAKED_BODY, + Abilities.HEATPROOF, + Abilities.THERMAL_EXCHANGE, + Abilities.THICK_FAT, + Abilities.WATER_BUBBLE, + Abilities.MAGMA_ARMOR, + Abilities.WATER_VEIL, + Abilities.STEAM_ENGINE, + Abilities.PRIMORDIAL_SEA +]; diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index 5fa8af95f4dd..b1adc478ab0f 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -21,6 +21,8 @@ import { Gender } from "#app/data/gender"; import { PermanentStat } from "#enums/stat"; import { VictoryPhase } from "#app/phases/victory-phase"; import { SummaryUiMode } from "#app/ui/summary-ui-handler"; +import { CustomPokemonData } from "#app/data/custom-pokemon-data"; +import { Abilities } from "#enums/abilities"; /** Will give +1 level every 10 waves */ export const STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER = 1; @@ -833,3 +835,21 @@ export function isPokemonValidForEncounterOptionSelection(pokemon: Pokemon, scen return null; } + +/** + * Permanently overrides the ability (not passive) of a pokemon. + * If the pokemon is a fusion, instead overrides the fused pokemon's ability. + */ +export function applyAbilityOverrideToPokemon(pokemon: Pokemon, ability: Abilities) { + if (pokemon.isFusion()) { + if (!pokemon.fusionCustomPokemonData) { + pokemon.fusionCustomPokemonData = new CustomPokemonData(); + } + pokemon.fusionCustomPokemonData.ability = ability; + } else { + if (!pokemon.customPokemonData) { + pokemon.customPokemonData = new CustomPokemonData(); + } + pokemon.customPokemonData.ability = ability; + } +} diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index 947ac9399893..96d1eb430fba 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -2580,11 +2580,11 @@ export function initSpecies() { new PokemonSpecies(Species.VAROOM, 9, false, false, false, "Single-Cyl Pokémon", Type.STEEL, Type.POISON, 1, 35, Abilities.OVERCOAT, Abilities.NONE, Abilities.SLOW_START, 300, 45, 70, 63, 30, 45, 47, 190, 50, 60, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.REVAVROOM, 9, false, false, false, "Multi-Cyl Pokémon", Type.STEEL, Type.POISON, 1.8, 120, Abilities.OVERCOAT, Abilities.NONE, Abilities.FILTER, 500, 80, 119, 90, 54, 67, 90, 75, 50, 175, GrowthRate.MEDIUM_FAST, 50, false, false, new PokemonForm("Normal", "", Type.STEEL, Type.POISON, 1.8, 120, Abilities.OVERCOAT, Abilities.NONE, Abilities.FILTER, 500, 80, 119, 90, 54, 67, 90, 75, 50, 175, false, null, true), - new PokemonForm("Segin Starmobile", "segin-starmobile", Type.STEEL, Type.DARK, 1.8, 240, Abilities.INTIMIDATE, Abilities.NONE, Abilities.INTIMIDATE, 600, 120, 129, 100, 59, 77, 115, 75, 50, 175), - new PokemonForm("Schedar Starmobile", "schedar-starmobile", Type.STEEL, Type.FIRE, 1.8, 240, Abilities.SPEED_BOOST, Abilities.NONE, Abilities.SPEED_BOOST, 600, 120, 129, 100, 59, 77, 115, 75, 50, 175), - new PokemonForm("Navi Starmobile", "navi-starmobile", Type.STEEL, Type.POISON, 1.8, 240, Abilities.TOXIC_DEBRIS, Abilities.NONE, Abilities.TOXIC_DEBRIS, 600, 120, 129, 100, 59, 77, 115, 75, 50, 175), - new PokemonForm("Ruchbah Starmobile", "ruchbah-starmobile", Type.STEEL, Type.FAIRY, 1.8, 240, Abilities.MISTY_SURGE, Abilities.NONE, Abilities.MISTY_SURGE, 600, 120, 129, 100, 59, 77, 115, 75, 50, 175), - new PokemonForm("Caph Starmobile", "caph-starmobile", Type.STEEL, Type.FIGHTING, 1.8, 240, Abilities.STAMINA, Abilities.NONE, Abilities.STAMINA, 600, 120, 129, 100, 59, 77, 115, 75, 50, 175), + new PokemonForm("Segin Starmobile", "segin-starmobile", Type.STEEL, Type.DARK, 1.8, 240, Abilities.INTIMIDATE, Abilities.NONE, Abilities.INTIMIDATE, 600, 110, 129, 100, 77, 79, 105, 75, 50, 175), + new PokemonForm("Schedar Starmobile", "schedar-starmobile", Type.STEEL, Type.FIRE, 1.8, 240, Abilities.SPEED_BOOST, Abilities.NONE, Abilities.SPEED_BOOST, 600, 110, 129, 100, 77, 79, 105, 75, 50, 175), + new PokemonForm("Navi Starmobile", "navi-starmobile", Type.STEEL, Type.POISON, 1.8, 240, Abilities.TOXIC_DEBRIS, Abilities.NONE, Abilities.TOXIC_DEBRIS, 600, 110, 129, 100, 77, 79, 105, 75, 50, 175), + new PokemonForm("Ruchbah Starmobile", "ruchbah-starmobile", Type.STEEL, Type.FAIRY, 1.8, 240, Abilities.MISTY_SURGE, Abilities.NONE, Abilities.MISTY_SURGE, 600, 110, 129, 100, 77, 79, 105, 75, 50, 175), + new PokemonForm("Caph Starmobile", "caph-starmobile", Type.STEEL, Type.FIGHTING, 1.8, 240, Abilities.STAMINA, Abilities.NONE, Abilities.STAMINA, 600, 110, 129, 100, 77, 79, 105, 75, 50, 175), ), new PokemonSpecies(Species.CYCLIZAR, 9, false, false, false, "Mount Pokémon", Type.DRAGON, Type.NORMAL, 1.6, 63, Abilities.SHED_SKIN, Abilities.NONE, Abilities.REGENERATOR, 501, 70, 95, 65, 85, 65, 121, 190, 50, 175, GrowthRate.MEDIUM_SLOW, 50, false), new PokemonSpecies(Species.ORTHWORM, 9, false, false, false, "Earthworm Pokémon", Type.STEEL, null, 2.5, 310, Abilities.EARTH_EATER, Abilities.NONE, Abilities.SAND_VEIL, 480, 70, 85, 145, 60, 55, 65, 25, 50, 240, GrowthRate.SLOW, 50, false), diff --git a/src/data/trainer-config.ts b/src/data/trainer-config.ts index fcc13975270c..bc69b6110752 100644 --- a/src/data/trainer-config.ts +++ b/src/data/trainer-config.ts @@ -2500,6 +2500,22 @@ export const trainerConfigs: TrainerConfigs = { [TrainerType.BUG_TYPE_SUPERFAN]: new TrainerConfig(++t).setMoneyMultiplier(2.25).setEncounterBgm(TrainerType.ACE_TRAINER) .setPartyTemplates(new TrainerPartyTemplate(2, PartyMemberStrength.AVERAGE)), [TrainerType.EXPERT_POKEMON_BREEDER]: new TrainerConfig(++t).setMoneyMultiplier(3).setEncounterBgm(TrainerType.ACE_TRAINER).setLocalizedName("Expert Pokemon Breeder") - .setPartyTemplates(new TrainerPartyTemplate(3, PartyMemberStrength.STRONG)) + .setPartyTemplates(new TrainerPartyTemplate(3, PartyMemberStrength.AVERAGE)), + [TrainerType.FUTURE_SELF_M]: new TrainerConfig(++t) + .setMoneyMultiplier(0) + .setEncounterBgm("mystery_encounter_weird_dream") + .setBattleBgm("mystery_encounter_weird_dream") + .setMixedBattleBgm("mystery_encounter_weird_dream") + .setVictoryBgm("mystery_encounter_weird_dream") + .setLocalizedName("Future Self M") + .setPartyTemplates(new TrainerPartyTemplate(6, PartyMemberStrength.STRONG)), + [TrainerType.FUTURE_SELF_F]: new TrainerConfig(++t) + .setMoneyMultiplier(0) + .setEncounterBgm("mystery_encounter_weird_dream") + .setBattleBgm("mystery_encounter_weird_dream") + .setMixedBattleBgm("mystery_encounter_weird_dream") + .setVictoryBgm("mystery_encounter_weird_dream") + .setLocalizedName("Future Self F") + .setPartyTemplates(new TrainerPartyTemplate(6, PartyMemberStrength.STRONG)) }; diff --git a/src/enums/trainer-type.ts b/src/enums/trainer-type.ts index cb7509067b5c..708faf691962 100644 --- a/src/enums/trainer-type.ts +++ b/src/enums/trainer-type.ts @@ -116,6 +116,8 @@ export enum TrainerType { VITO, BUG_TYPE_SUPERFAN, EXPERT_POKEMON_BREEDER, + FUTURE_SELF_M, + FUTURE_SELF_F, BROCK = 200, MISTY, diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index a3d7429ed9b3..d41c1f9eefa3 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -3,7 +3,7 @@ import BattleScene, { AnySound } from "#app/battle-scene"; import { Variant, VariantSet, variantColorCache } from "#app/data/variant"; import { variantData } from "#app/data/variant"; import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "#app/ui/battle-info"; -import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr, MoveTarget, CombinedPledgeStabBoostAttr } from "#app/data/move"; +import Move, { 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 } from "#app/data/move"; import { default as PokemonSpecies, PokemonSpeciesForm, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species"; import { CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER, getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters"; import { starterPassiveAbilities } from "#app/data/balance/passives"; @@ -93,7 +93,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { public stats: integer[]; public ivs: integer[]; public nature: Nature; - public natureOverride: Nature | -1; public moveset: (PokemonMove | null)[]; public status: Status | null; public friendship: integer; @@ -2121,7 +2120,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { // Trainers get a weight bump to stat buffing moves movePool = movePool.map(m => [ m[0], m[1] * (allMoves[m[0]].getAttrs(StatStageChangeAttr).some(a => a.stages > 1 && a.selfTarget) ? 1.25 : 1) ]); // Trainers get a weight decrease to multiturn moves - movePool = movePool.map(m => [ m[0], m[1] * (!!allMoves[m[0]].hasAttr(ChargeAttr) || !!allMoves[m[0]].hasAttr(RechargeAttr) ? 0.7 : 1) ]); + movePool = movePool.map(m => [ m[0], m[1] * (!!allMoves[m[0]].isChargingMove() || !!allMoves[m[0]].hasAttr(RechargeAttr) ? 0.7 : 1) ]); } // Weight towards higher power moves, by reducing the power of moves below the highest power. @@ -4283,7 +4282,6 @@ export class PlayerPokemon extends Pokemon { if (newEvolution.condition?.predicate(this)) { const newPokemon = this.scene.addPlayerPokemon(this.species, this.level, this.abilityIndex, this.formIndex, undefined, this.shiny, this.variant, this.ivs, this.nature); - newPokemon.natureOverride = this.natureOverride; newPokemon.passive = this.passive; newPokemon.moveset = this.moveset.slice(); newPokemon.moveset = this.copyMoveset(); diff --git a/src/loading-scene.ts b/src/loading-scene.ts index 4f673fd2cfc9..26936bcdaadd 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -165,6 +165,8 @@ export class LoadingScene extends SceneBase { this.loadImage("discord", "ui"); this.loadImage("google", "ui"); this.loadImage("settings_icon", "ui"); + this.loadImage("link_icon", "ui"); + this.loadImage("unlink_icon", "ui"); this.loadImage("default_bg", "arenas"); // Load arena images diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index 8e7853a41bb8..3e475c62590f 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -10,7 +10,9 @@ import { getStatusEffectDescriptor, StatusEffect } from "#app/data/status-effect import { Type } from "#app/data/type"; import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; -import { AddPokeballModifier, AddVoucherModifier, AttackTypeBoosterModifier, BaseStatModifier, BerryModifier, BoostBugSpawnModifier, BypassSpeedChanceModifier, ContactHeldItemTransferChanceModifier, CritBoosterModifier, DamageMoneyRewardModifier, DoubleBattleChanceBoosterModifier, EnemyAttackStatusEffectChanceModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, EvolutionItemModifier, EvolutionStatBoosterModifier, EvoTrackerModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, FusePokemonModifier, GigantamaxAccessModifier, HealingBoosterModifier, HealShopCostModifier, HiddenAbilityRateBoosterModifier, HitHealModifier, IvScannerModifier, LevelIncrementBoosterModifier, LockModifierTiersModifier, MapModifier, MegaEvolutionAccessModifier, MoneyInterestModifier, MoneyMultiplierModifier, MoneyRewardModifier, MultipleParticipantExpBonusModifier, PokemonAllMovePpRestoreModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, PokemonInstantReviveModifier, PokemonLevelIncrementModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PokemonNatureChangeModifier, PokemonNatureWeightModifier, PokemonPpRestoreModifier, PokemonPpUpModifier, PokemonStatusHealModifier, PreserveBerryModifier, RememberMoveModifier, ResetNegativeStatStageModifier, ShinyRateBoosterModifier, SpeciesCritBoosterModifier, SpeciesStatBoosterModifier, SurviveDamageModifier, SwitchEffectTransferModifier, TempCritBoosterModifier, TempStatStageBoosterModifier, TerastallizeAccessModifier, TerastallizeModifier, TmModifier, TurnHealModifier, TurnHeldItemTransferModifier, TurnStatusEffectModifier, type EnemyPersistentModifier, type Modifier, type PersistentModifier } from "#app/modifier/modifier"; +import { + AddPokeballModifier, AddVoucherModifier, AttackTypeBoosterModifier, BaseStatModifier, BerryModifier, BoostBugSpawnModifier, BypassSpeedChanceModifier, ContactHeldItemTransferChanceModifier, CritBoosterModifier, DamageMoneyRewardModifier, DoubleBattleChanceBoosterModifier, EnemyAttackStatusEffectChanceModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, EvolutionItemModifier, EvolutionStatBoosterModifier, EvoTrackerModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, FusePokemonModifier, GigantamaxAccessModifier, HealingBoosterModifier, HealShopCostModifier, HiddenAbilityRateBoosterModifier, HitHealModifier, IvScannerModifier, LevelIncrementBoosterModifier, LockModifierTiersModifier, MapModifier, MegaEvolutionAccessModifier, MoneyInterestModifier, MoneyMultiplierModifier, MoneyRewardModifier, MultipleParticipantExpBonusModifier, PokemonAllMovePpRestoreModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, PokemonInstantReviveModifier, PokemonLevelIncrementModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PokemonNatureChangeModifier, PokemonNatureWeightModifier, PokemonPpRestoreModifier, PokemonPpUpModifier, PokemonStatusHealModifier, PreserveBerryModifier, RememberMoveModifier, ResetNegativeStatStageModifier, ShinyRateBoosterModifier, SpeciesCritBoosterModifier, SpeciesStatBoosterModifier, SurviveDamageModifier, SwitchEffectTransferModifier, TempCritBoosterModifier, TempStatStageBoosterModifier, TerastallizeAccessModifier, TerastallizeModifier, TmModifier, TurnHealModifier, TurnHeldItemTransferModifier, TurnStatusEffectModifier, type EnemyPersistentModifier, type Modifier, type PersistentModifier, TempExtraModifierModifier +} from "#app/modifier/modifier"; import { ModifierTier } from "#app/modifier/modifier-tier"; import Overrides from "#app/overrides"; import { Unlockables } from "#app/system/unlockables"; @@ -1561,6 +1563,7 @@ export const modifierTypes = { VOUCHER_PREMIUM: () => new AddVoucherModifierType(VoucherType.PREMIUM, 1), GOLDEN_POKEBALL: () => new ModifierType("modifierType:ModifierType.GOLDEN_POKEBALL", "pb_gold", (type, _args) => new ExtraModifierModifier(type), undefined, "se/pb_bounce_1"), + SILVER_POKEBALL: () => new ModifierType("modifierType:ModifierType.SILVER_POKEBALL", "pb_silver", (type, _args) => new TempExtraModifierModifier(type, 100), undefined, "se/pb_bounce_1"), ENEMY_DAMAGE_BOOSTER: () => new ModifierType("modifierType:ModifierType.ENEMY_DAMAGE_BOOSTER", "wl_item_drop", (type, _args) => new EnemyDamageBoosterModifier(type, 5)), ENEMY_DAMAGE_REDUCTION: () => new ModifierType("modifierType:ModifierType.ENEMY_DAMAGE_REDUCTION", "wl_guard_spec", (type, _args) => new EnemyDamageReducerModifier(type, 2.5)), @@ -1577,13 +1580,13 @@ export const modifierTypes = { if (pregenArgs) { return new PokemonBaseStatTotalModifierType(pregenArgs[0] as number); } - return new PokemonBaseStatTotalModifierType(randSeedInt(20)); + return new PokemonBaseStatTotalModifierType(randSeedInt(20, 1)); }), MYSTERY_ENCOUNTER_OLD_GATEAU: () => new ModifierTypeGenerator((party: Pokemon[], pregenArgs?: any[]) => { if (pregenArgs) { return new PokemonBaseStatFlatModifierType(pregenArgs[0] as number, pregenArgs[1] as Stat[]); } - return new PokemonBaseStatFlatModifierType(randSeedInt(20), [ Stat.HP, Stat.ATK, Stat.DEF ]); + return new PokemonBaseStatFlatModifierType(randSeedInt(20, 1), [ Stat.HP, Stat.ATK, Stat.DEF ]); }), MYSTERY_ENCOUNTER_BLACK_SLUDGE: () => new ModifierTypeGenerator((party: Pokemon[], pregenArgs?: any[]) => { if (pregenArgs) { diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index b699c2483c9f..11f16f103a5b 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -404,6 +404,14 @@ export abstract class LapsingPersistentModifier extends PersistentModifier { this.battleCount = this.maxBattles; } + /** + * Updates an existing modifier with a new `maxBattles` and `battleCount`. + */ + setNewBattleCount(count: number): void { + this.maxBattles = count; + this.battleCount = count; + } + getMaxBattles(): number { return this.maxBattles; } @@ -960,7 +968,7 @@ export class EvoTrackerModifier extends PokemonHeldItemModifier { this.stackCount = pokemon ? pokemon.evoCounter + pokemon.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length - + pokemon.scene.findModifiers(m => m instanceof MoneyMultiplierModifier || m instanceof ExtraModifierModifier).length + + pokemon.scene.findModifiers(m => m instanceof MoneyMultiplierModifier || m instanceof ExtraModifierModifier || m instanceof TempExtraModifierModifier).length : this.stackCount; const text = scene.add.bitmapText(10, 15, "item-count", this.stackCount.toString(), 11); @@ -975,7 +983,7 @@ export class EvoTrackerModifier extends PokemonHeldItemModifier { getMaxHeldItemCount(pokemon: Pokemon): number { this.stackCount = pokemon.evoCounter + pokemon.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length - + pokemon.scene.findModifiers(m => m instanceof MoneyMultiplierModifier || m instanceof ExtraModifierModifier).length; + + pokemon.scene.findModifiers(m => m instanceof MoneyMultiplierModifier || m instanceof ExtraModifierModifier || m instanceof TempExtraModifierModifier).length; return 999; } } @@ -3288,6 +3296,60 @@ export class ExtraModifierModifier extends PersistentModifier { } } +/** + * Modifier used for timed boosts to the player's shop item rewards. + * @extends LapsingPersistentModifier + * @see {@linkcode apply} + */ +export class TempExtraModifierModifier extends LapsingPersistentModifier { + constructor(type: ModifierType, maxBattles: number, battleCount?: number, stackCount?: number) { + super(type, maxBattles, battleCount, stackCount); + } + + /** + * Goes through existing modifiers for any that match Silver Pokeball, + * which will then add the max count of the new item to the existing count of the current item. + * If no existing Silver Pokeballs are found, will add a new one. + * @param modifiers {@linkcode PersistentModifier} array of the player's modifiers + * @param _virtual N/A + * @param scene + * @returns true if the modifier was successfully added or applied, false otherwise + */ + add(modifiers: PersistentModifier[], _virtual: boolean, scene: BattleScene): boolean { + for (const modifier of modifiers) { + if (this.match(modifier)) { + const modifierInstance = modifier as TempExtraModifierModifier; + const newBattleCount = this.getMaxBattles() + modifierInstance.getBattleCount(); + + modifierInstance.setNewBattleCount(newBattleCount); + scene.playSound("se/restore"); + return true; + } + } + + modifiers.push(this); + return true; + } + + clone() { + return new TempExtraModifierModifier(this.type, this.getMaxBattles(), this.getBattleCount(), this.stackCount); + } + + match(modifier: Modifier): boolean { + return (modifier instanceof TempExtraModifierModifier); + } + + /** + * Increases the current rewards in the battle by the `stackCount`. + * @returns `true` if the shop reward number modifier applies successfully + * @param count {@linkcode NumberHolder} that holds the resulting shop item reward count + */ + apply(count: NumberHolder): boolean { + count.value += this.getStackCount(); + return true; + } +} + export abstract class EnemyPersistentModifier extends PersistentModifier { constructor(type: ModifierType, stackCount?: number) { super(type, stackCount); diff --git a/src/overrides.ts b/src/overrides.ts index e1bfbd240f0e..6760db79205d 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -75,6 +75,8 @@ class DefaultOverrides { readonly ITEM_UNLOCK_OVERRIDE: Unlockables[] = []; /** Set to `true` to show all tutorials */ readonly BYPASS_TUTORIAL_SKIP_OVERRIDE: boolean = false; + /** Set to `true` to force Paralysis and Freeze to always activate, or `false` to force them to not activate */ + readonly STATUS_ACTIVATION_OVERRIDE: boolean | null = null; // ---------------- // PLAYER OVERRIDES diff --git a/src/phases/move-charge-phase.ts b/src/phases/move-charge-phase.ts new file mode 100644 index 000000000000..d1dc340b81bc --- /dev/null +++ b/src/phases/move-charge-phase.ts @@ -0,0 +1,84 @@ +import BattleScene from "#app/battle-scene"; +import { BattlerIndex } from "#app/battle"; +import { MoveChargeAnim } from "#app/data/battle-anims"; +import { applyMoveChargeAttrs, MoveEffectAttr, InstantChargeAttr } from "#app/data/move"; +import Pokemon, { MoveResult, PokemonMove } from "#app/field/pokemon"; +import { BooleanHolder } from "#app/utils"; +import { MovePhase } from "#app/phases/move-phase"; +import { PokemonPhase } from "#app/phases/pokemon-phase"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { MoveEndPhase } from "#app/phases/move-end-phase"; + +/** + * Phase for the "charging turn" of two-turn moves (e.g. Dig). + * @extends {@linkcode PokemonPhase} + */ +export class MoveChargePhase extends PokemonPhase { + /** The move instance that this phase applies */ + public move: PokemonMove; + /** The field index targeted by the move (Charging moves assume single target) */ + public targetIndex: BattlerIndex; + + constructor(scene: BattleScene, battlerIndex: BattlerIndex, targetIndex: BattlerIndex, move: PokemonMove) { + super(scene, battlerIndex); + this.move = move; + this.targetIndex = targetIndex; + } + + public override start() { + super.start(); + + const user = this.getUserPokemon(); + const target = this.getTargetPokemon(); + const move = this.move.getMove(); + + // If the target is somehow not defined, or the move is somehow not a ChargingMove, + // immediately end this phase. + if (!target || !(move.isChargingMove())) { + console.warn("Invalid parameters for MoveChargePhase"); + return super.end(); + } + + new MoveChargeAnim(move.chargeAnim, move.id, user).play(this.scene, false, () => { + move.showChargeText(user, target); + + applyMoveChargeAttrs(MoveEffectAttr, user, target, move).then(() => { + user.addTag(BattlerTagType.CHARGING, 1, move.id, user.id); + this.end(); + }); + }); + } + + /** Checks the move's instant charge conditions, then ends this phase. */ + public override end() { + const user = this.getUserPokemon(); + const move = this.move.getMove(); + + if (move.isChargingMove()) { + const instantCharge = new BooleanHolder(false); + + applyMoveChargeAttrs(InstantChargeAttr, user, null, move, instantCharge); + + if (instantCharge.value) { + // this MoveEndPhase will be duplicated by the queued MovePhase if not removed + this.scene.tryRemovePhase((phase) => phase instanceof MoveEndPhase && phase.getPokemon() === user); + // queue a new MovePhase for this move's attack phase + this.scene.unshiftPhase(new MovePhase(this.scene, user, [ this.targetIndex ], this.move, false)); + } else { + user.getMoveQueue().push({ move: move.id, targets: [ this.targetIndex ]}); + } + + // Add this move's charging phase to the user's move history + user.pushMoveHistory({ move: this.move.moveId, targets: [ this.targetIndex ], result: MoveResult.OTHER }); + } + super.end(); + } + + public getUserPokemon(): Pokemon { + return (this.player ? this.scene.getPlayerField() : this.scene.getEnemyField())[this.fieldIndex]; + } + + public getTargetPokemon(): Pokemon | undefined { + return this.scene.getField(true).find((p) => this.targetIndex === p.getBattlerIndex()); + } +} diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 8d1a255d2682..2b898f7d66ba 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -4,7 +4,7 @@ import { applyPreAttackAbAttrs, AddSecondStrikeAbAttr, IgnoreMoveEffectsAbAttr, import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag"; import { MoveAnim } from "#app/data/battle-anims"; import { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag } from "#app/data/battler-tags"; -import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, OneHitKOAttr, MoveEffectTrigger, ChargeAttr, MoveCategory, NoEffectAttr, HitsTagAttr, ToxicAccuracyAttr } from "#app/data/move"; +import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, OneHitKOAttr, MoveEffectTrigger, MoveCategory, NoEffectAttr, HitsTagAttr, ToxicAccuracyAttr } from "#app/data/move"; import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms"; import { BattlerTagType } from "#app/enums/battler-tag-type"; import { Moves } from "#app/enums/moves"; @@ -24,10 +24,10 @@ export class MoveEffectPhase extends PokemonPhase { super(scene, battlerIndex); this.move = move; /** - * 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 - * of the left Pokemon and gets hit unless this is checked. - */ + * 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 + * of the left Pokemon and gets hit unless this is checked. + */ if (targets.includes(battlerIndex) && this.move.getMove().moveTarget === MoveTarget.ALL_NEAR_OTHERS) { const i = targets.indexOf(battlerIndex); targets.splice(i, i + 1); @@ -49,9 +49,9 @@ export class MoveEffectPhase extends PokemonPhase { } /** - * Does an effect from this move override other effects on this turn? - * e.g. Charging moves (Fly, etc.) on their first turn of use. - */ + * Does an effect from this move override other effects on this turn? + * e.g. Charging moves (Fly, etc.) on their first turn of use. + */ const overridden = new Utils.BooleanHolder(false); /** The {@linkcode Move} object from {@linkcode allMoves} invoked by this phase */ const move = this.move.getMove(); @@ -66,10 +66,10 @@ export class MoveEffectPhase extends PokemonPhase { user.lapseTags(BattlerTagLapseType.MOVE_EFFECT); /** - * If this phase is for the first hit of the invoked move, - * resolve the move's total hit count. This block combines the - * effects of the move itself, Parental Bond, and Multi-Lens to do so. - */ + * If this phase is for the first hit of the invoked move, + * resolve the move's total hit count. This block combines the + * effects of the move itself, Parental Bond, and Multi-Lens to do so. + */ if (user.turnData.hitsLeft === -1) { const hitCount = new Utils.IntegerHolder(1); // Assume single target for multi hit @@ -86,16 +86,16 @@ export class MoveEffectPhase extends PokemonPhase { } /** - * Log to be entered into the user's move history once the move result is resolved. - * Note that `result` (a {@linkcode MoveResult}) logs whether the move was successfully - * used in the sense of "Does it have an effect on the user?". - */ + * Log to be entered into the user's move history once the move result is resolved. + * Note that `result` (a {@linkcode MoveResult}) logs whether the move was successfully + * used in the sense of "Does it have an effect on the user?". + */ const moveHistoryEntry = { move: this.move.moveId, targets: this.targets, result: MoveResult.PENDING, virtual: this.move.virtual }; /** - * Stores results of hit checks of the invoked move against all targets, organized by battler index. - * @see {@linkcode hitCheck} - */ + * Stores results of hit checks of the invoked move against all targets, organized by battler index. + * @see {@linkcode hitCheck} + */ const targetHitChecks = Object.fromEntries(targets.map(p => [ p.getBattlerIndex(), this.hitCheck(p) ])); const hasActiveTargets = targets.some(t => t.isActive(true)); @@ -104,11 +104,10 @@ export class MoveEffectPhase extends PokemonPhase { && !targets[0].getTag(SemiInvulnerableTag); /** - * If no targets are left for the move to hit (FAIL), or the invoked move is 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 no targets are left for the move to hit (FAIL), or the invoked move is 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)) { this.stopMultiHit(); if (hasActiveTargets) { @@ -154,9 +153,9 @@ export class MoveEffectPhase extends PokemonPhase { && !target.getTag(SemiInvulnerableTag); /** - * If the move missed a target, stop all future hits against that target - * and move on to the next target (if there is one). - */ + * If the move missed a target, stop all future hits against that target + * and move on to the next target (if there is one). + */ if (!isImmune && !isProtected && !targetHitChecks[target.getBattlerIndex()]) { this.stopMultiHit(target); this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: getPokemonNameWithAffix(target) })); @@ -177,23 +176,23 @@ export class MoveEffectPhase extends PokemonPhase { } /** - * Since all fail/miss checks have applied, the move is considered successfully applied. - * It's worth noting that if the move has no effect or is protected against, this assignment - * is overwritten and the move is logged as a FAIL. - */ + * Since all fail/miss checks have applied, the move is considered successfully applied. + * It's worth noting that if the move has no effect or is protected against, this assignment + * is overwritten and the move is logged as a FAIL. + */ moveHistoryEntry.result = MoveResult.SUCCESS; /** - * Stores the result of applying the invoked move to the target. - * If the target is protected, the result is always `NO_EFFECT`. - * Otherwise, the hit result is based on type effectiveness, immunities, - * and other factors that may negate the attack or status application. - * - * Internally, the call to {@linkcode Pokemon.apply} is where damage is calculated - * (for attack moves) and the target's HP is updated. However, this isn't - * made visible to the user until the resulting {@linkcode DamagePhase} - * is invoked. - */ + * Stores the result of applying the invoked move to the target. + * If the target is protected, the result is always `NO_EFFECT`. + * Otherwise, the hit result is based on type effectiveness, immunities, + * and other factors that may negate the attack or status application. + * + * Internally, the call to {@linkcode Pokemon.apply} is where damage is calculated + * (for attack moves) and the target's HP is updated. However, this isn't + * made visible to the user until the resulting {@linkcode DamagePhase} + * is invoked. + */ const hitResult = !isProtected ? target.apply(user, move) : HitResult.NO_EFFECT; /** Does {@linkcode hitResult} indicate that damage was dealt to the target? */ @@ -211,9 +210,9 @@ export class MoveEffectPhase extends PokemonPhase { } /** - * If the move has no effect on the target (i.e. the target is protected or immune), - * change the logged move result to FAIL. - */ + * If the move has no effect on the target (i.e. the target is protected or immune), + * change the logged move result to FAIL. + */ if (hitResult === HitResult.NO_EFFECT) { moveHistoryEntry.result = MoveResult.FAIL; } @@ -222,43 +221,41 @@ export class MoveEffectPhase extends PokemonPhase { const lastHit = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive()); /** - * If the user can change forms by using the invoked move, - * it only changes forms after the move's last hit - * (see Relic Song's interaction with Parental Bond when used by Meloetta). - */ + * If the user can change forms by using the invoked move, + * it only changes forms after the move's last hit + * (see Relic Song's interaction with Parental Bond when used by Meloetta). + */ if (lastHit) { this.scene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger); } /** - * Create a Promise that applys *all* effects from the invoked move's MoveEffectAttrs. - * These are ordered by trigger type (see {@linkcode MoveEffectTrigger}), and each trigger - * type requires different conditions to be met with respect to the move's hit result. - */ + * Create a Promise that applys *all* effects from the invoked move's MoveEffectAttrs. + * These are ordered by trigger type (see {@linkcode MoveEffectTrigger}), and each trigger + * type requires different conditions to be met with respect to the move's hit result. + */ applyAttrs.push(new Promise(resolve => { // Apply all effects with PRE_MOVE triggers (if the target isn't immune to the move) applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.PRE_APPLY && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && hitResult !== HitResult.NO_EFFECT, user, target, move).then(() => { // All other effects require the move to not have failed or have been cancelled to trigger if (hitResult !== HitResult.FAIL) { - /** Are the move's effects tied to the first turn of a charge move? */ - const chargeEffect = !!move.getAttrs(ChargeAttr).find(ca => ca.usedChargeEffect(user, this.getTarget() ?? null, move)); /** - * If the invoked move's effects are meant to trigger during the move's "charge turn," - * ignore all effects after this point. - * Otherwise, apply all self-targeted POST_APPLY effects. - */ - Utils.executeIf(!chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_APPLY - && attr.selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, move)).then(() => { + * If the invoked move's effects are meant to trigger during the move's "charge turn," + * ignore all effects after this point. + * Otherwise, apply all self-targeted POST_APPLY effects. + */ + applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_APPLY + && attr.selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, move).then(() => { // All effects past this point require the move to have hit the target if (hitResult !== HitResult.NO_EFFECT) { // Apply all non-self-targeted POST_APPLY effects applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.POST_APPLY && !(attr as MoveEffectAttr).selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, this.move.getMove()).then(() => { /** - * If the move hit, and the target doesn't have Shield Dust, - * apply the chance to flinch the target gained from King's Rock - */ + * If the move hit, and the target doesn't have Shield Dust, + * apply the chance to flinch the target gained from King's Rock + */ if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !move.hitsSubstitute(user, target)) { const flinched = new Utils.BooleanHolder(false); user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched); @@ -267,7 +264,7 @@ export class MoveEffectPhase extends PokemonPhase { } } // If the move was not protected against, apply all HIT effects - Utils.executeIf(!isProtected && !chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.HIT + Utils.executeIf(!isProtected, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.HIT && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && (!attr.firstTargetOnly || firstTarget), user, target, this.move.getMove()).then(() => { // Apply the target's post-defend ability effects (as long as the target is active or can otherwise apply them) return Utils.executeIf(!target.isFainted() || target.canApplyAbility(), () => applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult).then(() => { @@ -286,9 +283,9 @@ export class MoveEffectPhase extends PokemonPhase { // Apply the user's post-attack ability effects applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult).then(() => { /** - * If the invoked move is an attack, apply the user's chance to - * steal an item from the target granted by Grip Claw - */ + * If the invoked move is an attack, apply the user's chance to + * steal an item from the target granted by Grip Claw + */ if (this.move.getMove() instanceof AttackMove) { this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target); } @@ -343,12 +340,12 @@ export class MoveEffectPhase extends PokemonPhase { end() { const user = this.getUserPokemon(); /** - * If this phase isn't for the invoked move's last strike, - * unshift another MoveEffectPhase for the next strike. - * Otherwise, queue a message indicating the number of times the move has struck - * (if the move has struck more than once), then apply the heal from Shell Bell - * to the user. - */ + * If this phase isn't for the invoked move's last strike, + * unshift another MoveEffectPhase for the next strike. + * Otherwise, queue a message indicating the number of times the move has struck + * (if the move has struck more than once), then apply the heal from Shell Bell + * to the user. + */ if (user) { if (user.turnData.hitsLeft && --user.turnData.hitsLeft >= 1 && this.getTarget()?.isActive()) { this.scene.unshiftPhase(this.getNewHitPhase()); @@ -447,9 +444,9 @@ export class MoveEffectPhase extends PokemonPhase { } /** - * Removes the given {@linkcode Pokemon} from this phase's target list - * @param target {@linkcode Pokemon} the Pokemon to be removed - */ + * Removes the given {@linkcode Pokemon} from this phase's target list + * @param target {@linkcode Pokemon} the Pokemon to be removed + */ removeTarget(target: Pokemon): void { const targetIndex = this.targets.findIndex(ind => ind === target.getBattlerIndex()); if (targetIndex !== -1) { @@ -458,19 +455,19 @@ export class MoveEffectPhase extends PokemonPhase { } /** - * Prevents subsequent strikes of this phase's invoked move from occurring - * @param target {@linkcode Pokemon} if defined, only stop subsequent - * strikes against this Pokemon - */ + * Prevents subsequent strikes of this phase's invoked move from occurring + * @param target {@linkcode Pokemon} if defined, only stop subsequent + * strikes against this Pokemon + */ stopMultiHit(target?: Pokemon): void { /** If given a specific target, remove the target from subsequent strikes */ if (target) { this.removeTarget(target); } /** - * If no target specified, or the specified target was the last of this move's - * targets, completely cancel all subsequent strikes. - */ + * If no target specified, or the specified target was the last of this move's + * targets, completely cancel all subsequent strikes. + */ if (!target || this.targets.length === 0 ) { this.getUserPokemon()!.turnData.hitCount = 1; // TODO: is the bang correct here? this.getUserPokemon()!.turnData.hitsLeft = 1; // TODO: is the bang correct here? diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index e9d8887e9cb0..a516fd8593dd 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -3,14 +3,15 @@ import BattleScene from "#app/battle-scene"; import { applyAbAttrs, applyPostMoveUsedAbAttrs, applyPreAttackAbAttrs, BlockRedirectAbAttr, IncreasePpAbAttr, PokemonTypeChangeAbAttr, PostMoveUsedAbAttr, RedirectMoveAbAttr, ReduceStatusEffectDurationAbAttr } from "#app/data/ability"; import { CommonAnim } from "#app/data/battle-anims"; import { BattlerTagLapseType, CenterOfAttentionTag } from "#app/data/battler-tags"; -import { allMoves, applyMoveAttrs, BypassRedirectAttr, BypassSleepAttr, ChargeAttr, CopyMoveAttr, HealStatusEffectAttr, MoveFlags, PreMoveMessageAttr } from "#app/data/move"; +import { allMoves, applyMoveAttrs, BypassRedirectAttr, BypassSleepAttr, CopyMoveAttr, HealStatusEffectAttr, MoveFlags, PreMoveMessageAttr } from "#app/data/move"; import { SpeciesFormChangePreMoveTrigger } from "#app/data/pokemon-forms"; import { getStatusEffectActivationText, getStatusEffectHealText } from "#app/data/status-effect"; import { Type } from "#app/data/type"; import { getTerrainBlockMessage } from "#app/data/weather"; import { MoveUsedEvent } from "#app/events/battle-scene"; -import Pokemon, { MoveResult, PokemonMove, TurnMove } from "#app/field/pokemon"; +import Pokemon, { MoveResult, PokemonMove } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; +import Overrides from "#app/overrides"; import { BattlePhase } from "#app/phases/battle-phase"; import { CommonAnimPhase } from "#app/phases/common-anim-phase"; import { MoveEffectPhase } from "#app/phases/move-effect-phase"; @@ -22,6 +23,7 @@ import { BattlerTagType } from "#enums/battler-tag-type"; import { Moves } from "#enums/moves"; import { StatusEffect } from "#enums/status-effect"; import i18next from "i18next"; +import { MoveChargePhase } from "#app/phases/move-charge-phase"; export class MovePhase extends BattlePhase { protected _pokemon: Pokemon; @@ -134,6 +136,8 @@ export class MovePhase extends BattlePhase { if (this.cancelled || this.failed) { this.handlePreMoveFailures(); + } else if (this.move.getMove().isChargingMove() && !this.pokemon.getTag(BattlerTagType.CHARGING)) { + this.chargeMove(); } else { this.useMove(); } @@ -168,10 +172,7 @@ export class MovePhase extends BattlePhase { switch (this.pokemon.status.effect) { case StatusEffect.PARALYSIS: - if (!this.pokemon.randSeedInt(4)) { - activated = true; - this.cancelled = true; - } + activated = (!this.pokemon.randSeedInt(4) || Overrides.STATUS_ACTIVATION_OVERRIDE === true) && Overrides.STATUS_ACTIVATION_OVERRIDE !== false; break; case StatusEffect.SLEEP: applyMoveAttrs(BypassSleepAttr, this.pokemon, null, this.move.getMove()); @@ -180,16 +181,22 @@ export class MovePhase extends BattlePhase { this.pokemon.status.sleepTurnsRemaining = turnsRemaining.value; healed = this.pokemon.status.sleepTurnsRemaining <= 0; activated = !healed && !this.pokemon.getTag(BattlerTagType.BYPASS_SLEEP); - this.cancelled = activated; break; case StatusEffect.FREEZE: - healed = !!this.move.getMove().findAttr(attr => attr instanceof HealStatusEffectAttr && attr.selfTarget && attr.isOfEffect(StatusEffect.FREEZE)) || !this.pokemon.randSeedInt(5); + healed = + !!this.move.getMove().findAttr((attr) => + attr instanceof HealStatusEffectAttr + && attr.selfTarget + && attr.isOfEffect(StatusEffect.FREEZE)) + || (!this.pokemon.randSeedInt(5) && Overrides.STATUS_ACTIVATION_OVERRIDE !== true) + || Overrides.STATUS_ACTIVATION_OVERRIDE === false; + activated = !healed; - this.cancelled = activated; break; } if (activated) { + this.cancel(); this.scene.queueMessage(getStatusEffectActivationText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon))); this.scene.unshiftPhase(new CommonAnimPhase(this.scene, this.pokemon.getBattlerIndex(), undefined, CommonAnim.POISON + (this.pokemon.status.effect - 1))); } else if (healed) { @@ -222,12 +229,15 @@ export class MovePhase extends BattlePhase { this.showMoveText(); - // TODO: Clean up implementation of two-turn moves. if (moveQueue.length > 0) { // Using .shift here clears out two turn moves once they've been used this.ignorePp = moveQueue.shift()?.ignorePP ?? false; } + if (this.pokemon.getTag(BattlerTagType.CHARGING)?.sourceMove === this.move.moveId) { + this.pokemon.lapseTag(BattlerTagType.CHARGING); + } + // "commit" to using the move, deducting PP. if (!this.ignorePp) { const ppUsed = 1 + this.getPpIncreaseFromPressure(targets); @@ -291,6 +301,9 @@ export class MovePhase extends BattlePhase { } this.showFailedText(failedText); + + // Remove the user from its semi-invulnerable state (if applicable) + this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT); } // Handle Dancer, which triggers immediately after a move is used (rather than waiting on `this.end()`). @@ -302,6 +315,35 @@ export class MovePhase extends BattlePhase { } } + /** Queues a {@linkcode MoveChargePhase} for this phase's invoked move. */ + protected chargeMove() { + const move = this.move.getMove(); + const targets = this.getActiveTargetPokemon(); + + if (move.applyConditions(this.pokemon, targets[0], move)) { + // Protean and Libero apply on the charging turn of charge moves + applyPreAttackAbAttrs(PokemonTypeChangeAbAttr, this.pokemon, null, this.move.getMove()); + + this.showMoveText(); + this.scene.unshiftPhase(new MoveChargePhase(this.scene, this.pokemon.getBattlerIndex(), this.targets[0], this.move)); + } else { + this.pokemon.pushMoveHistory({ move: this.move.moveId, targets: this.targets, result: MoveResult.FAIL, virtual: this.move.virtual }); + + let failedText: string | undefined; + const failureMessage = move.getFailedText(this.pokemon, targets[0], move, new BooleanHolder(false)); + + if (failureMessage) { + failedText = failureMessage; + } + + this.showMoveText(); + this.showFailedText(failedText); + + // Remove the user from its semi-invulnerable state (if applicable) + this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT); + } + } + /** * Queues a {@linkcode MoveEndPhase} if the move wasn't a {@linkcode followUp} and {@linkcode canMove()} returns `true`, * then ends the phase. @@ -415,8 +457,6 @@ export class MovePhase extends BattlePhase { * - Lapses `AFTER_MOVE` tags: * - This handles the effects of {@link Moves.SUBSTITUTE Substitute} * - Removes the second turn of charge moves - * - * TODO: handle charge moves more gracefully */ protected handlePreMoveFailures(): void { if (this.cancelled || this.failed) { @@ -448,18 +488,7 @@ export class MovePhase extends BattlePhase { return; } - if (this.move.getMove().hasAttr(ChargeAttr)) { - const lastMove = this.pokemon.getLastXMoves() as TurnMove[]; - if (!lastMove.length || lastMove[0].move !== this.move.getMove().id || lastMove[0].result !== MoveResult.OTHER) { - this.scene.queueMessage(i18next.t("battle:useMove", { - pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon), - moveName: this.move.getName() - }), 500); - return; - } - } - - if (this.pokemon.getTag(BattlerTagType.RECHARGING || BattlerTagType.INTERRUPTED)) { + if (this.pokemon.getTag(BattlerTagType.RECHARGING) || this.pokemon.getTag(BattlerTagType.INTERRUPTED)) { return; } diff --git a/src/phases/select-modifier-phase.ts b/src/phases/select-modifier-phase.ts index 38d5cfb4a103..e5a60692bb4d 100644 --- a/src/phases/select-modifier-phase.ts +++ b/src/phases/select-modifier-phase.ts @@ -1,7 +1,7 @@ import BattleScene from "#app/battle-scene"; import { ModifierTier } from "#app/modifier/modifier-tier"; import { regenerateModifierPoolThresholds, ModifierTypeOption, ModifierType, getPlayerShopModifierTypeOptionsForWave, PokemonModifierType, FusePokemonModifierType, PokemonMoveModifierType, TmModifierType, RememberMoveModifierType, PokemonPpRestoreModifierType, PokemonPpUpModifierType, ModifierPoolType, getPlayerModifierTypeOptions } from "#app/modifier/modifier-type"; -import { ExtraModifierModifier, HealShopCostModifier, Modifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { ExtraModifierModifier, HealShopCostModifier, Modifier, PokemonHeldItemModifier, TempExtraModifierModifier } from "#app/modifier/modifier"; import ModifierSelectUiHandler, { SHOP_OPTIONS_ROW_LIMIT } from "#app/ui/modifier-select-ui-handler"; import PartyUiHandler, { PartyUiMode, PartyOption } from "#app/ui/party-ui-handler"; import { Mode } from "#app/ui/ui"; @@ -45,6 +45,7 @@ export class SelectModifierPhase extends BattlePhase { const modifierCount = new Utils.IntegerHolder(3); if (this.isPlayer()) { this.scene.applyModifiers(ExtraModifierModifier, true, modifierCount); + this.scene.applyModifiers(TempExtraModifierModifier, true, modifierCount); } // If custom modifiers are specified, overrides default item count @@ -274,7 +275,13 @@ export class SelectModifierPhase extends BattlePhase { // Otherwise, continue with custom multiplier multiplier = this.customModifierSettings.rerollMultiplier; } - return Math.min(Math.ceil(this.scene.currentBattle.waveIndex / 10) * baseValue * Math.pow(2, this.rerollCount) * multiplier, Number.MAX_SAFE_INTEGER); + + const baseMultiplier = Math.min(Math.ceil(this.scene.currentBattle.waveIndex / 10) * baseValue * (2 ** this.rerollCount) * multiplier, Number.MAX_SAFE_INTEGER); + + // Apply Black Sludge to reroll cost + const modifiedRerollCost = new NumberHolder(baseMultiplier); + this.scene.applyModifier(HealShopCostModifier, true, modifiedRerollCost); + return modifiedRerollCost.value; } getPoolType(): ModifierPoolType { diff --git a/src/phases/switch-summon-phase.ts b/src/phases/switch-summon-phase.ts index 07761b10d6e8..37652b3cfa43 100644 --- a/src/phases/switch-summon-phase.ts +++ b/src/phases/switch-summon-phase.ts @@ -65,8 +65,9 @@ export class SwitchSummonPhase extends SummonPhase { const pokemon = this.getPokemon(); + (this.player ? this.scene.getEnemyField() : this.scene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.removeTagsBySourceId(pokemon.id)); + if (this.switchType === SwitchType.SWITCH) { - (this.player ? this.scene.getEnemyField() : this.scene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.removeTagsBySourceId(pokemon.id)); const substitute = pokemon.getTag(SubstituteTag); if (substitute) { this.scene.tweens.add({ diff --git a/src/phases/victory-phase.ts b/src/phases/victory-phase.ts index e900ff97fc6e..1faa31655dfe 100644 --- a/src/phases/victory-phase.ts +++ b/src/phases/victory-phase.ts @@ -25,12 +25,17 @@ export class VictoryPhase extends PokemonPhase { start() { super.start(); - this.scene.gameData.gameStats.pokemonDefeated++; + const isMysteryEncounter = this.scene.currentBattle.isBattleMysteryEncounter(); + + // update Pokemon defeated count except for MEs that disable it + if (!isMysteryEncounter || !this.scene.currentBattle.mysteryEncounter?.preventGameStatsUpdates) { + this.scene.gameData.gameStats.pokemonDefeated++; + } const expValue = this.getPokemon().getExpValue(); this.scene.applyPartyExp(expValue, true); - if (this.scene.currentBattle.isBattleMysteryEncounter()) { + if (isMysteryEncounter) { handleMysteryEncounterVictory(this.scene, false, this.isExpOnly); return this.end(); } diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 5f9aad634088..c00159a7fd76 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -1569,6 +1569,10 @@ export class GameData { } setPokemonSeen(pokemon: Pokemon, incrementCount: boolean = true, trainer: boolean = false): void { + // Some Mystery Encounters block updates to these stats + if (this.scene.currentBattle?.isBattleMysteryEncounter() && this.scene.currentBattle.mysteryEncounter?.preventGameStatsUpdates) { + return; + } const dexEntry = this.dexData[pokemon.species.speciesId]; dexEntry.seenAttr |= pokemon.getDexAttr(); if (incrementCount) { diff --git a/src/system/pokemon-data.ts b/src/system/pokemon-data.ts index e681c995b260..421739a9da1d 100644 --- a/src/system/pokemon-data.ts +++ b/src/system/pokemon-data.ts @@ -92,7 +92,6 @@ export default class PokemonData { this.stats = source.stats; this.ivs = source.ivs; this.nature = source.nature !== undefined ? source.nature : 0 as Nature; - this.natureOverride = source.natureOverride !== undefined ? source.natureOverride : -1; this.friendship = source.friendship !== undefined ? source.friendship : getPokemonSpecies(this.species).baseFriendship; this.metLevel = source.metLevel || 5; this.metBiome = source.metBiome !== undefined ? source.metBiome : -1; @@ -117,6 +116,8 @@ export default class PokemonData { this.customPokemonData = new CustomPokemonData(source.customPokemonData); + // Deprecated, but needed for session data migration + this.natureOverride = source.natureOverride; this.mysteryEncounterPokemonData = new CustomPokemonData(source.mysteryEncounterPokemonData); this.fusionMysteryEncounterPokemonData = new CustomPokemonData(source.fusionMysteryEncounterPokemonData); diff --git a/src/test/abilities/imposter.test.ts b/src/test/abilities/imposter.test.ts index b7b8e0c5ccab..7aaac5ca8c45 100644 --- a/src/test/abilities/imposter.test.ts +++ b/src/test/abilities/imposter.test.ts @@ -36,9 +36,7 @@ describe("Abilities - Imposter", () => { }); it("should copy species, ability, gender, all stats except HP, all stat stages, moveset, and types of target", async () => { - await game.startBattle([ - Species.DITTO - ]); + await game.classicMode.startBattle([ Species.DITTO ]); game.move.select(Moves.SPLASH); await game.phaseInterceptor.to(TurnEndPhase); @@ -78,9 +76,7 @@ describe("Abilities - Imposter", () => { it("should copy in-battle overridden stats", async () => { game.override.enemyMoveset([ Moves.POWER_SPLIT ]); - await game.startBattle([ - Species.DITTO - ]); + await game.classicMode.startBattle([ Species.DITTO ]); const player = game.scene.getPlayerPokemon()!; const enemy = game.scene.getEnemyPokemon()!; @@ -97,4 +93,18 @@ describe("Abilities - Imposter", () => { expect(player.getStat(Stat.SPATK, false)).toBe(avgSpAtk); expect(enemy.getStat(Stat.SPATK, false)).toBe(avgSpAtk); }); + + it("should set each move's pp to a maximum of 5", async () => { + game.override.enemyMoveset([ Moves.SWORDS_DANCE, Moves.GROWL, Moves.SKETCH, Moves.RECOVER ]); + + await game.classicMode.startBattle([ Species.DITTO ]); + const player = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.TACKLE); + await game.phaseInterceptor.to(TurnEndPhase); + + player.getMoveset().forEach(move => { + expect(move!.getMovePp()).toBeLessThanOrEqual(5); + }); + }); }); diff --git a/src/test/abilities/volt_absorb.test.ts b/src/test/abilities/volt_absorb.test.ts index ec82b00ec5ac..4fee7653b990 100644 --- a/src/test/abilities/volt_absorb.test.ts +++ b/src/test/abilities/volt_absorb.test.ts @@ -52,6 +52,7 @@ describe("Abilities - Volt Absorb", () => { expect(playerPokemon.getTag(BattlerTagType.CHARGED)).toBeDefined(); expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); }); + it("should activate regardless of accuracy checks", async () => { game.override.moveset(Moves.THUNDERBOLT); game.override.enemyMoveset(Moves.SPLASH); @@ -71,6 +72,7 @@ describe("Abilities - Volt Absorb", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); }); + it("regardless of accuracy should not trigger on pokemon in semi invulnerable state", async () => { game.override.moveset(Moves.THUNDERBOLT); game.override.enemyMoveset(Moves.DIVE); @@ -84,9 +86,7 @@ describe("Abilities - Volt Absorb", () => { game.move.select(Moves.THUNDERBOLT); enemyPokemon.hp = enemyPokemon.hp - 1; await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); - await game.phaseInterceptor.to("MoveEffectPhase"); - await game.move.forceMiss(); await game.phaseInterceptor.to("BerryPhase", false); expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); }); diff --git a/src/test/arena/arena_gravity.test.ts b/src/test/arena/arena_gravity.test.ts index b69828965718..13e9c23a35c1 100644 --- a/src/test/arena/arena_gravity.test.ts +++ b/src/test/arena/arena_gravity.test.ts @@ -1,8 +1,8 @@ +import { BattlerIndex } from "#app/battle"; import { allMoves } from "#app/data/move"; -import { Abilities } from "#app/enums/abilities"; -import { ArenaTagType } from "#app/enums/arena-tag-type"; -import { MoveEffectPhase } from "#app/phases/move-effect-phase"; -import { TurnEndPhase } from "#app/phases/turn-end-phase"; +import { Abilities } from "#enums/abilities"; +import { ArenaTagType } from "#enums/arena-tag-type"; +import { BattlerTagType } from "#enums/battler-tag-type"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import GameManager from "#test/utils/gameManager"; @@ -31,7 +31,8 @@ describe("Arena - Gravity", () => { .ability(Abilities.UNNERVE) .enemyAbility(Abilities.BALL_FETCH) .enemySpecies(Species.SHUCKLE) - .enemyMoveset(Moves.SPLASH); + .enemyMoveset(Moves.SPLASH) + .enemyLevel(5); }); // Reference: https://bulbapedia.bulbagarden.net/wiki/Gravity_(move) @@ -42,102 +43,121 @@ describe("Arena - Gravity", () => { vi.spyOn(moveToCheck, "calculateBattleAccuracy"); // Setup Gravity on first turn - await game.startBattle([ Species.PIKACHU ]); + await game.classicMode.startBattle([ Species.PIKACHU ]); game.move.select(Moves.GRAVITY); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined(); // Use non-OHKO move on second turn await game.toNextTurn(); game.move.select(Moves.TACKLE); - await game.phaseInterceptor.to(MoveEffectPhase); + await game.phaseInterceptor.to("MoveEffectPhase"); - expect(moveToCheck.calculateBattleAccuracy).toHaveReturnedWith(100 * 1.67); + expect(moveToCheck.calculateBattleAccuracy).toHaveLastReturnedWith(100 * 1.67); }); it("OHKO move accuracy is not affected", async () => { - game.override.startingLevel(5); - game.override.enemyLevel(5); - /** See Fissure {@link https://bulbapedia.bulbagarden.net/wiki/Fissure_(move)} */ const moveToCheck = allMoves[Moves.FISSURE]; vi.spyOn(moveToCheck, "calculateBattleAccuracy"); // Setup Gravity on first turn - await game.startBattle([ Species.PIKACHU ]); + await game.classicMode.startBattle([ Species.PIKACHU ]); game.move.select(Moves.GRAVITY); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined(); // Use OHKO move on second turn await game.toNextTurn(); game.move.select(Moves.FISSURE); - await game.phaseInterceptor.to(MoveEffectPhase); + await game.phaseInterceptor.to("MoveEffectPhase"); - expect(moveToCheck.calculateBattleAccuracy).toHaveReturnedWith(30); + expect(moveToCheck.calculateBattleAccuracy).toHaveLastReturnedWith(30); }); describe("Against flying types", () => { it("can be hit by ground-type moves now", async () => { game.override - .startingLevel(5) - .enemyLevel(5) .enemySpecies(Species.PIDGEOT) .moveset([ Moves.GRAVITY, Moves.EARTHQUAKE ]); - await game.startBattle([ Species.PIKACHU ]); + await game.classicMode.startBattle([ Species.PIKACHU ]); const pidgeot = game.scene.getEnemyPokemon()!; vi.spyOn(pidgeot, "getAttackTypeEffectiveness"); // Try earthquake on 1st turn (fails!); game.move.select(Moves.EARTHQUAKE); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); - expect(pidgeot.getAttackTypeEffectiveness).toHaveReturnedWith(0); + expect(pidgeot.getAttackTypeEffectiveness).toHaveLastReturnedWith(0); // Setup Gravity on 2nd turn await game.toNextTurn(); game.move.select(Moves.GRAVITY); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined(); // Use ground move on 3rd turn await game.toNextTurn(); game.move.select(Moves.EARTHQUAKE); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); - expect(pidgeot.getAttackTypeEffectiveness).toHaveReturnedWith(1); + expect(pidgeot.getAttackTypeEffectiveness).toHaveLastReturnedWith(1); }); it("keeps super-effective moves super-effective after using gravity", async () => { game.override - .startingLevel(5) - .enemyLevel(5) .enemySpecies(Species.PIDGEOT) .moveset([ Moves.GRAVITY, Moves.THUNDERBOLT ]); - await game.startBattle([ Species.PIKACHU ]); + await game.classicMode.startBattle([ Species.PIKACHU ]); const pidgeot = game.scene.getEnemyPokemon()!; vi.spyOn(pidgeot, "getAttackTypeEffectiveness"); // Setup Gravity on 1st turn game.move.select(Moves.GRAVITY); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined(); // Use electric move on 2nd turn await game.toNextTurn(); game.move.select(Moves.THUNDERBOLT); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); - expect(pidgeot.getAttackTypeEffectiveness).toHaveReturnedWith(2); + expect(pidgeot.getAttackTypeEffectiveness).toHaveLastReturnedWith(2); }); }); + + it("cancels Fly if its user is semi-invulnerable", async () => { + game.override + .enemySpecies(Species.SNORLAX) + .enemyMoveset(Moves.FLY) + .moveset([ Moves.GRAVITY, Moves.SPLASH ]); + + await game.classicMode.startBattle([ Species.CHARIZARD ]); + + const charizard = game.scene.getPlayerPokemon()!; + const snorlax = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.SPLASH); + + await game.toNextTurn(); + expect(snorlax.getTag(BattlerTagType.FLYING)).toBeDefined(); + + game.move.select(Moves.GRAVITY); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + + await game.phaseInterceptor.to("MoveEffectPhase"); + expect(snorlax.getTag(BattlerTagType.INTERRUPTED)).toBeDefined(); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(charizard.hp).toBe(charizard.getMaxHp()); + }); }); diff --git a/src/test/data/status-effect.test.ts b/src/test/data/status-effect.test.ts index 8b37da45d8d1..1b1a97fc51f0 100644 --- a/src/test/data/status-effect.test.ts +++ b/src/test/data/status-effect.test.ts @@ -8,10 +8,10 @@ import { getStatusEffectOverlapText, } from "#app/data/status-effect"; import { MoveResult } from "#app/field/pokemon"; -import GameManager from "#app/test/utils/gameManager"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; import { mockI18next } from "#test/utils/testUtils"; import i18next from "i18next"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -306,58 +306,98 @@ describe("Status Effect Messages", () => { }); }); -describe("Status Effects - Sleep", () => { - let phaserGame: Phaser.Game; - let game: GameManager; +describe("Status Effects", () => { + describe("Paralysis", () => { + let phaserGame: Phaser.Game; + let game: GameManager; - beforeAll(() => { - phaserGame = new Phaser.Game({ - type: Phaser.HEADLESS, + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); }); - }); - afterEach(() => { - game.phaseInterceptor.restoreOg(); - }); + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + + game.override + .enemySpecies(Species.MAGIKARP) + .enemyMoveset(Moves.SPLASH) + .enemyAbility(Abilities.BALL_FETCH) + .moveset([ Moves.QUICK_ATTACK ]) + .ability(Abilities.BALL_FETCH) + .statusEffect(StatusEffect.PARALYSIS); + }); + + it("causes the pokemon's move to fail when activated", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.QUICK_ATTACK); + await game.move.forceStatusActivation(true); + await game.toNextTurn(); - beforeEach(() => { - game = new GameManager(phaserGame); - game.override - .moveset([ Moves.SPLASH ]) - .ability(Abilities.BALL_FETCH) - .battleType("single") - .disableCrits() - .enemySpecies(Species.MAGIKARP) - .enemyAbility(Abilities.BALL_FETCH) - .enemyMoveset(Moves.SPLASH); + expect(game.scene.getEnemyPokemon()!.isFullHp()).toBe(true); + expect(game.scene.getPlayerPokemon()!.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL); + }); }); - it("should last the appropriate number of turns", async () => { - await game.classicMode.startBattle([ Species.FEEBAS ]); + describe("Sleep", () => { + let phaserGame: Phaser.Game; + let game: GameManager; - const player = game.scene.getPlayerPokemon()!; - player.status = new Status(StatusEffect.SLEEP, 0, 4); + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); - game.move.select(Moves.SPLASH); - await game.toNextTurn(); + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([ Moves.SPLASH ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); - expect(player.status.effect).toBe(StatusEffect.SLEEP); + it("should last the appropriate number of turns", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); - game.move.select(Moves.SPLASH); - await game.toNextTurn(); + const player = game.scene.getPlayerPokemon()!; + player.status = new Status(StatusEffect.SLEEP, 0, 4); - expect(player.status.effect).toBe(StatusEffect.SLEEP); + game.move.select(Moves.SPLASH); + await game.toNextTurn(); - game.move.select(Moves.SPLASH); - await game.toNextTurn(); + expect(player.status.effect).toBe(StatusEffect.SLEEP); - expect(player.status.effect).toBe(StatusEffect.SLEEP); - expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL); + game.move.select(Moves.SPLASH); + await game.toNextTurn(); - game.move.select(Moves.SPLASH); - await game.toNextTurn(); + expect(player.status.effect).toBe(StatusEffect.SLEEP); - expect(player.status?.effect).toBeUndefined(); - expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS); + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + expect(player.status.effect).toBe(StatusEffect.SLEEP); + expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + expect(player.status?.effect).toBeUndefined(); + expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS); + }); }); }); diff --git a/src/test/moves/baton_pass.test.ts b/src/test/moves/baton_pass.test.ts index 5e6e4be21ba6..9d4a9358715f 100644 --- a/src/test/moves/baton_pass.test.ts +++ b/src/test/moves/baton_pass.test.ts @@ -34,7 +34,7 @@ describe("Moves - Baton Pass", () => { .disableCrits(); }); - it("transfers all stat stages when player uses it", async() => { + it("transfers all stat stages when player uses it", async () => { // arrange await game.classicMode.startBattle([ Species.RAICHU, Species.SHUCKLE ]); @@ -91,7 +91,7 @@ describe("Moves - Baton Pass", () => { ]); }, 20000); - it("doesn't transfer effects that aren't transferrable", async() => { + it("doesn't transfer effects that aren't transferrable", async () => { game.override.enemyMoveset([ Moves.SALT_CURE ]); await game.classicMode.startBattle([ Species.PIKACHU, Species.FEEBAS ]); @@ -106,4 +106,28 @@ describe("Moves - Baton Pass", () => { expect(player2.findTag((t) => t.tagType === BattlerTagType.SALT_CURED)).toBeUndefined(); }, 20000); + + it("doesn't allow binding effects from the user to persist", async () => { + game.override.moveset([ Moves.FIRE_SPIN, Moves.BATON_PASS ]); + + await game.classicMode.startBattle([ Species.MAGIKARP, Species.FEEBAS ]); + + const enemy = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.FIRE_SPIN); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.move.forceHit(); + + await game.toNextTurn(); + + expect(enemy.getTag(BattlerTagType.FIRE_SPIN)).toBeDefined(); + + game.move.select(Moves.BATON_PASS); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + expect(enemy.getTag(BattlerTagType.FIRE_SPIN)).toBeUndefined(); + }); }); diff --git a/src/test/moves/dig.test.ts b/src/test/moves/dig.test.ts new file mode 100644 index 000000000000..4c6b5d3b75d3 --- /dev/null +++ b/src/test/moves/dig.test.ts @@ -0,0 +1,114 @@ +import { BattlerIndex } from "#app/battle"; +import { allMoves } from "#app/data/move"; +import { Abilities } from "#enums/abilities"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { StatusEffect } from "#enums/status-effect"; +import { MoveResult } from "#app/field/pokemon"; +import { describe, beforeAll, afterEach, beforeEach, it, expect } from "vitest"; +import GameManager from "#test/utils/gameManager"; + +describe("Moves - Dig", () => { + 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.DIG) + .battleType("single") + .startingLevel(100) + .enemySpecies(Species.SNORLAX) + .enemyLevel(100) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.TACKLE); + }); + + it("should make the user semi-invulnerable, then attack over 2 turns", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.DIG); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getTag(BattlerTagType.UNDERGROUND)).toBeDefined(); + expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.MISS); + expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp()); + expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); + expect(playerPokemon.getMoveQueue()[0].move).toBe(Moves.DIG); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getTag(BattlerTagType.UNDERGROUND)).toBeUndefined(); + expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); + expect(playerPokemon.getMoveHistory()).toHaveLength(2); + + const playerDig = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.DIG); + expect(playerDig?.ppUsed).toBe(1); + }); + + it("should not allow the user to evade attacks from Pokemon with No Guard", async () => { + game.override.enemyAbility(Abilities.NO_GUARD); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.DIG); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp()); + expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS); + }); + + it("should not expend PP when the attack phase is cancelled", async () => { + game.override + .enemyAbility(Abilities.NO_GUARD) + .enemyMoveset(Moves.SPORE); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.DIG); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getTag(BattlerTagType.UNDERGROUND)).toBeUndefined(); + expect(playerPokemon.status?.effect).toBe(StatusEffect.SLEEP); + + const playerDig = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.DIG); + expect(playerDig?.ppUsed).toBe(0); + }); + + it("should cause the user to take double damage from Earthquake", async () => { + await game.classicMode.startBattle([ Species.DONDOZO ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + const preDigEarthquakeDmg = playerPokemon.getAttackDamage(enemyPokemon, allMoves[Moves.EARTHQUAKE]).damage; + + game.move.select(Moves.DIG); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + + await game.phaseInterceptor.to("MoveEffectPhase"); + + const postDigEarthquakeDmg = playerPokemon.getAttackDamage(enemyPokemon, allMoves[Moves.EARTHQUAKE]).damage; + // these hopefully get avoid rounding errors :shrug: + expect(postDigEarthquakeDmg).toBeGreaterThanOrEqual(2 * preDigEarthquakeDmg); + expect(postDigEarthquakeDmg).toBeLessThan(2 * (preDigEarthquakeDmg + 1)); + }); +}); diff --git a/src/test/moves/dive.test.ts b/src/test/moves/dive.test.ts new file mode 100644 index 000000000000..b60416d7740b --- /dev/null +++ b/src/test/moves/dive.test.ts @@ -0,0 +1,137 @@ +import { BattlerTagType } from "#enums/battler-tag-type"; +import { StatusEffect } from "#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 Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, it, expect } from "vitest"; +import { WeatherType } from "#enums/weather-type"; + +describe("Moves - Dive", () => { + 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.DIVE) + .battleType("single") + .startingLevel(100) + .enemySpecies(Species.SNORLAX) + .enemyLevel(100) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.TACKLE); + }); + + it("should make the user semi-invulnerable, then attack over 2 turns", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.DIVE); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getTag(BattlerTagType.UNDERWATER)).toBeDefined(); + expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.MISS); + expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp()); + expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); + expect(playerPokemon.getMoveQueue()[0].move).toBe(Moves.DIVE); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getTag(BattlerTagType.UNDERWATER)).toBeUndefined(); + expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); + expect(playerPokemon.getMoveHistory()).toHaveLength(2); + + const playerDive = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.DIVE); + expect(playerDive?.ppUsed).toBe(1); + }); + + it("should not allow the user to evade attacks from Pokemon with No Guard", async () => { + game.override.enemyAbility(Abilities.NO_GUARD); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.DIVE); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp()); + expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS); + }); + + it("should not expend PP when the attack phase is cancelled", async () => { + game.override + .enemyAbility(Abilities.NO_GUARD) + .enemyMoveset(Moves.SPORE); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.DIVE); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getTag(BattlerTagType.UNDERWATER)).toBeUndefined(); + expect(playerPokemon.status?.effect).toBe(StatusEffect.SLEEP); + + const playerDive = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.DIVE); + expect(playerDive?.ppUsed).toBe(0); + }); + + it("should trigger on-contact post-defend ability effects", async () => { + game.override + .enemyAbility(Abilities.ROUGH_SKIN) + .enemyMoveset(Moves.SPLASH); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.DIVE); + + await game.phaseInterceptor.to("TurnEndPhase"); + + await game.phaseInterceptor.to("MoveEndPhase"); + expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp()); + expect(enemyPokemon.battleData.abilitiesApplied[0]).toBe(Abilities.ROUGH_SKIN); + }); + + it("should cancel attack after Harsh Sunlight is set", async () => { + game.override.enemyMoveset(Moves.SPLASH); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.DIVE); + + await game.phaseInterceptor.to("TurnEndPhase"); + await game.phaseInterceptor.to("TurnStartPhase", false); + game.scene.arena.trySetWeather(WeatherType.HARSH_SUN, false); + + await game.phaseInterceptor.to("MoveEndPhase"); + expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL); + expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); + expect(playerPokemon.getTag(BattlerTagType.UNDERWATER)).toBeUndefined(); + + const playerDive = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.DIVE); + expect(playerDive?.ppUsed).toBe(1); + }); +}); diff --git a/src/test/moves/electro_shot.test.ts b/src/test/moves/electro_shot.test.ts new file mode 100644 index 000000000000..1373b4941eb7 --- /dev/null +++ b/src/test/moves/electro_shot.test.ts @@ -0,0 +1,104 @@ +import { BattlerTagType } from "#enums/battler-tag-type"; +import { Stat } from "#enums/stat"; +import { WeatherType } from "#enums/weather-type"; +import { MoveResult } from "#app/field/pokemon"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, it, expect } from "vitest"; + +describe("Moves - Electro Shot", () => { + 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.ELECTRO_SHOT) + .battleType("single") + .startingLevel(100) + .enemySpecies(Species.SNORLAX) + .enemyLevel(100) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should increase the user's Sp. Atk on the first turn, then attack on the second turn", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.ELECTRO_SHOT); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getTag(BattlerTagType.CHARGING)).toBeDefined(); + expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); + expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.OTHER); + expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getTag(BattlerTagType.CHARGING)).toBeUndefined(); + expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); + expect(playerPokemon.getMoveHistory()).toHaveLength(2); + expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1); + expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS); + + const playerElectroShot = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.ELECTRO_SHOT); + expect(playerElectroShot?.ppUsed).toBe(1); + }); + + it.each([ + { weatherType: WeatherType.RAIN, name: "Rain" }, + { weatherType: WeatherType.HEAVY_RAIN, name: "Heavy Rain" } + ])("should fully resolve in one turn if $name is active", async ({ weatherType }) => { + game.override.weather(weatherType); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.ELECTRO_SHOT); + + await game.phaseInterceptor.to("MoveEffectPhase", false); + expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1); + + await game.phaseInterceptor.to("MoveEndPhase"); + expect(playerPokemon.getTag(BattlerTagType.CHARGING)).toBeUndefined(); + expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); + expect(playerPokemon.getMoveHistory()).toHaveLength(2); + expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS); + + const playerElectroShot = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.ELECTRO_SHOT); + expect(playerElectroShot?.ppUsed).toBe(1); + }); + + it("should only increase Sp. Atk once with Multi-Lens", async () => { + game.override + .weather(WeatherType.RAIN) + .startingHeldItems([{ name: "MULTI_LENS", count: 1 }]); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.ELECTRO_SHOT); + + await game.phaseInterceptor.to("MoveEndPhase"); + expect(playerPokemon.turnData.hitCount).toBe(2); + expect(playerPokemon.getStatStage(Stat.SPATK)).toBe(1); + }); +}); diff --git a/src/test/moves/fly.test.ts b/src/test/moves/fly.test.ts new file mode 100644 index 000000000000..6ae758fe3dc6 --- /dev/null +++ b/src/test/moves/fly.test.ts @@ -0,0 +1,122 @@ +import { BattlerTagType } from "#enums/battler-tag-type"; +import { StatusEffect } from "#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 Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, it, expect, vi } from "vitest"; +import { BattlerIndex } from "#app/battle"; +import { allMoves } from "#app/data/move"; + +describe("Moves - Fly", () => { + 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.FLY) + .battleType("single") + .startingLevel(100) + .enemySpecies(Species.SNORLAX) + .enemyLevel(100) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.TACKLE); + + vi.spyOn(allMoves[Moves.FLY], "accuracy", "get").mockReturnValue(100); + }); + + it("should make the user semi-invulnerable, then attack over 2 turns", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.FLY); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getTag(BattlerTagType.FLYING)).toBeDefined(); + expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.MISS); + expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp()); + expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); + expect(playerPokemon.getMoveQueue()[0].move).toBe(Moves.FLY); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getTag(BattlerTagType.FLYING)).toBeUndefined(); + expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); + expect(playerPokemon.getMoveHistory()).toHaveLength(2); + + const playerFly = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.FLY); + expect(playerFly?.ppUsed).toBe(1); + }); + + it("should not allow the user to evade attacks from Pokemon with No Guard", async () => { + game.override.enemyAbility(Abilities.NO_GUARD); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.FLY); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp()); + expect(enemyPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS); + }); + + it("should not expend PP when the attack phase is cancelled", async () => { + game.override + .enemyAbility(Abilities.NO_GUARD) + .enemyMoveset(Moves.SPORE); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.FLY); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getTag(BattlerTagType.FLYING)).toBeUndefined(); + expect(playerPokemon.status?.effect).toBe(StatusEffect.SLEEP); + + const playerFly = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.FLY); + expect(playerFly?.ppUsed).toBe(0); + }); + + it("should be cancelled when another Pokemon uses Gravity", async () => { + game.override.enemyMoveset([ Moves.SPLASH, Moves.GRAVITY ]); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.FLY); + + await game.forceEnemyMove(Moves.SPLASH); + + await game.toNextTurn(); + await game.forceEnemyMove(Moves.GRAVITY); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL); + expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); + + const playerFly = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.FLY); + expect(playerFly?.ppUsed).toBe(0); + }); +}); diff --git a/src/test/moves/geomancy.test.ts b/src/test/moves/geomancy.test.ts new file mode 100644 index 000000000000..6e2f40b91447 --- /dev/null +++ b/src/test/moves/geomancy.test.ts @@ -0,0 +1,78 @@ +import { EffectiveStat, Stat } from "#enums/stat"; +import { MoveResult } from "#app/field/pokemon"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, it, expect } from "vitest"; + +describe("Moves - Geomancy", () => { + 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.GEOMANCY) + .battleType("single") + .startingLevel(100) + .enemySpecies(Species.SNORLAX) + .enemyLevel(100) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should boost the user's stats on the second turn of use", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const player = game.scene.getPlayerPokemon()!; + const affectedStats: EffectiveStat[] = [ Stat.SPATK, Stat.SPDEF, Stat.SPD ]; + + game.move.select(Moves.GEOMANCY); + + await game.phaseInterceptor.to("TurnEndPhase"); + affectedStats.forEach((stat) => expect(player.getStatStage(stat)).toBe(0)); + expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.OTHER); + + await game.phaseInterceptor.to("TurnEndPhase"); + affectedStats.forEach((stat) => expect(player.getStatStage(stat)).toBe(2)); + expect(player.getMoveHistory()).toHaveLength(2); + expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS); + + const playerGeomancy = player.getMoveset().find((mv) => mv && mv.moveId === Moves.GEOMANCY); + expect(playerGeomancy?.ppUsed).toBe(1); + }); + + it("should execute over 2 turns between waves", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const player = game.scene.getPlayerPokemon()!; + const affectedStats: EffectiveStat[] = [ Stat.SPATK, Stat.SPDEF, Stat.SPD ]; + + game.move.select(Moves.GEOMANCY); + + await game.phaseInterceptor.to("MoveEndPhase", false); + await game.doKillOpponents(); + + await game.toNextWave(); + + await game.phaseInterceptor.to("TurnEndPhase"); + affectedStats.forEach((stat) => expect(player.getStatStage(stat)).toBe(2)); + expect(player.getMoveHistory()).toHaveLength(2); + expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS); + + const playerGeomancy = player.getMoveset().find((mv) => mv && mv.moveId === Moves.GEOMANCY); + expect(playerGeomancy?.ppUsed).toBe(1); + }); +}); diff --git a/src/test/moves/solar_beam.test.ts b/src/test/moves/solar_beam.test.ts new file mode 100644 index 000000000000..ebec338932ab --- /dev/null +++ b/src/test/moves/solar_beam.test.ts @@ -0,0 +1,102 @@ +import { allMoves } from "#app/data/move"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { WeatherType } from "#enums/weather-type"; +import { MoveResult } from "#app/field/pokemon"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, it, expect, vi } from "vitest"; + +describe("Moves - Solar Beam", () => { + 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.SOLAR_BEAM) + .battleType("single") + .startingLevel(100) + .enemySpecies(Species.SNORLAX) + .enemyLevel(100) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should deal damage in two turns if no weather is active", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.SOLAR_BEAM); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getTag(BattlerTagType.CHARGING)).toBeDefined(); + expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); + expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.OTHER); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getTag(BattlerTagType.CHARGING)).toBeUndefined(); + expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); + expect(playerPokemon.getMoveHistory()).toHaveLength(2); + expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS); + + const playerSolarBeam = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.SOLAR_BEAM); + expect(playerSolarBeam?.ppUsed).toBe(1); + }); + + it.each([ + { weatherType: WeatherType.SUNNY, name: "Sun" }, + { weatherType: WeatherType.HARSH_SUN, name: "Harsh Sun" } + ])("should deal damage in one turn if $name is active", async ({ weatherType }) => { + game.override.weather(weatherType); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.SOLAR_BEAM); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getTag(BattlerTagType.CHARGING)).toBeUndefined(); + expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); + expect(playerPokemon.getMoveHistory()).toHaveLength(2); + expect(playerPokemon.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS); + + const playerSolarBeam = playerPokemon.getMoveset().find(mv => mv && mv.moveId === Moves.SOLAR_BEAM); + expect(playerSolarBeam?.ppUsed).toBe(1); + }); + + it.each([ + { weatherType: WeatherType.RAIN, name: "Rain" }, + { weatherType: WeatherType.HEAVY_RAIN, name: "Heavy Rain" } + ])("should have its power halved in $name", async ({ weatherType }) => { + game.override.weather(weatherType); + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const solarBeam = allMoves[Moves.SOLAR_BEAM]; + + vi.spyOn(solarBeam, "calculateBattlePower"); + + game.move.select(Moves.SOLAR_BEAM); + + await game.phaseInterceptor.to("TurnEndPhase"); + await game.phaseInterceptor.to("TurnEndPhase"); + expect(solarBeam.calculateBattlePower).toHaveLastReturnedWith(60); + }); +}); diff --git a/src/test/moves/transform.test.ts b/src/test/moves/transform.test.ts index 079fdfa5685c..8c0f5eda7b20 100644 --- a/src/test/moves/transform.test.ts +++ b/src/test/moves/transform.test.ts @@ -36,9 +36,7 @@ describe("Moves - Transform", () => { }); it("should copy species, ability, gender, all stats except HP, all stat stages, moveset, and types of target", async () => { - await game.startBattle([ - Species.DITTO - ]); + await game.classicMode.startBattle([ Species.DITTO ]); game.move.select(Moves.TRANSFORM); await game.phaseInterceptor.to(TurnEndPhase); @@ -78,9 +76,7 @@ describe("Moves - Transform", () => { it("should copy in-battle overridden stats", async () => { game.override.enemyMoveset([ Moves.POWER_SPLIT ]); - await game.startBattle([ - Species.DITTO - ]); + await game.classicMode.startBattle([ Species.DITTO ]); const player = game.scene.getPlayerPokemon()!; const enemy = game.scene.getEnemyPokemon()!; @@ -97,4 +93,18 @@ describe("Moves - Transform", () => { expect(player.getStat(Stat.SPATK, false)).toBe(avgSpAtk); expect(enemy.getStat(Stat.SPATK, false)).toBe(avgSpAtk); }); + + it ("should set each move's pp to a maximum of 5", async () => { + game.override.enemyMoveset([ Moves.SWORDS_DANCE, Moves.GROWL, Moves.SKETCH, Moves.RECOVER ]); + + await game.classicMode.startBattle([ Species.DITTO ]); + const player = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.TRANSFORM); + await game.phaseInterceptor.to(TurnEndPhase); + + player.getMoveset().forEach(move => { + expect(move!.getMovePp()).toBeLessThanOrEqual(5); + }); + }); }); diff --git a/src/test/moves/whirlwind.test.ts b/src/test/moves/whirlwind.test.ts index cc31b2591a23..c16f38111f21 100644 --- a/src/test/moves/whirlwind.test.ts +++ b/src/test/moves/whirlwind.test.ts @@ -41,7 +41,8 @@ describe("Moves - Whirlwind", () => { const staraptor = game.scene.getPlayerPokemon()!; game.move.select(move); - await game.toNextTurn(); + + await game.phaseInterceptor.to("BerryPhase", false); expect(staraptor.findTag((t) => t.tagType === BattlerTagType.FLYING)).toBeDefined(); expect(game.scene.getEnemyPokemon()!.getLastXMoves(1)[0].result).toBe(MoveResult.MISS); diff --git a/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts b/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts index 69c0a114645b..66d628ef82f8 100644 --- a/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts @@ -206,7 +206,7 @@ describe("Delibird-y - Mystery Encounter", () => { expect(candyJarAfter?.stackCount).toBe(1); }); - it("Should remove Reviver Seed and give the player a Healing Charm", async () => { + it("Should remove Reviver Seed and give the player a Berry Pouch", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); // Set 1 Reviver Seed on party lead @@ -220,11 +220,11 @@ describe("Delibird-y - Mystery Encounter", () => { await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 }); const reviverSeedAfter = scene.findModifier(m => m instanceof PokemonInstantReviveModifier); - const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier); + const berryPouchAfter = scene.findModifier(m => m instanceof PreserveBerryModifier); expect(reviverSeedAfter).toBeUndefined(); - expect(healingCharmAfter).toBeDefined(); - expect(healingCharmAfter?.stackCount).toBe(1); + expect(berryPouchAfter).toBeDefined(); + expect(berryPouchAfter?.stackCount).toBe(1); }); it("Should give the player a Shell Bell if they have max stacks of Candy Jars", async () => { @@ -256,13 +256,13 @@ describe("Delibird-y - Mystery Encounter", () => { expect(shellBellAfter?.stackCount).toBe(1); }); - it("Should give the player a Shell Bell if they have max stacks of Healing Charms", async () => { + it("Should give the player a Shell Bell if they have max stacks of Berry Pouches", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); - // 5 Healing Charms + // 3 Berry Pouches scene.modifiers = []; - const healingCharm = generateModifierType(scene, modifierTypes.HEALING_CHARM)!.newModifier() as HealingBoosterModifier; - healingCharm.stackCount = 5; + const healingCharm = generateModifierType(scene, modifierTypes.BERRY_POUCH)!.newModifier() as PreserveBerryModifier; + healingCharm.stackCount = 3; await scene.addModifier(healingCharm, true, false, false, true); // Set 1 Reviver Seed on party lead @@ -275,12 +275,12 @@ describe("Delibird-y - Mystery Encounter", () => { await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 }); const reviverSeedAfter = scene.findModifier(m => m instanceof PokemonInstantReviveModifier); - const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier); + const healingCharmAfter = scene.findModifier(m => m instanceof PreserveBerryModifier); const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier); expect(reviverSeedAfter).toBeUndefined(); expect(healingCharmAfter).toBeDefined(); - expect(healingCharmAfter?.stackCount).toBe(5); + expect(healingCharmAfter?.stackCount).toBe(3); expect(shellBellAfter).toBeDefined(); expect(shellBellAfter?.stackCount).toBe(1); }); @@ -347,7 +347,7 @@ describe("Delibird-y - Mystery Encounter", () => { }); }); - it("Should decrease held item stacks and give the player a Berry Pouch", async () => { + it("Should decrease held item stacks and give the player a Healing Charm", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); // Set 2 Soul Dew on party lead @@ -361,14 +361,14 @@ describe("Delibird-y - Mystery Encounter", () => { await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier); - const berryPouchAfter = scene.findModifier(m => m instanceof PreserveBerryModifier); + const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier); expect(soulDewAfter?.stackCount).toBe(1); - expect(berryPouchAfter).toBeDefined(); - expect(berryPouchAfter?.stackCount).toBe(1); + expect(healingCharmAfter).toBeDefined(); + expect(healingCharmAfter?.stackCount).toBe(1); }); - it("Should remove held item and give the player a Berry Pouch", async () => { + it("Should remove held item and give the player a Healing Charm", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); // Set 1 Soul Dew on party lead @@ -382,20 +382,20 @@ describe("Delibird-y - Mystery Encounter", () => { await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier); - const berryPouchAfter = scene.findModifier(m => m instanceof PreserveBerryModifier); + const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier); expect(soulDewAfter).toBeUndefined(); - expect(berryPouchAfter).toBeDefined(); - expect(berryPouchAfter?.stackCount).toBe(1); + expect(healingCharmAfter).toBeDefined(); + expect(healingCharmAfter?.stackCount).toBe(1); }); - it("Should give the player a Shell Bell if they have max stacks of Berry Pouches", async () => { + it("Should give the player a Shell Bell if they have max stacks of Healing Charms", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); // 5 Healing Charms scene.modifiers = []; - const healingCharm = generateModifierType(scene, modifierTypes.BERRY_POUCH)!.newModifier() as PreserveBerryModifier; - healingCharm.stackCount = 3; + const healingCharm = generateModifierType(scene, modifierTypes.HEALING_CHARM)!.newModifier() as HealingBoosterModifier; + healingCharm.stackCount = 5; await scene.addModifier(healingCharm, true, false, false, true); // Set 1 Soul Dew on party lead @@ -408,12 +408,12 @@ describe("Delibird-y - Mystery Encounter", () => { await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier); - const berryPouchAfter = scene.findModifier(m => m instanceof PreserveBerryModifier); + const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier); const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier); expect(soulDewAfter).toBeUndefined(); - expect(berryPouchAfter).toBeDefined(); - expect(berryPouchAfter?.stackCount).toBe(3); + expect(healingCharmAfter).toBeDefined(); + expect(healingCharmAfter?.stackCount).toBe(5); expect(shellBellAfter).toBeDefined(); expect(shellBellAfter?.stackCount).toBe(1); }); diff --git a/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts b/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts index a4f303d121f7..5a270f1cbec1 100644 --- a/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts @@ -12,7 +12,7 @@ import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encount import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils"; import { Moves } from "#enums/moves"; import BattleScene from "#app/battle-scene"; -import { PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { AttackTypeBoosterModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; import { Type } from "#app/data/type"; import { Status, StatusEffect } from "#app/data/status-effect"; import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; @@ -22,6 +22,8 @@ import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; import { CommandPhase } from "#app/phases/command-phase"; import { MovePhase } from "#app/phases/move-phase"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { Abilities } from "#enums/abilities"; import i18next from "i18next"; const namespace = "mysteryEncounters/fieryFallout"; @@ -42,10 +44,11 @@ describe("Fiery Fallout - Mystery Encounter", () => { beforeEach(async () => { game = new GameManager(phaserGame); scene = game.scene; - game.override.mysteryEncounterChance(100); - game.override.startingWave(defaultWave); - game.override.startingBiome(defaultBiome); - game.override.disableTrainerWaves(); + game.override.mysteryEncounterChance(100) + .startingWave(defaultWave) + .startingBiome(defaultBiome) + .disableTrainerWaves() + .moveset([ Moves.PAYBACK, Moves.THUNDERBOLT ]); // Required for attack type booster item generation vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( new Map([ @@ -109,12 +112,16 @@ describe("Fiery Fallout - Mystery Encounter", () => { { species: getPokemonSpecies(Species.VOLCARONA), isBoss: false, - gender: Gender.MALE + gender: Gender.MALE, + tags: [ BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON ], + mysteryEncounterBattleEffects: expect.any(Function) }, { species: getPokemonSpecies(Species.VOLCARONA), isBoss: false, - gender: Gender.FEMALE + gender: Gender.FEMALE, + tags: [ BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON ], + mysteryEncounterBattleEffects: expect.any(Function) } ], doubleBattle: true, @@ -157,12 +164,11 @@ describe("Fiery Fallout - Mystery Encounter", () => { expect(enemyField[0].gender).not.toEqual(enemyField[1].gender); // Should be opposite gender const movePhases = phaseSpy.mock.calls.filter(p => p[0] instanceof MovePhase).map(p => p[0]); - expect(movePhases.length).toBe(4); + expect(movePhases.length).toBe(2); expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.FIRE_SPIN).length).toBe(2); // Fire spin used twice before battle - expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.QUIVER_DANCE).length).toBe(2); // Quiver Dance used twice before battle }); - it("should give charcoal to lead pokemon", async () => { + it("should give attack type boosting item to lead pokemon", async () => { await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty); await runMysteryEncounterToEnd(game, 1, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); @@ -172,8 +178,8 @@ describe("Fiery Fallout - Mystery Encounter", () => { const leadPokemonId = scene.getParty()?.[0].id; const leadPokemonItems = scene.findModifiers(m => m instanceof PokemonHeldItemModifier && (m as PokemonHeldItemModifier).pokemonId === leadPokemonId, true) as PokemonHeldItemModifier[]; - const charcoal = leadPokemonItems.find(i => i.type.name === "Charcoal"); - expect(charcoal).toBeDefined; + const item = leadPokemonItems.find(i => i instanceof AttackTypeBoosterModifier); + expect(item).toBeDefined; }); }); @@ -193,7 +199,7 @@ describe("Fiery Fallout - Mystery Encounter", () => { }); }); - it("should damage all non-fire party PKM by 20% and randomly burn 1", async () => { + it("should damage all non-fire party PKM by 20%, and burn + give Heatproof to a random Pokemon", async () => { await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty); const party = scene.getParty(); @@ -210,7 +216,8 @@ describe("Fiery Fallout - Mystery Encounter", () => { burnablePokemon.forEach((pkm) => { expect(pkm.hp, `${pkm.name} should have received 20% damage: ${pkm.hp} / ${pkm.getMaxHp()} HP`).toBe(pkm.getMaxHp() - Math.floor(pkm.getMaxHp() * 0.2)); }); - expect(burnablePokemon.some(pkm => pkm?.status?.effect === StatusEffect.BURN)).toBeTruthy(); + expect(burnablePokemon.some(pkm => pkm.status?.effect === StatusEffect.BURN)).toBeTruthy(); + expect(burnablePokemon.some(pkm => pkm.customPokemonData.ability === Abilities.HEATPROOF)); notBurnablePokemon.forEach((pkm) => expect(pkm.hp, `${pkm.name} should be full hp: ${pkm.hp} / ${pkm.getMaxHp()} HP`).toBe(pkm.getMaxHp())); }); @@ -241,17 +248,15 @@ describe("Fiery Fallout - Mystery Encounter", () => { }); }); - it("should give charcoal to lead pokemon", async () => { + it("should give attack type boosting item to lead pokemon", async () => { await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty); await runMysteryEncounterToEnd(game, 3); await game.phaseInterceptor.to(SelectModifierPhase, false); expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); - const leadPokemonId = scene.getParty()?.[0].id; - const leadPokemonItems = scene.findModifiers(m => m instanceof PokemonHeldItemModifier - && (m as PokemonHeldItemModifier).pokemonId === leadPokemonId, true) as PokemonHeldItemModifier[]; - const charcoal = leadPokemonItems.find(i => i.type.name === "Charcoal"); - expect(charcoal).toBeDefined; + const leadPokemonItems = scene.getParty()?.[0].getHeldItems() as PokemonHeldItemModifier[]; + const item = leadPokemonItems.find(i => i instanceof AttackTypeBoosterModifier); + expect(item).toBeDefined; }); it("should leave encounter without battle", async () => { @@ -264,7 +269,7 @@ describe("Fiery Fallout - Mystery Encounter", () => { }); it("should be disabled if not enough FIRE types are in party", async () => { - await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, [ Species.MAGIKARP, Species.ARCANINE ]); + await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, [ Species.MAGIKARP ]); await game.phaseInterceptor.to(MysteryEncounterPhase, false); const encounterPhase = scene.getCurrentPhase(); diff --git a/src/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts b/src/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts index d761f2d1b211..8286c6a694b9 100644 --- a/src/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts @@ -86,7 +86,7 @@ describe("Trash to Treasure - Mystery Encounter", () => { expect(TrashToTreasureEncounter.enemyPartyConfigs).toEqual([ { - levelAdditiveModifier: 1, + levelAdditiveModifier: 0.5, disableSwitch: true, pokemonConfigs: [ { diff --git a/src/test/mystery-encounter/encounters/weird-dream-encounter.test.ts b/src/test/mystery-encounter/encounters/weird-dream-encounter.test.ts index 0d463655a526..c1fa6d83a18a 100644 --- a/src/test/mystery-encounter/encounters/weird-dream-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/weird-dream-encounter.test.ts @@ -5,7 +5,7 @@ import { Species } from "#app/enums/species"; import GameManager from "#app/test/utils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounter-test-utils"; +import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils"; import BattleScene from "#app/battle-scene"; import { Mode } from "#app/ui/ui"; import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; @@ -15,6 +15,8 @@ import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; import { WeirdDreamEncounter } from "#app/data/mystery-encounters/encounters/weird-dream-encounter"; import * as EncounterTransformationSequence from "#app/data/mystery-encounters/utils/encounter-transformation-sequence"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; +import { CommandPhase } from "#app/phases/command-phase"; +import { ModifierTier } from "#app/modifier/modifier-tier"; const namespace = "mysteryEncounters/weirdDream"; const defaultParty = [ Species.MAGBY, Species.HAUNTER, Species.ABRA ]; @@ -70,7 +72,7 @@ describe("Weird Dream - Mystery Encounter", () => { expect(WeirdDreamEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}:title`); expect(WeirdDreamEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}:description`); expect(WeirdDreamEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}:query`); - expect(WeirdDreamEncounter.options.length).toBe(2); + expect(WeirdDreamEncounter.options.length).toBe(3); }); it("should initialize fully", async () => { @@ -132,7 +134,7 @@ describe("Weird Dream - Mystery Encounter", () => { expect(plus40To50.length).toBe(1); }); - it("should have 1 Memory Mushroom, 5 Rogue Balls, and 2 Mints in rewards", async () => { + it("should have 1 Memory Mushroom, 5 Rogue Balls, and 3 Mints in rewards", async () => { await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty); await runMysteryEncounterToEnd(game, 1); await game.phaseInterceptor.to(SelectModifierPhase, false); @@ -141,11 +143,12 @@ describe("Weird Dream - Mystery Encounter", () => { expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; - expect(modifierSelectHandler.options.length).toEqual(4); + expect(modifierSelectHandler.options.length).toEqual(5); expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("MEMORY_MUSHROOM"); expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toEqual("ROGUE_BALL"); expect(modifierSelectHandler.options[2].modifierTypeOption.type.id).toEqual("MINT"); expect(modifierSelectHandler.options[3].modifierTypeOption.type.id).toEqual("MINT"); + expect(modifierSelectHandler.options[3].modifierTypeOption.type.id).toEqual("MINT"); }); it("should leave encounter without battle", async () => { @@ -158,7 +161,7 @@ describe("Weird Dream - Mystery Encounter", () => { }); }); - describe("Option 2 - Leave", () => { + describe("Option 2 - Battle Future Self", () => { it("should have the correct properties", () => { const option = WeirdDreamEncounter.options[1]; expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); @@ -174,17 +177,63 @@ describe("Weird Dream - Mystery Encounter", () => { }); }); - it("should reduce party levels by 12.5%", async () => { + it("should start a battle against the player's transformation team", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty); + await runMysteryEncounterToEnd(game, 2, undefined, true); + + const enemyField = scene.getEnemyField(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyField.length).toBe(1); + expect(scene.getEnemyParty().length).toBe(scene.getParty().length); + }); + + it("should have 2 Rogue/2 Ultra/2 Great items in rewards", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty); + await runMysteryEncounterToEnd(game, 2, undefined, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(6); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.tier - modifierSelectHandler.options[0].modifierTypeOption.upgradeCount).toEqual(ModifierTier.ROGUE); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.tier - modifierSelectHandler.options[1].modifierTypeOption.upgradeCount).toEqual(ModifierTier.ROGUE); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.tier - modifierSelectHandler.options[2].modifierTypeOption.upgradeCount).toEqual(ModifierTier.ULTRA); + expect(modifierSelectHandler.options[3].modifierTypeOption.type.tier - modifierSelectHandler.options[3].modifierTypeOption.upgradeCount).toEqual(ModifierTier.ULTRA); + expect(modifierSelectHandler.options[4].modifierTypeOption.type.tier - modifierSelectHandler.options[4].modifierTypeOption.upgradeCount).toEqual(ModifierTier.GREAT); + expect(modifierSelectHandler.options[5].modifierTypeOption.type.tier - modifierSelectHandler.options[5].modifierTypeOption.upgradeCount).toEqual(ModifierTier.GREAT); + }); + }); + + describe("Option 3 - Leave", () => { + it("should have the correct properties", () => { + const option = WeirdDreamEncounter.options[2]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}:option.3.label`, + buttonTooltip: `${namespace}:option.3.tooltip`, + selected: [ + { + text: `${namespace}:option.3.selected`, + }, + ], + }); + }); + + it("should reduce party levels by 10%", async () => { const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty); const levelsPrior = scene.getParty().map(p => p.level); - await runMysteryEncounterToEnd(game, 2); + await runMysteryEncounterToEnd(game, 3); const levelsAfter = scene.getParty().map(p => p.level); for (let i = 0; i < levelsPrior.length; i++) { - expect(Math.max(Math.ceil(0.8875 * levelsPrior[i]), 1)).toBe(levelsAfter[i]); + expect(Math.max(Math.ceil(0.9 * levelsPrior[i]), 1)).toBe(levelsAfter[i]); expect(scene.getParty()[i].levelExp).toBe(0); } @@ -195,7 +244,7 @@ describe("Weird Dream - Mystery Encounter", () => { const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty); - await runMysteryEncounterToEnd(game, 2); + await runMysteryEncounterToEnd(game, 3); expect(leaveEncounterWithoutBattleSpy).toBeCalled(); }); diff --git a/src/test/utils/helpers/moveHelper.ts b/src/test/utils/helpers/moveHelper.ts index a0667d91f4c6..73fe63395fd1 100644 --- a/src/test/utils/helpers/moveHelper.ts +++ b/src/test/utils/helpers/moveHelper.ts @@ -1,12 +1,13 @@ import { BattlerIndex } from "#app/battle"; -import { Moves } from "#app/enums/moves"; +import Overrides from "#app/overrides"; import { CommandPhase } from "#app/phases/command-phase"; import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { Command } from "#app/ui/command-ui-handler"; import { Mode } from "#app/ui/ui"; +import { Moves } from "#enums/moves"; +import { getMovePosition } from "#test/utils/gameManagerUtils"; +import { GameManagerHelper } from "#test/utils/helpers/gameManagerHelper"; import { vi } from "vitest"; -import { getMovePosition } from "../gameManagerUtils"; -import { GameManagerHelper } from "./gameManagerHelper"; /** * Helper to handle a Pokemon's move @@ -17,7 +18,7 @@ export class MoveHelper extends GameManagerHelper { * {@linkcode MoveEffectPhase.hitCheck | hitCheck}'s return value to `true`. * Used to force a move to hit. */ - async forceHit(): Promise { + public async forceHit(): Promise { await this.game.phaseInterceptor.to(MoveEffectPhase, false); vi.spyOn(this.game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true); } @@ -26,9 +27,9 @@ export class MoveHelper extends GameManagerHelper { * Intercepts {@linkcode MoveEffectPhase} and mocks the * {@linkcode MoveEffectPhase.hitCheck | hitCheck}'s return value to `false`. * Used to force a move to miss. - * @param firstTargetOnly Whether the move should force miss on the first target only, in the case of multi-target moves. + * @param firstTargetOnly - Whether the move should force miss on the first target only, in the case of multi-target moves. */ - async forceMiss(firstTargetOnly: boolean = false): Promise { + public async forceMiss(firstTargetOnly: boolean = false): Promise { await this.game.phaseInterceptor.to(MoveEffectPhase, false); const hitCheck = vi.spyOn(this.game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck"); @@ -40,12 +41,12 @@ export class MoveHelper extends GameManagerHelper { } /** - * Select the move to be used by the given Pokemon(-index). Triggers during the next {@linkcode CommandPhase} - * @param move the move to use - * @param pkmIndex the pokemon index. Relevant for double-battles only (defaults to 0) - * @param targetIndex The {@linkcode BattlerIndex} of the Pokemon to target for single-target moves, or `null` if a manual call to `selectTarget()` is required - */ - select(move: Moves, pkmIndex: 0 | 1 = 0, targetIndex?: BattlerIndex | null) { + * Select the move to be used by the given Pokemon(-index). Triggers during the next {@linkcode CommandPhase} + * @param move - the move to use + * @param pkmIndex - the pokemon index. Relevant for double-battles only (defaults to 0) + * @param targetIndex - The {@linkcode BattlerIndex} of the Pokemon to target for single-target moves, or `null` if a manual call to `selectTarget()` is required + */ + public select(move: Moves, pkmIndex: 0 | 1 = 0, targetIndex?: BattlerIndex | null) { const movePosition = getMovePosition(this.game.scene, pkmIndex, move); this.game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { @@ -59,4 +60,15 @@ export class MoveHelper extends GameManagerHelper { this.game.selectTarget(movePosition, targetIndex); } } + + /** + * Forces the Paralysis or Freeze status to activate on the next move by temporarily mocking {@linkcode Overrides.STATUS_ACTIVATION_OVERRIDE}, + * advancing to the next `MovePhase`, and then resetting the override to `null` + * @param activated - `true` to force the status to activate, `false` to force the status to not activate (will cause Freeze to heal) + */ + public async forceStatusActivation(activated: boolean): Promise { + vi.spyOn(Overrides, "STATUS_ACTIVATION_OVERRIDE", "get").mockReturnValue(activated); + await this.game.phaseInterceptor.to("MovePhase"); + vi.spyOn(Overrides, "STATUS_ACTIVATION_OVERRIDE", "get").mockReturnValue(null); + } } diff --git a/src/test/utils/helpers/overridesHelper.ts b/src/test/utils/helpers/overridesHelper.ts index ec4d8dbbe4c1..404f5c34a26d 100644 --- a/src/test/utils/helpers/overridesHelper.ts +++ b/src/test/utils/helpers/overridesHelper.ts @@ -29,7 +29,7 @@ export class OverridesHelper extends GameManagerHelper { * @warning Any event listeners that are attached to [NewArenaEvent](events\battle-scene.ts) may need to be handled down the line * @param biome the biome to set */ - startingBiome(biome: Biome): this { + public startingBiome(biome: Biome): this { this.game.scene.newArena(biome); this.log(`Starting biome set to ${Biome[biome]} (=${biome})!`); return this; @@ -38,9 +38,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the starting wave (index) * @param wave the wave (index) to set. Classic: `1`-`200` - * @returns this + * @returns `this` */ - startingWave(wave: number): this { + public startingWave(wave: number): this { vi.spyOn(Overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(wave); this.log(`Starting wave set to ${wave}!`); return this; @@ -49,9 +49,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the player (pokemon) starting level * @param level the (pokemon) level to set - * @returns this + * @returns `this` */ - startingLevel(level: Species | number): this { + public startingLevel(level: Species | number): this { vi.spyOn(Overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(level); this.log(`Player Pokemon starting level set to ${level}!`); return this; @@ -62,7 +62,7 @@ export class OverridesHelper extends GameManagerHelper { * @param value the XP multiplier to set * @returns `this` */ - xpMultiplier(value: number): this { + public xpMultiplier(value: number): this { vi.spyOn(Overrides, "XP_MULTIPLIER_OVERRIDE", "get").mockReturnValue(value); this.log(`XP Multiplier set to ${value}!`); return this; @@ -71,9 +71,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the player (pokemon) starting held items * @param items the items to hold - * @returns this + * @returns `this` */ - startingHeldItems(items: ModifierOverride[]) { + public startingHeldItems(items: ModifierOverride[]): this { vi.spyOn(Overrides, "STARTING_HELD_ITEMS_OVERRIDE", "get").mockReturnValue(items); this.log("Player Pokemon starting held items set to:", items); return this; @@ -82,9 +82,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the player (pokemon) {@linkcode Species | species} * @param species the (pokemon) {@linkcode Species | species} to set - * @returns this + * @returns `this` */ - starterSpecies(species: Species | number): this { + public starterSpecies(species: Species | number): this { vi.spyOn(Overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(species); this.log(`Player Pokemon species set to ${Species[species]} (=${species})!`); return this; @@ -92,9 +92,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the player (pokemon) to be a random fusion - * @returns this + * @returns `this` */ - enableStarterFusion(): this { + public enableStarterFusion(): this { vi.spyOn(Overrides, "STARTER_FUSION_OVERRIDE", "get").mockReturnValue(true); this.log("Player Pokemon is a random fusion!"); return this; @@ -103,9 +103,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the player (pokemon) fusion species * @param species the fusion species to set - * @returns this + * @returns `this` */ - starterFusionSpecies(species: Species | number): this { + public starterFusionSpecies(species: Species | number): this { vi.spyOn(Overrides, "STARTER_FUSION_SPECIES_OVERRIDE", "get").mockReturnValue(species); this.log(`Player Pokemon fusion species set to ${Species[species]} (=${species})!`); return this; @@ -114,9 +114,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the player (pokemons) forms * @param forms the (pokemon) forms to set - * @returns this + * @returns `this` */ - starterForms(forms: Partial>): this { + public starterForms(forms: Partial>): this { vi.spyOn(Overrides, "STARTER_FORM_OVERRIDES", "get").mockReturnValue(forms); const formsStr = Object.entries(forms) .map(([ speciesId, formIndex ]) => `${Species[speciesId]}=${formIndex}`) @@ -128,9 +128,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the player's starting modifiers * @param modifiers the modifiers to set - * @returns this + * @returns `this` */ - startingModifier(modifiers: ModifierOverride[]): this { + public startingModifier(modifiers: ModifierOverride[]): this { vi.spyOn(Overrides, "STARTING_MODIFIER_OVERRIDE", "get").mockReturnValue(modifiers); this.log(`Player starting modifiers set to: ${modifiers}`); return this; @@ -139,9 +139,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the player (pokemon) {@linkcode Abilities | ability} * @param ability the (pokemon) {@linkcode Abilities | ability} to set - * @returns this + * @returns `this` */ - ability(ability: Abilities): this { + public ability(ability: Abilities): this { vi.spyOn(Overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(ability); this.log(`Player Pokemon ability set to ${Abilities[ability]} (=${ability})!`); return this; @@ -150,9 +150,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the player (pokemon) **passive** {@linkcode Abilities | ability} * @param passiveAbility the (pokemon) **passive** {@linkcode Abilities | ability} to set - * @returns this + * @returns `this` */ - passiveAbility(passiveAbility: Abilities): this { + public passiveAbility(passiveAbility: Abilities): this { vi.spyOn(Overrides, "PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(passiveAbility); this.log(`Player Pokemon PASSIVE ability set to ${Abilities[passiveAbility]} (=${passiveAbility})!`); return this; @@ -161,9 +161,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the player (pokemon) {@linkcode Moves | moves}set * @param moveset the {@linkcode Moves | moves}set to set - * @returns this + * @returns `this` */ - moveset(moveset: Moves | Moves[]): this { + public moveset(moveset: Moves | Moves[]): this { vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue(moveset); if (!Array.isArray(moveset)) { moveset = [ moveset ]; @@ -178,7 +178,7 @@ export class OverridesHelper extends GameManagerHelper { * @param statusEffect the {@linkcode StatusEffect | status-effect} to set * @returns */ - statusEffect(statusEffect: StatusEffect): this { + public statusEffect(statusEffect: StatusEffect): this { vi.spyOn(Overrides, "STATUS_OVERRIDE", "get").mockReturnValue(statusEffect); this.log(`Player Pokemon status-effect set to ${StatusEffect[statusEffect]} (=${statusEffect})!`); return this; @@ -186,9 +186,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override each wave to not have standard trainer battles - * @returns this + * @returns `this` */ - disableTrainerWaves(): this { + public disableTrainerWaves(): this { const realFn = getGameMode; vi.spyOn(GameMode, "getGameMode").mockImplementation((gameMode: GameModes) => { const mode = realFn(gameMode); @@ -201,9 +201,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override each wave to not have critical hits - * @returns this + * @returns `this` */ - disableCrits() { + public disableCrits(): this { vi.spyOn(Overrides, "NEVER_CRIT_OVERRIDE", "get").mockReturnValue(true); this.log("Critical hits are disabled!"); return this; @@ -212,9 +212,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the {@linkcode WeatherType | weather (type)} * @param type {@linkcode WeatherType | weather type} to set - * @returns this + * @returns `this` */ - weather(type: WeatherType): this { + public weather(type: WeatherType): this { vi.spyOn(Overrides, "WEATHER_OVERRIDE", "get").mockReturnValue(type); this.log(`Weather set to ${Weather[type]} (=${type})!`); return this; @@ -223,9 +223,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the seed * @param seed the seed to set - * @returns this + * @returns `this` */ - seed(seed: string): this { + public seed(seed: string): this { vi.spyOn(this.game.scene, "resetSeed").mockImplementation(() => { this.game.scene.waveSeed = seed; Phaser.Math.RND.sow([ seed ]); @@ -239,9 +239,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the battle type (single or double) * @param battleType battle type to set - * @returns this + * @returns `this` */ - battleType(battleType: "single" | "double" | null): this { + public battleType(battleType: "single" | "double" | null): this { vi.spyOn(Overrides, "BATTLE_TYPE_OVERRIDE", "get").mockReturnValue(battleType); this.log(`Battle type set to ${battleType} only!`); return this; @@ -250,9 +250,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the enemy (pokemon) {@linkcode Species | species} * @param species the (pokemon) {@linkcode Species | species} to set - * @returns this + * @returns `this` */ - enemySpecies(species: Species | number): this { + public enemySpecies(species: Species | number): this { vi.spyOn(Overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(species); this.log(`Enemy Pokemon species set to ${Species[species]} (=${species})!`); return this; @@ -260,9 +260,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the enemy (pokemon) to be a random fusion - * @returns this + * @returns `this` */ - enableEnemyFusion(): this { + public enableEnemyFusion(): this { vi.spyOn(Overrides, "OPP_FUSION_OVERRIDE", "get").mockReturnValue(true); this.log("Enemy Pokemon is a random fusion!"); return this; @@ -271,9 +271,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the enemy (pokemon) fusion species * @param species the fusion species to set - * @returns this + * @returns `this` */ - enemyFusionSpecies(species: Species | number): this { + public enemyFusionSpecies(species: Species | number): this { vi.spyOn(Overrides, "OPP_FUSION_SPECIES_OVERRIDE", "get").mockReturnValue(species); this.log(`Enemy Pokemon fusion species set to ${Species[species]} (=${species})!`); return this; @@ -282,9 +282,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the enemy (pokemon) {@linkcode Abilities | ability} * @param ability the (pokemon) {@linkcode Abilities | ability} to set - * @returns this + * @returns `this` */ - enemyAbility(ability: Abilities): this { + public enemyAbility(ability: Abilities): this { vi.spyOn(Overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(ability); this.log(`Enemy Pokemon ability set to ${Abilities[ability]} (=${ability})!`); return this; @@ -293,9 +293,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the enemy (pokemon) **passive** {@linkcode Abilities | ability} * @param passiveAbility the (pokemon) **passive** {@linkcode Abilities | ability} to set - * @returns this + * @returns `this` */ - enemyPassiveAbility(passiveAbility: Abilities): this { + public enemyPassiveAbility(passiveAbility: Abilities): this { vi.spyOn(Overrides, "OPP_PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(passiveAbility); this.log(`Enemy Pokemon PASSIVE ability set to ${Abilities[passiveAbility]} (=${passiveAbility})!`); return this; @@ -304,9 +304,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the enemy (pokemon) {@linkcode Moves | moves}set * @param moveset the {@linkcode Moves | moves}set to set - * @returns this + * @returns `this` */ - enemyMoveset(moveset: Moves | Moves[]): this { + public enemyMoveset(moveset: Moves | Moves[]): this { vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue(moveset); if (!Array.isArray(moveset)) { moveset = [ moveset ]; @@ -319,9 +319,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the enemy (pokemon) level * @param level the level to set - * @returns this + * @returns `this` */ - enemyLevel(level: number): this { + public enemyLevel(level: number): this { vi.spyOn(Overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(level); this.log(`Enemy Pokemon level set to ${level}!`); return this; @@ -332,7 +332,7 @@ export class OverridesHelper extends GameManagerHelper { * @param statusEffect the {@linkcode StatusEffect | status-effect} to set * @returns */ - enemyStatusEffect(statusEffect: StatusEffect): this { + public enemyStatusEffect(statusEffect: StatusEffect): this { vi.spyOn(Overrides, "OPP_STATUS_OVERRIDE", "get").mockReturnValue(statusEffect); this.log(`Enemy Pokemon status-effect set to ${StatusEffect[statusEffect]} (=${statusEffect})!`); return this; @@ -341,9 +341,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the enemy (pokemon) held items * @param items the items to hold - * @returns this + * @returns `this` */ - enemyHeldItems(items: ModifierOverride[]) { + public enemyHeldItems(items: ModifierOverride[]): this { vi.spyOn(Overrides, "OPP_HELD_ITEMS_OVERRIDE", "get").mockReturnValue(items); this.log("Enemy Pokemon held items set to:", items); return this; @@ -354,7 +354,7 @@ export class OverridesHelper extends GameManagerHelper { * @param unlockable The Unlockable(s) to enable. * @returns `this` */ - enableUnlockable(unlockable: Unlockables[]) { + public enableUnlockable(unlockable: Unlockables[]): this { vi.spyOn(Overrides, "ITEM_UNLOCK_OVERRIDE", "get").mockReturnValue(unlockable); this.log("Temporarily unlocked the following content: ", unlockable); return this; @@ -363,9 +363,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the items rolled at the end of a battle * @param items the items to be rolled - * @returns this + * @returns `this` */ - itemRewards(items: ModifierOverride[]) { + public itemRewards(items: ModifierOverride[]): this { vi.spyOn(Overrides, "ITEM_REWARD_OVERRIDE", "get").mockReturnValue(items); this.log("Item rewards set to:", items); return this; @@ -375,8 +375,9 @@ export class OverridesHelper extends GameManagerHelper { * Override player shininess * @param shininess - `true` or `false` to force the player's pokemon to be shiny or not shiny, * `null` to disable the override and re-enable RNG shinies. + * @returns `this` */ - shiny(shininess: boolean | null): this { + public shiny(shininess: boolean | null): this { vi.spyOn(Overrides, "SHINY_OVERRIDE", "get").mockReturnValue(shininess); if (shininess === null) { this.log("Disabled player Pokemon shiny override!"); @@ -389,8 +390,9 @@ export class OverridesHelper extends GameManagerHelper { /** * Override player shiny variant * @param variant - The player's shiny variant. + * @returns `this` */ - shinyVariant(variant: Variant): this { + public shinyVariant(variant: Variant): this { vi.spyOn(Overrides, "VARIANT_OVERRIDE", "get").mockReturnValue(variant); this.log(`Set player Pokemon's shiny variant to ${variant}!`); return this; @@ -420,23 +422,38 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the enemy (Pokemon) to have the given amount of health segments * @param healthSegments the number of segments to give - * default: 0, the health segments will be handled like in the game based on wave, level and species - * 1: the Pokemon will not be a boss - * 2+: the Pokemon will be a boss with the given number of health segments - * @returns this + * - `0` (default): the health segments will be handled like in the game based on wave, level and species + * - `1`: the Pokemon will not be a boss + * - `2`+: the Pokemon will be a boss with the given number of health segments + * @returns `this` */ - enemyHealthSegments(healthSegments: number) { + public enemyHealthSegments(healthSegments: number): this { vi.spyOn(Overrides, "OPP_HEALTH_SEGMENTS_OVERRIDE", "get").mockReturnValue(healthSegments); this.log("Enemy Pokemon health segments set to:", healthSegments); return this; } + /** + * Override statuses (Paralysis and Freeze) to always or never activate + * @param activate - `true` to force activation, `false` to force no activation, `null` to disable the override + * @returns `this` + */ + public statusActivation(activate: boolean | null): this { + vi.spyOn(Overrides, "STATUS_ACTIVATION_OVERRIDE", "get").mockReturnValue(activate); + if (activate !== null) { + this.log(`Paralysis and Freeze forced to ${activate ? "always" : "never"} activate!`); + } else { + this.log("Status activation override disabled!"); + } + return this; + } + /** * Override the encounter chance for a mystery encounter. * @param percentage the encounter chance in % - * @returns spy instance + * @returns `this` */ - mysteryEncounterChance(percentage: number) { + public mysteryEncounterChance(percentage: number): this { const maxRate: number = 256; // 100% const rate = maxRate * (percentage / 100); vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_RATE_OVERRIDE", "get").mockReturnValue(rate); @@ -446,10 +463,10 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the encounter chance for a mystery encounter. - * @returns spy instance - * @param tier + * @param tier - The {@linkcode MysteryEncounterTier} to encounter + * @returns `this` */ - mysteryEncounterTier(tier: MysteryEncounterTier) { + public mysteryEncounterTier(tier: MysteryEncounterTier): this { vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_TIER_OVERRIDE", "get").mockReturnValue(tier); this.log(`Mystery encounter tier set to ${tier}!`); return this; @@ -457,10 +474,10 @@ export class OverridesHelper extends GameManagerHelper { /** * Override the encounter that spawns for the scene - * @param encounterType - * @returns spy instance + * @param encounterType - The {@linkcode MysteryEncounterType} of the encounter + * @returns `this` */ - mysteryEncounter(encounterType: MysteryEncounterType) { + public mysteryEncounter(encounterType: MysteryEncounterType): this { vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_OVERRIDE", "get").mockReturnValue(encounterType); this.log(`Mystery encounter override set to ${encounterType}!`); return this; diff --git a/src/ui/admin-ui-handler.ts b/src/ui/admin-ui-handler.ts index c73c02e66c28..6249e54d8c33 100644 --- a/src/ui/admin-ui-handler.ts +++ b/src/ui/admin-ui-handler.ts @@ -2,37 +2,83 @@ import BattleScene from "#app/battle-scene"; import { ModalConfig } from "./modal-ui-handler"; import { Mode } from "./ui"; import * as Utils from "../utils"; -import { FormModalUiHandler } from "./form-modal-ui-handler"; +import { FormModalUiHandler, InputFieldConfig } from "./form-modal-ui-handler"; import { Button } from "#app/enums/buttons"; +import { TextStyle } from "./text"; export default class AdminUiHandler extends FormModalUiHandler { + private adminMode: AdminMode; + private adminResult: AdminSearchInfo; + private config: ModalConfig; + + private readonly buttonGap = 10; + // http response from the server when a username isn't found in the server + private readonly httpUserNotFoundErrorCode: number = 404; + private readonly ERR_REQUIRED_FIELD = (field: string) => { + if (field === "username") { + return `${Utils.formatText(field)} is required`; + } else { + return `${Utils.formatText(field)} Id is required`; + } + }; + // returns a string saying whether a username has been successfully linked/unlinked to discord/google + private readonly SUCCESS_SERVICE_MODE = (service: string, mode: string) => { + return `Username and ${service} successfully ${mode.toLowerCase()}ed`; + }; + private readonly ERR_USERNAME_NOT_FOUND: string = "Username not found!"; + private readonly ERR_GENERIC_ERROR: string = "There was an error"; + constructor(scene: BattleScene, mode: Mode | null = null) { super(scene, mode); } - setup(): void { - super.setup(); - } - - getModalTitle(config?: ModalConfig): string { + override getModalTitle(): string { return "Admin panel"; } - getFields(config?: ModalConfig): string[] { - return [ "Username", "Discord ID" ]; + override getWidth(): number { + return this.adminMode === AdminMode.ADMIN ? 180 : 160; } - getWidth(config?: ModalConfig): number { - return 160; + override getMargin(): [number, number, number, number] { + return [ 0, 0, 0, 0 ]; } - getMargin(config?: ModalConfig): [number, number, number, number] { - return [ 0, 0, 48, 0 ]; + override getButtonLabels(): string[] { + switch (this.adminMode) { + case AdminMode.LINK: + return [ "Link Account", "Cancel" ]; + case AdminMode.SEARCH: + return [ "Find account", "Cancel" ]; + case AdminMode.ADMIN: + return [ "Back to search", "Cancel" ]; + default: + return [ "Activate ADMIN", "Cancel" ]; + } } - getButtonLabels(config?: ModalConfig): string[] { - return [ "Link account", "Cancel" ]; + override getInputFieldConfigs(): InputFieldConfig[] { + const inputFieldConfigs: InputFieldConfig[] = []; + switch (this.adminMode) { + case AdminMode.LINK: + inputFieldConfigs.push( { label: "Username" }); + inputFieldConfigs.push( { label: "Discord ID" }); + break; + case AdminMode.SEARCH: + inputFieldConfigs.push( { label: "Username" }); + break; + case AdminMode.ADMIN: + const adminResult = this.adminResult ?? { username: "", discordId: "", googleId: "", lastLoggedIn: "", registered: "" }; + // Discord and Google ID fields that are not empty get locked, other fields are all locked + inputFieldConfigs.push( { label: "Username", isReadOnly: true }); + inputFieldConfigs.push( { label: "Discord ID", isReadOnly: adminResult.discordId !== "" }); + inputFieldConfigs.push( { label: "Google ID", isReadOnly: adminResult.googleId !== "" }); + inputFieldConfigs.push( { label: "Last played", isReadOnly: true }); + inputFieldConfigs.push( { label: "Registered", isReadOnly: true }); + break; + } + return inputFieldConfigs; } processInput(button: Button): boolean { @@ -45,44 +91,281 @@ export default class AdminUiHandler extends FormModalUiHandler { } show(args: any[]): boolean { + this.config = args[0] as ModalConfig; // config + this.adminMode = args[1] as AdminMode; // admin mode + this.adminResult = args[2] ?? { username: "", discordId: "", googleId: "", lastLoggedIn: "", registered: "" }; // admin result, if any + const isMessageError = args[3]; // is the message shown a success or error + + const fields = this.getInputFieldConfigs(); + const hasTitle = !!this.getModalTitle(); + + this.updateFields(fields, hasTitle); + this.updateContainer(this.config); + + const labels = this.getButtonLabels(); + for (let i = 0; i < labels.length; i++) { + this.buttonLabels[i].setText(labels[i]); // sets the label text + } + + this.errorMessage.setPosition(10, (hasTitle ? 31 : 5) + 20 * (fields.length - 1) + 16 + this.getButtonTopMargin()); // sets the position of the message dynamically + if (isMessageError) { + this.errorMessage.setColor(this.getTextColor(TextStyle.SUMMARY_PINK)); + this.errorMessage.setShadowColor(this.getTextColor(TextStyle.SUMMARY_PINK, true)); + } else { + this.errorMessage.setColor(this.getTextColor(TextStyle.SUMMARY_GREEN)); + this.errorMessage.setShadowColor(this.getTextColor(TextStyle.SUMMARY_GREEN, true)); + } + if (super.show(args)) { - const config = args[0] as ModalConfig; + this.populateFields(this.adminMode, this.adminResult); const originalSubmitAction = this.submitAction; this.submitAction = (_) => { this.submitAction = originalSubmitAction; - this.scene.ui.setMode(Mode.LOADING, { buttonActions: []}); - const onFail = error => { - this.scene.ui.setMode(Mode.ADMIN, Object.assign(config, { errorMessage: error?.trim() })); - this.scene.ui.playError(); - }; - if (!this.inputs[0].text) { - return onFail("Username is required"); + const adminSearchResult: AdminSearchInfo = this.convertInputsToAdmin(); // this converts the input texts into a single object for use later + const validFields = this.areFieldsValid(this.adminMode); + if (validFields.error) { + this.scene.ui.setMode(Mode.LOADING, { buttonActions: []}); // this is here to force a loading screen to allow the admin tool to reopen again if there's an error + return this.showMessage(validFields.errorMessage ?? "", adminSearchResult, true); } - if (!this.inputs[1].text) { - return onFail("Discord Id is required"); + this.scene.ui.setMode(Mode.LOADING, { buttonActions: []}); + if (this.adminMode === AdminMode.LINK) { + this.adminLinkUnlink(adminSearchResult, "discord", "Link") // calls server to link discord + .then(response => { + if (response.error) { + return this.showMessage(response.errorType, adminSearchResult, true); // error or some kind + } else { + return this.showMessage(this.SUCCESS_SERVICE_MODE("discord", "link"), adminSearchResult, false); // success + } + }); + } else if (this.adminMode === AdminMode.SEARCH) { + this.adminSearch(adminSearchResult) // admin search for username + .then(response => { + if (response.error) { + return this.showMessage(response.errorType, adminSearchResult, true); // failure + } + this.updateAdminPanelInfo(response.adminSearchResult ?? adminSearchResult); // success + }); + } else if (this.adminMode === AdminMode.ADMIN) { + this.updateAdminPanelInfo(adminSearchResult, AdminMode.SEARCH); } - Utils.apiPost("admin/account/discord-link", `username=${encodeURIComponent(this.inputs[0].text)}&discordId=${encodeURIComponent(this.inputs[1].text)}`, "application/x-www-form-urlencoded", true) - .then(response => { - if (!response.ok) { - console.error(response); - } - this.inputs[0].setText(""); - this.inputs[1].setText(""); - this.scene.ui.revertMode(); - }) - .catch((err) => { - console.error(err); - this.scene.ui.revertMode(); - }); return false; }; return true; } return false; + } + + showMessage(message: string, adminResult: AdminSearchInfo, isError: boolean) { + this.scene.ui.setMode(Mode.ADMIN, Object.assign(this.config, { errorMessage: message?.trim() }), this.adminMode, adminResult, isError); + if (isError) { + this.scene.ui.playError(); + } else { + this.scene.ui.playSelect(); + } + } + + /** + * This is used to update the fields' text when loading in a new admin ui handler. It uses the {@linkcode adminResult} + * to update the input text based on the {@linkcode adminMode}. For a linking adminMode, it sets the username and discord. + * For a search adminMode, it sets the username. For an admin adminMode, it sets all the info from adminResult in the + * appropriate text boxes, and also sets the link/unlink icons for discord/google depending on the result + */ + private populateFields(adminMode: AdminMode, adminResult: AdminSearchInfo) { + switch (adminMode) { + case AdminMode.LINK: + this.inputs[0].setText(adminResult.username); + this.inputs[1].setText(adminResult.discordId); + break; + case AdminMode.SEARCH: + this.inputs[0].setText(adminResult.username); + break; + case AdminMode.ADMIN: + Object.keys(adminResult).forEach((aR, i) => { + this.inputs[i].setText(adminResult[aR]); + if (aR === "discordId" || aR === "googleId") { // this is here to add the icons for linking/unlinking of google/discord IDs + const nineSlice = this.inputContainers[i].list.find(iC => iC.type === "NineSlice"); + const img = this.scene.add.image(this.inputContainers[i].x + nineSlice!.width + this.buttonGap, this.inputContainers[i].y + (Math.floor(nineSlice!.height / 2)), adminResult[aR] === "" ? "link_icon" : "unlink_icon"); + img.setName(`adminBtn_${aR}`); + img.setOrigin(0.5, 0.5); + img.setInteractive(); + img.on("pointerdown", () => { + const service = aR.toLowerCase().replace("id", ""); // this takes our key (discordId or googleId) and removes the "Id" at the end to make it more url friendly + const mode = adminResult[aR] === "" ? "Link" : "Unlink"; // this figures out if we're linking or unlinking a service + const validFields = this.areFieldsValid(this.adminMode, service); + if (validFields.error) { + this.scene.ui.setMode(Mode.LOADING, { buttonActions: []}); // this is here to force a loading screen to allow the admin tool to reopen again if there's an error + return this.showMessage(validFields.errorMessage ?? "", adminResult, true); + } + this.adminLinkUnlink(this.convertInputsToAdmin(), service, mode).then(response => { // attempts to link/unlink depending on the service + if (response.error) { + this.scene.ui.setMode(Mode.LOADING, { buttonActions: []}); + return this.showMessage(response.errorType, adminResult, true); // fail + } else { // success, reload panel with new results + this.scene.ui.setMode(Mode.LOADING, { buttonActions: []}); + this.adminSearch(adminResult) + .then(response => { + if (response.error) { + return this.showMessage(response.errorType, adminResult, true); + } + return this.showMessage(this.SUCCESS_SERVICE_MODE(service, mode), response.adminSearchResult ?? adminResult, false); + }); + } + }); + }); + this.addInteractionHoverEffect(img); + this.modalContainer.add(img); + } + }); + break; + } + } + + private areFieldsValid(adminMode: AdminMode, service?: string): { error: boolean; errorMessage?: string; } { + switch (adminMode) { + case AdminMode.LINK: + if (!this.inputs[0].text) { // username missing from link panel + return { + error: true, + errorMessage: this.ERR_REQUIRED_FIELD("username") + }; + } + if (!this.inputs[1].text) { // discordId missing from linking panel + return { + error: true, + errorMessage: this.ERR_REQUIRED_FIELD("discord") + }; + } + break; + case AdminMode.SEARCH: + if (!this.inputs[0].text) { // username missing from search panel + return { + error: true, + errorMessage: this.ERR_REQUIRED_FIELD("username") + }; + } + break; + case AdminMode.ADMIN: + if (!this.inputs[1].text && service === "discord") { // discordId missing from admin panel + return { + error: true, + errorMessage: this.ERR_REQUIRED_FIELD(service) + }; + } + if (!this.inputs[2].text && service === "google") { // googleId missing from admin panel + return { + error: true, + errorMessage: this.ERR_REQUIRED_FIELD(service) + }; + } + break; + } + return { + error: false + }; + } + + private convertInputsToAdmin(): AdminSearchInfo { + return { + username: this.inputs[0]?.node ? this.inputs[0].text : "", + discordId: this.inputs[1]?.node ? this.inputs[1]?.text : "", + googleId: this.inputs[2]?.node ? this.inputs[2]?.text : "", + lastLoggedIn: this.inputs[3]?.node ? this.inputs[3]?.text : "", + registered: this.inputs[4]?.node ? this.inputs[4]?.text : "" + }; + } + + private async adminSearch(adminSearchResult: AdminSearchInfo) { + try { + const adminInfo = await Utils.apiFetch(`admin/account/adminSearch?username=${encodeURIComponent(adminSearchResult.username)}`, true); + if (!adminInfo.ok) { // error - if adminInfo.status === this.httpUserNotFoundErrorCode that means the username can't be found in the db + return { adminSearchResult: adminSearchResult, error: true, errorType: adminInfo.status === this.httpUserNotFoundErrorCode ? this.ERR_USERNAME_NOT_FOUND : this.ERR_GENERIC_ERROR }; + } else { // success + const adminInfoJson: AdminSearchInfo = await adminInfo.json(); + return { adminSearchResult: adminInfoJson, error: false }; + } + } catch (err) { + console.error(err); + return { error: true, errorType: err }; + } + } + + private async adminLinkUnlink(adminSearchResult: AdminSearchInfo, service: string, mode: string) { + try { + const response = await Utils.apiPost(`admin/account/${service}${mode}`, `username=${encodeURIComponent(adminSearchResult.username)}&${service}Id=${encodeURIComponent(service === "discord" ? adminSearchResult.discordId : adminSearchResult.googleId)}`, "application/x-www-form-urlencoded", true); + if (!response.ok) { // error - if response.status === this.httpUserNotFoundErrorCode that means the username can't be found in the db + return { adminSearchResult: adminSearchResult, error: true, errorType: response.status === this.httpUserNotFoundErrorCode ? this.ERR_USERNAME_NOT_FOUND : this.ERR_GENERIC_ERROR }; + } else { // success! + return { adminSearchResult: adminSearchResult, error: false }; + } + } catch (err) { + console.error(err); + return { error: true, errorType: err }; + } + } + private updateAdminPanelInfo(adminSearchResult: AdminSearchInfo, mode?: AdminMode) { + mode = mode ?? AdminMode.ADMIN; + this.scene.ui.setMode(Mode.ADMIN, { + buttonActions: [ + // we double revert here and below to go back 2 layers of menus + () => { + this.scene.ui.revertMode(); + this.scene.ui.revertMode(); + }, + () => { + this.scene.ui.revertMode(); + this.scene.ui.revertMode(); + } + ] + }, mode, adminSearchResult); } clear(): void { super.clear(); + + // this is used to remove the existing fields on the admin panel so they can be updated + + const itemsToRemove: string[] = [ "formLabel", "adminBtn" ]; // this is the start of the names for each element we want to remove + const removeArray: any[] = []; + const mC = this.modalContainer.list; + for (let i = mC.length - 1; i >= 0; i--) { + /* This code looks for a few things before destroying the specific field; first it looks to see if the name of the element is %like% the itemsToRemove labels + * this means that anything with, for example, "formLabel", will be true. + * It then also checks for any containers that are within this.modalContainer, and checks if any of its child elements are of type rexInputText + * and if either of these conditions are met, the element is destroyed. + */ + if (itemsToRemove.some(iTR => mC[i].name.includes(iTR)) || (mC[i].type === "Container" && (mC[i] as Phaser.GameObjects.Container).list.find(m => m.type === "rexInputText"))) { + removeArray.push(mC[i]); + } + } + + while (removeArray.length > 0) { + this.modalContainer.remove(removeArray.pop(), true); + } } } + +export enum AdminMode { + LINK, + SEARCH, + ADMIN +} + +export function getAdminModeName(adminMode: AdminMode): string { + switch (adminMode) { + case AdminMode.LINK: + return "Link"; + case AdminMode.SEARCH: + return "Search"; + default: + return ""; + } +} + +interface AdminSearchInfo { + username: string; + discordId: string; + googleId: string; + lastLoggedIn: string; + registered: string; +} diff --git a/src/ui/form-modal-ui-handler.ts b/src/ui/form-modal-ui-handler.ts index 331154263ade..65ee9f2db10c 100644 --- a/src/ui/form-modal-ui-handler.ts +++ b/src/ui/form-modal-ui-handler.ts @@ -5,7 +5,6 @@ import { TextStyle, addTextInputObject, addTextObject } from "./text"; import { WindowVariant, addWindow } from "./ui-theme"; import InputText from "phaser3-rex-plugins/plugins/inputtext"; import * as Utils from "../utils"; -import i18next from "i18next"; import { Button } from "#enums/buttons"; export interface FormModalConfig extends ModalConfig { @@ -19,6 +18,7 @@ export abstract class FormModalUiHandler extends ModalUiHandler { protected errorMessage: Phaser.GameObjects.Text; protected submitAction: Function | null; protected tween: Phaser.Tweens.Tween; + protected formLabels: Phaser.GameObjects.Text[]; constructor(scene: BattleScene, mode: Mode | null = null) { super(scene, mode); @@ -26,12 +26,18 @@ export abstract class FormModalUiHandler extends ModalUiHandler { this.editing = false; this.inputContainers = []; this.inputs = []; + this.formLabels = []; } - abstract getFields(): string[]; + /** + * Get configuration for all fields that should be part of the modal + * Gets used by {@linkcode updateFields} to add the proper text inputs and labels to the view + * @returns array of {@linkcode InputFieldConfig} + */ + abstract getInputFieldConfigs(): InputFieldConfig[]; getHeight(config?: ModalConfig): number { - return 20 * this.getFields().length + (this.getModalTitle() ? 26 : 0) + ((config as FormModalConfig)?.errorMessage ? 12 : 0) + this.getButtonTopMargin() + 28; + return 20 * this.getInputFieldConfigs().length + (this.getModalTitle() ? 26 : 0) + ((config as FormModalConfig)?.errorMessage ? 12 : 0) + this.getButtonTopMargin() + 28; } getReadableErrorMessage(error: string): string { @@ -45,37 +51,50 @@ export abstract class FormModalUiHandler extends ModalUiHandler { setup(): void { super.setup(); - const fields = this.getFields(); + const config = this.getInputFieldConfigs(); const hasTitle = !!this.getModalTitle(); - fields.forEach((field, f) => { - const label = addTextObject(this.scene, 10, (hasTitle ? 31 : 5) + 20 * f, field, TextStyle.TOOLTIP_CONTENT); + if (config.length >= 1) { + this.updateFields(config, hasTitle); + } + + this.errorMessage = addTextObject(this.scene, 10, (hasTitle ? 31 : 5) + 20 * (config.length - 1) + 16 + this.getButtonTopMargin(), "", TextStyle.TOOLTIP_CONTENT); + this.errorMessage.setColor(this.getTextColor(TextStyle.SUMMARY_PINK)); + this.errorMessage.setShadowColor(this.getTextColor(TextStyle.SUMMARY_PINK, true)); + this.errorMessage.setVisible(false); + this.modalContainer.add(this.errorMessage); + } + + protected updateFields(fieldsConfig: InputFieldConfig[], hasTitle: boolean) { + this.inputContainers = []; + this.inputs = []; + this.formLabels = []; + fieldsConfig.forEach((config, f) => { + const label = addTextObject(this.scene, 10, (hasTitle ? 31 : 5) + 20 * f, config.label, TextStyle.TOOLTIP_CONTENT); + label.name = "formLabel" + f; - this.modalContainer.add(label); + this.formLabels.push(label); + this.modalContainer.add(this.formLabels[this.formLabels.length - 1]); const inputContainer = this.scene.add.container(70, (hasTitle ? 28 : 2) + 20 * f); inputContainer.setVisible(false); const inputBg = addWindow(this.scene, 0, 0, 80, 16, false, false, 0, 0, WindowVariant.XTHIN); - const isPassword = field.includes(i18next.t("menu:password")) || field.includes(i18next.t("menu:confirmPassword")); - const input = addTextInputObject(this.scene, 4, -2, 440, 116, TextStyle.TOOLTIP_CONTENT, { type: isPassword ? "password" : "text", maxLength: isPassword ? 64 : 20 }); + const isPassword = config?.isPassword; + const isReadOnly = config?.isReadOnly; + const input = addTextInputObject(this.scene, 4, -2, 440, 116, TextStyle.TOOLTIP_CONTENT, { type: isPassword ? "password" : "text", maxLength: isPassword ? 64 : 20, readOnly: isReadOnly }); input.setOrigin(0, 0); inputContainer.add(inputBg); inputContainer.add(input); - this.modalContainer.add(inputContainer); this.inputContainers.push(inputContainer); + this.modalContainer.add(inputContainer); + this.inputs.push(input); }); - - this.errorMessage = addTextObject(this.scene, 10, (hasTitle ? 31 : 5) + 20 * (fields.length - 1) + 16 + this.getButtonTopMargin(), "", TextStyle.TOOLTIP_CONTENT); - this.errorMessage.setColor(this.getTextColor(TextStyle.SUMMARY_PINK)); - this.errorMessage.setShadowColor(this.getTextColor(TextStyle.SUMMARY_PINK, true)); - this.errorMessage.setVisible(false); - this.modalContainer.add(this.errorMessage); } show(args: any[]): boolean { @@ -149,3 +168,9 @@ export abstract class FormModalUiHandler extends ModalUiHandler { } } } + +export interface InputFieldConfig { + label: string, + isPassword?: boolean, + isReadOnly?: boolean +} diff --git a/src/ui/login-form-ui-handler.ts b/src/ui/login-form-ui-handler.ts index 8be432ad6c1a..26a2a225ec6a 100644 --- a/src/ui/login-form-ui-handler.ts +++ b/src/ui/login-form-ui-handler.ts @@ -1,4 +1,4 @@ -import { FormModalUiHandler } from "./form-modal-ui-handler"; +import { FormModalUiHandler, InputFieldConfig } from "./form-modal-ui-handler"; import { ModalConfig } from "./modal-ui-handler"; import * as Utils from "../utils"; import { Mode } from "./ui"; @@ -17,9 +17,9 @@ interface BuildInteractableImageOpts { export default class LoginFormUiHandler extends FormModalUiHandler { private readonly ERR_USERNAME: string = "invalid username"; - private readonly ERR_PASSWORD: string = "invalid password"; - private readonly ERR_ACCOUNT_EXIST: string = "account doesn't exist"; - private readonly ERR_PASSWORD_MATCH: string = "password doesn't match"; + private readonly ERR_PASSWORD: string = "invalid password"; + private readonly ERR_ACCOUNT_EXIST: string = "account doesn't exist"; + private readonly ERR_PASSWORD_MATCH: string = "password doesn't match"; private readonly ERR_NO_SAVES: string = "No save files found"; private readonly ERR_TOO_MANY_SAVES: string = "Too many save files found"; @@ -75,10 +75,6 @@ export default class LoginFormUiHandler extends FormModalUiHandler { return i18next.t("menu:login"); } - override getFields(_config?: ModalConfig): string[] { - return [ i18next.t("menu:username"), i18next.t("menu:password") ]; - } - override getWidth(_config?: ModalConfig): number { return 160; } @@ -106,14 +102,21 @@ export default class LoginFormUiHandler extends FormModalUiHandler { case this.ERR_PASSWORD_MATCH: return i18next.t("menu:unmatchingPassword"); case this.ERR_NO_SAVES: - return i18next.t("menu:noSaves"); + return "P01: " + i18next.t("menu:noSaves"); case this.ERR_TOO_MANY_SAVES: - return i18next.t("menu:tooManySaves"); + return "P02: " + i18next.t("menu:tooManySaves"); } return super.getReadableErrorMessage(error); } + override getInputFieldConfigs(): InputFieldConfig[] { + const inputFieldConfigs: InputFieldConfig[] = []; + inputFieldConfigs.push({ label: i18next.t("menu:username") }); + inputFieldConfigs.push({ label: i18next.t("menu:password"), isPassword: true }); + return inputFieldConfigs; + } + override show(args: any[]): boolean { if (super.show(args)) { @@ -164,7 +167,7 @@ export default class LoginFormUiHandler extends FormModalUiHandler { [ this.discordImage, this.googleImage, this.usernameInfoImage ].forEach((img) => img.off("pointerdown")); } - private processExternalProvider(config: ModalConfig) : void { + private processExternalProvider(config: ModalConfig): void { this.externalPartyTitle.setText(i18next.t("menu:orUse") ?? ""); this.externalPartyTitle.setX(20 + this.externalPartyTitle.text.length); this.externalPartyTitle.setVisible(true); diff --git a/src/ui/menu-ui-handler.ts b/src/ui/menu-ui-handler.ts index 0f4c2d2f53e3..301d54daa3ad 100644 --- a/src/ui/menu-ui-handler.ts +++ b/src/ui/menu-ui-handler.ts @@ -13,6 +13,7 @@ import { GameDataType } from "#enums/game-data-type"; import BgmBar from "#app/ui/bgm-bar"; import AwaitableUiHandler from "./awaitable-ui-handler"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; +import { AdminMode, getAdminModeName } from "./admin-ui-handler"; enum MenuOptions { GAME_SETTINGS, @@ -387,16 +388,41 @@ export default class MenuUiHandler extends MessageUiHandler { communityOptions.push({ label: "Admin", handler: () => { - ui.playSelect(); - ui.setOverlayMode(Mode.ADMIN, { - buttonActions: [ - () => { - ui.revertMode(); - }, - () => { - ui.revertMode(); + + const skippedAdminModes: AdminMode[] = [ AdminMode.ADMIN ]; // this is here so that we can skip the menu populating enums that aren't meant for the menu, such as the AdminMode.ADMIN + const options: OptionSelectItem[] = []; + Object.values(AdminMode).filter((v) => !isNaN(Number(v)) && !skippedAdminModes.includes(v as AdminMode)).forEach((mode) => { // this gets all the enums in a way we can use + options.push({ + label: getAdminModeName(mode as AdminMode), + handler: () => { + ui.playSelect(); + ui.setOverlayMode(Mode.ADMIN, { + buttonActions: [ + // we double revert here and below to go back 2 layers of menus + () => { + ui.revertMode(); + ui.revertMode(); + }, + () => { + ui.revertMode(); + ui.revertMode(); + } + ] + }, mode); // mode is our AdminMode enum + return true; } - ] + }); + }); + options.push({ + label: "Cancel", + handler: () => { + ui.revertMode(); + return true; + } + }); + this.scene.ui.setOverlayMode(Mode.OPTION_SELECT, { + options: options, + delay: 0 }); return true; }, diff --git a/src/ui/modal-ui-handler.ts b/src/ui/modal-ui-handler.ts index 60204f18c4af..79f1e8afeed1 100644 --- a/src/ui/modal-ui-handler.ts +++ b/src/ui/modal-ui-handler.ts @@ -15,12 +15,14 @@ export abstract class ModalUiHandler extends UiHandler { protected titleText: Phaser.GameObjects.Text; protected buttonContainers: Phaser.GameObjects.Container[]; protected buttonBgs: Phaser.GameObjects.NineSlice[]; + protected buttonLabels: Phaser.GameObjects.Text[]; constructor(scene: BattleScene, mode: Mode | null = null) { super(scene, mode); this.buttonContainers = []; this.buttonBgs = []; + this.buttonLabels = []; } abstract getModalTitle(config?: ModalConfig): string; @@ -75,6 +77,7 @@ export abstract class ModalUiHandler extends UiHandler { const buttonContainer = this.scene.add.container(0, buttonTopMargin); + this.buttonLabels.push(buttonLabel); this.buttonBgs.push(buttonBg); this.buttonContainers.push(buttonContainer); diff --git a/src/ui/pokemon-info-container.ts b/src/ui/pokemon-info-container.ts index 242e59c599b2..5c3a22639dd3 100644 --- a/src/ui/pokemon-info-container.ts +++ b/src/ui/pokemon-info-container.ts @@ -279,11 +279,8 @@ export default class PokemonInfoContainer extends Phaser.GameObjects.Container { this.pokemonAbilityText.setColor(getTextColor(abilityTextStyle, false, this.scene.uiTheme)); this.pokemonAbilityText.setShadowColor(getTextColor(abilityTextStyle, true, this.scene.uiTheme)); - - const ownedAbilityAttrs = pokemon.scene.gameData.starterData[pokemon.species.getRootSpeciesId()].abilityAttr; - // Check if the player owns ability for the root form - const playerOwnsThisAbility = pokemon.checkIfPlayerHasAbilityOfStarter(ownedAbilityAttrs); + const playerOwnsThisAbility = pokemon.checkIfPlayerHasAbilityOfStarter(starterEntry.abilityAttr); if (!playerOwnsThisAbility) { this.pokemonAbilityLabelText.setColor(getTextColor(TextStyle.SUMMARY_BLUE, false, this.scene.uiTheme)); diff --git a/src/ui/registration-form-ui-handler.ts b/src/ui/registration-form-ui-handler.ts index 2f8486bcdb3c..fc9eb85cbaf4 100644 --- a/src/ui/registration-form-ui-handler.ts +++ b/src/ui/registration-form-ui-handler.ts @@ -1,4 +1,4 @@ -import { FormModalUiHandler } from "./form-modal-ui-handler"; +import { FormModalUiHandler, InputFieldConfig } from "./form-modal-ui-handler"; import { ModalConfig } from "./modal-ui-handler"; import * as Utils from "../utils"; import { Mode } from "./ui"; @@ -24,10 +24,6 @@ export default class RegistrationFormUiHandler extends FormModalUiHandler { return i18next.t("menu:register"); } - getFields(config?: ModalConfig): string[] { - return [ i18next.t("menu:username"), i18next.t("menu:password"), i18next.t("menu:confirmPassword") ]; - } - getWidth(config?: ModalConfig): number { return 160; } @@ -61,6 +57,14 @@ export default class RegistrationFormUiHandler extends FormModalUiHandler { return super.getReadableErrorMessage(error); } + override getInputFieldConfigs(): InputFieldConfig[] { + const inputFieldConfigs: InputFieldConfig[] = []; + inputFieldConfigs.push({ label: i18next.t("menu:username") }); + inputFieldConfigs.push({ label: i18next.t("menu:password"), isPassword: true }); + inputFieldConfigs.push({ label: i18next.t("menu:confirmPassword"), isPassword: true }); + return inputFieldConfigs; + } + setup(): void { super.setup(); diff --git a/src/ui/rename-form-ui-handler.ts b/src/ui/rename-form-ui-handler.ts index 078177cafb1f..6e4c4c6809d4 100644 --- a/src/ui/rename-form-ui-handler.ts +++ b/src/ui/rename-form-ui-handler.ts @@ -1,4 +1,4 @@ -import { FormModalUiHandler } from "./form-modal-ui-handler"; +import { FormModalUiHandler, InputFieldConfig } from "./form-modal-ui-handler"; import { ModalConfig } from "./modal-ui-handler"; import i18next from "i18next"; import { PlayerPokemon } from "#app/field/pokemon"; @@ -8,10 +8,6 @@ export default class RenameFormUiHandler extends FormModalUiHandler { return i18next.t("menu:renamePokemon"); } - getFields(config?: ModalConfig): string[] { - return [ i18next.t("menu:nickname") ]; - } - getWidth(config?: ModalConfig): number { return 160; } @@ -33,6 +29,10 @@ export default class RenameFormUiHandler extends FormModalUiHandler { return super.getReadableErrorMessage(error); } + override getInputFieldConfigs(): InputFieldConfig[] { + return [{ label: i18next.t("menu:nickname") }]; + } + show(args: any[]): boolean { if (super.show(args)) { const config = args[0] as ModalConfig; diff --git a/src/ui/test-dialogue-ui-handler.ts b/src/ui/test-dialogue-ui-handler.ts index 34fb80ecc89d..bf0e7f6723f0 100644 --- a/src/ui/test-dialogue-ui-handler.ts +++ b/src/ui/test-dialogue-ui-handler.ts @@ -1,4 +1,4 @@ -import { FormModalUiHandler } from "./form-modal-ui-handler"; +import { FormModalUiHandler, InputFieldConfig } from "./form-modal-ui-handler"; import { ModalConfig } from "./modal-ui-handler"; import i18next from "i18next"; import { PlayerPokemon } from "#app/field/pokemon"; @@ -43,10 +43,6 @@ export default class TestDialogueUiHandler extends FormModalUiHandler { return "Test Dialogue"; } - getFields(config?: ModalConfig): string[] { - return [ "Dialogue" ]; - } - getWidth(config?: ModalConfig): number { return 300; } @@ -68,8 +64,15 @@ export default class TestDialogueUiHandler extends FormModalUiHandler { return super.getReadableErrorMessage(error); } + override getInputFieldConfigs(): InputFieldConfig[] { + return [{ label: "Dialogue" }]; + } + show(args: any[]): boolean { const ui = this.getUi(); + const hasTitle = !!this.getModalTitle(); + this.updateFields(this.getInputFieldConfigs(), hasTitle); + this.updateContainer(args[0] as ModalConfig); const input = this.inputs[0]; input.setMaxLength(255);