diff --git a/README.md b/README.md index 0f9ed992352..b26fd56a7b1 100644 --- a/README.md +++ b/README.md @@ -55,8 +55,8 @@ Check out [Github Issues](https://github.com/pagefaultgames/pokerogue/issues) to - Pokémon Sword/Shield - Pokémon Legends: Arceus - Pokémon Scarlet/Violet - - Firel (Custom Laboratory, Metropolis, Seabed, and Space biome music) - - Lmz (Custom Jungle biome music) + - Firel (Custom Ice Cave, Laboratory, Metropolis, Plains, Power Plant, Seabed, Space, and Volcano biome music) + - Lmz (Custom Ancient Ruins, Jungle, and Lake biome music) - Andr06 (Custom Slum and Sea biome music) ### 🎵 Sound Effects diff --git a/index.css b/index.css index 1274f2fcead..2ec106516d2 100644 --- a/index.css +++ b/index.css @@ -26,10 +26,36 @@ body { #app { display: flex; justify-content: center; + align-items: center; } #app > div:first-child { - transform-origin: top !important; + transform-origin: center !important; +} + +/* + Supports automatic vertical centering as suggested in PR#1114, but only via CSS + + Condition factorized to deduce CSS rules: + true if (isLandscape && !isMobile() && !hasTouchscreen() || (hasTouchscreen() && !isTouchControlsEnabled)) +*/ + +/* isLandscape && !isMobile() && !hasTouchscreen() */ +@media (orientation: landscape) and (pointer: fine) { + #app { + align-items: center; + } +} + +@media (pointer: coarse) { + /* hasTouchscreen() && !isTouchControlsEnabled */ + body:has(> #touchControls[class=visible]) #app { + align-items: start; + } + + body:has(> #touchControls[class=visible]) #app > div:first-child { + transform-origin: top !important; + } } #layout:fullscreen #dpad, #layout:fullscreen { diff --git a/public/audio/bgm/ice_cave.mp3 b/public/audio/bgm/ice_cave.mp3 index 5d1b9e9e354..9d1c7c06bf0 100644 Binary files a/public/audio/bgm/ice_cave.mp3 and b/public/audio/bgm/ice_cave.mp3 differ diff --git a/public/audio/bgm/jungle.mp3 b/public/audio/bgm/jungle.mp3 index 3a21c9bdb41..fbb770b0ad3 100644 Binary files a/public/audio/bgm/jungle.mp3 and b/public/audio/bgm/jungle.mp3 differ diff --git a/public/audio/bgm/laboratory.mp3 b/public/audio/bgm/laboratory.mp3 index e2b617e590a..1eba66ef56f 100644 Binary files a/public/audio/bgm/laboratory.mp3 and b/public/audio/bgm/laboratory.mp3 differ diff --git a/public/audio/bgm/lake.mp3 b/public/audio/bgm/lake.mp3 index c61fef15e42..e6762935770 100644 Binary files a/public/audio/bgm/lake.mp3 and b/public/audio/bgm/lake.mp3 differ diff --git a/public/audio/bgm/metropolis.mp3 b/public/audio/bgm/metropolis.mp3 index 98c2eb396b6..514c9ae15b1 100644 Binary files a/public/audio/bgm/metropolis.mp3 and b/public/audio/bgm/metropolis.mp3 differ diff --git a/public/audio/bgm/mystery_encounter_delibirdy.mp3 b/public/audio/bgm/mystery_encounter_delibirdy.mp3 new file mode 100644 index 00000000000..515a429aaba Binary files /dev/null and b/public/audio/bgm/mystery_encounter_delibirdy.mp3 differ diff --git a/public/audio/bgm/mystery_encounter_fun_and_games.mp3 b/public/audio/bgm/mystery_encounter_fun_and_games.mp3 index a9660d75e90..7864bf7a73d 100644 Binary files a/public/audio/bgm/mystery_encounter_fun_and_games.mp3 and b/public/audio/bgm/mystery_encounter_fun_and_games.mp3 differ diff --git a/public/audio/bgm/mystery_encounter_gen_5_gts.mp3 b/public/audio/bgm/mystery_encounter_gen_5_gts.mp3 index 989a7f9c598..d03c8f8d4d5 100644 Binary files a/public/audio/bgm/mystery_encounter_gen_5_gts.mp3 and b/public/audio/bgm/mystery_encounter_gen_5_gts.mp3 differ diff --git a/public/audio/bgm/mystery_encounter_gen_6_gts.mp3 b/public/audio/bgm/mystery_encounter_gen_6_gts.mp3 index 2c574da66ae..c921a01c204 100644 Binary files a/public/audio/bgm/mystery_encounter_gen_6_gts.mp3 and b/public/audio/bgm/mystery_encounter_gen_6_gts.mp3 differ diff --git a/public/audio/bgm/mystery_encounter_weird_dream.mp3 b/public/audio/bgm/mystery_encounter_weird_dream.mp3 index a630fe549db..433e07bab08 100644 Binary files a/public/audio/bgm/mystery_encounter_weird_dream.mp3 and b/public/audio/bgm/mystery_encounter_weird_dream.mp3 differ diff --git a/public/audio/bgm/plains.mp3 b/public/audio/bgm/plains.mp3 index 6c7a008bce6..ff364600b4a 100644 Binary files a/public/audio/bgm/plains.mp3 and b/public/audio/bgm/plains.mp3 differ diff --git a/public/audio/bgm/power_plant.mp3 b/public/audio/bgm/power_plant.mp3 index 9813ad40a11..152667fcba6 100644 Binary files a/public/audio/bgm/power_plant.mp3 and b/public/audio/bgm/power_plant.mp3 differ diff --git a/public/audio/bgm/ruins.mp3 b/public/audio/bgm/ruins.mp3 index 62f31893423..3692c71562f 100644 Binary files a/public/audio/bgm/ruins.mp3 and b/public/audio/bgm/ruins.mp3 differ diff --git a/public/audio/bgm/volcano.mp3 b/public/audio/bgm/volcano.mp3 index 093bb86813b..a67bbd111de 100644 Binary files a/public/audio/bgm/volcano.mp3 and b/public/audio/bgm/volcano.mp3 differ diff --git a/public/battle-anims/clanging-scales.json b/public/battle-anims/clanging-scales.json index de1a3d5248f..e2135a1a9b4 100644 --- a/public/battle-anims/clanging-scales.json +++ b/public/battle-anims/clanging-scales.json @@ -27,7 +27,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 0, @@ -115,7 +115,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 0, @@ -215,7 +215,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 0, @@ -315,7 +315,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 0, @@ -414,7 +414,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 0, @@ -538,7 +538,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 23, @@ -685,7 +685,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": -19, @@ -784,7 +784,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 26, @@ -883,7 +883,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 23.5, @@ -994,7 +994,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 9, @@ -1069,7 +1069,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": -18.5, @@ -1157,7 +1157,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 37.5, @@ -1221,7 +1221,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 0, @@ -1284,7 +1284,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 0, @@ -1348,7 +1348,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 0, @@ -1448,7 +1448,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 0, @@ -1548,7 +1548,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 0, @@ -1647,7 +1647,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 0, @@ -1759,7 +1759,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": -25.5, @@ -1870,7 +1870,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 12, @@ -1957,7 +1957,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": -27, @@ -2044,7 +2044,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": -16, @@ -2143,7 +2143,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": -26.5, @@ -2230,7 +2230,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 23, @@ -2306,7 +2306,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": 24, @@ -2346,7 +2346,7 @@ "opacity": 255, "locked": true, "priority": 1, - "focus": 2 + "focus": 1 }, { "x": -27, diff --git a/public/images/mystery-encounters/weird_dream_woman.json b/public/images/mystery-encounters/weird_dream_woman.json index 66a9b8d68db..49ebc001d18 100644 --- a/public/images/mystery-encounters/weird_dream_woman.json +++ b/public/images/mystery-encounters/weird_dream_woman.json @@ -5,29 +5,29 @@ "format": "RGBA8888", "size": { "w": 78, - "h": 87 + "h": 86 }, "scale": 1, "frames": [ { "filename": "0001.png", "rotated": false, - "trimmed": true, + "trimmed": false, "sourceSize": { - "w": 80, - "h": 87 + "w": 78, + "h": 86 }, "spriteSourceSize": { - "x": 1, + "x": 0, "y": 0, "w": 78, - "h": 87 + "h": 86 }, "frame": { "x": 0, "y": 0, "w": 78, - "h": 87 + "h": 86 } } ] @@ -36,6 +36,6 @@ "meta": { "app": "https://www.codeandweb.com/texturepacker", "version": "3.0", - "smartupdate": "$TexturePacker:SmartUpdate:d3cce87ee0e3a880d840bffe9373d5d4:7c776d33b75abad1fe36b14a5e5734af:56468b7a2883e66dadcd2af13ebd8010$" + "smartupdate": "$TexturePacker:SmartUpdate:65266da62e9d2953511c0d68ae431345:c1ca63690bed8dd5af71bb443910c830:56468b7a2883e66dadcd2af13ebd8010$" } } diff --git a/public/images/mystery-encounters/weird_dream_woman.png b/public/images/mystery-encounters/weird_dream_woman.png index 1b8d142ed5b..50d04667152 100644 Binary files a/public/images/mystery-encounters/weird_dream_woman.png and b/public/images/mystery-encounters/weird_dream_woman.png differ diff --git a/public/images/statuses_ca_ES.json b/public/images/statuses_ca_ES.json new file mode 100644 index 00000000000..be1b78e0e41 --- /dev/null +++ b/public/images/statuses_ca_ES.json @@ -0,0 +1,188 @@ +{ + "textures": [ + { + "image": "statuses_ca_ES.png", + "format": "RGBA8888", + "size": { + "w": 22, + "h": 64 + }, + "scale": 1, + "frames": [ + { + "filename": "pokerus", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 22, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + }, + "frame": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + } + }, + { + "filename": "burn", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 8, + "w": 20, + "h": 8 + } + }, + { + "filename": "faint", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 16, + "w": 20, + "h": 8 + } + }, + { + "filename": "freeze", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 24, + "w": 20, + "h": 8 + } + }, + { + "filename": "paralysis", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 32, + "w": 20, + "h": 8 + } + }, + { + "filename": "poison", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 40, + "w": 20, + "h": 8 + } + }, + { + "filename": "sleep", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 48, + "w": 20, + "h": 8 + } + }, + { + "filename": "toxic", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 56, + "w": 20, + "h": 8 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:37686e85605d17b806f22d43081c1139:70535ffee63ba61b3397d8470c2c8982:e6649238c018d3630e55681417c698ca$" + } +} diff --git a/public/images/statuses_ca_ES.png b/public/images/statuses_ca_ES.png new file mode 100644 index 00000000000..d372b989be9 Binary files /dev/null and b/public/images/statuses_ca_ES.png differ diff --git a/public/images/statuses_de.json b/public/images/statuses_de.json new file mode 100644 index 00000000000..90840b8eeb1 --- /dev/null +++ b/public/images/statuses_de.json @@ -0,0 +1,188 @@ +{ + "textures": [ + { + "image": "statuses_de.png", + "format": "RGBA8888", + "size": { + "w": 22, + "h": 64 + }, + "scale": 1, + "frames": [ + { + "filename": "pokerus", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 22, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + }, + "frame": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + } + }, + { + "filename": "burn", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 8, + "w": 20, + "h": 8 + } + }, + { + "filename": "faint", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 16, + "w": 20, + "h": 8 + } + }, + { + "filename": "freeze", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 24, + "w": 20, + "h": 8 + } + }, + { + "filename": "paralysis", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 32, + "w": 20, + "h": 8 + } + }, + { + "filename": "poison", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 40, + "w": 20, + "h": 8 + } + }, + { + "filename": "sleep", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 48, + "w": 20, + "h": 8 + } + }, + { + "filename": "toxic", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 56, + "w": 20, + "h": 8 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:37686e85605d17b806f22d43081c1139:70535ffee63ba61b3397d8470c2c8982:e6649238c018d3630e55681417c698ca$" + } +} diff --git a/public/images/statuses_de.png b/public/images/statuses_de.png new file mode 100644 index 00000000000..ab85384d591 Binary files /dev/null and b/public/images/statuses_de.png differ diff --git a/public/images/statuses_es.json b/public/images/statuses_es.json new file mode 100644 index 00000000000..4b44aa117e4 --- /dev/null +++ b/public/images/statuses_es.json @@ -0,0 +1,188 @@ +{ + "textures": [ + { + "image": "statuses_es.png", + "format": "RGBA8888", + "size": { + "w": 22, + "h": 64 + }, + "scale": 1, + "frames": [ + { + "filename": "pokerus", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 22, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + }, + "frame": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + } + }, + { + "filename": "burn", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 8, + "w": 20, + "h": 8 + } + }, + { + "filename": "faint", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 16, + "w": 20, + "h": 8 + } + }, + { + "filename": "freeze", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 24, + "w": 20, + "h": 8 + } + }, + { + "filename": "paralysis", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 32, + "w": 20, + "h": 8 + } + }, + { + "filename": "poison", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 40, + "w": 20, + "h": 8 + } + }, + { + "filename": "sleep", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 48, + "w": 20, + "h": 8 + } + }, + { + "filename": "toxic", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 56, + "w": 20, + "h": 8 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:37686e85605d17b806f22d43081c1139:70535ffee63ba61b3397d8470c2c8982:e6649238c018d3630e55681417c698ca$" + } +} diff --git a/public/images/statuses_es.png b/public/images/statuses_es.png new file mode 100644 index 00000000000..d372b989be9 Binary files /dev/null and b/public/images/statuses_es.png differ diff --git a/public/images/statuses_fr.json b/public/images/statuses_fr.json new file mode 100644 index 00000000000..78f78a0856c --- /dev/null +++ b/public/images/statuses_fr.json @@ -0,0 +1,188 @@ +{ + "textures": [ + { + "image": "statuses_fr.png", + "format": "RGBA8888", + "size": { + "w": 22, + "h": 64 + }, + "scale": 1, + "frames": [ + { + "filename": "pokerus", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 22, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + }, + "frame": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + } + }, + { + "filename": "burn", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 8, + "w": 20, + "h": 8 + } + }, + { + "filename": "faint", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 16, + "w": 20, + "h": 8 + } + }, + { + "filename": "freeze", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 24, + "w": 20, + "h": 8 + } + }, + { + "filename": "paralysis", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 32, + "w": 20, + "h": 8 + } + }, + { + "filename": "poison", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 40, + "w": 20, + "h": 8 + } + }, + { + "filename": "sleep", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 48, + "w": 20, + "h": 8 + } + }, + { + "filename": "toxic", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 56, + "w": 20, + "h": 8 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:37686e85605d17b806f22d43081c1139:70535ffee63ba61b3397d8470c2c8982:e6649238c018d3630e55681417c698ca$" + } +} diff --git a/public/images/statuses_fr.png b/public/images/statuses_fr.png new file mode 100644 index 00000000000..95989cd5d97 Binary files /dev/null and b/public/images/statuses_fr.png differ diff --git a/public/images/statuses_it.json b/public/images/statuses_it.json new file mode 100644 index 00000000000..76fc9ae8b4b --- /dev/null +++ b/public/images/statuses_it.json @@ -0,0 +1,188 @@ +{ + "textures": [ + { + "image": "statuses_it.png", + "format": "RGBA8888", + "size": { + "w": 22, + "h": 64 + }, + "scale": 1, + "frames": [ + { + "filename": "pokerus", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 22, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + }, + "frame": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + } + }, + { + "filename": "burn", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 8, + "w": 20, + "h": 8 + } + }, + { + "filename": "faint", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 16, + "w": 20, + "h": 8 + } + }, + { + "filename": "freeze", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 24, + "w": 20, + "h": 8 + } + }, + { + "filename": "paralysis", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 32, + "w": 20, + "h": 8 + } + }, + { + "filename": "poison", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 40, + "w": 20, + "h": 8 + } + }, + { + "filename": "sleep", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 48, + "w": 20, + "h": 8 + } + }, + { + "filename": "toxic", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 56, + "w": 20, + "h": 8 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:37686e85605d17b806f22d43081c1139:70535ffee63ba61b3397d8470c2c8982:e6649238c018d3630e55681417c698ca$" + } +} diff --git a/public/images/statuses_it.png b/public/images/statuses_it.png new file mode 100644 index 00000000000..d372b989be9 Binary files /dev/null and b/public/images/statuses_it.png differ diff --git a/public/images/statuses_ja.json b/public/images/statuses_ja.json new file mode 100644 index 00000000000..8de633e8e43 --- /dev/null +++ b/public/images/statuses_ja.json @@ -0,0 +1,188 @@ +{ + "textures": [ + { + "image": "statuses_ja.png", + "format": "RGBA8888", + "size": { + "w": 22, + "h": 64 + }, + "scale": 1, + "frames": [ + { + "filename": "pokerus", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 22, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + }, + "frame": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + } + }, + { + "filename": "burn", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 8, + "w": 20, + "h": 8 + } + }, + { + "filename": "faint", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 16, + "w": 20, + "h": 8 + } + }, + { + "filename": "freeze", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 24, + "w": 20, + "h": 8 + } + }, + { + "filename": "paralysis", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 32, + "w": 20, + "h": 8 + } + }, + { + "filename": "poison", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 40, + "w": 20, + "h": 8 + } + }, + { + "filename": "sleep", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 48, + "w": 20, + "h": 8 + } + }, + { + "filename": "toxic", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 56, + "w": 20, + "h": 8 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:37686e85605d17b806f22d43081c1139:70535ffee63ba61b3397d8470c2c8982:e6649238c018d3630e55681417c698ca$" + } +} diff --git a/public/images/statuses_ja.png b/public/images/statuses_ja.png new file mode 100644 index 00000000000..305fbe9168c Binary files /dev/null and b/public/images/statuses_ja.png differ diff --git a/public/images/statuses_ko.json b/public/images/statuses_ko.json new file mode 100644 index 00000000000..7e1e2dd6cda --- /dev/null +++ b/public/images/statuses_ko.json @@ -0,0 +1,188 @@ +{ + "textures": [ + { + "image": "statuses_ko.png", + "format": "RGBA8888", + "size": { + "w": 22, + "h": 64 + }, + "scale": 1, + "frames": [ + { + "filename": "pokerus", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 22, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + }, + "frame": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + } + }, + { + "filename": "burn", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 8, + "w": 20, + "h": 8 + } + }, + { + "filename": "faint", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 16, + "w": 20, + "h": 8 + } + }, + { + "filename": "freeze", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 24, + "w": 20, + "h": 8 + } + }, + { + "filename": "paralysis", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 32, + "w": 20, + "h": 8 + } + }, + { + "filename": "poison", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 40, + "w": 20, + "h": 8 + } + }, + { + "filename": "sleep", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 48, + "w": 20, + "h": 8 + } + }, + { + "filename": "toxic", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 56, + "w": 20, + "h": 8 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:37686e85605d17b806f22d43081c1139:70535ffee63ba61b3397d8470c2c8982:e6649238c018d3630e55681417c698ca$" + } +} diff --git a/public/images/statuses_ko.png b/public/images/statuses_ko.png new file mode 100644 index 00000000000..d372b989be9 Binary files /dev/null and b/public/images/statuses_ko.png differ diff --git a/public/images/statuses_pt_BR.json b/public/images/statuses_pt_BR.json new file mode 100644 index 00000000000..b607991af8f --- /dev/null +++ b/public/images/statuses_pt_BR.json @@ -0,0 +1,188 @@ +{ + "textures": [ + { + "image": "statuses_pt_BR.png", + "format": "RGBA8888", + "size": { + "w": 22, + "h": 64 + }, + "scale": 1, + "frames": [ + { + "filename": "pokerus", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 22, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + }, + "frame": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + } + }, + { + "filename": "burn", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 8, + "w": 20, + "h": 8 + } + }, + { + "filename": "faint", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 16, + "w": 20, + "h": 8 + } + }, + { + "filename": "freeze", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 24, + "w": 20, + "h": 8 + } + }, + { + "filename": "paralysis", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 32, + "w": 20, + "h": 8 + } + }, + { + "filename": "poison", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 40, + "w": 20, + "h": 8 + } + }, + { + "filename": "sleep", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 48, + "w": 20, + "h": 8 + } + }, + { + "filename": "toxic", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 56, + "w": 20, + "h": 8 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:37686e85605d17b806f22d43081c1139:70535ffee63ba61b3397d8470c2c8982:e6649238c018d3630e55681417c698ca$" + } +} diff --git a/public/images/statuses_pt_BR.png b/public/images/statuses_pt_BR.png new file mode 100644 index 00000000000..3073540e8a2 Binary files /dev/null and b/public/images/statuses_pt_BR.png differ diff --git a/public/images/statuses_zh_CN.json b/public/images/statuses_zh_CN.json new file mode 100644 index 00000000000..28760650ecd --- /dev/null +++ b/public/images/statuses_zh_CN.json @@ -0,0 +1,188 @@ +{ + "textures": [ + { + "image": "statuses_zh_CN.png", + "format": "RGBA8888", + "size": { + "w": 22, + "h": 64 + }, + "scale": 1, + "frames": [ + { + "filename": "pokerus", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 22, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + }, + "frame": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + } + }, + { + "filename": "burn", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 8, + "w": 20, + "h": 8 + } + }, + { + "filename": "faint", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 16, + "w": 20, + "h": 8 + } + }, + { + "filename": "freeze", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 24, + "w": 20, + "h": 8 + } + }, + { + "filename": "paralysis", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 32, + "w": 20, + "h": 8 + } + }, + { + "filename": "poison", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 40, + "w": 20, + "h": 8 + } + }, + { + "filename": "sleep", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 48, + "w": 20, + "h": 8 + } + }, + { + "filename": "toxic", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 56, + "w": 20, + "h": 8 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:37686e85605d17b806f22d43081c1139:70535ffee63ba61b3397d8470c2c8982:e6649238c018d3630e55681417c698ca$" + } +} diff --git a/public/images/statuses_zh_CN.png b/public/images/statuses_zh_CN.png new file mode 100644 index 00000000000..d372b989be9 Binary files /dev/null and b/public/images/statuses_zh_CN.png differ diff --git a/public/images/statuses_zh_TW.json b/public/images/statuses_zh_TW.json new file mode 100644 index 00000000000..bf05b2ab0d5 --- /dev/null +++ b/public/images/statuses_zh_TW.json @@ -0,0 +1,188 @@ +{ + "textures": [ + { + "image": "statuses.png", + "format": "RGBA8888", + "size": { + "w": 22, + "h": 64 + }, + "scale": 1, + "frames": [ + { + "filename": "pokerus", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 22, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + }, + "frame": { + "x": 0, + "y": 0, + "w": 22, + "h": 8 + } + }, + { + "filename": "burn", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 8, + "w": 20, + "h": 8 + } + }, + { + "filename": "faint", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 16, + "w": 20, + "h": 8 + } + }, + { + "filename": "freeze", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 24, + "w": 20, + "h": 8 + } + }, + { + "filename": "paralysis", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 32, + "w": 20, + "h": 8 + } + }, + { + "filename": "poison", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 40, + "w": 20, + "h": 8 + } + }, + { + "filename": "sleep", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 48, + "w": 20, + "h": 8 + } + }, + { + "filename": "toxic", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 56, + "w": 20, + "h": 8 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:37686e85605d17b806f22d43081c1139:70535ffee63ba61b3397d8470c2c8982:e6649238c018d3630e55681417c698ca$" + } +} diff --git a/public/images/statuses_zh_TW.png b/public/images/statuses_zh_TW.png new file mode 100644 index 00000000000..d372b989be9 Binary files /dev/null and b/public/images/statuses_zh_TW.png differ diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 516662617f1..753efdaf62c 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -1911,6 +1911,19 @@ export default class BattleScene extends SceneBase { return false; } + /** + * Fades out current track for `delay` ms, then fades in new track. + * @param newBgmKey + * @param destroy + * @param delay + */ + fadeAndSwitchBgm(newBgmKey: string, destroy: boolean = false, delay: number = 2000) { + this.fadeOutBgm(delay, destroy); + this.time.delayedCall(delay, () => { + this.playBgm(newBgmKey); + }); + } + playSound(sound: string | AnySound, config?: object): AnySound { const key = typeof sound === "string" ? sound : sound.key; config = config ?? {}; @@ -2157,6 +2170,16 @@ export default class BattleScene extends SceneBase { return 13.13; case "battle_macro_boss": //SWSH Rose Battle return 11.42; + case "mystery_encounter_gen_5_gts": // BW GTS + return 8.52; + case "mystery_encounter_gen_6_gts": // XY GTS + return 9.24; + case "mystery_encounter_fun_and_games": // EoS Guildmaster Wigglytuff + return 4.78; + case "mystery_encounter_weird_dream": // EoS Temporal Spire + return 41.42; + case "mystery_encounter_delibirdy": // Firel Delibirdy + return 82.28; } return 0; @@ -2603,7 +2626,7 @@ export default class BattleScene extends SceneBase { } party.forEach((enemyPokemon: EnemyPokemon, i: integer) => { - if (heldModifiersConfigs && i < heldModifiersConfigs.length && heldModifiersConfigs[i] && heldModifiersConfigs[i].length > 0) { + if (heldModifiersConfigs && i < heldModifiersConfigs.length && heldModifiersConfigs[i]) { heldModifiersConfigs[i].forEach(mt => { let modifier: PokemonHeldItemModifier; if (mt.modifier instanceof PokemonHeldItemModifierType) { @@ -2614,8 +2637,7 @@ export default class BattleScene extends SceneBase { } const stackCount = mt.stackCount ?? 1; modifier.stackCount = stackCount; - // TODO: set isTransferable - // modifier.isTransferrable = mt.isTransferable ?? true; + modifier.isTransferable = mt.isTransferable ?? modifier.isTransferable; this.addEnemyModifier(modifier, true); }); } else { diff --git a/src/battle.ts b/src/battle.ts index a886a0eb771..b857314b4bf 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -14,7 +14,7 @@ import { PlayerGender } from "#enums/player-gender"; import { Species } from "#enums/species"; import { TrainerType } from "#enums/trainer-type"; import i18next from "#app/plugins/i18n"; -import MysteryEncounter from "./data/mystery-encounters/mystery-encounter"; +import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; export enum BattleType { @@ -157,7 +157,7 @@ export default class Battle { } addPostBattleLoot(enemyPokemon: EnemyPokemon): void { - this.postBattleLoot.push(...enemyPokemon.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && m.pokemonId === enemyPokemon.id && m.isTransferrable, false).map(i => { + this.postBattleLoot.push(...enemyPokemon.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && m.pokemonId === enemyPokemon.id && m.isTransferable, false).map(i => { const ret = i as PokemonHeldItemModifier; //@ts-ignore - this is awful to fix/change ret.pokemonId = null; diff --git a/src/data/ability.ts b/src/data/ability.ts index 944ee10244a..58233e9839e 100755 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1670,7 +1670,7 @@ export class PostAttackStealHeldItemAbAttr extends PostAttackAbAttr { applyPostAttackAfterMoveTypeCheck(pokemon: Pokemon, passive: boolean, simulated: boolean, defender: Pokemon, move: Move, hitResult: HitResult, args: any[]): Promise { return new Promise(resolve => { if (!simulated && hitResult < HitResult.NO_EFFECT && (!this.stealCondition || this.stealCondition(pokemon, defender, move))) { - const heldItems = this.getTargetHeldItems(defender).filter(i => i.isTransferrable); + const heldItems = this.getTargetHeldItems(defender).filter(i => i.isTransferable); if (heldItems.length) { const stolenItem = heldItems[pokemon.randSeedInt(heldItems.length)]; pokemon.scene.tryTransferHeldItemModifier(stolenItem, pokemon, false).then(success => { @@ -1763,7 +1763,7 @@ export class PostDefendStealHeldItemAbAttr extends PostDefendAbAttr { applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): Promise { return new Promise(resolve => { if (!simulated && hitResult < HitResult.NO_EFFECT && (!this.condition || this.condition(pokemon, attacker, move))) { - const heldItems = this.getTargetHeldItems(attacker).filter(i => i.isTransferrable); + const heldItems = this.getTargetHeldItems(attacker).filter(i => i.isTransferable); if (heldItems.length) { const stolenItem = heldItems[pokemon.randSeedInt(heldItems.length)]; pokemon.scene.tryTransferHeldItemModifier(stolenItem, pokemon, false).then(success => { @@ -2625,7 +2625,11 @@ export class PreStatStageChangeAbAttr extends AbAttr { } } +/** + * Protect one or all {@linkcode BattleStat} from reductions caused by other Pokémon's moves and Abilities + */ export class ProtectStatAbAttr extends PreStatStageChangeAbAttr { + /** {@linkcode BattleStat} to protect or `undefined` if **all** {@linkcode BattleStat} are protected */ private protectedStat?: BattleStat; constructor(protectedStat?: BattleStat) { @@ -2634,7 +2638,17 @@ export class ProtectStatAbAttr extends PreStatStageChangeAbAttr { this.protectedStat = protectedStat; } - applyPreStatStageChange(_pokemon: Pokemon, _passive: boolean, simulated: boolean, stat: BattleStat, cancelled: Utils.BooleanHolder, _args: any[]): boolean { + /** + * Apply the {@linkcode ProtectedStatAbAttr} to an interaction + * @param _pokemon + * @param _passive + * @param simulated + * @param stat the {@linkcode BattleStat} being affected + * @param cancelled The {@linkcode Utils.BooleanHolder} that will be set to true if the stat is protected + * @param _args + * @returns true if the stat is protected, false otherwise + */ + applyPreStatStageChange(_pokemon: Pokemon, _passive: boolean, _simulated: boolean, stat: BattleStat, cancelled: Utils.BooleanHolder, _args: any[]): boolean { if (Utils.isNullOrUndefined(this.protectedStat) || stat === this.protectedStat) { cancelled.value = true; return true; @@ -3757,7 +3771,7 @@ export class StatStageChangeMultiplierAbAttr extends AbAttr { this.multiplier = multiplier; } - apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { + override apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { (args[0] as Utils.IntegerHolder).value *= this.multiplier; return true; diff --git a/src/data/battle-anims.ts b/src/data/battle-anims.ts index f07d6cb2409..d972e48df7c 100644 --- a/src/data/battle-anims.ts +++ b/src/data/battle-anims.ts @@ -488,14 +488,14 @@ export function initMoveAnim(scene: BattleScene, move: Moves): Promise { } else { moveAnims.set(move, null); const defaultMoveAnim = allMoves[move] instanceof AttackMove ? Moves.TACKLE : allMoves[move] instanceof SelfStatusMove ? Moves.FOCUS_ENERGY : Moves.TAIL_WHIP; - const moveName = Moves[move].toLowerCase().replace(/\_/g, "-"); + const fetchAnimAndResolve = (move: Moves) => { - scene.cachedFetch(`./battle-anims/${moveName}.json`) + scene.cachedFetch(`./battle-anims/${Utils.animationFileName(move)}.json`) .then(response => { const contentType = response.headers.get("content-type"); if (!response.ok || contentType?.indexOf("application/json") === -1) { - console.error(`Could not load animation file for move '${moveName}'`, response.status, response.statusText); - populateMoveAnim(move, moveAnims.get(defaultMoveAnim)); + useDefaultAnim(move, defaultMoveAnim); + logMissingMoveAnim(move, response.status, response.statusText); return resolve(); } return response.json(); @@ -515,6 +515,11 @@ export function initMoveAnim(scene: BattleScene, move: Moves): Promise { } else { resolve(); } + }) + .catch(error => { + useDefaultAnim(move, defaultMoveAnim); + logMissingMoveAnim(move, error); + return resolve(); }); }; fetchAnimAndResolve(move); @@ -522,6 +527,29 @@ export function initMoveAnim(scene: BattleScene, move: Moves): Promise { }); } +/** + * Populates the default animation for the given move. + * + * @param move the move to populate an animation for + * @param defaultMoveAnim the move to use as the default animation + */ +function useDefaultAnim(move: Moves, defaultMoveAnim: Moves) { + populateMoveAnim(move, moveAnims.get(defaultMoveAnim)); +} + +/** + * Helper method for printing a warning to the console when a move animation is missing. + * + * @param move the move to populate an animation for + * @param optionalParams parameters to add to the error logging + * + * @remarks use {@linkcode useDefaultAnim} to use a default animation + */ +function logMissingMoveAnim(move: Moves, ...optionalParams: any[]) { + const moveName = Utils.animationFileName(move); + console.warn(`Could not load animation file for move '${moveName}'`, ...optionalParams); +} + /** * Fetches animation configs to be used in a Mystery Encounter * @param scene diff --git a/src/data/egg.ts b/src/data/egg.ts index 0219f4f5b47..b37240a2028 100644 --- a/src/data/egg.ts +++ b/src/data/egg.ts @@ -1,6 +1,6 @@ import BattleScene from "../battle-scene"; import PokemonSpecies, { getPokemonSpecies, speciesStarters } from "./pokemon-species"; -import { VariantTier } from "../enums/variant-tiers"; +import { VariantTier } from "../enums/variant-tier"; import * as Utils from "../utils"; import Overrides from "#app/overrides"; import { pokemonPrevolutions } from "./pokemon-evolutions"; @@ -178,7 +178,7 @@ export class Egg { // be done because species with no variants get filtered at rollSpecies but if the // species is set via options or the legendary gacha pokemon gets choosen the check never happens if (this._species && !getPokemonSpecies(this._species).hasVariants()) { - this._variantTier = VariantTier.COMMON; + this._variantTier = VariantTier.STANDARD; } // Needs this._tier so it needs to be generated afer the tier override if bought from same species this._eggMoveIndex = eggOptions?.eggMoveIndex ?? this.rollEggMoveIndex(); @@ -494,12 +494,12 @@ export class Egg { // place but I don't want to touch the pokemon class. private rollVariant(): VariantTier { if (!this.isShiny) { - return VariantTier.COMMON; + return VariantTier.STANDARD; } const rand = Utils.randSeedInt(10); if (rand >= 4) { - return VariantTier.COMMON; // 6/10 + return VariantTier.STANDARD; // 6/10 } else if (rand >= 1) { return VariantTier.RARE; // 3/10 } else { diff --git a/src/data/move.ts b/src/data/move.ts index 10c98e0a7f7..740bde75508 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -1,16 +1,15 @@ -import { ChargeAnim, MoveChargeAnim, initMoveAnim, loadMoveAnimAssets } from "./battle-anims"; -import { EncoreTag, GulpMissileTag, HelpingHandTag, SemiInvulnerableTag, ShellTrapTag, StockpilingTag, TrappedTag, SubstituteTag, TypeBoostTag } from "./battler-tags"; +import { ChargeAnim, initMoveAnim, loadMoveAnimAssets, MoveChargeAnim } from "./battle-anims"; +import { EncoreTag, GulpMissileTag, HelpingHandTag, SemiInvulnerableTag, ShellTrapTag, StockpilingTag, SubstituteTag, TrappedTag, TypeBoostTag } from "./battler-tags"; import { getPokemonNameWithAffix } from "../messages"; import Pokemon, { AttackMoveResult, EnemyPokemon, HitResult, MoveResult, PlayerPokemon, PokemonMove, TurnMove } from "../field/pokemon"; -import { StatusEffect, getStatusEffectHealText, isNonVolatileStatusEffect, getNonVolatileStatusEffects } from "./status-effect"; +import { getNonVolatileStatusEffects, getStatusEffectHealText, isNonVolatileStatusEffect, StatusEffect } from "./status-effect"; import { getTypeDamageMultiplier, Type } from "./type"; -import { Constructor } from "#app/utils"; +import { Constructor, NumberHolder } from "#app/utils"; import * as Utils from "../utils"; import { WeatherType } from "./weather"; import { ArenaTagSide, ArenaTrapTag, WeakenMoveTypeTag } from "./arena-tag"; -import { UnswappableAbilityAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, BlockRecoilDamageAttr, BlockOneHitKOAbAttr, IgnoreContactAbAttr, MaxMultiHitAbAttr, applyAbAttrs, BlockNonDirectDamageAbAttr, MoveAbilityBypassAbAttr, ReverseDrainAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, BlockItemTheftAbAttr, applyPostAttackAbAttrs, ConfusionOnStatusEffectAbAttr, HealFromBerryUseAbAttr, IgnoreProtectOnContactAbAttr, IgnoreMoveEffectsAbAttr, applyPreDefendAbAttrs, MoveEffectChanceMultiplierAbAttr, WonderSkinAbAttr, applyPreAttackAbAttrs, MoveTypeChangeAbAttr, UserFieldMoveTypePowerBoostAbAttr, FieldMoveTypePowerBoostAbAttr, AllyMoveCategoryPowerBoostAbAttr, VariableMovePowerAbAttr } from "./ability"; -import { allAbilities } from "./ability"; -import { PokemonHeldItemModifier, BerryModifier, PreserveBerryModifier, PokemonMoveAccuracyBoosterModifier, AttackTypeBoosterModifier, PokemonMultiHitModifier } from "../modifier/modifier"; +import { allAbilities, AllyMoveCategoryPowerBoostAbAttr, applyAbAttrs, applyPostAttackAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, BlockItemTheftAbAttr, BlockNonDirectDamageAbAttr, BlockOneHitKOAbAttr, BlockRecoilDamageAttr, ConfusionOnStatusEffectAbAttr, FieldMoveTypePowerBoostAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, HealFromBerryUseAbAttr, IgnoreContactAbAttr, IgnoreMoveEffectsAbAttr, IgnoreProtectOnContactAbAttr, MaxMultiHitAbAttr, MoveAbilityBypassAbAttr, MoveEffectChanceMultiplierAbAttr, MoveTypeChangeAbAttr, ReverseDrainAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, UnswappableAbilityAbAttr, UserFieldMoveTypePowerBoostAbAttr, VariableMovePowerAbAttr, WonderSkinAbAttr } from "./ability"; +import { AttackTypeBoosterModifier, BerryModifier, PokemonHeldItemModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PreserveBerryModifier } from "../modifier/modifier"; import { BattlerIndex, BattleType } from "../battle"; import { TerrainType } from "./terrain"; import { ModifierPoolType } from "#app/modifier/modifier-type"; @@ -25,7 +24,7 @@ import { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import { MoveUsedEvent } from "#app/events/battle-scene"; -import { Stat, type BattleStat, type EffectiveStat, BATTLE_STATS, EFFECTIVE_STATS, getStatKey } from "#app/enums/stat"; +import { BATTLE_STATS, type BattleStat, EFFECTIVE_STATS, type EffectiveStat, getStatKey, Stat } from "#app/enums/stat"; import { PartyStatusCurePhase } from "#app/phases/party-status-cure-phase"; import { BattleEndPhase } from "#app/phases/battle-end-phase"; import { MoveEndPhase } from "#app/phases/move-end-phase"; @@ -36,7 +35,6 @@ import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { SwitchPhase } from "#app/phases/switch-phase"; import { SwitchSummonPhase } from "#app/phases/switch-summon-phase"; import { SpeciesFormChangeRevertWeatherFormTrigger } from "./pokemon-forms"; -import { NumberHolder } from "#app/utils"; import { GameMode } from "#app/game-mode"; import { applyChallenges, ChallengeType } from "./challenge"; @@ -2136,7 +2134,7 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr { if (rand >= this.chance) { return resolve(false); } - const heldItems = this.getTargetHeldItems(target).filter(i => i.isTransferrable); + const heldItems = this.getTargetHeldItems(target).filter(i => i.isTransferable); if (heldItems.length) { const poolType = target.isPlayer() ? ModifierPoolType.PLAYER : target.hasTrainer() ? ModifierPoolType.TRAINER : ModifierPoolType.WILD; const highestItemTier = heldItems.map(m => m.type.getOrInferTier(poolType)).reduce((highestTier, tier) => Math.max(tier!, highestTier), 0); // TODO: is the bang after tier correct? @@ -2213,7 +2211,7 @@ export class RemoveHeldItemAttr extends MoveEffectAttr { } // Considers entire transferrable item pool by default (Knock Off). Otherwise berries only if specified (Incinerate). - let heldItems = this.getTargetHeldItems(target).filter(i => i.isTransferrable); + let heldItems = this.getTargetHeldItems(target).filter(i => i.isTransferable); if (this.berriesOnly) { heldItems = heldItems.filter(m => m instanceof BerryModifier && m.pokemonId === target.id, target.isPlayer()); @@ -2417,6 +2415,16 @@ export class BypassSleepAttr extends MoveAttr { return false; } + + /** + * Returns arbitrarily high score when Pokemon is asleep, otherwise shouldn't be used + * @param user + * @param target + * @param move + */ + getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { + return user.status && user.status.effect === StatusEffect.SLEEP ? 200 : -10; + } } /** @@ -3839,7 +3847,7 @@ export class StormAccuracyAttr extends VariableAccuracyAttr { * @extends VariableAccuracyAttr * @see {@linkcode apply} */ -export class MinimizeAccuracyAttr extends VariableAccuracyAttr { +export class AlwaysHitMinimizeAttr extends VariableAccuracyAttr { /** * @see {@linkcode apply} * @param user N/A @@ -3974,18 +3982,17 @@ export class StatusCategoryOnAllyAttr extends VariableMoveCategoryAttr { export class ShellSideArmCategoryAttr extends VariableMoveCategoryAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const category = (args[0] as Utils.NumberHolder); - const atkRatio = user.getEffectiveStat(Stat.ATK, target, move) / target.getEffectiveStat(Stat.DEF, user, move); - const specialRatio = user.getEffectiveStat(Stat.SPATK, target, move) / target.getEffectiveStat(Stat.SPDEF, user, move); - // Shell Side Arm is much more complicated than it looks, this is a partial implementation to try to achieve something similar to the games - if (atkRatio > specialRatio) { + const predictedPhysDmg = target.getBaseDamage(user, move, MoveCategory.PHYSICAL, true, true); + const predictedSpecDmg = target.getBaseDamage(user, move, MoveCategory.SPECIAL, true, true); + + if (predictedPhysDmg > predictedSpecDmg) { category.value = MoveCategory.PHYSICAL; return true; - } else if (atkRatio === specialRatio && user.randSeedInt(2) === 0) { + } else if (predictedPhysDmg === predictedSpecDmg && user.randSeedInt(2) === 0) { category.value = MoveCategory.PHYSICAL; return true; } - return false; } } @@ -4855,10 +4862,10 @@ export class RemoveAllSubstitutesAttr extends MoveEffectAttr { * Attribute used when a move hits a {@linkcode BattlerTagType} for double damage * @extends MoveAttr */ -export class HitsTagAttr extends MoveAttr { +export class DealsDoubleDamageToTagAttr extends MoveAttr { /** The {@linkcode BattlerTagType} this move hits */ public tagType: BattlerTagType; - /** Should this move deal double damage against {@linkcode HitsTagAttr.tagType}? */ + /** Should this move deal double damage against {@linkcode DealsDoubleDamageToTagAttr.tagType}? */ public doubleDamage: boolean; constructor(tagType: BattlerTagType, doubleDamage?: boolean) { @@ -6393,7 +6400,7 @@ export class AttackedByItemAttr extends MoveAttr { */ getCondition(): MoveConditionFunc { return (user: Pokemon, target: Pokemon, move: Move) => { - const heldItems = target.getHeldItems().filter(i => i.isTransferrable); + const heldItems = target.getHeldItems().filter(i => i.isTransferable); if (heldItems.length === 0) { return false; } @@ -6752,12 +6759,11 @@ export function initMoves() { new AttackMove(Moves.CUT, Type.NORMAL, MoveCategory.PHYSICAL, 50, 95, 30, -1, 0, 1) .slicingMove(), new AttackMove(Moves.GUST, Type.FLYING, MoveCategory.SPECIAL, 40, 100, 35, -1, 0, 1) - .attr(HitsTagAttr, BattlerTagType.FLYING, true) + .attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, true) .windMove(), new AttackMove(Moves.WING_ATTACK, Type.FLYING, MoveCategory.PHYSICAL, 60, 100, 35, -1, 0, 1), new StatusMove(Moves.WHIRLWIND, Type.NORMAL, -1, 20, -1, -6, 1) .attr(ForceSwitchOutAttr) - .attr(HitsTagAttr, BattlerTagType.FLYING, false) .ignoresSubstitute() .hidesTarget() .windMove(), @@ -6770,8 +6776,8 @@ export function initMoves() { new AttackMove(Moves.SLAM, Type.NORMAL, MoveCategory.PHYSICAL, 80, 75, 20, -1, 0, 1), new AttackMove(Moves.VINE_WHIP, Type.GRASS, MoveCategory.PHYSICAL, 45, 100, 25, -1, 0, 1), new AttackMove(Moves.STOMP, Type.NORMAL, MoveCategory.PHYSICAL, 65, 100, 20, 30, 0, 1) - .attr(MinimizeAccuracyAttr) - .attr(HitsTagAttr, BattlerTagType.MINIMIZED, true) + .attr(AlwaysHitMinimizeAttr) + .attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true) .attr(FlinchAttr), new AttackMove(Moves.DOUBLE_KICK, Type.FIGHTING, MoveCategory.PHYSICAL, 30, 100, 30, -1, 0, 1) .attr(MultiHitAttr, MultiHitType._2), @@ -6795,8 +6801,8 @@ export function initMoves() { .attr(OneHitKOAccuracyAttr), new AttackMove(Moves.TACKLE, Type.NORMAL, MoveCategory.PHYSICAL, 40, 100, 35, -1, 0, 1), new AttackMove(Moves.BODY_SLAM, Type.NORMAL, MoveCategory.PHYSICAL, 85, 100, 15, 30, 0, 1) - .attr(MinimizeAccuracyAttr) - .attr(HitsTagAttr, BattlerTagType.MINIMIZED, true) + .attr(AlwaysHitMinimizeAttr) + .attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true) .attr(StatusEffectAttr, StatusEffect.PARALYSIS), new AttackMove(Moves.WRAP, Type.NORMAL, MoveCategory.PHYSICAL, 15, 90, 20, -1, 0, 1) .attr(TrapAttr, BattlerTagType.WRAP), @@ -6864,7 +6870,7 @@ export function initMoves() { new AttackMove(Moves.HYDRO_PUMP, Type.WATER, MoveCategory.SPECIAL, 110, 80, 5, -1, 0, 1), new AttackMove(Moves.SURF, Type.WATER, MoveCategory.SPECIAL, 90, 100, 15, -1, 0, 1) .target(MoveTarget.ALL_NEAR_OTHERS) - .attr(HitsTagAttr, BattlerTagType.UNDERWATER, true) + .attr(DealsDoubleDamageToTagAttr, BattlerTagType.UNDERWATER, true) .attr(GulpMissileTagAttr), new AttackMove(Moves.ICE_BEAM, Type.ICE, MoveCategory.SPECIAL, 90, 100, 10, 10, 0, 1) .attr(StatusEffectAttr, StatusEffect.FREEZE), @@ -6947,18 +6953,18 @@ export function initMoves() { new AttackMove(Moves.THUNDER, Type.ELECTRIC, MoveCategory.SPECIAL, 110, 70, 10, 30, 0, 1) .attr(StatusEffectAttr, StatusEffect.PARALYSIS) .attr(ThunderAccuracyAttr) - .attr(HitsTagAttr, BattlerTagType.FLYING, false), + .attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, false), new AttackMove(Moves.ROCK_THROW, Type.ROCK, MoveCategory.PHYSICAL, 50, 90, 15, -1, 0, 1) .makesContact(false), new AttackMove(Moves.EARTHQUAKE, Type.GROUND, MoveCategory.PHYSICAL, 100, 100, 10, -1, 0, 1) - .attr(HitsTagAttr, BattlerTagType.UNDERGROUND, true) - .attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.GRASSY ? 0.5 : 1) + .attr(DealsDoubleDamageToTagAttr, BattlerTagType.UNDERGROUND, true) + .attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.GRASSY && target.isGrounded() ? 0.5 : 1) .makesContact(false) .target(MoveTarget.ALL_NEAR_OTHERS), new AttackMove(Moves.FISSURE, Type.GROUND, MoveCategory.PHYSICAL, 200, 30, 5, -1, 0, 1) .attr(OneHitKOAttr) .attr(OneHitKOAccuracyAttr) - .attr(HitsTagAttr, BattlerTagType.UNDERGROUND, false) + .attr(DealsDoubleDamageToTagAttr, BattlerTagType.UNDERGROUND, false) .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) @@ -7346,8 +7352,8 @@ export function initMoves() { new AttackMove(Moves.MAGNITUDE, Type.GROUND, MoveCategory.PHYSICAL, -1, 100, 30, -1, 0, 2) .attr(PreMoveMessageAttr, magnitudeMessageFunc) .attr(MagnitudePowerAttr) - .attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.GRASSY ? 0.5 : 1) - .attr(HitsTagAttr, BattlerTagType.UNDERGROUND, true) + .attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.GRASSY && target.isGrounded() ? 0.5 : 1) + .attr(DealsDoubleDamageToTagAttr, BattlerTagType.UNDERGROUND, true) .makesContact(false) .target(MoveTarget.ALL_NEAR_OTHERS), new AttackMove(Moves.DYNAMIC_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 50, 5, 100, 0, 2) @@ -7403,7 +7409,7 @@ export function initMoves() { new AttackMove(Moves.CROSS_CHOP, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 80, 5, -1, 0, 2) .attr(HighCritAttr), new AttackMove(Moves.TWISTER, Type.DRAGON, MoveCategory.SPECIAL, 40, 100, 20, 20, 0, 2) - .attr(HitsTagAttr, BattlerTagType.FLYING, true) + .attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, true) .attr(FlinchAttr) .windMove() .target(MoveTarget.ALL_NEAR_ENEMIES), @@ -7435,7 +7441,7 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.DEF ], -1), new AttackMove(Moves.WHIRLPOOL, Type.WATER, MoveCategory.SPECIAL, 35, 85, 15, -1, 0, 2) .attr(TrapAttr, BattlerTagType.WHIRLPOOL) - .attr(HitsTagAttr, BattlerTagType.UNDERWATER, true), + .attr(DealsDoubleDamageToTagAttr, BattlerTagType.UNDERWATER, true), new AttackMove(Moves.BEAT_UP, Type.DARK, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 2) .attr(MultiHitAttr, MultiHitType.BEAT_UP) .attr(BeatUpAttr) @@ -7533,7 +7539,7 @@ export function initMoves() { .attr(AddBattlerTagAttr, BattlerTagType.DROWSY, false, true) .condition((user, target, move) => !target.status && !target.scene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY)), new AttackMove(Moves.KNOCK_OFF, Type.DARK, MoveCategory.PHYSICAL, 65, 100, 20, -1, 0, 3) - .attr(MovePowerMultiplierAttr, (user, target, move) => target.getHeldItems().filter(i => i.isTransferrable).length > 0 ? 1.5 : 1) + .attr(MovePowerMultiplierAttr, (user, target, move) => target.getHeldItems().filter(i => i.isTransferable).length > 0 ? 1.5 : 1) .attr(RemoveHeldItemAttr, false), new AttackMove(Moves.ENDEAVOR, Type.NORMAL, MoveCategory.PHYSICAL, -1, 100, 5, -1, 0, 3) .attr(MatchHpAttr) @@ -7658,7 +7664,7 @@ export function initMoves() { new AttackMove(Moves.EXTRASENSORY, Type.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 20, 10, 0, 3) .attr(FlinchAttr), new AttackMove(Moves.SKY_UPPERCUT, Type.FIGHTING, MoveCategory.PHYSICAL, 85, 90, 15, -1, 0, 3) - .attr(HitsTagAttr, BattlerTagType.FLYING) + .attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING) .punchingMove(), new AttackMove(Moves.SAND_TOMB, Type.GROUND, MoveCategory.PHYSICAL, 35, 85, 15, -1, 0, 3) .attr(TrapAttr, BattlerTagType.SAND_TOMB) @@ -7889,8 +7895,8 @@ export function initMoves() { new AttackMove(Moves.DRAGON_PULSE, Type.DRAGON, MoveCategory.SPECIAL, 85, 100, 10, -1, 0, 4) .pulseMove(), new AttackMove(Moves.DRAGON_RUSH, Type.DRAGON, MoveCategory.PHYSICAL, 100, 75, 10, 20, 0, 4) - .attr(MinimizeAccuracyAttr) - .attr(HitsTagAttr, BattlerTagType.MINIMIZED, true) + .attr(AlwaysHitMinimizeAttr) + .attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true) .attr(FlinchAttr), new AttackMove(Moves.POWER_GEM, Type.ROCK, MoveCategory.SPECIAL, 80, 100, 20, -1, 0, 4), new AttackMove(Moves.DRAIN_PUNCH, Type.FIGHTING, MoveCategory.PHYSICAL, 75, 100, 10, -1, 0, 4) @@ -8087,7 +8093,7 @@ export function initMoves() { .attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false, 1, 1, true) .attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED) .attr(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN]) - .attr(HitsTagAttr, BattlerTagType.FLYING, false) + .attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, false) .makesContact(false), new AttackMove(Moves.STORM_THROW, Type.FIGHTING, MoveCategory.PHYSICAL, 60, 100, 10, -1, 0, 5) .attr(CritOnlyAttr), @@ -8100,9 +8106,9 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true) .danceMove(), new AttackMove(Moves.HEAVY_SLAM, Type.STEEL, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 5) - .attr(MinimizeAccuracyAttr) + .attr(AlwaysHitMinimizeAttr) .attr(CompareWeightPowerAttr) - .attr(HitsTagAttr, BattlerTagType.MINIMIZED, true), + .attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true), new AttackMove(Moves.SYNCHRONOISE, Type.PSYCHIC, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 5) .target(MoveTarget.ALL_NEAR_OTHERS) .condition(unknownTypeCondition) @@ -8185,7 +8191,7 @@ export function initMoves() { new StatusMove(Moves.QUASH, Type.DARK, 100, 15, -1, 0, 5) .unimplemented(), new AttackMove(Moves.ACROBATICS, Type.FLYING, MoveCategory.PHYSICAL, 55, 100, 15, -1, 0, 5) - .attr(MovePowerMultiplierAttr, (user, target, move) => Math.max(1, 2 - 0.2 * user.getHeldItems().filter(i => i.isTransferrable).reduce((v, m) => v + m.stackCount, 0))), + .attr(MovePowerMultiplierAttr, (user, target, move) => Math.max(1, 2 - 0.2 * user.getHeldItems().filter(i => i.isTransferable).reduce((v, m) => v + m.stackCount, 0))), new StatusMove(Moves.REFLECT_TYPE, Type.NORMAL, -1, 15, -1, 0, 5) .ignoresSubstitute() .attr(CopyTypeAttr), @@ -8221,7 +8227,7 @@ export function initMoves() { .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.BULLDOZE, Type.GROUND, MoveCategory.PHYSICAL, 60, 100, 20, 100, 0, 5) .attr(StatStageChangeAttr, [ Stat.SPD ], -1) - .attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.GRASSY ? 0.5 : 1) + .attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.GRASSY && target.isGrounded() ? 0.5 : 1) .makesContact(false) .target(MoveTarget.ALL_NEAR_OTHERS), new AttackMove(Moves.FROST_BREATH, Type.ICE, MoveCategory.SPECIAL, 60, 90, 10, 100, 0, 5) @@ -8253,12 +8259,14 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.DEF ], -1) .slicingMove(), new AttackMove(Moves.HEAT_CRASH, Type.FIRE, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 5) - .attr(MinimizeAccuracyAttr) + .attr(AlwaysHitMinimizeAttr) .attr(CompareWeightPowerAttr) - .attr(HitsTagAttr, BattlerTagType.MINIMIZED, true), + .attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true), new AttackMove(Moves.LEAF_TORNADO, Type.GRASS, MoveCategory.SPECIAL, 65, 90, 10, 50, 0, 5) .attr(StatStageChangeAttr, [ Stat.ACC ], -1), new AttackMove(Moves.STEAMROLLER, Type.BUG, MoveCategory.PHYSICAL, 65, 100, 20, 30, 0, 5) + .attr(AlwaysHitMinimizeAttr) + .attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true) .attr(FlinchAttr), new SelfStatusMove(Moves.COTTON_GUARD, Type.GRASS, -1, 10, -1, 0, 5) .attr(StatStageChangeAttr, [ Stat.DEF ], 3, true), @@ -8271,7 +8279,7 @@ export function initMoves() { new AttackMove(Moves.HURRICANE, Type.FLYING, MoveCategory.SPECIAL, 110, 70, 10, 30, 0, 5) .attr(ThunderAccuracyAttr) .attr(ConfuseAttr) - .attr(HitsTagAttr, BattlerTagType.FLYING, false) + .attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, false) .windMove(), new AttackMove(Moves.HEAD_CHARGE, Type.NORMAL, MoveCategory.PHYSICAL, 120, 100, 15, -1, 0, 5) .attr(RecoilAttr) @@ -8325,9 +8333,9 @@ export function initMoves() { .attr(LastMoveDoublePowerAttr, Moves.FUSION_FLARE) .makesContact(false), new AttackMove(Moves.FLYING_PRESS, Type.FIGHTING, MoveCategory.PHYSICAL, 100, 95, 10, -1, 0, 6) - .attr(MinimizeAccuracyAttr) + .attr(AlwaysHitMinimizeAttr) .attr(FlyingTypeMultiplierAttr) - .attr(HitsTagAttr, BattlerTagType.MINIMIZED, true) + .attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true) .condition(failOnGravityCondition), new StatusMove(Moves.MAT_BLOCK, Type.FIGHTING, -1, 10, -1, 0, 6) .target(MoveTarget.USER_SIDE) @@ -8498,8 +8506,8 @@ export function initMoves() { new AttackMove(Moves.THOUSAND_ARROWS, Type.GROUND, MoveCategory.PHYSICAL, 90, 100, 10, -1, 0, 6) .attr(NeutralDamageAgainstFlyingTypeMultiplierAttr) .attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, false, false, 1, 1, true) - .attr(HitsTagAttr, BattlerTagType.FLYING, false) - .attr(HitsTagAttr, BattlerTagType.MAGNET_RISEN, false) + .attr(DealsDoubleDamageToTagAttr, BattlerTagType.FLYING, false) + .attr(DealsDoubleDamageToTagAttr, BattlerTagType.MAGNET_RISEN, false) .attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED) .attr(RemoveBattlerTagAttr, [BattlerTagType.FLYING, BattlerTagType.MAGNET_RISEN]) .makesContact(false) @@ -8756,6 +8764,8 @@ export function initMoves() { .partial() .ignoresVirtual(), new AttackMove(Moves.MALICIOUS_MOONSAULT, Type.DARK, MoveCategory.PHYSICAL, 180, -1, 1, -1, 0, 7) + .attr(AlwaysHitMinimizeAttr) + .attr(DealsDoubleDamageToTagAttr, BattlerTagType.MINIMIZED, true) .partial() .ignoresVirtual(), new AttackMove(Moves.OCEANIC_OPERETTA, Type.WATER, MoveCategory.SPECIAL, 195, -1, 1, -1, 0, 7) @@ -9103,7 +9113,7 @@ export function initMoves() { new AttackMove(Moves.SHELL_SIDE_ARM, Type.POISON, MoveCategory.SPECIAL, 90, 100, 10, 20, 0, 8) .attr(ShellSideArmCategoryAttr) .attr(StatusEffectAttr, StatusEffect.POISON) - .partial(), + .partial(), // Physical version of the move does not make contact new AttackMove(Moves.MISTY_EXPLOSION, Type.FAIRY, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 8) .attr(SacrificialAttr) .target(MoveTarget.ALL_NEAR_OTHERS) diff --git a/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts b/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts index b66ca10c9f5..f7666fa1b37 100644 --- a/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts +++ b/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts @@ -2,7 +2,7 @@ import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattl import { trainerConfigs, } from "#app/data/trainer-config"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { TrainerType } from "#enums/trainer-type"; import { Species } from "#enums/species"; @@ -99,7 +99,7 @@ export const ATrainersTestEncounter: MysteryEncounter = const trainerConfig = trainerConfigs[trainerType].clone(); const trainerSpriteKey = trainerConfig.getSpriteKey(); encounter.enemyPartyConfigs.push({ - levelAdditiveMultiplier: 1, + levelAdditiveModifier: 1, trainerConfig: trainerConfig }); @@ -152,7 +152,6 @@ export const ATrainersTestEncounter: MysteryEncounter = }; encounter.setDialogueToken("eggType", i18next.t(`${namespace}.eggTypes.epic`)); setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.SACRED_ASH], guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ULTRA], fillRemaining: true }, [eggOptions]); - return initBattleWithEnemyConfig(scene, config); } ) @@ -180,7 +179,7 @@ export const ATrainersTestEncounter: MysteryEncounter = ) .withOutroDialogue([ { - text: `${namespace}.outro`, - }, + text: `${namespace}.outro` + } ]) .build(); diff --git a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts index a9a273c6ec4..18f998192ce 100644 --- a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts +++ b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts @@ -4,9 +4,9 @@ import { BerryModifierType, modifierTypes, PokemonHeldItemModifierType } from "# import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Species } from "#enums/species"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; -import { PersistentModifierRequirement } from "../mystery-encounter-requirements"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { PersistentModifierRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; @@ -208,7 +208,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = // Calculate boss mon const config: EnemyPartyConfig = { - levelAdditiveMultiplier: 1, + levelAdditiveModifier: 1, pokemonConfigs: [ { species: getPokemonSpecies(Species.GREEDENT), @@ -291,7 +291,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = const encounter = scene.currentBattle.mysteryEncounter!; const berryMap = encounter.misc.berryItemsMap; - // Returns 2/5 of the berries stolen from each Pokemon + // Returns 2/5 of the berries stolen to each Pokemon const party = scene.getParty(); party.forEach(pokemon => { const stolenBerries: BerryModifier[] = berryMap.get(pokemon.id); @@ -310,6 +310,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = } } }); + await scene.updateModifiers(true); transitionMysteryEncounterIntroVisuals(scene, true, true, 500); leaveEncounterWithoutBattle(scene, true); 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 9f38b5a4dea..eb43424a8ff 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 @@ -3,9 +3,9 @@ import { modifierTypes } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Species } from "#enums/species"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; -import { AbilityRequirement, CombinationPokemonRequirement, MoveRequirement } from "../mystery-encounter-requirements"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { AbilityRequirement, CombinationPokemonRequirement, MoveRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { getHighestStatTotalPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { EXTORTION_ABILITIES, EXTORTION_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; import { getPokemonSpecies } from "#app/data/pokemon-species"; diff --git a/src/data/mystery-encounters/encounters/berries-abound-encounter.ts b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts index 7e6914cabdd..d1497c54527 100644 --- a/src/data/mystery-encounters/encounters/berries-abound-encounter.ts +++ b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts @@ -17,13 +17,13 @@ import { randSeedInt } from "#app/utils"; import { BattlerTagType } from "#enums/battler-tag-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { getPokemonNameWithAffix } from "#app/messages"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { TrainerSlot } from "#app/data/trainer-config"; -import { applyModifierTypeToPlayerPokemon, getHighestStatPlayerPokemon, getSpriteKeysFromPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { applyModifierTypeToPlayerPokemon, getEncounterPokemonLevelForWave, getHighestStatPlayerPokemon, getSpriteKeysFromPokemon, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import PokemonData from "#app/system/pokemon-data"; import { BerryModifier } from "#app/modifier/modifier"; import i18next from "#app/plugins/i18n"; @@ -56,12 +56,11 @@ export const BerriesAboundEncounter: MysteryEncounter = const encounter = scene.currentBattle.mysteryEncounter!; // Calculate boss mon - const level = (scene.currentBattle.enemyLevels?.[0] ?? scene.currentBattle.waveIndex) + Math.max(Math.round((scene.currentBattle.waveIndex / 10)), 0); + const level = getEncounterPokemonLevelForWave(scene, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER); const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getParty()), true); const bossPokemon = new EnemyPokemon(scene, bossSpecies, level, TrainerSlot.NONE, true); encounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(bossPokemon)); const config: EnemyPartyConfig = { - levelAdditiveMultiplier: 1, pokemonConfigs: [{ level: level, species: bossSpecies, 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 7fdaec35dc3..d79ac087725 100644 --- a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts +++ b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts @@ -19,7 +19,7 @@ import { PartyMemberStrength } from "#enums/party-member-strength"; import BattleScene from "#app/battle-scene"; import * as Utils from "#app/utils"; import { isNullOrUndefined, randSeedInt, randSeedShuffle } from "#app/utils"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { TrainerType } from "#enums/trainer-type"; import { Species } from "#enums/species"; @@ -192,6 +192,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = 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() .withAutoHideIntroVisuals(false) @@ -286,7 +287,8 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = // Player gets different rewards depending on the number of bug types they have const numBugTypes = scene.getParty().filter(p => p.isOfType(Type.BUG, true)).length; - encounter.setDialogueToken("numBugTypes", numBugTypes.toString()); + const numBugTypesText = i18next.t(`${namespace}.numBugTypes`, { count: numBugTypes }); + encounter.setDialogueToken("numBugTypes", numBugTypesText); if (numBugTypes < 2) { setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.SUPER_LURE, modifierTypes.GREAT_BALL], fillRemaining: false }); diff --git a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts index 061d2a33e8a..d930e43c45f 100644 --- a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts +++ b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts @@ -5,7 +5,7 @@ import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifi import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { PartyMemberStrength } from "#enums/party-member-strength"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { Species } from "#enums/species"; import { TrainerType } from "#enums/trainer-type"; @@ -246,12 +246,12 @@ export const ClowningAroundEncounter: MysteryEncounter = const party = scene.getParty(); let mostHeldItemsPokemon = party[0]; let count = mostHeldItemsPokemon.getHeldItems() - .filter(m => m.isTransferrable && !(m instanceof BerryModifier)) + .filter(m => m.isTransferable && !(m instanceof BerryModifier)) .reduce((v, m) => v + m.stackCount, 0); party.forEach(pokemon => { const nextCount = pokemon.getHeldItems() - .filter(m => m.isTransferrable && !(m instanceof BerryModifier)) + .filter(m => m.isTransferable && !(m instanceof BerryModifier)) .reduce((v, m) => v + m.stackCount, 0); if (nextCount > count) { mostHeldItemsPokemon = pokemon; @@ -276,7 +276,7 @@ export const ClowningAroundEncounter: MysteryEncounter = // Shuffle Transferable held items in the same tier (only shuffles Ultra and Rogue atm) let numUltra = 0; let numRogue = 0; - items.filter(m => m.isTransferrable && !(m instanceof BerryModifier)) + items.filter(m => m.isTransferable && !(m instanceof BerryModifier)) .forEach(m => { const type = m.type.withTierFromPool(); const tier = type.tier ?? ModifierTier.ULTRA; diff --git a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts index 046e2b2f876..bd308a5919b 100644 --- a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts +++ b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts @@ -3,8 +3,8 @@ import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/po import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Species } from "#enums/species"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { getPokemonSpecies } from "#app/data/pokemon-species"; @@ -19,7 +19,7 @@ import { MoveRequirement } from "#app/data/mystery-encounters/mystery-encounter- import { DANCING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; import { BattlerIndex } from "#app/battle"; -import { catchPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { catchPokemon, getEncounterPokemonLevelForWave, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { PokeballType } from "#enums/pokeball"; import { modifierTypes } from "#app/modifier/modifier-type"; import { LearnMovePhase } from "#app/phases/learn-move-phase"; @@ -107,7 +107,7 @@ export const DancingLessonsEncounter: MysteryEncounter = const encounter = scene.currentBattle.mysteryEncounter!; const species = getPokemonSpecies(Species.ORICORIO); - const level = (scene.currentBattle.enemyLevels?.[0] ?? scene.currentBattle.waveIndex) + Math.max(Math.round((scene.currentBattle.waveIndex / 10)), 0); + const level = getEncounterPokemonLevelForWave(scene, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER); const enemyPokemon = new EnemyPokemon(scene, species, level, TrainerSlot.NONE, false); if (!enemyPokemon.moveset.some(m => m && m.getMove().id === Moves.REVELATION_DANCE)) { if (enemyPokemon.moveset.length < 4) { @@ -146,7 +146,6 @@ export const DancingLessonsEncounter: MysteryEncounter = encounter.loadAssets.push(oricorio.loadAssets()); const config: EnemyPartyConfig = { - levelAdditiveMultiplier: 1, pokemonConfigs: [{ species: species, dataSource: oricorioData, diff --git a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts index 212ff6ed1bb..09b058ab7c9 100644 --- a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts +++ b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts @@ -5,8 +5,8 @@ import { Species } from "#enums/species"; import BattleScene from "#app/battle-scene"; import { modifierTypes } from "#app/modifier/modifier-type"; import { getPokemonSpecies } from "#app/data/pokemon-species"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { EnemyPartyConfig, EnemyPokemonConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, } from "../utils/encounter-phase-utils"; import { getRandomPlayerPokemon, getRandomSpeciesByStarterTier } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; @@ -18,7 +18,7 @@ import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; /** i18n namespace for encounter */ const namespace = "mysteryEncounter:darkDeal"; -/** Exclude Ultra Beasts (inludes Cosmog/Solgaleo/Lunala/Necrozma), Paradox (includes Miraidon/Koraidon), Eternatus, and egg-locked mythicals */ +/** Exclude Ultra Beasts (inludes Cosmog/Solgaleo/Lunala/Necrozma), Paradox (includes Miraidon/Koraidon), Eternatus, and Mythicals */ const excludedBosses = [ Species.NECROZMA, Species.COSMOG, @@ -63,11 +63,24 @@ const excludedBosses = [ Species.CELEBI, Species.DEOXYS, Species.JIRACHI, + Species.DARKRAI, Species.PHIONE, Species.MANAPHY, Species.ARCEUS, + Species.SHAYMIN, Species.VICTINI, + Species.MELOETTA, + Species.KELDEO, + Species.GENESECT, + Species.DIANCIE, + Species.HOOPA, + Species.VOLCANION, + Species.MAGEARNA, + Species.MARSHADOW, + Species.ZERAORA, + Species.ZARUDE, Species.MELTAN, + Species.MELMETAL, Species.PECHARUNT, ]; @@ -151,7 +164,7 @@ export const DarkDealEncounter: MysteryEncounter = // Starter egg tier, 35/50/10/5 %odds for tiers 6/7/8/9+ const roll = randSeedInt(100); const starterTier: number | [number, number] = - roll > 65 ? 6 : roll > 15 ? 7 : roll > 5 ? 8 : [9, 10]; + roll >= 65 ? 6 : roll >= 15 ? 7 : roll >= 5 ? 8 : [9, 10]; const bossSpecies = getPokemonSpecies(getRandomSpeciesByStarterTier(starterTier, excludedBosses, bossTypes)); const pokemonConfig: EnemyPokemonConfig = { species: bossSpecies, diff --git a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts index ed9344d3c95..25959abe19e 100644 --- a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts +++ b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts @@ -4,9 +4,9 @@ import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifi import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Species } from "#enums/species"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; -import { CombinationPokemonRequirement, HeldItemRequirement, MoneyRequirement } from "../mystery-encounter-requirements"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { CombinationPokemonRequirement, HeldItemRequirement, MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; @@ -33,6 +33,8 @@ const OPTION_3_DISALLOWED_MODIFIERS = [ "PokemonBaseStatTotalModifier" ]; +const DELIBIRDY_MONEY_PRICE_MULTIPLIER = 1.5; + /** * Delibird-y encounter. * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3804 | GitHub Issue #3804} @@ -42,7 +44,7 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.DELIBIRDY) .withEncounterTier(MysteryEncounterTier.GREAT) .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) - .withSceneRequirement(new MoneyRequirement(0, 2)) // Must have enough money for it to spawn at the very least + .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) @@ -93,12 +95,18 @@ export const DelibirdyEncounter: MysteryEncounter = .withOnInit((scene: BattleScene) => { const encounter = scene.currentBattle.mysteryEncounter!; encounter.setDialogueToken("delibirdName", getPokemonSpecies(Species.DELIBIRD).getName()); + + scene.loadBgm("mystery_encounter_delibirdy", "mystery_encounter_delibirdy.mp3"); + return true; + }) + .withOnVisualsStart((scene: BattleScene) => { + scene.fadeAndSwitchBgm("mystery_encounter_delibirdy"); return true; }) .withOption( MysteryEncounterOptionBuilder .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) - .withSceneMoneyRequirement(0, 2) // Must have money to spawn + .withSceneMoneyRequirement(0, DELIBIRDY_MONEY_PRICE_MULTIPLIER) // Must have money to spawn .withDialogue({ buttonLabel: `${namespace}.option.1.label`, buttonTooltip: `${namespace}.option.1.tooltip`, diff --git a/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts b/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts index e35ca08b6a0..104b46bce8a 100644 --- a/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts +++ b/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts @@ -9,7 +9,7 @@ import { Species } from "#enums/species"; import BattleScene from "#app/battle-scene"; import MysteryEncounter, { MysteryEncounterBuilder, -} from "../mystery-encounter"; +} from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; diff --git a/src/data/mystery-encounters/encounters/field-trip-encounter.ts b/src/data/mystery-encounters/encounters/field-trip-encounter.ts index e0101d60a2a..d03a3c1fcca 100644 --- a/src/data/mystery-encounters/encounters/field-trip-encounter.ts +++ b/src/data/mystery-encounters/encounters/field-trip-encounter.ts @@ -6,7 +6,7 @@ import { modifierTypes } from "#app/modifier/modifier-type"; import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { Stat } from "#enums/stat"; diff --git a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts index 1861abcc7a4..470c4b96c82 100644 --- a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts +++ b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts @@ -3,8 +3,8 @@ import { EnemyPartyConfig, initBattleWithEnemyConfig, loadCustomMovesForEncounte import { AttackTypeBoosterModifierType, modifierTypes, } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { TypeRequirement } from "../mystery-encounter-requirements"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { 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"; diff --git a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts index c163a2fc194..aa11a07f218 100644 --- a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts +++ b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts @@ -17,12 +17,12 @@ import { } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { MoveRequirement } from "../mystery-encounter-requirements"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MoveRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { TrainerSlot } from "#app/data/trainer-config"; -import { getSpriteKeysFromPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { getEncounterPokemonLevelForWave, getSpriteKeysFromPokemon, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import PokemonData from "#app/system/pokemon-data"; import { BattlerTagType } from "#enums/battler-tag-type"; import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; @@ -54,12 +54,11 @@ export const FightOrFlightEncounter: MysteryEncounter = const encounter = scene.currentBattle.mysteryEncounter!; // Calculate boss mon - const level = (scene.currentBattle.enemyLevels?.[0] ?? scene.currentBattle.waveIndex) + Math.max(Math.round((scene.currentBattle.waveIndex / 10)), 0); + const level = getEncounterPokemonLevelForWave(scene, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER); const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getParty()), true); const bossPokemon = new EnemyPokemon(scene, bossSpecies, level, TrainerSlot.NONE, true); encounter.setDialogueToken("enemyPokemon", bossPokemon.getNameToRender()); const config: EnemyPartyConfig = { - levelAdditiveMultiplier: 1, pokemonConfigs: [{ level: level, species: bossSpecies, @@ -69,7 +68,7 @@ export const FightOrFlightEncounter: MysteryEncounter = mysteryEncounterBattleEffects: (pokemon: Pokemon) => { queueEncounterMessage(pokemon.scene, `${namespace}.option.1.stat_boost`); // Randomly boost 1 stat 2 stages - pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [randSeedInt(5)], 2)); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [randSeedInt(4, 1)], 2)); } }], }; diff --git a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts index a544657e47c..9ca7c7c2865 100644 --- a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts +++ b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts @@ -1,7 +1,7 @@ import { leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterRewards, transitionMysteryEncounterIntroVisuals, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { TrainerSlot } from "#app/data/trainer-config"; import Pokemon, { FieldPosition, PlayerPokemon } from "#app/field/pokemon"; @@ -84,12 +84,7 @@ export const FunAndGamesEncounter: MysteryEncounter = return true; }) .withOnVisualsStart((scene: BattleScene) => { - // Change the bgm - scene.fadeOutBgm(2000, false); - scene.time.delayedCall(2000, () => { - scene.playBgm("mystery_encounter_fun_and_games"); - }); - + scene.fadeAndSwitchBgm("mystery_encounter_fun_and_games"); return true; }) .withOption(MysteryEncounterOptionBuilder @@ -175,7 +170,9 @@ async function summonPlayerPokemon(scene: BattleScene) { const party = scene.getParty(); const chosenIndex = party.indexOf(playerPokemon); if (chosenIndex !== 0) { - [party[chosenIndex], party[0]] = [party[chosenIndex], party[chosenIndex]]; + const leadPokemon = party[0]; + party[0] = playerPokemon; + party[chosenIndex] = leadPokemon; } // Do trainer summon animation diff --git a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts index 1c5a1f009d9..55d4953d438 100644 --- a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts +++ b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts @@ -4,7 +4,7 @@ import { ModifierTier } from "#app/modifier/modifier-tier"; import { getPlayerModifierTypeOptions, ModifierPoolType, ModifierTypeOption, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { Species } from "#enums/species"; import PokemonSpecies, { allSpecies, getPokemonSpecies } from "#app/data/pokemon-species"; @@ -23,6 +23,7 @@ import { getPokeballAtlasKey, getPokeballTintColor, PokeballType } from "#app/da import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { trainerNamePools } from "#app/data/trainer-names"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import { addPokemonDataToDexAndValidateAchievements } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounter:globalTradeSystem"; @@ -118,17 +119,13 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = return true; }) .withOnVisualsStart((scene: BattleScene) => { - // Change the bgm - scene.fadeOutBgm(1500, false); - scene.time.delayedCall(1500, () => { - scene.playBgm(scene.currentBattle.mysteryEncounter!.misc.bgmKey); - }); - + scene.fadeAndSwitchBgm(scene.currentBattle.mysteryEncounter!.misc.bgmKey); return true; }) .withOption( MysteryEncounterOptionBuilder .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withHasDexProgress(true) .withDialogue({ buttonLabel: `${namespace}.option.1.label`, buttonTooltip: `${namespace}.option.1.tooltip`, @@ -200,6 +197,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = await doPokemonTradeSequence(scene, tradedPokemon, newPlayerPokemon); await showEncounterText(scene, `${namespace}.trade_received`, null, 0, true, 4000); scene.playBgm(encounter.misc.bgmKey); + await addPokemonDataToDexAndValidateAchievements(scene, newPlayerPokemon); await hideTradeBackground(scene); tradedPokemon.destroy(); @@ -210,6 +208,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = .withOption( MysteryEncounterOptionBuilder .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withHasDexProgress(true) .withDialogue({ buttonLabel: `${namespace}.option.2.label`, buttonTooltip: `${namespace}.option.2.tooltip`, @@ -218,8 +217,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = const encounter = scene.currentBattle.mysteryEncounter!; const onPokemonSelected = (pokemon: PlayerPokemon) => { // Randomly generate a Wonder Trade pokemon - // const randomTradeOption = generateTradeOption(scene.getParty().map(p => p.species)); - const randomTradeOption = getPokemonSpecies(Species.BURMY); + const randomTradeOption = generateTradeOption(scene.getParty().map(p => p.species)); const tradePokemon = new EnemyPokemon(scene, randomTradeOption, pokemon.level, TrainerSlot.NONE, false); // Extra shiny roll at 1/128 odds (boosted by events and charms) if (!tradePokemon.shiny) { @@ -280,7 +278,8 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = await showTradeBackground(scene); await doPokemonTradeSequence(scene, tradedPokemon, newPlayerPokemon); await showEncounterText(scene, `${namespace}.trade_received`, null, 0, true, 4000); - scene.playBgm(scene.currentBattle.mysteryEncounter!.misc.bgmKey); + scene.playBgm(encounter.misc.bgmKey); + await addPokemonDataToDexAndValidateAchievements(scene, newPlayerPokemon); await hideTradeBackground(scene); tradedPokemon.destroy(); @@ -301,7 +300,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = const onPokemonSelected = (pokemon: PlayerPokemon) => { // Get Pokemon held items and filter for valid ones const validItems = pokemon.getHeldItems().filter((it) => { - return it.isTransferrable; + return it.isTransferable; }); return validItems.map((modifier: PokemonHeldItemModifier) => { @@ -322,7 +321,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = const selectableFilter = (pokemon: Pokemon) => { // If pokemon has items to trade const meetsReqs = pokemon.getHeldItems().filter((it) => { - return it.isTransferrable; + return it.isTransferable; }).length > 0; if (!meetsReqs) { return getEncounterText(scene, `${namespace}.option.3.invalid_selection`) ?? null; diff --git a/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts b/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts index 16568d8cb7d..509ffb11b26 100644 --- a/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts +++ b/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts @@ -3,8 +3,8 @@ import { Moves } from "#app/enums/moves"; import { Species } from "#app/enums/species"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { leaveEncounterWithoutBattle, setEncounterExp } from "../utils/encounter-phase-utils"; import { applyDamageToPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; diff --git a/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts b/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts index 71a44bd6852..ac257a8975f 100644 --- a/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts +++ b/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts @@ -15,7 +15,7 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { PartyMemberStrength } from "#enums/party-member-strength"; import BattleScene from "#app/battle-scene"; import * as Utils from "#app/utils"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; @@ -75,7 +75,7 @@ export const MysteriousChallengersEncounter: MysteryEncounter = const hardSpriteKey = hardConfig.getSpriteKey(female, hardConfig.doubleOnly); encounter.enemyPartyConfigs.push({ trainerConfig: hardConfig, - levelAdditiveMultiplier: 1, + levelAdditiveModifier: 1, female: female, }); @@ -98,7 +98,7 @@ export const MysteriousChallengersEncounter: MysteryEncounter = const brutalSpriteKey = brutalConfig.getSpriteKey(female, brutalConfig.doubleOnly); encounter.enemyPartyConfigs.push({ trainerConfig: brutalConfig, - levelAdditiveMultiplier: 1.5, + levelAdditiveModifier: 1.5, female: female, }); diff --git a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts index 26e846ed874..18b2db53ba2 100644 --- a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts +++ b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts @@ -5,8 +5,8 @@ import { ModifierTier } from "#app/modifier/modifier-tier"; import { randSeedInt } from "#app/utils"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { getPokemonSpecies } from "#app/data/pokemon-species"; @@ -68,7 +68,7 @@ export const MysteriousChestEncounter: MysteryEncounter = // Calculate boss mon const config: EnemyPartyConfig = { - levelAdditiveMultiplier: 0.5, + levelAdditiveModifier: 0.5, disableSwitch: true, pokemonConfigs: [ { @@ -179,6 +179,7 @@ export const MysteriousChestEncounter: MysteryEncounter = encounter.setDialogueToken("pokeName", highestLevelPokemon.getNameToRender()); await showEncounterText(scene, `${namespace}.option.1.bad`); transitionMysteryEncounterIntroVisuals(scene, true, true, 500); + setEncounterRewards(scene, { fillRemaining: true }); await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]); } } diff --git a/src/data/mystery-encounters/encounters/part-timer-encounter.ts b/src/data/mystery-encounters/encounters/part-timer-encounter.ts index 2abbb53c333..f5486d34ea9 100644 --- a/src/data/mystery-encounters/encounters/part-timer-encounter.ts +++ b/src/data/mystery-encounters/encounters/part-timer-encounter.ts @@ -2,8 +2,8 @@ import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/myst import { leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterExp, setEncounterRewards, transitionMysteryEncounterIntroVisuals, updatePlayerMoney } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { MoveRequirement } from "../mystery-encounter-requirements"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MoveRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { Stat } from "#enums/stat"; diff --git a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts index 49abf98cf49..2690460757f 100644 --- a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts +++ b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts @@ -1,7 +1,7 @@ import { initSubsequentOptionSelect, leaveEncounterWithoutBattle, transitionMysteryEncounterIntroVisuals, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import MysteryEncounterOption, { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { TrainerSlot } from "#app/data/trainer-config"; import { HiddenAbilityRateBoosterModifier, IvScannerModifier } from "#app/modifier/modifier"; @@ -25,7 +25,7 @@ const namespace = "mysteryEncounter:safariZone"; const TRAINER_THROW_ANIMATION_TIMES = [512, 184, 768]; -const SAFARI_MONEY_MULTIPLIER = 2.75; +const SAFARI_MONEY_MULTIPLIER = 2; /** * Safari Zone encounter. 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 8ee4782def5..933f184351a 100644 --- a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts +++ b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts @@ -5,9 +5,9 @@ import { randSeedInt } from "#app/utils"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Species } from "#enums/species"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; -import { MoneyRequirement } from "../mystery-encounter-requirements"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { getEncounterText, queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { applyDamageToPokemon, applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; @@ -19,6 +19,9 @@ import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; /** the i18n namespace for this encounter */ const namespace = "mysteryEncounter:shadyVitaminDealer"; +const VITAMIN_DEALER_CHEAP_PRICE_MULTIPLIER = 1.5; +const VITAMIN_DEALER_EXPENSIVE_PRICE_MULTIPLIER = 3.5; + /** * Shady Vitamin Dealer encounter. * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3798 | GitHub Issue #3798} @@ -28,7 +31,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.SHADY_VITAMIN_DEALER) .withEncounterTier(MysteryEncounterTier.COMMON) .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) - .withSceneRequirement(new MoneyRequirement(0, 1.5)) // Must have the money for at least the cheap deal + .withSceneRequirement(new MoneyRequirement(0, VITAMIN_DEALER_CHEAP_PRICE_MULTIPLIER)) // Must have the money for at least the cheap deal .withPrimaryPokemonHealthRatioRequirement([0.5, 1]) // At least 1 Pokemon must have above half HP .withIntroSpriteConfigs([ { @@ -64,7 +67,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = .withOption( MysteryEncounterOptionBuilder .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) - .withSceneMoneyRequirement(0, 1.5) + .withSceneMoneyRequirement(0, VITAMIN_DEALER_CHEAP_PRICE_MULTIPLIER) .withDialogue({ buttonLabel: `${namespace}.option.1.label`, buttonTooltip: `${namespace}.option.1.tooltip`, @@ -115,7 +118,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = await applyModifierTypeToPlayerPokemon(scene, chosenPokemon, modType); } - leaveEncounterWithoutBattle(scene); + leaveEncounterWithoutBattle(scene, true); }) .withPostOptionPhase(async (scene: BattleScene) => { // Damage and status applied after dealer leaves (to make thematic sense) @@ -142,7 +145,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = .withOption( MysteryEncounterOptionBuilder .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) - .withSceneMoneyRequirement(0, 3.5) + .withSceneMoneyRequirement(0, VITAMIN_DEALER_EXPENSIVE_PRICE_MULTIPLIER) .withDialogue({ buttonLabel: `${namespace}.option.2.label`, buttonTooltip: `${namespace}.option.2.tooltip`, @@ -193,7 +196,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = await applyModifierTypeToPlayerPokemon(scene, chosenPokemon, modType); } - leaveEncounterWithoutBattle(scene); + leaveEncounterWithoutBattle(scene, true); }) .withPostOptionPhase(async (scene: BattleScene) => { // Status applied after dealer leaves (to make thematic sense) diff --git a/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts index b9f08b12ffd..bfccc46ee0f 100644 --- a/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts +++ b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts @@ -1,22 +1,24 @@ import { STEALING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Species } from "#enums/species"; import BattleScene from "#app/battle-scene"; import { StatusEffect } from "#app/data/status-effect"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; -import { MoveRequirement } from "../mystery-encounter-requirements"; -import { EnemyPartyConfig, EnemyPokemonConfig, initBattleWithEnemyConfig, loadCustomMovesForEncounter, leaveEncounterWithoutBattle, setEncounterExp, setEncounterRewards, } from "../utils/encounter-phase-utils"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { MoveRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; +import { EnemyPartyConfig, EnemyPokemonConfig, generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, loadCustomMovesForEncounter, setEncounterExp, setEncounterRewards, } from "../utils/encounter-phase-utils"; import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { Moves } from "#enums/moves"; import { BattlerIndex } from "#app/battle"; -import { PokemonMove } from "#app/field/pokemon"; +import { AiType, PokemonMove } from "#app/field/pokemon"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { PartyHealPhase } from "#app/phases/party-heal-phase"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import { BerryType } from "#enums/berry-type"; +import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; /** i18n namespace for the encounter */ const namespace = "mysteryEncounter:slumberingSnorlax"; @@ -38,7 +40,7 @@ export const SlumberingSnorlaxEncounter: MysteryEncounter = fileRoot: "pokemon", hasShadow: true, tint: 0.25, - scale: 1.5, + scale: 1.25, repeat: true, y: 5, }, @@ -58,10 +60,22 @@ export const SlumberingSnorlaxEncounter: MysteryEncounter = species: bossSpecies, isBoss: true, status: [StatusEffect.SLEEP, 5], // Extra turns on timer for Snorlax's start of fight moves - moveSet: [Moves.REST, Moves.SLEEP_TALK, Moves.CRUNCH, Moves.GIGA_IMPACT] + moveSet: [Moves.REST, Moves.SLEEP_TALK, Moves.CRUNCH, Moves.GIGA_IMPACT], + modifierConfigs: [ + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.SITRUS]) as PokemonHeldItemModifierType, + stackCount: 2 + }, + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.ENIGMA]) as PokemonHeldItemModifierType, + stackCount: 2 + }, + ], + mysteryEncounterPokemonData: new MysteryEncounterPokemonData({ spriteScale: 1.25 }), + aiType: AiType.SMART // Required to ensure Snorlax uses Sleep Talk while it is asleep }; const config: EnemyPartyConfig = { - levelAdditiveMultiplier: 0.5, + levelAdditiveModifier: 0.5, pokemonConfigs: [pokemonConfig], }; encounter.enemyPartyConfigs = [config]; diff --git a/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts b/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts index bf976458fdd..10b0e5222b3 100644 --- a/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts +++ b/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts @@ -2,8 +2,8 @@ import { EnemyPartyConfig, generateModifierTypeOption, initBattleWithEnemyConfig import { randSeedInt } from "#app/utils"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { MoneyRequirement, WaveModulusRequirement } from "../mystery-encounter-requirements"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MoneyRequirement, WaveModulusRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import Pokemon, { EnemyPokemon } from "#app/field/pokemon"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; @@ -20,12 +20,13 @@ import { getPokemonNameWithAffix } from "#app/messages"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { Stat } from "#enums/stat"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import { getEncounterPokemonLevelForWave, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; /** the i18n namespace for this encounter */ const namespace = "mysteryEncounter:teleportingHijinks"; -const MONEY_COST_MULTIPLIER = 2.5; -const BIOME_CANDIDATES = [Biome.SPACE, Biome.FAIRY_CAVE, Biome.LABORATORY, Biome.ISLAND]; +const MONEY_COST_MULTIPLIER = 1.75; +const BIOME_CANDIDATES = [Biome.SPACE, Biome.FAIRY_CAVE, Biome.LABORATORY, Biome.ISLAND, Biome.WASTELAND, Biome.DOJO]; const MACHINE_INTERFACING_TYPES = [Type.ELECTRIC, Type.STEEL]; /** @@ -130,7 +131,7 @@ export const TeleportingHijinksEncounter: MysteryEncounter = const encounter = scene.currentBattle.mysteryEncounter!; // Init enemy - const level = (scene.currentBattle.enemyLevels?.[0] ?? scene.currentBattle.waveIndex) + Math.max(Math.round((scene.currentBattle.waveIndex / 10)), 0); + const level = getEncounterPokemonLevelForWave(scene, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER); const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getParty()), true); const bossPokemon = new EnemyPokemon(scene, bossSpecies, level, TrainerSlot.NONE, true); encounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(bossPokemon)); @@ -166,7 +167,7 @@ async function doBiomeTransitionDialogueAndBattleInit(scene: BattleScene) { await showEncounterText(scene, `${namespace}.attacked`); // Init enemy - const level = (scene.currentBattle.enemyLevels?.[0] ?? scene.currentBattle.waveIndex) + Math.max(Math.round((scene.currentBattle.waveIndex / 10)), 0); + const level = getEncounterPokemonLevelForWave(scene, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER); const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getParty()), true); const bossPokemon = new EnemyPokemon(scene, bossSpecies, level, TrainerSlot.NONE, true); encounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(bossPokemon)); diff --git a/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts b/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts index 16b0c421bd4..c26c6aa3b7f 100644 --- a/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts @@ -2,8 +2,8 @@ import { leaveEncounterWithoutBattle, transitionMysteryEncounterIntroVisuals, up import { isNullOrUndefined, randSeedInt } from "#app/utils"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { MoneyRequirement } from "../mystery-encounter-requirements"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { catchPokemon, getRandomSpeciesByStarterTier, getSpriteKeysFromPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { getPokemonSpecies, speciesStarters } from "#app/data/pokemon-species"; import { Species } from "#enums/species"; @@ -15,11 +15,15 @@ import PokemonData from "#app/system/pokemon-data"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import { Abilities } from "#enums/abilities"; /** the i18n namespace for this encounter */ const namespace = "mysteryEncounter:pokemonSalesman"; -const MAX_POKEMON_PRICE_MULTIPLIER = 6; +const MAX_POKEMON_PRICE_MULTIPLIER = 4; + +/** Odds of shiny magikarp will be 1/value */ +const SHINY_MAGIKARP_WEIGHT = 100; /** * Pokemon Salesman encounter. @@ -58,12 +62,12 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter = const tries = 0; // Reroll any species that don't have HAs - while (isNullOrUndefined(species.abilityHidden) && tries < 5) { + while ((isNullOrUndefined(species.abilityHidden) || species.abilityHidden === Abilities.NONE) && tries < 5) { species = getPokemonSpecies(getRandomSpeciesByStarterTier([0, 5])); } let pokemon: PlayerPokemon; - if (isNullOrUndefined(species.abilityHidden) || randSeedInt(100) === 0) { + if (randSeedInt(SHINY_MAGIKARP_WEIGHT) === 0 || isNullOrUndefined(species.abilityHidden) || species.abilityHidden === Abilities.NONE) { // If no HA mon found or you roll 1%, give shiny Magikarp species = getPokemonSpecies(Species.MAGIKARP); const hiddenIndex = species.ability2 ? 2 : 1; diff --git a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts index 047aa0d83f6..55cb10644e8 100644 --- a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts @@ -2,7 +2,7 @@ import { EnemyPartyConfig, initBattleWithEnemyConfig, loadCustomMovesForEncounte import { modifierTypes, PokemonHeldItemModifierType, } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import { Species } from "#enums/species"; import { Nature } from "#app/data/nature"; @@ -36,6 +36,7 @@ export const TheStrongStuffEncounter: MysteryEncounter = .withEncounterTier(MysteryEncounterTier.GREAT) .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) .withScenePartySizeRequirement(3, 6) // Must have at least 3 pokemon in party + .withMaxAllowedEncounters(1) .withHideWildIntroMessage(true) .withAutoHideIntroVisuals(false) .withIntroSpriteConfigs([ @@ -70,7 +71,7 @@ export const TheStrongStuffEncounter: MysteryEncounter = // Calculate boss mon const config: EnemyPartyConfig = { - levelAdditiveMultiplier: 1, + levelAdditiveModifier: 1, disableSwitch: true, pokemonConfigs: [ { @@ -159,6 +160,11 @@ export const TheStrongStuffEncounter: MysteryEncounter = encounter.setDialogueToken("increaseValue", BST_INCREASE_VALUE.toString()); await showEncounterText(scene, `${namespace}.option.1.selected_2`, null, undefined, true); + encounter.dialogue.outro = [ + { + text: `${namespace}.outro`, + } + ]; setEncounterRewards(scene, { fillRemaining: true }); leaveEncounterWithoutBattle(scene, true); return true; @@ -192,6 +198,7 @@ export const TheStrongStuffEncounter: MysteryEncounter = ignorePp: true }); + encounter.dialogue.outro = []; transitionMysteryEncounterIntroVisuals(scene, true, true, 500); await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]); } diff --git a/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts index 902aefcb490..60061efbc7a 100644 --- a/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts @@ -2,7 +2,7 @@ import { EnemyPartyConfig, generateModifierType, generateModifierTypeOption, ini import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { TrainerType } from "#enums/trainer-type"; import { Species } from "#enums/species"; @@ -146,7 +146,7 @@ async function spawnNextTrainerOrEndEncounter(scene: BattleScene) { // Give 10x Voucher const newModifier = modifierTypes.VOUCHER_PREMIUM().newModifier(); - scene.addModifier(newModifier); + await scene.addModifier(newModifier); scene.playSound("item_fanfare"); await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: newModifier?.type.name })); diff --git a/src/data/mystery-encounters/encounters/training-session-encounter.ts b/src/data/mystery-encounters/encounters/training-session-encounter.ts index 6c0f1706fa5..33d841b7f02 100644 --- a/src/data/mystery-encounters/encounters/training-session-encounter.ts +++ b/src/data/mystery-encounters/encounters/training-session-encounter.ts @@ -3,7 +3,7 @@ import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattl import { getNatureName, Nature } from "#app/data/nature"; import { speciesStarters } from "#app/data/pokemon-species"; import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; -import { PokemonFormChangeItemModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { PokemonHeldItemModifier } from "#app/modifier/modifier"; import { AbilityAttr } from "#app/system/game-data"; import PokemonData from "#app/system/pokemon-data"; import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; @@ -11,8 +11,8 @@ import { isNullOrUndefined, randSeedShuffle } from "#app/utils"; import { BattlerTagType } from "#enums/battler-tag-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { getEncounterText, queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; @@ -34,6 +34,7 @@ export const TrainingSessionEncounter: MysteryEncounter = .withEncounterTier(MysteryEncounterTier.ULTRA) .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) .withScenePartySizeRequirement(2, 6, true) // Must have at least 2 unfainted pokemon in party + .withFleeAllowed(false) .withHideWildIntroMessage(true) .withIntroSpriteConfigs([ { @@ -97,12 +98,7 @@ export const TrainingSessionEncounter: MysteryEncounter = 5 ); const modifiers = new ModifiersHolder(); - const config = getEnemyConfig( - scene, - playerPokemon, - segments, - modifiers - ); + const config = getEnemyConfig(scene, playerPokemon, segments, modifiers); scene.removePokemonFromPlayerParty(playerPokemon, false); const onBeforeRewardsPhase = () => { @@ -163,6 +159,7 @@ export const TrainingSessionEncounter: MysteryEncounter = // Add pokemon and mods back scene.getParty().push(playerPokemon); for (const mod of modifiers.value) { + mod.pokemonId = playerPokemon.id; scene.addModifier(mod, true, false, false, true); } scene.updateModifiers(true); @@ -230,17 +227,9 @@ export const TrainingSessionEncounter: MysteryEncounter = // Spawn medium training session with chosen pokemon // Every 40 waves, add +1 boss segment, capping at 6 - const segments = Math.min( - 2 + Math.floor(scene.currentBattle.waveIndex / 40), - 6 - ); + const segments = Math.min(2 + Math.floor(scene.currentBattle.waveIndex / 40), 6); const modifiers = new ModifiersHolder(); - const config = getEnemyConfig( - scene, - playerPokemon, - segments, - modifiers - ); + const config = getEnemyConfig(scene, playerPokemon, segments, modifiers); scene.removePokemonFromPlayerParty(playerPokemon, false); const onBeforeRewardsPhase = () => { @@ -377,6 +366,7 @@ export const TrainingSessionEncounter: MysteryEncounter = // Add pokemon and mods back scene.getParty().push(playerPokemon); for (const mod of modifiers.value) { + mod.pokemonId = playerPokemon.id; scene.addModifier(mod, true, false, false, true); } scene.updateModifiers(true); @@ -410,10 +400,12 @@ function getEnemyConfig(scene: BattleScene, playerPokemon: PlayerPokemon, segmen playerPokemon.resetSummonData(); // Passes modifiers by reference - modifiers.value = playerPokemon.getHeldItems().filter(m => !(m instanceof PokemonFormChangeItemModifier)); + modifiers.value = playerPokemon.getHeldItems(); const modifierConfigs = modifiers.value.map((mod) => { return { - modifier: mod + modifier: mod.clone(), + isTransferable: false, + stackCount: mod.stackCount }; }) as HeldModifierConfig[]; 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 ec6291f2a8c..d295c8ab548 100644 --- a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts +++ b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts @@ -2,8 +2,8 @@ import { EnemyPartyConfig, EnemyPokemonConfig, generateModifierType, initBattleW import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { Species } from "#enums/species"; @@ -67,7 +67,7 @@ export const TrashToTreasureEncounter: MysteryEncounter = moveSet: [Moves.PAYBACK, Moves.GUNK_SHOT, Moves.STOMPING_TANTRUM, Moves.DRAIN_PUNCH] }; const config: EnemyPartyConfig = { - levelAdditiveMultiplier: 1, + levelAdditiveModifier: 1, pokemonConfigs: [pokemonConfig], disableSwitch: true }; diff --git a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts index f9148b87f9b..0816c9cd2a6 100644 --- a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts +++ b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts @@ -5,8 +5,8 @@ import Pokemon, { EnemyPokemon, PokemonMove } from "#app/field/pokemon"; import { getPartyLuckValue } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { MoveRequirement, PersistentModifierRequirement } from "../mystery-encounter-requirements"; +import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; +import { MoveRequirement, PersistentModifierRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { TrainerSlot } from "#app/data/trainer-config"; diff --git a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts index 476cc98f503..ed8986d99bb 100644 --- a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts +++ b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts @@ -2,8 +2,8 @@ import { Type } from "#app/data/type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Species } from "#enums/species"; import BattleScene from "#app/battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +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 { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; @@ -14,7 +14,7 @@ import { HiddenAbilityRateBoosterModifier, PokemonFormChangeItemModifier, Pokemo import { achvs } from "#app/system/achv"; import { speciesEggMoves } from "#app/data/egg-moves"; import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; -import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { modifierTypes } from "#app/modifier/modifier-type"; import i18next from "#app/plugins/i18n"; import { doPokemonTransformationSequence, TransformationScreenPosition } from "#app/data/mystery-encounters/utils/encounter-transformation-sequence"; @@ -130,12 +130,7 @@ export const WeirdDreamEncounter: MysteryEncounter = return true; }) .withOnVisualsStart((scene: BattleScene) => { - // Change the bgm - scene.fadeOutBgm(3000, false); - scene.time.delayedCall(3000, () => { - scene.playBgm("mystery_encounter_weird_dream"); - }); - + scene.fadeAndSwitchBgm("mystery_encounter_weird_dream"); return true; }) .withOption( @@ -340,7 +335,7 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon const newStarterUnlocked = await scene.gameData.setPokemonCaught(newPokemon, true, false, false); if (newStarterUnlocked) { atLeastOneNewStarter = true; - queueEncounterMessage(scene, i18next.t("battle:addedAsAStarter", { pokemonName: getPokemonSpecies(speciesRootForm).getName() })); + await showEncounterText(scene, i18next.t("battle:addedAsAStarter", { pokemonName: getPokemonSpecies(speciesRootForm).getName() })); } } diff --git a/src/data/mystery-encounters/mystery-encounter-option.ts b/src/data/mystery-encounters/mystery-encounter-option.ts index fb3daf53a8b..865877445c1 100644 --- a/src/data/mystery-encounters/mystery-encounter-option.ts +++ b/src/data/mystery-encounters/mystery-encounter-option.ts @@ -3,7 +3,7 @@ import { Moves } from "#app/enums/moves"; import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; import BattleScene from "#app/battle-scene"; import { Type } from "../type"; -import { EncounterPokemonRequirement, EncounterSceneRequirement, MoneyRequirement, TypeRequirement } from "./mystery-encounter-requirements"; +import { EncounterPokemonRequirement, EncounterSceneRequirement, MoneyRequirement, TypeRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { CanLearnMoveRequirement, CanLearnMoveRequirementOptions } from "./requirements/can-learn-move-requirement"; import { isNullOrUndefined, randSeedInt } from "#app/utils"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; diff --git a/src/data/mystery-encounters/mystery-encounter.ts b/src/data/mystery-encounters/mystery-encounter.ts index a204ea848da..2a5f6fda7e1 100644 --- a/src/data/mystery-encounters/mystery-encounter.ts +++ b/src/data/mystery-encounters/mystery-encounter.ts @@ -25,6 +25,9 @@ export interface EncounterStartOfBattleEffect { followUp?: boolean; } +const DEFAULT_MAX_ALLOWED_ENCOUNTERS = 2; +const DEFAULT_MAX_ALLOWED_ROGUE_ENCOUNTERS = 1; + /** * Used by {@linkcode MysteryEncounterBuilder} class to define required/optional properties on the {@linkcode MysteryEncounter} class when building. * @@ -42,6 +45,7 @@ export interface IMysteryEncounter { autoHideIntroVisuals: boolean; enterIntroVisualsFromRight: boolean; catchAllowed: boolean; + fleeAllowed: boolean; continuousEncounter: boolean; maxAllowedEncounters: number; hasBattleAnimationsWithoutTargets: boolean; @@ -110,6 +114,11 @@ export default class MysteryEncounter implements IMysteryEncounter { * Default false */ catchAllowed: boolean; + /** + * If true, allows fleeing from a wild encounter (trainer battle MEs auto-disable fleeing) + * Default true + */ + fleeAllowed: boolean; /** * If true, encounter will continuously run through multiple battles/puzzles/etc. instead of going to next wave * MUST EVENTUALLY BE DISABLED TO CONTINUE TO NEXT WAVE @@ -246,8 +255,8 @@ export default class MysteryEncounter implements IMysteryEncounter { this.encounterTier = this.encounterTier ?? MysteryEncounterTier.COMMON; this.dialogue = this.dialogue ?? {}; this.spriteConfigs = this.spriteConfigs ? [...this.spriteConfigs] : []; - // Default max is 1 for ROGUE encounters, 3 for others - this.maxAllowedEncounters = this.maxAllowedEncounters ?? this.encounterTier === MysteryEncounterTier.ROGUE ? 1 : 3; + // Default max is 1 for ROGUE encounters, 2 for others + this.maxAllowedEncounters = this.maxAllowedEncounters ?? this.encounterTier === MysteryEncounterTier.ROGUE ? DEFAULT_MAX_ALLOWED_ROGUE_ENCOUNTERS : DEFAULT_MAX_ALLOWED_ENCOUNTERS; this.encounterMode = MysteryEncounterMode.DEFAULT; this.requirements = this.requirements ? this.requirements : []; this.hideBattleIntroMessage = this.hideBattleIntroMessage ?? false; @@ -520,6 +529,7 @@ export class MysteryEncounterBuilder implements Partial { enterIntroVisualsFromRight: boolean = false; continuousEncounter: boolean = false; catchAllowed: boolean = false; + fleeAllowed: boolean = true; lockEncounterRewardTiers: boolean = false; startOfBattleEffectsComplete: boolean = false; hasBattleAnimationsWithoutTargets: boolean = false; @@ -580,8 +590,8 @@ export class MysteryEncounterBuilder implements Partial { * There should be at least 2 options defined and no more than 4. * If complex use {@linkcode MysteryEncounterBuilder.withOption} * - * @param dialogue - {@linkcode OptionTextDisplay} - * @param callback - {@linkcode OptionPhaseCallback} + * @param dialogue {@linkcode OptionTextDisplay} + * @param callback {@linkcode OptionPhaseCallback} * @returns */ withSimpleDexProgressOption(dialogue: OptionTextDisplay, callback: OptionPhaseCallback): this & Pick { @@ -732,7 +742,7 @@ export class MysteryEncounterBuilder implements Partial { * * @param min min wave (or exact size if only min is given) * @param max optional max size. If not given, defaults to min => exact wave - * @param excludeFainted - if true, only counts unfainted mons + * @param excludeFainted if true, only counts unfainted mons * @returns */ withScenePartySizeRequirement(min: number, max?: number, excludeFainted: boolean = false): this & Required> { @@ -798,7 +808,7 @@ export class MysteryEncounterBuilder implements Partial { * NOTE: If rewards are dependent on options selected, runtime data, etc., * It may be better to programmatically set doEncounterRewards elsewhere. * There is a helper function in mystery-encounter utils, setEncounterRewards(), which can be called programmatically to set rewards - * @param doEncounterRewards - synchronous callback function to perform during rewards phase of the encounter + * @param doEncounterRewards Synchronous callback function to perform during rewards phase of the encounter * @returns */ withRewards(doEncounterRewards: (scene: BattleScene) => boolean): this & Required> { @@ -812,7 +822,7 @@ export class MysteryEncounterBuilder implements Partial { * NOTE: If rewards are dependent on options selected, runtime data, etc., * It may be better to programmatically set doEncounterExp elsewhere. * There is a helper function in mystery-encounter utils, setEncounterExp(), which can be called programmatically to set rewards - * @param doEncounterExp - synchronous callback function to perform during rewards phase of the encounter + * @param doEncounterExp Synchronous callback function to perform during rewards phase of the encounter * @returns */ withExp(doEncounterExp: (scene: BattleScene) => boolean): this & Required> { @@ -823,7 +833,7 @@ export class MysteryEncounterBuilder implements Partial { * Can be used to perform init logic before intro visuals are shown and before the MysteryEncounterPhase begins * Useful for performing things like procedural generation of intro sprites, etc. * - * @param onInit - synchronous callback function to perform as soon as the encounter is selected for the next phase + * @param onInit Synchronous callback function to perform as soon as the encounter is selected for the next phase * @returns */ withOnInit(onInit: (scene: BattleScene) => boolean): this & Required> { @@ -833,7 +843,7 @@ export class MysteryEncounterBuilder implements Partial { /** * Can be used to perform some extra logic (usually animations) when the enemy field is finished sliding in * - * @param onVisualsStart - synchronous callback function to perform as soon as the enemy field finishes sliding in + * @param onVisualsStart Synchronous callback function to perform as soon as the enemy field finishes sliding in * @returns */ withOnVisualsStart(onVisualsStart: (scene: BattleScene) => boolean): this & Required> { @@ -843,7 +853,7 @@ export class MysteryEncounterBuilder implements Partial { /** * Can set whether catching is allowed or not on the encounter * This flag can also be programmatically set inside option event functions or elsewhere - * @param catchAllowed - if true, allows enemy pokemon to be caught during the encounter + * @param catchAllowed If `true`, allows enemy pokemon to be caught during the encounter * @returns */ withCatchAllowed(catchAllowed: boolean): this & Required> { @@ -851,7 +861,16 @@ export class MysteryEncounterBuilder implements Partial { } /** - * @param hideBattleIntroMessage - if true, will not show the trainerAppeared/wildAppeared/bossAppeared message for an encounter + * Can set whether fleeing is allowed or not on the encounter + * @param fleeAllowed If `false`, prevents fleeing from a wild battle (trainer battle MEs already have flee disabled) + * @returns + */ + withFleeAllowed(fleeAllowed: boolean): this & Required> { + return Object.assign(this, { fleeAllowed }); + } + + /** + * @param hideBattleIntroMessage If `true`, will not show the trainerAppeared/wildAppeared/bossAppeared message for an encounter * @returns */ withHideWildIntroMessage(hideBattleIntroMessage: boolean): this & Required> { @@ -859,7 +878,7 @@ export class MysteryEncounterBuilder implements Partial { } /** - * @param autoHideIntroVisuals - if false, will not hide the intro visuals that are displayed at the beginning of encounter + * @param autoHideIntroVisuals If `false`, will not hide the intro visuals that are displayed at the beginning of encounter * @returns */ withAutoHideIntroVisuals(autoHideIntroVisuals: boolean): this & Required> { @@ -867,7 +886,7 @@ export class MysteryEncounterBuilder implements Partial { } /** - * @param enterIntroVisualsFromRight - If true, will slide in intro visuals from the right side of the screen. If false, slides in from left, as normal + * @param enterIntroVisualsFromRight If `true`, will slide in intro visuals from the right side of the screen. If false, slides in from left, as normal * Default false * @returns */ @@ -878,7 +897,7 @@ export class MysteryEncounterBuilder implements Partial { /** * Add a title for the encounter * - * @param title - title of the encounter + * @param title Title of the encounter * @returns */ withTitle(title: string): this { @@ -898,7 +917,7 @@ export class MysteryEncounterBuilder implements Partial { /** * Add a description of the encounter * - * @param description - description of the encounter + * @param description Description of the encounter * @returns */ withDescription(description: string): this { @@ -918,7 +937,7 @@ export class MysteryEncounterBuilder implements Partial { /** * Add a query for the encounter * - * @param query - query to use for the encounter + * @param query Query to use for the encounter * @returns */ withQuery(query: string): this { @@ -938,7 +957,7 @@ export class MysteryEncounterBuilder implements Partial { /** * Add outro dialogue/s for the encounter * - * @param dialogue - outro dialogue/s + * @param dialogue Outro dialogue(s) * @returns */ withOutroDialogue(dialogue: MysteryEncounterDialogue["outro"] = []): this { diff --git a/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts b/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts index bbdf2ee5a4d..a0b4edd4a36 100644 --- a/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts +++ b/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts @@ -2,7 +2,7 @@ import BattleScene from "#app/battle-scene"; import { Moves } from "#app/enums/moves"; import { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import { isNullOrUndefined } from "#app/utils"; -import { EncounterPokemonRequirement } from "../mystery-encounter-requirements"; +import { EncounterPokemonRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; /** * {@linkcode CanLearnMoveRequirement} options diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index 2cd369fbaad..85d583fc898 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -3,7 +3,7 @@ import { biomeLinks, BiomePoolTier } from "#app/data/biomes"; import MysteryEncounterOption from "#app/data/mystery-encounters/mystery-encounter-option"; import { AVERAGE_ENCOUNTERS_PER_RUN_TARGET, WEIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/mystery-encounters"; import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; -import Pokemon, { FieldPosition, PlayerPokemon, PokemonMove, PokemonSummonData } from "#app/field/pokemon"; +import Pokemon, { AiType, FieldPosition, PlayerPokemon, PokemonMove, PokemonSummonData } from "#app/field/pokemon"; import { CustomModifierSettings, ModifierPoolType, ModifierType, ModifierTypeGenerator, ModifierTypeOption, modifierTypes, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; import { MysteryEncounterBattlePhase, MysteryEncounterBattleStartCleanupPhase, MysteryEncounterPhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phases"; import PokemonData from "#app/system/pokemon-data"; @@ -85,21 +85,24 @@ export interface EnemyPokemonConfig { modifierConfigs?: HeldModifierConfig[]; tags?: BattlerTagType[]; dataSource?: PokemonData; + aiType?: AiType; } export interface EnemyPartyConfig { - /** Formula for enemy: level += waveIndex / 10 * levelAdditive */ - levelAdditiveMultiplier?: number; + /** Formula for enemy level: level += waveIndex / 10 * levelAdditiveModifier */ + levelAdditiveModifier?: number; doubleBattle?: boolean; /** Generates trainer battle solely off trainer type */ trainerType?: TrainerType; /** More customizable option for configuring trainer battle */ trainerConfig?: TrainerConfig; pokemonConfigs?: EnemyPokemonConfig[]; - /** True for female trainer, false for male */ + /** `true` for female trainer, false for male */ female?: boolean; - /** True will prevent player from switching */ + /** `true` will prevent player from switching */ disableSwitch?: boolean; + /** `true` or leaving undefined will increment dex seen count for the encounter battle, `false` will not */ + countAsSeen?: boolean; } /** @@ -156,10 +159,10 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig: // ME levels are modified by an additive value that scales with wave index // Base scaling: Every 10 waves, modifier gets +1 level - // This can be amplified or counteracted by setting levelAdditiveMultiplier in config - // levelAdditiveMultiplier value of 0.5 will halve the modifier scaling, 2 will double it, etc. + // This can be amplified or counteracted by setting levelAdditiveModifier in config + // levelAdditiveModifier value of 0.5 will halve the modifier scaling, 2 will double it, etc. // Leaving null/undefined will disable level scaling - const mult: number = !isNullOrUndefined(partyConfig.levelAdditiveMultiplier) ? partyConfig.levelAdditiveMultiplier! : 0; + const mult: number = !isNullOrUndefined(partyConfig.levelAdditiveModifier) ? partyConfig.levelAdditiveModifier! : 0; const additive = Math.max(Math.round((scene.currentBattle.waveIndex / 10) * mult), 0); battle.enemyLevels = battle.enemyLevels.map(level => level + additive); @@ -210,7 +213,7 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig: enemyPokemon.resetSummonData(); } - if (!loaded) { + if (!loaded && isNullOrUndefined(partyConfig.countAsSeen) || partyConfig.countAsSeen) { scene.gameData.setPokemonSeen(enemyPokemon, true, !!(trainerType || trainerConfig)); } @@ -286,6 +289,11 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig: enemyPokemon.summonData.gender = config.gender!; } + // Set AI type + if (!isNullOrUndefined(config.aiType)) { + enemyPokemon.aiType = config.aiType!; + } + // Set moves if (config?.moveSet && config.moveSet.length > 0) { const moves = config.moveSet.map(m => new PokemonMove(m)); @@ -702,19 +710,19 @@ export function handleMysteryEncounterVictory(scene: BattleScene, addHealPhase: if (encounter.continuousEncounter || doNotContinue) { return; } else if (encounter.encounterMode === MysteryEncounterMode.NO_BATTLE) { - scene.pushPhase(new EggLapsePhase(scene)); scene.pushPhase(new MysteryEncounterRewardsPhase(scene, addHealPhase)); + scene.pushPhase(new EggLapsePhase(scene)); } else if (!scene.getEnemyParty().find(p => encounter.encounterMode !== MysteryEncounterMode.TRAINER_BATTLE ? p.isOnField() : !p?.isFainted(true))) { scene.pushPhase(new BattleEndPhase(scene)); if (encounter.encounterMode === MysteryEncounterMode.TRAINER_BATTLE) { scene.pushPhase(new TrainerVictoryPhase(scene)); } if (scene.gameMode.isEndless || !scene.gameMode.isWaveFinal(scene.currentBattle.waveIndex)) { + scene.pushPhase(new MysteryEncounterRewardsPhase(scene, addHealPhase)); if (!encounter.doContinueEncounter) { // Only lapse eggs once for multi-battle encounters scene.pushPhase(new EggLapsePhase(scene)); } - scene.pushPhase(new MysteryEncounterRewardsPhase(scene, addHealPhase)); } } } diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index bac8bded9ba..86c86010c29 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -19,6 +19,10 @@ import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifi 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"; + +/** Will give +1 level every 10 waves */ +export const STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER = 1; /** * Gets the sprite key and file root for a given PokemonSpecies (accounts for gender, shiny, variants, forms, and experimental) @@ -289,10 +293,12 @@ export async function modifyPlayerPokemonBST(pokemon: PlayerPokemon, value: numb */ export async function applyModifierTypeToPlayerPokemon(scene: BattleScene, pokemon: PlayerPokemon, modType: PokemonHeldItemModifierType, fallbackModifierType?: PokemonHeldItemModifierType) { // Check if the Pokemon has max stacks of that item already + const modifier = modType.newModifier(pokemon); const existing = scene.findModifier(m => ( m instanceof PokemonHeldItemModifier && m.type.id === modType.id && - m.pokemonId === pokemon.id + m.pokemonId === pokemon.id && + m.matchType(modifier) )) as PokemonHeldItemModifier; // At max stacks @@ -305,7 +311,6 @@ export async function applyModifierTypeToPlayerPokemon(scene: BattleScene, pokem return applyModifierTypeToPlayerPokemon(scene, pokemon, fallbackModifierType); } - const modifier = modType.newModifier(pokemon); await scene.addModifier(modifier, false, false, false, true); } @@ -327,7 +332,7 @@ export function trainerThrowPokeball(scene: BattleScene, pokemon: EnemyPokemon, const _3m = 3 * pokemon.getMaxHp(); const _2h = 2 * pokemon.hp; const catchRate = pokemon.species.catchRate; - const pokeballMultiplier = getPokeballCatchMultiplier(this.pokeballType); + const pokeballMultiplier = getPokeballCatchMultiplier(pokeballType); const statusMultiplier = pokemon.status ? getStatusEffectCatchRateMultiplier(pokemon.status.effect) : 1; const x = Math.round((((_3m - _2h) * catchRate * pokeballMultiplier) / _3m) * statusMultiplier); ballTwitchRate = Math.round(65536 / Math.sqrt(Math.sqrt(255 / x))); @@ -501,8 +506,6 @@ function failCatch(scene: BattleScene, pokemon: EnemyPokemon, originalY: number, * @param isObtain */ export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, pokeball: Phaser.GameObjects.Sprite | null, pokeballType: PokeballType, showCatchObtainMessage: boolean = true, isObtain: boolean = false): Promise { - scene.unshiftPhase(new VictoryPhase(scene, pokemon.id, true)); - const speciesForm = !pokemon.fusionSpecies ? pokemon.getSpeciesForm() : pokemon.getFusionSpeciesForm(); if (speciesForm.abilityHidden && (pokemon.fusionSpecies ? pokemon.fusionAbilityIndex : pokemon.abilityIndex) === speciesForm.getAbilityCount() - 1) { @@ -528,6 +531,11 @@ export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, po return new Promise(resolve => { const doPokemonCatchMenu = () => { const end = () => { + // Ensure the pokemon is in the enemy party in all situations + if (!scene.getEnemyParty().some(p => p.id === pokemon.id)) { + scene.getEnemyParty().push(pokemon); + } + scene.unshiftPhase(new VictoryPhase(scene, pokemon.id, true)); scene.pokemonInfoContainer.hide(); if (pokeball) { removePb(scene, pokeball); @@ -539,8 +547,8 @@ export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, po scene.field.remove(pokemon, true); } }; - const addToParty = () => { - const newPokemon = pokemon.addToParty(pokeballType); + const addToParty = (slotIndex?: number) => { + const newPokemon = pokemon.addToParty(pokeballType, slotIndex); const modifiers = scene.findModifiers(m => m instanceof PokemonHeldItemModifier, false); if (scene.getParty().filter(p => p.isShiny()).length === 6) { scene.validateAchv(achvs.SHINY_PARTY); @@ -559,12 +567,19 @@ export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, po if (scene.getParty().length === 6) { const promptRelease = () => { scene.ui.showText(i18next.t("battle:partyFull", { pokemonName: pokemon.getNameToRender() }), null, () => { - scene.pokemonInfoContainer.makeRoomForConfirmUi(); + scene.pokemonInfoContainer.makeRoomForConfirmUi(1, true); scene.ui.setMode(Mode.CONFIRM, () => { - scene.ui.setMode(Mode.PARTY, PartyUiMode.RELEASE, 0, (slotIndex: number, _option: PartyOption) => { + const newPokemon = scene.addPlayerPokemon(pokemon.species, pokemon.level, pokemon.abilityIndex, pokemon.formIndex, pokemon.gender, pokemon.shiny, pokemon.variant, pokemon.ivs, pokemon.nature, pokemon); + scene.ui.setMode(Mode.SUMMARY, newPokemon, 0, SummaryUiMode.DEFAULT, () => { + scene.ui.setMode(Mode.MESSAGE).then(() => { + promptRelease(); + }); + }, false); + }, () => { + scene.ui.setMode(Mode.PARTY, PartyUiMode.RELEASE, 0, (slotIndex: integer, _option: PartyOption) => { scene.ui.setMode(Mode.MESSAGE).then(() => { if (slotIndex < 6) { - addToParty(); + addToParty(slotIndex); } else { promptRelease(); } @@ -575,7 +590,7 @@ export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, po removePokemon(); end(); }); - }); + }, "fullParty"); }); }; promptRelease(); @@ -711,13 +726,50 @@ export function getGoldenBugNetSpecies(): PokemonSpecies { const roll = randSeedInt(totalWeight); let w = 0; - for (const species of GOLDEN_BUG_NET_SPECIES_POOL) { - w += species[1]; + for (const speciesWeightPair of GOLDEN_BUG_NET_SPECIES_POOL) { + w += speciesWeightPair[1]; if (roll < w) { - return getPokemonSpecies(species); + return getPokemonSpecies(speciesWeightPair[0]); } } // Defaults to Scyther return getPokemonSpecies(Species.SCYTHER); } + +/** + * Generates a Pokemon level for a given wave, with an option to increase/decrease by a scaling modifier + * @param scene + * @param levelAdditiveModifier Default 0. will add +(1 level / 10 waves * levelAdditiveModifier) to the level calculation + */ +export function getEncounterPokemonLevelForWave(scene: BattleScene, levelAdditiveModifier: number = 0) { + const currentBattle = scene.currentBattle; + // Default to use the first generated level from enemyLevels, or generate a new one if it DNE + const baseLevel = currentBattle.enemyLevels && currentBattle.enemyLevels?.length > 0 ? currentBattle.enemyLevels[0] : currentBattle.getLevelForWave(); + + // Add a level scaling modifier that is (+1 level per 10 waves) * levelAdditiveModifier + return baseLevel + Math.max(Math.round((currentBattle.waveIndex / 10) * levelAdditiveModifier), 0); +} + +export async function addPokemonDataToDexAndValidateAchievements(scene: BattleScene, pokemon: PlayerPokemon) { + const speciesForm = !pokemon.fusionSpecies ? pokemon.getSpeciesForm() : pokemon.getFusionSpeciesForm(); + + if (speciesForm.abilityHidden && (pokemon.fusionSpecies ? pokemon.fusionAbilityIndex : pokemon.abilityIndex) === speciesForm.getAbilityCount() - 1) { + scene.validateAchv(achvs.HIDDEN_ABILITY); + } + + if (pokemon.species.subLegendary) { + scene.validateAchv(achvs.CATCH_SUB_LEGENDARY); + } + + if (pokemon.species.legendary) { + scene.validateAchv(achvs.CATCH_LEGENDARY); + } + + if (pokemon.species.mythical) { + scene.validateAchv(achvs.CATCH_MYTHICAL); + } + + scene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs); + return scene.gameData.setPokemonCaught(pokemon, true, false, false); +} diff --git a/src/data/mystery-encounters/utils/encounter-transformation-sequence.ts b/src/data/mystery-encounters/utils/encounter-transformation-sequence.ts index fd9d43829e5..fcadb101817 100644 --- a/src/data/mystery-encounters/utils/encounter-transformation-sequence.ts +++ b/src/data/mystery-encounters/utils/encounter-transformation-sequence.ts @@ -103,7 +103,7 @@ export function doPokemonTransformationSequence(scene: BattleScene, previousPoke scene.time.delayedCall(1000, () => { pokemonEvoTintSprite.setScale(0.25); pokemonEvoTintSprite.setVisible(true); - doCycle(scene, 2, 6, pokemonTintSprite, pokemonEvoTintSprite).then(() => { + doCycle(scene, 1.5, 6, pokemonTintSprite, pokemonEvoTintSprite).then(() => { pokemonEvoSprite.setVisible(true); doCircleInward(scene, transformationBaseBg, transformationContainer, xOffset, yOffset); @@ -115,7 +115,7 @@ export function doPokemonTransformationSequence(scene: BattleScene, previousPoke delay: 150, easing: "Sine.easeIn", onComplete: () => { - scene.time.delayedCall(2500, () => { + scene.time.delayedCall(3000, () => { resolve(); scene.tweens.add({ targets: pokemonEvoSprite, diff --git a/src/data/pokemon-forms.ts b/src/data/pokemon-forms.ts index 4fc833939e4..a904f497b0f 100644 --- a/src/data/pokemon-forms.ts +++ b/src/data/pokemon-forms.ts @@ -684,7 +684,7 @@ export const pokemonFormChanges: PokemonFormChanges = { new SpeciesFormChange(Species.GROUDON, "", SpeciesFormKey.PRIMAL, new SpeciesFormChangeItemTrigger(FormChangeItem.RED_ORB)) ], [Species.RAYQUAZA]: [ - new SpeciesFormChange(Species.RAYQUAZA, "", SpeciesFormKey.MEGA, new SpeciesFormChangeCompoundTrigger(new SpeciesFormChangeItemTrigger(FormChangeItem.RAYQUAZITE), new SpeciesFormChangeMoveLearnedTrigger(Moves.DRAGON_ASCENT))) + new SpeciesFormChange(Species.RAYQUAZA, "", SpeciesFormKey.MEGA, new SpeciesFormChangeItemTrigger(FormChangeItem.RAYQUAZITE)) ], [Species.DEOXYS]: [ new SpeciesFormChange(Species.DEOXYS, "normal", "attack", new SpeciesFormChangeItemTrigger(FormChangeItem.SHARP_METEORITE)), diff --git a/src/enums/variant-tiers.ts b/src/enums/variant-tiers.ts deleted file mode 100644 index 20a0e8ec4e4..00000000000 --- a/src/enums/variant-tiers.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum VariantTier { - COMMON, - RARE, - EPIC -} diff --git a/src/field/arena.ts b/src/field/arena.ts index 0466c01c82b..9ce19c85283 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -746,7 +746,7 @@ export class Arena { case Biome.TOWN: return 7.288; case Biome.PLAINS: - return 7.693; + return 17.485; case Biome.GRASS: return 1.995; case Biome.TALL_GRASS: @@ -762,7 +762,7 @@ export class Arena { case Biome.BEACH: return 3.462; case Biome.LAKE: - return 5.350; + return 7.215; case Biome.SEABED: return 2.600; case Biome.MOUNTAIN: @@ -774,13 +774,13 @@ export class Arena { case Biome.DESERT: return 1.143; case Biome.ICE_CAVE: - return 15.010; + return 0.000; case Biome.MEADOW: return 3.891; case Biome.POWER_PLANT: - return 2.810; + return 9.447; case Biome.VOLCANO: - return 5.116; + return 17.637; case Biome.GRAVEYARD: return 3.232; case Biome.DOJO: @@ -788,7 +788,7 @@ export class Arena { case Biome.FACTORY: return 4.985; case Biome.RUINS: - return 2.270; + return 0.000; case Biome.WASTELAND: return 6.336; case Biome.ABYSS: diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 9c8c1e6ce46..1019bcf86ab 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -3,7 +3,7 @@ import BattleScene, { AnySound } from "../battle-scene"; import { Variant, VariantSet, variantColorCache } from "#app/data/variant"; import { variantData } from "#app/data/variant"; import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "../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 } from "../data/move"; +import Move, { HighCritAttr, DealsDoubleDamageToTagAttr, 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 } from "../data/move"; import { default as PokemonSpecies, PokemonSpeciesForm, SpeciesFormKey, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm, getStarterValueFriendshipCap, speciesStarters, starterPassiveAbilities } from "../data/pokemon-species"; import { Constructor, isNullOrUndefined, randSeedInt } from "#app/utils"; import * as Utils from "../utils"; @@ -95,6 +95,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { public metLevel: integer; public metBiome: Biome | -1; public metSpecies: Species; + public metWave: number; public luck: integer; public pauseEvolutions: boolean; public pokerus: boolean; @@ -194,6 +195,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.luck = dataSource.luck; this.metBiome = dataSource.metBiome; this.metSpecies = dataSource.metSpecies ?? (this.metBiome !== -1 ? this.species.speciesId : this.species.getRootSpeciesId(true)); + this.metWave = dataSource.metWave ?? (this.metBiome === -1 ? -1 : 0); this.pauseEvolutions = dataSource.pauseEvolutions; this.pokerus = !!dataSource.pokerus; this.evoCounter = dataSource.evoCounter ?? 0; @@ -240,6 +242,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.metLevel = level; this.metBiome = scene.currentBattle ? scene.arena.biomeType : -1; this.metSpecies = species.speciesId; + this.metWave = scene.currentBattle ? scene.currentBattle.waveIndex : -1; this.pokerus = false; if (level > 1) { @@ -1270,13 +1273,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param attrType {@linkcode AbAttr} The ability attribute to check for. * @param canApply {@linkcode Boolean} If false, it doesn't check whether the ability is currently active * @param ignoreOverride {@linkcode Boolean} If true, it ignores ability changing effects - * @returns {AbAttr[]} A list of all the ability attributes on this ability. + * @returns A list of all the ability attributes on this ability. */ - getAbilityAttrs(attrType: { new(...args: any[]): AbAttr }, canApply: boolean = true, ignoreOverride?: boolean): AbAttr[] { - const abilityAttrs: AbAttr[] = []; + getAbilityAttrs(attrType: { new(...args: any[]): T }, canApply: boolean = true, ignoreOverride?: boolean): T[] { + const abilityAttrs: T[] = []; if (!canApply || this.canApplyAbility()) { - abilityAttrs.push(...this.getAbility(ignoreOverride).getAttrs(attrType)); + abilityAttrs.push(...this.getAbility(ignoreOverride).getAttrs(attrType)); } if (!canApply || this.canApplyAbility(true)) { @@ -1510,7 +1513,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const immuneTags = this.findTags(tag => tag instanceof TypeImmuneTag && tag.immuneType === moveType); for (const tag of immuneTags) { - if (move && !move.getAttrs(HitsTagAttr).some(attr => attr.tagType === tag.tagType)) { + if (move && !move.getAttrs(DealsDoubleDamageToTagAttr).some(attr => attr.tagType === tag.tagType)) { typeMultiplier.value = 0; break; } @@ -2319,11 +2322,61 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return accuracyMultiplier.value / evasionMultiplier.value; } + /** + * Calculates the base damage of the given move against this Pokemon when attacked by the given source. + * Used during damage calculation and for Shell Side Arm's forecasting effect. + * @param source the attacking {@linkcode Pokemon}. + * @param move the {@linkcode Move} used in the attack. + * @param moveCategory the move's {@linkcode MoveCategory} after variable-category effects are applied. + * @param ignoreAbility if `true`, ignores this Pokemon's defensive ability effects (defaults to `false`). + * @param ignoreSourceAbility if `true`, ignore's the attacking Pokemon's ability effects (defaults to `false`). + * @param isCritical if `true`, calculates effective stats as if the hit were critical (defaults to `false`). + * @param simulated if `true`, suppresses changes to game state during calculation (defaults to `true`). + * @returns The move's base damage against this Pokemon when used by the source Pokemon. + */ + getBaseDamage(source: Pokemon, move: Move, moveCategory: MoveCategory, ignoreAbility: boolean = false, ignoreSourceAbility: boolean = false, isCritical: boolean = false, simulated: boolean = true): number { + const isPhysical = moveCategory === MoveCategory.PHYSICAL; + + /** A base damage multiplier based on the source's level */ + const levelMultiplier = (2 * source.level / 5 + 2); + + /** The power of the move after power boosts from abilities, etc. have applied */ + const power = move.calculateBattlePower(source, this, simulated); + + /** + * The attacker's offensive stat for the given move's category. + * Critical hits cause negative stat stages to be ignored. + */ + const sourceAtk = new Utils.NumberHolder(source.getEffectiveStat(isPhysical ? Stat.ATK : Stat.SPATK, this, undefined, ignoreSourceAbility, ignoreAbility, isCritical, simulated)); + applyMoveAttrs(VariableAtkAttr, source, this, move, sourceAtk); + + /** + * This Pokemon's defensive stat for the given move's category. + * Critical hits cause positive stat stages to be ignored. + */ + const targetDef = new Utils.NumberHolder(this.getEffectiveStat(isPhysical ? Stat.DEF : Stat.SPDEF, source, move, ignoreAbility, ignoreSourceAbility, isCritical, simulated)); + applyMoveAttrs(VariableDefAttr, source, this, move, targetDef); + + /** + * The attack's base damage, as determined by the source's level, move power + * and Attack stat as well as this Pokemon's Defense stat + */ + const baseDamage = ((levelMultiplier * power * sourceAtk.value / targetDef.value) / 50) + 2; + + /** Debug message for non-simulated calls (i.e. when damage is actually dealt) */ + if (!simulated) { + console.log("base damage", baseDamage, move.name, power, sourceAtk.value, targetDef.value); + } + + return baseDamage; + } + /** * Calculates the damage of an attack made by another Pokemon against this Pokemon * @param source {@linkcode Pokemon} the attacking Pokemon * @param move {@linkcode Pokemon} the move used in the attack * @param ignoreAbility If `true`, ignores this Pokemon's defensive ability effects + * @param ignoreSourceAbility If `true`, ignores the attacking Pokemon's ability effects * @param isCritical If `true`, calculates damage for a critical hit. * @param simulated If `true`, suppresses changes to game state during the calculation. * @returns a {@linkcode DamageCalculationResult} object with three fields: @@ -2392,35 +2445,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { }; } - // ----- BEGIN BASE DAMAGE MULTIPLIERS ----- - - /** A base damage multiplier based on the source's level */ - const levelMultiplier = (2 * source.level / 5 + 2); - - /** The power of the move after power boosts from abilities, etc. have applied */ - const power = move.calculateBattlePower(source, this, simulated); - - /** - * The attacker's offensive stat for the given move's category. - * Critical hits ignore negative stat stages. - */ - const sourceAtk = new Utils.NumberHolder(source.getEffectiveStat(isPhysical ? Stat.ATK : Stat.SPATK, this, undefined, ignoreSourceAbility, ignoreAbility, isCritical, simulated)); - applyMoveAttrs(VariableAtkAttr, source, this, move, sourceAtk); - - /** - * This Pokemon's defensive stat for the given move's category. - * Critical hits ignore positive stat stages. - */ - const targetDef = new Utils.NumberHolder(this.getEffectiveStat(isPhysical ? Stat.DEF : Stat.SPDEF, source, move, ignoreAbility, ignoreSourceAbility, isCritical, simulated)); - applyMoveAttrs(VariableDefAttr, source, this, move, targetDef); - /** * The attack's base damage, as determined by the source's level, move power * and Attack stat as well as this Pokemon's Defense stat */ - const baseDamage = ((levelMultiplier * power * sourceAtk.value / targetDef.value) / 50) + 2; - - // ------ END BASE DAMAGE MULTIPLIERS ------ + const baseDamage = this.getBaseDamage(source, move, moveCategory, ignoreAbility, ignoreSourceAbility, isCritical, simulated); /** 25% damage debuff on moves hitting more than one non-fainted target (regardless of immunities) */ const { targets, multiple } = getMoveTargets(source, move.id); @@ -2486,13 +2515,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.scene.arena.applyTagsForSide(WeakenMoveScreenTag, defendingSide, move.category, this.scene.currentBattle.double, screenMultiplier); /** - * For each {@linkcode HitsTagAttr} the move has, doubles the damage of the move if: + * For each {@linkcode DealsDoubleDamageToTagAttr} the move has, doubles the damage of the move if: * The target has a {@linkcode BattlerTagType} that this move interacts with * AND * The move doubles damage when used against that tag */ const hitsTagMultiplier = new Utils.NumberHolder(1); - move.getAttrs(HitsTagAttr).filter(hta => hta.doubleDamage).forEach(hta => { + move.getAttrs(DealsDoubleDamageToTagAttr).filter(hta => hta.doubleDamage).forEach(hta => { if (this.getTag(hta.tagType)) { hitsTagMultiplier.value *= 2; } @@ -2546,7 +2575,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { // debug message for when damage is applied (i.e. not simulated) if (!simulated) { - console.log("damage", damage.value, move.name, power, sourceAtk, targetDef); + console.log("damage", damage.value, move.name); } let hitResult: HitResult; @@ -4081,6 +4110,7 @@ export class PlayerPokemon extends Pokemon { newPokemon.metLevel = this.metLevel; newPokemon.metBiome = this.metBiome; newPokemon.metSpecies = this.metSpecies; + newPokemon.metWave = this.metWave; newPokemon.fusionSpecies = this.fusionSpecies; newPokemon.fusionFormIndex = this.fusionFormIndex; newPokemon.fusionAbilityIndex = this.fusionAbilityIndex; @@ -4088,6 +4118,7 @@ export class PlayerPokemon extends Pokemon { newPokemon.fusionVariant = this.fusionVariant; newPokemon.fusionGender = this.fusionGender; newPokemon.fusionLuck = this.fusionLuck; + newPokemon.usedTMs = this.usedTMs; this.scene.getParty().push(newPokemon); newPokemon.evolve((!isFusion ? newEvolution : new FusionSpeciesFormEvolution(this.id, newEvolution)), evoSpecies); @@ -4779,6 +4810,7 @@ export class EnemyPokemon extends Pokemon { this.pokeball = pokeballType; this.metLevel = this.level; this.metBiome = this.scene.arena.biomeType; + this.metWave = this.scene.currentBattle.waveIndex; this.metSpecies = this.species.speciesId; const newPokemon = this.scene.addPlayerPokemon(this.species, this.level, this.abilityIndex, this.formIndex, this.gender, this.shiny, this.variant, this.ivs, this.nature, this); diff --git a/src/loading-scene.ts b/src/loading-scene.ts index d0818aa1e19..c3cb494d497 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -241,12 +241,15 @@ export class LoadingScene extends SceneBase { const lang = i18next.resolvedLanguage; if (lang !== "en") { if (Utils.verifyLang(lang)) { + this.loadAtlas(`statuses_${lang}`, ""); this.loadAtlas(`types_${lang}`, ""); } else { // Fallback to English + this.loadAtlas("statuses", ""); this.loadAtlas("types", ""); } } else { + this.loadAtlas("statuses", ""); this.loadAtlas("types", ""); } const availableLangs = ["en", "de", "it", "fr", "ja", "ko", "es", "pt-BR", "zh-CN"]; diff --git a/src/locales/de/pokemon-summary.json b/src/locales/de/pokemon-summary.json index 1790c6878b9..3104fc10151 100644 --- a/src/locales/de/pokemon-summary.json +++ b/src/locales/de/pokemon-summary.json @@ -11,7 +11,7 @@ "cancel": "Abbrechen", "memoString": "Wesen: {{natureFragment}}\n{{metFragment}}", "metFragment": { - "normal": "Herkunft: {{biome}}\nMit Lv. {{level}} erhalten.", + "normal": "Herkunft: {{biome}} - Welle {{wave}}\nMit Lv. {{level}} erhalten.", "apparently": "Herkunft: {{biome}}\nOffenbar mit Lv. {{level}} erhalten." }, "natureFragment": { diff --git a/src/locales/en/bgm-name.json b/src/locales/en/bgm-name.json index c2babed4dff..f38b62d14c6 100644 --- a/src/locales/en/bgm-name.json +++ b/src/locales/en/bgm-name.json @@ -108,17 +108,17 @@ "forest": "PMD EoS Dusk Forest", "grass": "PMD EoS Apple Woods", "graveyard": "PMD EoS Mystifying Forest", - "ice_cave": "PMD EoS Vast Ice Mountain", + "ice_cave": "Firel - -60F", "island": "PMD EoS Craggy Coast", "jungle": "Lmz - Jungle", "laboratory": "Firel - Laboratory", - "lake": "PMD EoS Crystal Cave", + "lake": "Lmz - Lake", "meadow": "PMD EoS Sky Peak Forest", "metropolis": "Firel - Metropolis", "mountain": "PMD EoS Mt. Horn", - "plains": "PMD EoS Sky Peak Prairie", - "power_plant": "PMD EoS Far Amp Plains", - "ruins": "PMD EoS Deep Sealed Ruin", + "plains": "Firel - Route 888", + "power_plant": "Firel - The Klink", + "ruins": "Lmz - Ancient Ruins", "sea": "Andr06 - Marine Mystique", "seabed": "Firel - Seabed", "slum": "Andr06 - Sneaky Snom", @@ -128,7 +128,7 @@ "tall_grass": "PMD EoS Foggy Forest", "temple": "PMD EoS Aegis Cave", "town": "PMD EoS Random Dungeon Theme 3", - "volcano": "PMD EoS Steam Cave", + "volcano": "Firel - Twisturn Volcano", "wasteland": "PMD EoS Hidden Highland", "encounter_ace_trainer": "BW Trainers' Eyes Meet (Ace Trainer)", "encounter_backpacker": "BW Trainers' Eyes Meet (Backpacker)", @@ -151,5 +151,6 @@ "mystery_encounter_weird_dream": "PMD EoS Temporal Spire", "mystery_encounter_fun_and_games": "PMD EoS Guildmaster Wigglytuff", "mystery_encounter_gen_5_gts": "BW GTS", - "mystery_encounter_gen_6_gts": "XY GTS" + "mystery_encounter_gen_6_gts": "XY GTS", + "mystery_encounter_delibirdy": "Firel - DeliDelivery!" } diff --git a/src/locales/en/mystery-encounters/an-offer-you-cant-refuse-dialogue.json b/src/locales/en/mystery-encounters/an-offer-you-cant-refuse-dialogue.json index 6dd54d302ab..e286d89691a 100644 --- a/src/locales/en/mystery-encounters/an-offer-you-cant-refuse-dialogue.json +++ b/src/locales/en/mystery-encounters/an-offer-you-cant-refuse-dialogue.json @@ -1,7 +1,7 @@ { "intro": "You're stopped by a rich looking boy.", "speaker": "Rich Boy", - "intro_dialogue": "Good day to you.$I can't help but notice that your\n{{strongestPokemon}} looks positively divine!$I've always wanted to have a pet like that!$I'd pay you handsomely,\nand also give you this old bauble!", + "intro_dialogue": "Good day to you.$I can't help but notice that your\n{{strongestPokemon}} looks positively divine!$I've always wanted to have a Pokémon like that!$I'd pay you handsomely,\nand also give you this old bauble!", "title": "An Offer You Can't Refuse", "description": "You're being offered a @[TOOLTIP_TITLE]{Shiny Charm} and {{price, money}} for your {{strongestPokemon}}!\n\nIt's an extremely good deal, but can you really bear to part with such a strong team member?", "query": "What will you do?", diff --git a/src/locales/en/mystery-encounters/bug-type-superfan-dialogue.json b/src/locales/en/mystery-encounters/bug-type-superfan-dialogue.json index 7df01326aed..09488addb98 100644 --- a/src/locales/en/mystery-encounters/bug-type-superfan-dialogue.json +++ b/src/locales/en/mystery-encounters/bug-type-superfan-dialogue.json @@ -17,9 +17,9 @@ "disabled_tooltip": "You need at least 1 Bug Type Pokémon on your team to select this.", "selected": "You show the trainer all your Bug Type Pokémon...", "selected_0_to_1": "Huh? You only have {{numBugTypes}}...$Guess I'm wasting my breath on someone like you...", - "selected_2_to_3": "Hey, you've got {{numBugTypes}} Bug Types!\nNot bad.$Here, this might help you on your journey to catch more!", - "selected_4_to_5": "What? You have {{numBugTypes}} Bug Types?\nNice!$You're not quite at my level, but I can see shades of myself in you!\n$Take this, my young apprentice!", - "selected_6": "Whoa! {{numBugTypes}} Bug Types!\n$You must love Bug Types almost as much as I do!$Here, take this as a token of our camaraderie!" + "selected_2_to_3": "Hey, you've got {{numBugTypes}}!\nNot bad.$Here, this might help you on your journey to catch more!", + "selected_4_to_5": "What? You have {{numBugTypes}}?\nNice!$You're not quite at my level, but I can see shades of myself in you!\n$Take this, my young apprentice!", + "selected_6": "Whoa! {{numBugTypes}}!\n$You must love Bug Types almost as much as I do!$Here, take this as a token of our camaraderie!" }, "3": { "label": "Gift a Bug Item", @@ -34,5 +34,7 @@ "battle_won": "Your knowledge and skill were perfect at exploiting our weaknesses!$In exchange for the valuable lesson,\nallow me to teach one of your Pokémon a Bug Type Move!", "teach_move_prompt": "Select a move to teach a Pokémon.", "confirm_no_teach": "You sure you don't want to learn one of these great moves?", - "outro": "I see great Bug Pokémon in your future!\nMay our paths cross again!$Bug out!" + "outro": "I see great Bug Pokémon in your future!\nMay our paths cross again!$Bug out!", + "numBugTypes_one": "{{count}} Bug Type", + "numBugTypes_other": "{{count}} Bug Types" } \ No newline at end of file diff --git a/src/locales/en/pokemon-summary.json b/src/locales/en/pokemon-summary.json index 80e0cdab010..458fad0efe0 100644 --- a/src/locales/en/pokemon-summary.json +++ b/src/locales/en/pokemon-summary.json @@ -11,7 +11,7 @@ "cancel": "Cancel", "memoString": "{{natureFragment}} nature,\n{{metFragment}}", "metFragment": { - "normal": "met at Lv{{level}},\n{{biome}}.", + "normal": "met at Lv{{level}},\n{{biome}}, Wave {{wave}}.", "apparently": "apparently met at Lv{{level}},\n{{biome}}." }, "natureFragment": { diff --git a/src/locales/es/pokemon-summary.json b/src/locales/es/pokemon-summary.json index e47335c8394..fe33c9418cc 100644 --- a/src/locales/es/pokemon-summary.json +++ b/src/locales/es/pokemon-summary.json @@ -11,7 +11,7 @@ "cancel": "Salir", "memoString": "Naturaleza {{natureFragment}},\n{{metFragment}}", "metFragment": { - "normal": "encontrado al Nv. {{level}},\n{{biome}}.", + "normal": "encontrado al Nv. {{level}},\n{{biome}}, Oleada {{wave}}.", "apparently": "aparentemente encontrado al Nv. {{level}},\n{{biome}}." } -} \ No newline at end of file +} diff --git a/src/locales/fr/pokemon-summary.json b/src/locales/fr/pokemon-summary.json index 01e712c8468..a038b3a51f9 100644 --- a/src/locales/fr/pokemon-summary.json +++ b/src/locales/fr/pokemon-summary.json @@ -11,7 +11,7 @@ "cancel": "Annuler", "memoString": "{{natureFragment}} de nature,\n{{metFragment}}", "metFragment": { - "normal": "rencontré au N.{{level}},\n{{biome}}.", + "normal": "rencontré au N.{{level}},\n{{biome}}, Vague {{wave}}.", "apparently": "apparemment rencontré au N.{{level}},\n{{biome}}." }, "natureFragment": { diff --git a/src/locales/it/pokemon-summary.json b/src/locales/it/pokemon-summary.json index f6c9290f783..81cd9a278b8 100644 --- a/src/locales/it/pokemon-summary.json +++ b/src/locales/it/pokemon-summary.json @@ -11,7 +11,7 @@ "cancel": "Annulla", "memoString": "Natura {{natureFragment}},\n{{metFragment}}", "metFragment": { - "normal": "incontrato al Lv.{{level}},\n{{biome}}.", + "normal": "incontrato al Lv.{{level}},\n{{biome}}, Onda {{wave}}.", "apparently": "apparentemente incontrato al Lv.{{level}},\n{{biome}}." } -} \ No newline at end of file +} diff --git a/src/locales/ja/pokemon-summary.json b/src/locales/ja/pokemon-summary.json index cf35befe6fd..9465bcd346d 100644 --- a/src/locales/ja/pokemon-summary.json +++ b/src/locales/ja/pokemon-summary.json @@ -11,7 +11,7 @@ "cancel": "キャンセル", "memoString": "{{natureFragment}}な性格。\n{{metFragment}}", "metFragment": { - "normal": "{{biome}}で\nLv.{{level}}の時に出会った。", + "normal": "ラウンド{{wave}}に{{biome}}で\nLv.{{level}}の時に出会った。", "apparently": "{{biome}}で\nLv.{{level}}の時に出会ったようだ。" }, "natureFragment": { diff --git a/src/locales/ko/party-ui-handler.json b/src/locales/ko/party-ui-handler.json index 468f33bf960..6e7ba120b71 100644 --- a/src/locales/ko/party-ui-handler.json +++ b/src/locales/ko/party-ui-handler.json @@ -13,8 +13,10 @@ "ALL": "전부", "PASS_BATON": "배턴터치한다", "UNPAUSE_EVOLUTION": "진화 재개", + "PAUSE_EVOLUTION": "진화 중지", "REVIVE": "되살린다", "RENAME": "닉네임 바꾸기", + "SELECT": "선택한다", "choosePokemon": "포켓몬을 선택하세요.", "doWhatWithThisPokemon": "포켓몬을 어떻게 하겠습니까?", "noEnergy": "{{pokemonName}}[[는]] 싸울 수 있는\n기력이 남아 있지 않습니다!", @@ -23,6 +25,7 @@ "tooManyItems": "{{pokemonName}}[[는]] 지닌 도구의 수가\n너무 많습니다", "anyEffect": "써도 효과가 없다.", "unpausedEvolutions": "{{pokemonName}}의 진화가 재개되었다.", + "pausedEvolutions": "{{pokemonName}}[[가]] 진화하지 않도록 했다.", "unspliceConfirmation": "{{pokemonName}}로부터 {{fusionName}}의 융합을 해제하시겠습니까?\n{{fusionName}}는 사라지게 됩니다.", "wasReverted": "{{fusionName}}은 {{pokemonName}}의 모습으로 돌아갔습니다!", "releaseConfirmation": "{{pokemonName}}[[를]]\n정말 놓아주겠습니까?", diff --git a/src/locales/ko/pokemon-summary.json b/src/locales/ko/pokemon-summary.json index d9119623662..ca4b7a22b65 100644 --- a/src/locales/ko/pokemon-summary.json +++ b/src/locales/ko/pokemon-summary.json @@ -11,7 +11,7 @@ "cancel": "그만둔다", "memoString": "{{natureFragment}}.\n{{metFragment}}", "metFragment": { - "normal": "{{biome}}에서\n레벨 {{level}}일 때 만났다.", + "normal": "{{biome}}에서 웨이브{{wave}},\n레벨 {{level}}일 때 만났다.", "apparently": "{{biome}}에서\n레벨 {{level}}일 때 만난 것 같다." }, "natureFragment": { diff --git a/src/locales/ko/settings.json b/src/locales/ko/settings.json index c10046385e1..d0655009a3c 100644 --- a/src/locales/ko/settings.json +++ b/src/locales/ko/settings.json @@ -11,6 +11,10 @@ "expGainsSpeed": "경험치 획득 속도", "expPartyDisplay": "파티 경험치 표시", "skipSeenDialogues": "본 대화 생략", + "eggSkip": "알 스킵", + "never": "안 함", + "always": "항상", + "ask": "확인하기", "battleStyle": "시합 룰", "enableRetries": "재도전 허용", "hideIvs": "개체값탐지기 효과 끄기", diff --git a/src/locales/pt_BR/pokemon-summary.json b/src/locales/pt_BR/pokemon-summary.json index 4c427dbac4f..14b736a0cf2 100644 --- a/src/locales/pt_BR/pokemon-summary.json +++ b/src/locales/pt_BR/pokemon-summary.json @@ -11,7 +11,7 @@ "cancel": "Cancelar", "memoString": "Natureza {{natureFragment}},\n{{metFragment}}", "metFragment": { - "normal": "encontrado no Nv.{{level}},\n{{biome}}.", + "normal": "encontrado no Nv.{{level}},\n{{biome}}, Onda {{wave}}.", "apparently": "aparentemente encontrado no Nv.{{level}},\n{{biome}}." }, "natureFragment": { diff --git a/src/locales/zh_TW/pokemon-summary.json b/src/locales/zh_TW/pokemon-summary.json index ddbbea63a3a..331330f5bdd 100644 --- a/src/locales/zh_TW/pokemon-summary.json +++ b/src/locales/zh_TW/pokemon-summary.json @@ -12,7 +12,7 @@ "memoString": "{{natureFragment}} 性格,\n{{metFragment}}", "metFragment": { - "normal": "met at Lv{{level}},\n{{biome}}.", + "normal": "met at Lv{{level}},\n{{biome}}, Wave {{wave}}.", "apparently": "命中注定般地相遇于Lv.{{level}},\n{{biome}}。" }, "natureFragment": { diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 0c4d2a63802..81a3f4f81cc 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -600,7 +600,7 @@ export class TerastallizeAccessModifier extends PersistentModifier { export abstract class PokemonHeldItemModifier extends PersistentModifier { public pokemonId: integer; - readonly isTransferrable: boolean = true; + public isTransferable: boolean = true; constructor(type: ModifierType, pokemonId: integer, stackCount?: integer) { super(type, stackCount); @@ -699,7 +699,7 @@ export abstract class PokemonHeldItemModifier extends PersistentModifier { export abstract class LapsingPokemonHeldItemModifier extends PokemonHeldItemModifier { protected battlesLeft: integer; - readonly isTransferrable: boolean = false; + public isTransferable: boolean = false; constructor(type: ModifierTypes.ModifierType, pokemonId: integer, battlesLeft?: integer, stackCount?: integer) { super(type, pokemonId, stackCount); @@ -736,7 +736,7 @@ export abstract class LapsingPokemonHeldItemModifier extends PokemonHeldItemModi export class TerastallizeModifier extends LapsingPokemonHeldItemModifier { public teraType: Type; - readonly isTransferrable: boolean = false; + public isTransferable: boolean = false; constructor(type: ModifierTypes.TerastallizeModifierType, pokemonId: integer, teraType: Type, battlesLeft?: integer, stackCount?: integer) { super(type, pokemonId, battlesLeft || 10, stackCount); @@ -799,7 +799,7 @@ export class TerastallizeModifier extends LapsingPokemonHeldItemModifier { */ export class BaseStatModifier extends PokemonHeldItemModifier { protected stat: PermanentStat; - readonly isTransferrable: boolean = false; + public isTransferable: boolean = false; constructor(type: ModifierType, pokemonId: integer, stat: PermanentStat, stackCount?: integer) { super(type, pokemonId, stackCount); @@ -843,7 +843,7 @@ export class BaseStatModifier extends PokemonHeldItemModifier { export class EvoTrackerModifier extends PokemonHeldItemModifier { protected species: Species; protected required: integer; - readonly isTransferrable: boolean = false; + public isTransferable: boolean = false; constructor(type: ModifierType, pokemonId: integer, species: Species, required: integer, stackCount?: integer) { super(type, pokemonId, stackCount); @@ -880,7 +880,7 @@ export class EvoTrackerModifier extends PokemonHeldItemModifier { */ export class PokemonBaseStatTotalModifier extends PokemonHeldItemModifier { private statModifier: integer; - readonly isTransferrable: boolean = false; + public isTransferable: boolean = false; constructor(type: ModifierTypes.PokemonBaseStatTotalModifierType, pokemonId: integer, statModifier: integer, stackCount?: integer) { super(type, pokemonId, stackCount); @@ -929,7 +929,7 @@ export class PokemonBaseStatTotalModifier extends PokemonHeldItemModifier { export class PokemonBaseStatFlatModifier extends PokemonHeldItemModifier { private statModifier: integer; private stats: Stat[]; - readonly isTransferrable: boolean = false; + public isTransferable: boolean = false; constructor (type: ModifierType, pokemonId: integer, statModifier: integer, stats: Stat[], stackCount?: integer) { super(type, pokemonId, stackCount); @@ -979,7 +979,7 @@ export class PokemonBaseStatFlatModifier extends PokemonHeldItemModifier { * Currently used by Macho Brace item */ export class PokemonIncrementingStatModifier extends PokemonHeldItemModifier { - readonly isTransferrable: boolean = false; + public isTransferable: boolean = false; constructor (type: ModifierType, pokemonId: integer, stackCount?: integer) { super(type, pokemonId, stackCount); @@ -2346,7 +2346,7 @@ export class PokemonMultiHitModifier extends PokemonHeldItemModifier { export class PokemonFormChangeItemModifier extends PokemonHeldItemModifier { public formChangeItem: FormChangeItem; public active: boolean; - readonly isTransferrable: boolean = false; + public isTransferable: boolean = false; constructor(type: ModifierTypes.FormChangeItemModifierType, pokemonId: integer, formChangeItem: FormChangeItem, active: boolean, stackCount?: integer) { super(type, pokemonId, stackCount); @@ -2691,7 +2691,7 @@ export abstract class HeldItemTransferModifier extends PokemonHeldItemModifier { const transferredModifierTypes: ModifierTypes.ModifierType[] = []; const itemModifiers = pokemon.scene.findModifiers(m => m instanceof PokemonHeldItemModifier - && m.pokemonId === targetPokemon.id && m.isTransferrable, targetPokemon.isPlayer()) as PokemonHeldItemModifier[]; + && m.pokemonId === targetPokemon.id && m.isTransferable, targetPokemon.isPlayer()) as PokemonHeldItemModifier[]; let highestItemTier = itemModifiers.map(m => m.type.getOrInferTier(poolType)).reduce((highestTier, tier) => Math.max(tier!, highestTier), 0); // TODO: is this bang correct? let tierItemModifiers = itemModifiers.filter(m => m.type.getOrInferTier(poolType) === highestItemTier); @@ -2736,7 +2736,7 @@ export abstract class HeldItemTransferModifier extends PokemonHeldItemModifier { * @see {@linkcode modifierTypes[MINI_BLACK_HOLE]} */ export class TurnHeldItemTransferModifier extends HeldItemTransferModifier { - isTransferrable: boolean = true; + isTransferable: boolean = true; constructor(type: ModifierType, pokemonId: integer, stackCount?: integer) { super(type, pokemonId, stackCount); } @@ -2762,7 +2762,7 @@ export class TurnHeldItemTransferModifier extends HeldItemTransferModifier { } setTransferrableFalse(): void { - this.isTransferrable = false; + this.isTransferable = false; } } diff --git a/src/overrides.ts b/src/overrides.ts index 6b550d152c2..35ca299721b 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -6,7 +6,7 @@ import { PokeballType } from "#enums/pokeball"; import { Species } from "#enums/species"; import { StatusEffect } from "#enums/status-effect"; import { TimeOfDay } from "#enums/time-of-day"; -import { VariantTier } from "#enums/variant-tiers"; +import { VariantTier } from "#enums/variant-tier"; import { WeatherType } from "#enums/weather-type"; import { type PokeballCounts } from "./battle-scene"; import { Gender } from "./data/gender"; @@ -70,6 +70,8 @@ class DefaultOverrides { [PokeballType.MASTER_BALL]: 0, }, }; + /** Set to `true` to show all tutorials */ + readonly BYPASS_TUTORIAL_SKIP: boolean = false; // ---------------- // PLAYER OVERRIDES diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index 86e42acb26b..66e39cf98a5 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -16,6 +16,7 @@ import i18next from "i18next"; import { FieldPhase } from "./field-phase"; import { SelectTargetPhase } from "./select-target-phase"; import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; +import { isNullOrUndefined } from "#app/utils"; export class CommandPhase extends FieldPhase { protected fieldIndex: integer; @@ -179,14 +180,16 @@ export class CommandPhase extends FieldPhase { case Command.POKEMON: case Command.RUN: const isSwitch = command === Command.POKEMON; - if (!isSwitch && this.scene.arena.biomeType === Biome.END) { + const { currentBattle, arena } = this.scene; + const mysteryEncounterFleeAllowed = currentBattle.mysteryEncounter?.fleeAllowed; + if (!isSwitch && (arena.biomeType === Biome.END || (!isNullOrUndefined(mysteryEncounterFleeAllowed) && !mysteryEncounterFleeAllowed))) { this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); this.scene.ui.setMode(Mode.MESSAGE); this.scene.ui.showText(i18next.t("battle:noEscapeForce"), null, () => { this.scene.ui.showText("", 0); this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); }, null, true); - } else if (!isSwitch && (this.scene.currentBattle.battleType === BattleType.TRAINER || this.scene.currentBattle.mysteryEncounter?.encounterMode === MysteryEncounterMode.TRAINER_BATTLE)) { + } else if (!isSwitch && (currentBattle.battleType === BattleType.TRAINER || currentBattle.mysteryEncounter?.encounterMode === MysteryEncounterMode.TRAINER_BATTLE)) { this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); this.scene.ui.setMode(Mode.MESSAGE); this.scene.ui.showText(i18next.t("battle:noEscapeTrainer"), null, () => { @@ -197,12 +200,12 @@ export class CommandPhase extends FieldPhase { const batonPass = isSwitch && args[0] as boolean; const trappedAbMessages: string[] = []; if (batonPass || !playerPokemon.isTrapped(trappedAbMessages)) { - this.scene.currentBattle.turnCommands[this.fieldIndex] = isSwitch + currentBattle.turnCommands[this.fieldIndex] = isSwitch ? { command: Command.POKEMON, cursor: cursor, args: args } : { command: Command.RUN }; success = true; if (!isSwitch && this.fieldIndex) { - this.scene.currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true; + currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true; } } else if (trappedAbMessages.length > 0) { if (!isSwitch) { @@ -219,7 +222,7 @@ export class CommandPhase extends FieldPhase { // trapTag should be defined at this point, but just in case... if (!trapTag) { - this.scene.currentBattle.turnCommands[this.fieldIndex] = isSwitch + currentBattle.turnCommands[this.fieldIndex] = isSwitch ? { command: Command.POKEMON, cursor: cursor, args: args } : { command: Command.RUN }; break; diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index 1d9567ee9b3..012738df9ab 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -216,8 +216,8 @@ export class EncounterPhase extends BattlePhase { this.scene.ui.setMode(Mode.MESSAGE).then(() => { if (!this.loaded) { - //@ts-ignore - this.scene.gameData.saveAll(this.scene, true, battle.waveIndex % 10 === 1 || this.scene.lastSavePlayTime >= 300).then(success => { // TODO: get rid of ts-ignore + this.trySetWeatherIfNewBiome(); // Set weather before session gets saved + this.scene.gameData.saveAll(this.scene, true, battle.waveIndex % 10 === 1 || (this.scene.lastSavePlayTime ?? 0) >= 300).then(success => { this.scene.disableMenu = false; if (!success) { return this.scene.reset(true); @@ -250,10 +250,6 @@ export class EncounterPhase extends BattlePhase { } } - if (!this.loaded) { - this.scene.arena.trySetWeather(getRandomWeatherType(this.scene.arena), false); - } - const enemyField = this.scene.getEnemyField(); this.scene.tweens.add({ targets: [this.scene.arenaEnemy, this.scene.currentBattle.trainer, enemyField, this.scene.arenaPlayer, this.scene.trainer].flat(), @@ -519,4 +515,18 @@ export class EncounterPhase extends BattlePhase { } return false; } + + /** + * Set biome weather if and only if this encounter is the start of a new biome. + * + * By using function overrides, this should happen if and only if this phase + * is exactly a NewBiomeEncounterPhase or an EncounterPhase (to account for + * Wave 1 of a Daily Run), but NOT NextEncounterPhase (which starts the next + * wave in the same biome). + */ + trySetWeatherIfNewBiome(): void { + if (!this.loaded) { + this.scene.arena.trySetWeather(getRandomWeatherType(this.scene.arena), false); + } + } } diff --git a/src/phases/game-over-phase.ts b/src/phases/game-over-phase.ts index 8ab191324c6..438ff1aecb0 100644 --- a/src/phases/game-over-phase.ts +++ b/src/phases/game-over-phase.ts @@ -60,6 +60,11 @@ export class GameOverPhase extends BattlePhase { this.scene.ui.fadeOut(1250).then(() => { this.scene.reset(); this.scene.clearPhaseQueue(); + // If this is a ME, clear any residual visual sprites before reloading + const encounter = this.scene.currentBattle.mysteryEncounter; + if (encounter?.introVisuals) { + this.scene.field.remove(encounter.introVisuals, true); + } this.scene.gameData.loadSession(this.scene, this.scene.sessionSlotId).then(() => { this.scene.pushPhase(new EncounterPhase(this.scene, true)); diff --git a/src/phases/message-phase.ts b/src/phases/message-phase.ts index 2244980c899..1d953801178 100644 --- a/src/phases/message-phase.ts +++ b/src/phases/message-phase.ts @@ -6,14 +6,16 @@ export class MessagePhase extends Phase { private callbackDelay: integer | null; private prompt: boolean | null; private promptDelay: integer | null; + private speaker?: string; - constructor(scene: BattleScene, text: string, callbackDelay?: integer | null, prompt?: boolean | null, promptDelay?: integer | null) { + constructor(scene: BattleScene, text: string, callbackDelay?: integer | null, prompt?: boolean | null, promptDelay?: integer | null, speaker?: string) { super(scene); this.text = text; this.callbackDelay = callbackDelay!; // TODO: is this bang correct? this.prompt = prompt!; // TODO: is this bang correct? this.promptDelay = promptDelay!; // TODO: is this bang correct? + this.speaker = speaker; } start() { @@ -21,11 +23,15 @@ export class MessagePhase extends Phase { if (this.text.indexOf("$") > -1) { const pageIndex = this.text.indexOf("$"); - this.scene.unshiftPhase(new MessagePhase(this.scene, this.text.slice(pageIndex + 1), this.callbackDelay, this.prompt, this.promptDelay)); + this.scene.unshiftPhase(new MessagePhase(this.scene, this.text.slice(pageIndex + 1), this.callbackDelay, this.prompt, this.promptDelay, this.speaker)); this.text = this.text.slice(0, pageIndex).trim(); } - this.scene.ui.showText(this.text, null, () => this.end(), this.callbackDelay || (this.prompt ? 0 : 1500), this.prompt, this.promptDelay); + if (this.speaker) { + this.scene.ui.showDialogue(this.text, this.speaker, null, () => this.end(), this.callbackDelay || (this.prompt ? 0 : 1500), this.promptDelay ?? 0); + } else { + this.scene.ui.showText(this.text, null, () => this.end(), this.callbackDelay || (this.prompt ? 0 : 1500), this.prompt, this.promptDelay); + } } end() { diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index c3199166e84..e2fca951b2f 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, MoveEffectTrigger, ChargeAttr, MoveCategory, NoEffectAttr, HitsTagAttr } from "#app/data/move"; +import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, MoveEffectTrigger, ChargeAttr, MoveCategory, NoEffectAttr, DealsDoubleDamageToTagAttr } 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"; @@ -394,7 +394,7 @@ export class MoveEffectPhase extends PokemonPhase { } const semiInvulnerableTag = target.getTag(SemiInvulnerableTag); - if (semiInvulnerableTag && !this.move.getMove().getAttrs(HitsTagAttr).some(hta => hta.tagType === semiInvulnerableTag.tagType)) { + if (semiInvulnerableTag && !this.move.getMove().getAttrs(DealsDoubleDamageToTagAttr).some(hta => hta.tagType === semiInvulnerableTag.tagType)) { return false; } diff --git a/src/phases/mystery-encounter-phases.ts b/src/phases/mystery-encounter-phases.ts index 6c9d3fd8c1d..007b69650b9 100644 --- a/src/phases/mystery-encounter-phases.ts +++ b/src/phases/mystery-encounter-phases.ts @@ -3,7 +3,7 @@ import BattleScene from "../battle-scene"; import { Phase } from "../phase"; import { Mode } from "../ui/ui"; import { transitionMysteryEncounterIntroVisuals, OptionSelectSettings } from "../data/mystery-encounters/utils/encounter-phase-utils"; -import MysteryEncounterOption, { OptionPhaseCallback } from "../data/mystery-encounters/mystery-encounter-option"; +import MysteryEncounterOption, { OptionPhaseCallback } from "#app/data/mystery-encounters/mystery-encounter-option"; import { getCharVariantFromDialogue } from "../data/dialogue"; import { TrainerSlot } from "../data/trainer-config"; import { BattleSpec } from "#enums/battle-spec"; diff --git a/src/phases/new-biome-encounter-phase.ts b/src/phases/new-biome-encounter-phase.ts index 48d928402de..2a526a22ee2 100644 --- a/src/phases/new-biome-encounter-phase.ts +++ b/src/phases/new-biome-encounter-phase.ts @@ -17,8 +17,6 @@ export class NewBiomeEncounterPhase extends NextEncounterPhase { } } - this.scene.arena.trySetWeather(getRandomWeatherType(this.scene.arena), false); - for (const pokemon of this.scene.getParty().filter(p => p.isOnField())) { applyAbAttrs(PostBiomeChangeAbAttr, pokemon, null); } @@ -41,4 +39,11 @@ export class NewBiomeEncounterPhase extends NextEncounterPhase { } }); } + + /** + * Set biome weather. + */ + trySetWeatherIfNewBiome(): void { + this.scene.arena.trySetWeather(getRandomWeatherType(this.scene.arena), false); + } } diff --git a/src/phases/next-encounter-phase.ts b/src/phases/next-encounter-phase.ts index 7de2472dd35..d63823e4167 100644 --- a/src/phases/next-encounter-phase.ts +++ b/src/phases/next-encounter-phase.ts @@ -67,4 +67,10 @@ export class NextEncounterPhase extends EncounterPhase { } }); } + + /** + * Do nothing (since this is simply the next wave in the same biome). + */ + trySetWeatherIfNewBiome(): void { + } } diff --git a/src/phases/select-modifier-phase.ts b/src/phases/select-modifier-phase.ts index 39a0da1167f..58fb13ac466 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, Modifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { ExtraModifierModifier, HealShopCostModifier, Modifier, PokemonHeldItemModifier } 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"; @@ -10,7 +10,7 @@ import * as Utils from "#app/utils"; import { BattlePhase } from "./battle-phase"; import Overrides from "#app/overrides"; import { CustomModifierSettings } from "#app/modifier/modifier-type"; -import { isNullOrUndefined } from "#app/utils"; +import { isNullOrUndefined, NumberHolder } from "#app/utils"; export class SelectModifierPhase extends BattlePhase { private rerollCount: integer; @@ -69,11 +69,11 @@ export class SelectModifierPhase extends BattlePhase { } let modifierType: ModifierType; let cost: integer; + const rerollCost = this.getRerollCost(typeOptions, this.scene.lockModifierTiers); switch (rowCursor) { case 0: switch (cursor) { case 0: - const rerollCost = this.getRerollCost(typeOptions, this.scene.lockModifierTiers); if (rerollCost < 0 || this.scene.money < rerollCost) { this.scene.ui.playError(); return false; @@ -94,7 +94,7 @@ export class SelectModifierPhase extends BattlePhase { this.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.MODIFIER_TRANSFER, -1, (fromSlotIndex: integer, itemIndex: integer, itemQuantity: integer, toSlotIndex: integer) => { if (toSlotIndex !== undefined && fromSlotIndex < 6 && toSlotIndex < 6 && fromSlotIndex !== toSlotIndex && itemIndex > -1) { const itemModifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier - && m.isTransferrable && m.pokemonId === party[fromSlotIndex].id) as PokemonHeldItemModifier[]; + && m.isTransferable && m.pokemonId === party[fromSlotIndex].id) as PokemonHeldItemModifier[]; const itemModifier = itemModifiers[itemIndex]; this.scene.tryTransferHeldItemModifier(itemModifier, party[toSlotIndex], true, itemQuantity); } else { @@ -108,6 +108,11 @@ export class SelectModifierPhase extends BattlePhase { }); break; case 3: + if (rerollCost < 0) { + // Reroll lock button is also disabled when reroll is disabled + this.scene.ui.playError(); + return false; + } this.scene.lockModifierTiers = !this.scene.lockModifierTiers; const uiHandler = this.scene.ui.getHandler() as ModifierSelectUiHandler; uiHandler.setRerollCost(this.getRerollCost(typeOptions, this.scene.lockModifierTiers)); @@ -133,7 +138,10 @@ export class SelectModifierPhase extends BattlePhase { if (shopOption.type) { modifierType = shopOption.type; } - cost = shopOption.cost; + // Apply Black Sludge to healing item cost + const healingItemCost = new NumberHolder(shopOption.cost); + this.scene.applyModifier(HealShopCostModifier, true, healingItemCost); + cost = healingItemCost.value; break; } diff --git a/src/phases/stat-stage-change-phase.ts b/src/phases/stat-stage-change-phase.ts index 55faaa29903..4418c38c849 100644 --- a/src/phases/stat-stage-change-phase.ts +++ b/src/phases/stat-stage-change-phase.ts @@ -6,7 +6,7 @@ import Pokemon from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { ResetNegativeStatStageModifier } from "#app/modifier/modifier"; import { handleTutorial, Tutorial } from "#app/tutorial"; -import * as Utils from "#app/utils"; +import { NumberHolder, BooleanHolder } from "#app/utils"; import i18next from "i18next"; import { PokemonPhase } from "./pokemon-phase"; import { Stat, type BattleStat, getStatKey, getStatStageChangeDescriptionKey } from "#enums/stat"; @@ -42,17 +42,23 @@ export class StatStageChangePhase extends PokemonPhase { return this.end(); } + const stages = new NumberHolder(this.stages); + + if (!this.ignoreAbilities) { + applyAbAttrs(StatStageChangeMultiplierAbAttr, pokemon, null, false, stages); + } + let simulate = false; const filteredStats = this.stats.filter(stat => { - const cancelled = new Utils.BooleanHolder(false); + const cancelled = new BooleanHolder(false); - if (!this.selfTarget && this.stages < 0) { + if (!this.selfTarget && stages.value < 0) { // TODO: Include simulate boolean when tag applications can be simulated this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, cancelled); } - if (!cancelled.value && !this.selfTarget && this.stages < 0) { + if (!cancelled.value && !this.selfTarget && stages.value < 0) { applyPreStatStageChangeAbAttrs(ProtectStatAbAttr, pokemon, stat, cancelled, simulate); } @@ -64,12 +70,6 @@ export class StatStageChangePhase extends PokemonPhase { return !cancelled.value; }); - const stages = new Utils.IntegerHolder(this.stages); - - if (!this.ignoreAbilities) { - applyAbAttrs(StatStageChangeMultiplierAbAttr, pokemon, null, false, stages); - } - const relLevels = filteredStats.map(s => (stages.value >= 1 ? Math.min(pokemon.getStatStage(s) + stages.value, 6) : Math.max(pokemon.getStatStage(s) + stages.value, -6)) - pokemon.getStatStage(s)); this.onChange && this.onChange(this.getPokemon(), filteredStats, relLevels); diff --git a/src/system/egg-data.ts b/src/system/egg-data.ts index 785ae364efe..1c9c903688a 100644 --- a/src/system/egg-data.ts +++ b/src/system/egg-data.ts @@ -1,6 +1,6 @@ import { EggTier } from "#enums/egg-type"; import { Species } from "#enums/species"; -import { VariantTier } from "#enums/variant-tiers"; +import { VariantTier } from "#enums/variant-tier"; import { EGG_SEED, Egg } from "../data/egg"; import { EggSourceType } from "#app/enums/egg-source-types"; diff --git a/src/system/pokemon-data.ts b/src/system/pokemon-data.ts index 5e6c0d93c8c..0fd90e448a1 100644 --- a/src/system/pokemon-data.ts +++ b/src/system/pokemon-data.ts @@ -38,8 +38,9 @@ export default class PokemonData { public status: Status | null; public friendship: integer; public metLevel: integer; - public metBiome: Biome | -1; + public metBiome: Biome | -1; // -1 for starters public metSpecies: Species; + public metWave: number; // 0 for unknown (previous saves), -1 for starters public luck: integer; public pauseEvolutions: boolean; public pokerus: boolean; @@ -90,14 +91,14 @@ export default class PokemonData { this.metLevel = source.metLevel || 5; this.metBiome = source.metBiome !== undefined ? source.metBiome : -1; this.metSpecies = source.metSpecies; + this.metWave = source.metWave ?? (this.metBiome === -1 ? -1 : 0); this.luck = source.luck !== undefined ? source.luck : (source.shiny ? (source.variant + 1) : 0); if (!forHistory) { this.pauseEvolutions = !!source.pauseEvolutions; + this.evoCounter = source.evoCounter ?? 0; } this.pokerus = !!source.pokerus; - this.evoCounter = source.evoCounter ?? 0; - this.fusionSpecies = sourcePokemon ? sourcePokemon.fusionSpecies?.speciesId : source.fusionSpecies; this.fusionFormIndex = source.fusionFormIndex; this.fusionAbilityIndex = source.fusionAbilityIndex; diff --git a/src/test/abilities/contrary.test.ts b/src/test/abilities/contrary.test.ts index 95a209395dc..5221e821e70 100644 --- a/src/test/abilities/contrary.test.ts +++ b/src/test/abilities/contrary.test.ts @@ -31,7 +31,7 @@ describe("Abilities - Contrary", () => { }); it("should invert stat changes when applied", async() => { - await game.startBattle([ + await game.classicMode.startBattle([ Species.SLOWBRO ]); @@ -39,4 +39,39 @@ describe("Abilities - Contrary", () => { expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1); }, 20000); + + describe("With Clear Body", () => { + it("should apply positive effects", async () => { + game.override + .enemyPassiveAbility(Abilities.CLEAR_BODY) + .moveset([Moves.TAIL_WHIP]); + await game.classicMode.startBattle([Species.SLOWBRO]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1); + + game.move.select(Moves.TAIL_WHIP); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(1); + }); + + it("should block negative effects", async () => { + game.override + .enemyPassiveAbility(Abilities.CLEAR_BODY) + .enemyMoveset([Moves.HOWL, Moves.HOWL, Moves.HOWL, Moves.HOWL]) + .moveset([Moves.SPLASH]); + await game.classicMode.startBattle([Species.SLOWBRO]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1); + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(1); + }); + }); }); diff --git a/src/test/arena/grassy_terrain.test.ts b/src/test/arena/grassy_terrain.test.ts index e8064676741..efb2539885d 100644 --- a/src/test/arena/grassy_terrain.test.ts +++ b/src/test/arena/grassy_terrain.test.ts @@ -26,16 +26,16 @@ describe("Arena - Grassy Terrain", () => { game.override .battleType("single") .disableCrits() - .enemyLevel(30) - .enemySpecies(Species.SNORLAX) - .enemyAbility(Abilities.BALL_FETCH) - .enemyMoveset(Moves.SPLASH) + .enemyLevel(1) + .enemySpecies(Species.SHUCKLE) + .enemyAbility(Abilities.STURDY) + .enemyMoveset(Moves.FLY) .moveset([Moves.GRASSY_TERRAIN, Moves.EARTHQUAKE]) - .ability(Abilities.BALL_FETCH); + .ability(Abilities.NO_GUARD); }); it("halves the damage of Earthquake", async () => { - await game.classicMode.startBattle([Species.FEEBAS]); + await game.classicMode.startBattle([Species.TAUROS]); const eq = allMoves[Moves.EARTHQUAKE]; vi.spyOn(eq, "calculateBattlePower"); @@ -53,4 +53,19 @@ describe("Arena - Grassy Terrain", () => { expect(eq.calculateBattlePower).toHaveReturnedWith(50); }, TIMEOUT); + + it("Does not halve the damage of Earthquake if opponent is not grounded", async () => { + await game.classicMode.startBattle([Species.NINJASK]); + + const eq = allMoves[Moves.EARTHQUAKE]; + vi.spyOn(eq, "calculateBattlePower"); + + game.move.select(Moves.GRASSY_TERRAIN); + await game.toNextTurn(); + + game.move.select(Moves.EARTHQUAKE); + await game.phaseInterceptor.to("BerryPhase"); + + expect(eq.calculateBattlePower).toHaveReturnedWith(100); + }, TIMEOUT); }); diff --git a/src/test/battle/battle.test.ts b/src/test/battle/battle.test.ts index 6e15bbd99d9..554692374d2 100644 --- a/src/test/battle/battle.test.ts +++ b/src/test/battle/battle.test.ts @@ -24,7 +24,8 @@ import { Moves } from "#enums/moves"; import { PlayerGender } from "#enums/player-gender"; import { Species } from "#enums/species"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { Biome } from "#app/enums/biome"; describe("Test Battle Phase", () => { let phaserGame: Phaser.Game; @@ -290,22 +291,27 @@ describe("Test Battle Phase", () => { expect(game.scene.currentBattle.turn).toBeGreaterThan(turn); }, 20000); - it("to next wave with pokemon killed, single", async () => { + it("does not set new weather if staying in same biome", async () => { const moveToUse = Moves.SPLASH; - game.override.battleType("single"); - game.override.starterSpecies(Species.MEWTWO); - game.override.enemySpecies(Species.RATTATA); - game.override.enemyAbility(Abilities.HYDRATION); - game.override.ability(Abilities.ZEN_MODE); - game.override.startingLevel(2000); - game.override.startingWave(3); - game.override.moveset([moveToUse]); + game.override + .battleType("single") + .starterSpecies(Species.MEWTWO) + .enemySpecies(Species.RATTATA) + .enemyAbility(Abilities.HYDRATION) + .ability(Abilities.ZEN_MODE) + .startingLevel(2000) + .startingWave(3) + .startingBiome(Biome.LAKE) + .moveset([moveToUse]); game.override.enemyMoveset([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]); - await game.startBattle(); + await game.classicMode.startBattle(); const waveIndex = game.scene.currentBattle.waveIndex; game.move.select(moveToUse); + + vi.spyOn(game.scene.arena, "trySetWeather"); await game.doKillOpponents(); await game.toNextWave(); + expect(game.scene.arena.trySetWeather).not.toHaveBeenCalled(); expect(game.scene.currentBattle.waveIndex).toBeGreaterThan(waveIndex); }, 20000); diff --git a/src/test/eggs/egg.test.ts b/src/test/eggs/egg.test.ts index 4f00e843b47..053ff8f1112 100644 --- a/src/test/eggs/egg.test.ts +++ b/src/test/eggs/egg.test.ts @@ -1,7 +1,7 @@ import { Egg, getLegendaryGachaSpeciesForTimestamp } from "#app/data/egg"; import { EggSourceType } from "#app/enums/egg-source-types"; import { EggTier } from "#app/enums/egg-type"; -import { VariantTier } from "#app/enums/variant-tiers"; +import { VariantTier } from "#app/enums/variant-tier"; import EggData from "#app/system/egg-data"; import * as Utils from "#app/utils"; import { Species } from "#enums/species"; @@ -136,9 +136,9 @@ describe("Egg Generation Tests", () => { expect(result).toBe(expectedResult); }); - it("should return a shiny common variant", () => { + it("should return a shiny standard variant", () => { const scene = game.scene; - const expectedVariantTier = VariantTier.COMMON; + const expectedVariantTier = VariantTier.STANDARD; const result = new Egg({ scene, isShiny: true, variantTier: expectedVariantTier, species: Species.BULBASAUR }).generatePlayerPokemon(scene).variant; diff --git a/src/test/moves/shell_side_arm.test.ts b/src/test/moves/shell_side_arm.test.ts new file mode 100644 index 00000000000..38d2556a1a2 --- /dev/null +++ b/src/test/moves/shell_side_arm.test.ts @@ -0,0 +1,87 @@ +import { BattlerIndex } from "#app/battle"; +import { allMoves, ShellSideArmCategoryAttr } from "#app/data/move"; +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 - Shell Side Arm", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + const TIMEOUT = 20 * 1000; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([Moves.SHELL_SIDE_ARM]) + .battleType("single") + .startingLevel(100) + .enemyLevel(100) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("becomes a physical attack if forecasted to deal more damage as physical", async () => { + game.override.enemySpecies(Species.SNORLAX); + + await game.classicMode.startBattle([Species.MANAPHY]); + + const shellSideArm = allMoves[Moves.SHELL_SIDE_ARM]; + const shellSideArmAttr = shellSideArm.getAttrs(ShellSideArmCategoryAttr)[0]; + vi.spyOn(shellSideArmAttr, "apply"); + + game.move.select(Moves.SHELL_SIDE_ARM); + + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(shellSideArmAttr.apply).toHaveLastReturnedWith(true); + }, TIMEOUT); + + it("remains a special attack if forecasted to deal more damage as special", async () => { + game.override.enemySpecies(Species.SLOWBRO); + + await game.classicMode.startBattle([Species.MANAPHY]); + + const shellSideArm = allMoves[Moves.SHELL_SIDE_ARM]; + const shellSideArmAttr = shellSideArm.getAttrs(ShellSideArmCategoryAttr)[0]; + vi.spyOn(shellSideArmAttr, "apply"); + + game.move.select(Moves.SHELL_SIDE_ARM); + + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(shellSideArmAttr.apply).toHaveLastReturnedWith(false); + }, TIMEOUT); + + it("respects stat stage changes when forecasting base damage", async () => { + game.override + .enemySpecies(Species.SNORLAX) + .enemyMoveset(Moves.COTTON_GUARD); + + await game.classicMode.startBattle([Species.MANAPHY]); + + const shellSideArm = allMoves[Moves.SHELL_SIDE_ARM]; + const shellSideArmAttr = shellSideArm.getAttrs(ShellSideArmCategoryAttr)[0]; + vi.spyOn(shellSideArmAttr, "apply"); + + game.move.select(Moves.SHELL_SIDE_ARM); + + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + + await game.phaseInterceptor.to("BerryPhase", false); + + expect(shellSideArmAttr.apply).toHaveLastReturnedWith(false); + }, TIMEOUT); +}); diff --git a/src/test/moves/steamroller.test.ts b/src/test/moves/steamroller.test.ts new file mode 100644 index 00000000000..cbbb3a22593 --- /dev/null +++ b/src/test/moves/steamroller.test.ts @@ -0,0 +1,58 @@ +import { BattlerIndex } from "#app/battle"; +import { allMoves } from "#app/data/move"; +import { BattlerTagType } from "#app/enums/battler-tag-type"; +import { DamageCalculationResult } from "#app/field/pokemon"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Moves - Steamroller", () => { + 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.STEAMROLLER]).battleType("single").enemyAbility(Abilities.BALL_FETCH); + }); + + it("should always hit a minimzed target with double damage", async () => { + game.override.enemySpecies(Species.DITTO).enemyMoveset(Moves.MINIMIZE); + await game.classicMode.startBattle([Species.IRON_BOULDER]); + + const ditto = game.scene.getEnemyPokemon()!; + vi.spyOn(ditto, "getAttackDamage"); + ditto.hp = 5000; + const steamroller = allMoves[Moves.STEAMROLLER]; + vi.spyOn(steamroller, "calculateBattleAccuracy"); + const ironBoulder = game.scene.getPlayerPokemon()!; + vi.spyOn(ironBoulder, "getAccuracyMultiplier"); + // Turn 1 + game.move.select(Moves.STEAMROLLER); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.toNextTurn(); + // Turn 2 + game.move.select(Moves.STEAMROLLER); + await game.toNextTurn(); + + const [dmgCalcTurn1, dmgCalcTurn2]: DamageCalculationResult[] = vi + .mocked(ditto.getAttackDamage) + .mock.results.map((r) => r.value); + + expect(dmgCalcTurn2.damage).toBeGreaterThanOrEqual(dmgCalcTurn1.damage * 2); + expect(ditto.getTag(BattlerTagType.MINIMIZED)).toBeDefined(); + expect(steamroller.calculateBattleAccuracy).toHaveReturnedWith(-1); + }); +}); diff --git a/src/test/moves/whirlwind.test.ts b/src/test/moves/whirlwind.test.ts new file mode 100644 index 00000000000..a591a3cd6c5 --- /dev/null +++ b/src/test/moves/whirlwind.test.ts @@ -0,0 +1,53 @@ +import { BattlerIndex } from "#app/battle"; +import { allMoves } from "#app/data/move"; +import { BattlerTagType } from "#app/enums/battler-tag-type"; +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 - Whirlwind", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .battleType("single") + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.WHIRLWIND) + .enemySpecies(Species.PIDGEY); + }); + + it.each([ + { move: Moves.FLY, name: "Fly" }, + { move: Moves.BOUNCE, name: "Bounce" }, + { move: Moves.SKY_DROP, name: "Sky Drop" }, + ])("should not hit a flying target: $name (=$move)", async ({ move }) => { + game.override.moveset([move]); + await game.classicMode.startBattle([Species.STARAPTOR]); + + const staraptor = game.scene.getPlayerPokemon()!; + const whirlwind = allMoves[Moves.WHIRLWIND]; + vi.spyOn(whirlwind, "getFailedText"); + + game.move.select(move); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.toNextTurn(); + + expect(staraptor.findTag((t) => t.tagType === BattlerTagType.FLYING)).toBeDefined(); + expect(whirlwind.getFailedText).toHaveBeenCalledOnce(); + }); +}); diff --git a/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts b/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts index 73ffad36258..5d30bc91de9 100644 --- a/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts @@ -98,7 +98,6 @@ describe("Berries Abound - Mystery Encounter", () => { const config = BerriesAboundEncounter.enemyPartyConfigs[0]; expect(config).toBeDefined(); - expect(config.levelAdditiveMultiplier).toBe(1); expect(config.pokemonConfigs?.[0].isBoss).toBe(true); expect(onInitResult).toBe(true); }); @@ -134,7 +133,7 @@ describe("Berries Abound - Mystery Encounter", () => { }); // TODO: there is some severe test flakiness occurring for this file, needs to be looked at/addressed in separate issue - it.skip("should reward the player with X berries based on wave", async () => { + it("should reward the player with X berries based on wave", async () => { await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); const numBerries = game.scene.currentBattle.mysteryEncounter!.misc.numBerries; diff --git a/src/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts b/src/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts index 735dcc709bf..86d79b57f80 100644 --- a/src/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts @@ -96,7 +96,6 @@ describe("Fight or Flight - Mystery Encounter", () => { const config = FightOrFlightEncounter.enemyPartyConfigs[0]; expect(config).toBeDefined(); - expect(config.levelAdditiveMultiplier).toBe(1); expect(config.pokemonConfigs?.[0].isBoss).toBe(true); expect(onInitResult).toBe(true); }); diff --git a/src/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts b/src/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts index de527538711..2c0d48e59c6 100644 --- a/src/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts @@ -117,12 +117,12 @@ describe("Mysterious Challengers - Mystery Encounter", () => { }, { trainerConfig: expect.any(TrainerConfig), - levelAdditiveMultiplier: 1, + levelAdditiveModifier: 1, female: expect.any(Boolean), }, { trainerConfig: expect.any(TrainerConfig), - levelAdditiveMultiplier: 1.5, + levelAdditiveModifier: 1.5, female: expect.any(Boolean), } ]); diff --git a/src/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts b/src/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts index 13860e83baa..2505f28aa43 100644 --- a/src/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts @@ -21,6 +21,8 @@ const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; const defaultBiome = Biome.CAVE; const defaultWave = 45; +const TRANSPORT_BIOMES = [Biome.SPACE, Biome.ISLAND, Biome.LABORATORY, Biome.FAIRY_CAVE, Biome.WASTELAND, Biome.DOJO]; + describe("Teleporting Hijinks - Mystery Encounter", () => { let phaserGame: Phaser.Game; let game: GameManager; @@ -183,7 +185,7 @@ describe("Teleporting Hijinks - Mystery Encounter", () => { await runMysteryEncounterToEnd(game, 1, undefined, true); expect(previousBiome).not.toBe(scene.arena.biomeType); - expect([Biome.SPACE, Biome.ISLAND, Biome.LABORATORY, Biome.FAIRY_CAVE]).toContain(scene.arena.biomeType); + expect(TRANSPORT_BIOMES).toContain(scene.arena.biomeType); }); it("should start a battle against an enraged boss", { retry: 5 }, async () => { @@ -246,7 +248,7 @@ describe("Teleporting Hijinks - Mystery Encounter", () => { await runMysteryEncounterToEnd(game, 2, undefined, true); expect(previousBiome).not.toBe(scene.arena.biomeType); - expect([Biome.SPACE, Biome.ISLAND, Biome.LABORATORY, Biome.FAIRY_CAVE]).toContain(scene.arena.biomeType); + expect(TRANSPORT_BIOMES).toContain(scene.arena.biomeType); }); it("should start a battle against an enraged boss", async () => { diff --git a/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts index be35ec31784..ef80271c423 100644 --- a/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts @@ -114,7 +114,7 @@ describe("The Strong Stuff - Mystery Encounter", () => { expect(TheStrongStuffEncounter.enemyPartyConfigs).toEqual([ { - levelAdditiveMultiplier: 1, + levelAdditiveModifier: 1, disableSwitch: true, pokemonConfigs: [ { 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 a4bfaea659a..0f89e90031c 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 @@ -102,7 +102,7 @@ describe("Trash to Treasure - Mystery Encounter", () => { expect(TrashToTreasureEncounter.enemyPartyConfigs).toEqual([ { - levelAdditiveMultiplier: 1, + levelAdditiveModifier: 1, disableSwitch: true, pokemonConfigs: [ { diff --git a/src/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts b/src/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts index f235609ee08..5b17ff3a2f7 100644 --- a/src/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts @@ -107,7 +107,7 @@ describe("Uncommon Breed - Mystery Encounter", () => { expect(onInitResult).toBe(true); }); - describe.skip("Option 1 - Fight", () => { + describe("Option 1 - Fight", () => { it("should have the correct properties", () => { const option = UncommonBreedEncounter.options[0]; expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); @@ -123,7 +123,7 @@ describe("Uncommon Breed - Mystery Encounter", () => { }); }); - it("should start a fight against the boss", async () => { + it.skip("should start a fight against the boss", async () => { const phaseSpy = vi.spyOn(scene, "pushPhase"); const unshiftPhaseSpy = vi.spyOn(scene, "unshiftPhase"); await game.runToMysteryEncounter(MysteryEncounterType.UNCOMMON_BREED, defaultParty); diff --git a/src/test/reload.test.ts b/src/test/reload.test.ts index a96a525ca2d..7c4523dd9ef 100644 --- a/src/test/reload.test.ts +++ b/src/test/reload.test.ts @@ -38,16 +38,15 @@ describe("Reload", () => { it("should not have RNG inconsistencies after a biome switch", async () => { game.override .startingWave(10) - .startingBiome(Biome.CAVE) // Will lead to biomes with randomly generated weather .battleType("single") - .startingLevel(100) - .enemyLevel(1000) + .startingLevel(100) // Avoid levelling up + .enemyLevel(1000) // Avoid opponent dying before game.doKillOpponents() .disableTrainerWaves() .moveset([Moves.KOWTOW_CLEAVE]) .enemyMoveset(Moves.SPLASH); await game.dailyMode.startBattle(); - // Transition from Daily Run Wave 10 to Wave 11 in order to trigger biome switch + // Transition from Wave 10 to Wave 11 in order to trigger biome switch game.move.select(Moves.KOWTOW_CLEAVE); await game.phaseInterceptor.to("DamagePhase"); await game.doKillOpponents(); @@ -63,6 +62,34 @@ describe("Reload", () => { expect(preReloadRngState).toBe(postReloadRngState); }, 20000); + it("should not have weather inconsistencies after a biome switch", async () => { + game.override + .startingWave(10) + .startingBiome(Biome.ICE_CAVE) // Will lead to Snowy Forest with randomly generated weather + .battleType("single") + .startingLevel(100) // Avoid levelling up + .enemyLevel(1000) // Avoid opponent dying before game.doKillOpponents() + .disableTrainerWaves() + .moveset([Moves.KOWTOW_CLEAVE]) + .enemyMoveset(Moves.SPLASH); + await game.classicMode.startBattle(); // Apparently daily mode would override the biome + + // Transition from Wave 10 to Wave 11 in order to trigger biome switch + game.move.select(Moves.KOWTOW_CLEAVE); + await game.phaseInterceptor.to("DamagePhase"); + await game.doKillOpponents(); + await game.toNextWave(); + expect(game.phaseInterceptor.log).toContain("NewBiomeEncounterPhase"); + + const preReloadWeather = game.scene.arena.weather; + + await game.reload.reloadSession(); + + const postReloadWeather = game.scene.arena.weather; + + expect(postReloadWeather).toStrictEqual(preReloadWeather); + }, 20000); + it("should not have RNG inconsistencies at a Daily run wild Pokemon fight", async () => { await game.dailyMode.startBattle(); diff --git a/src/test/utils/gameManager.ts b/src/test/utils/gameManager.ts index 452956ab466..36423c5e18f 100644 --- a/src/test/utils/gameManager.ts +++ b/src/test/utils/gameManager.ts @@ -31,7 +31,6 @@ import TargetSelectUiHandler from "#app/ui/target-select-ui-handler"; import { Mode } from "#app/ui/ui"; import { Button } from "#enums/buttons"; import { ExpNotification } from "#enums/exp-notification"; -import { GameDataType } from "#enums/game-data-type"; import { PlayerGender } from "#enums/player-gender"; import { Species } from "#enums/species"; import { generateStarter, waitUntil } from "#test/utils/gameManagerUtils"; @@ -371,13 +370,11 @@ export default class GameManager { * @returns A promise that resolves with the exported save data. */ exportSaveToTest(): Promise { + const saveKey = "x0i2O7WRiANTqPmZ"; return new Promise(async (resolve) => { - await this.scene.gameData.saveAll(this.scene, true, true, true, true); - this.scene.reset(true); - await waitUntil(() => this.scene.ui?.getMode() === Mode.TITLE); - await this.scene.gameData.tryExportData(GameDataType.SESSION, 0); - await waitUntil(() => localStorage.hasOwnProperty("toExport")); - return resolve(localStorage.getItem("toExport")!); // TODO: is this bang correct?; + const sessionSaveData = this.scene.gameData.getSessionSaveData(this.scene); + const encryptedSaveData = AES.encrypt(JSON.stringify(sessionSaveData), saveKey).toString(); + resolve(encryptedSaveData); }); } diff --git a/src/test/utils/helpers/reloadHelper.ts b/src/test/utils/helpers/reloadHelper.ts index c15347b08c9..e0e538120cc 100644 --- a/src/test/utils/helpers/reloadHelper.ts +++ b/src/test/utils/helpers/reloadHelper.ts @@ -5,11 +5,27 @@ import { vi } from "vitest"; import { BattleStyle } from "#app/enums/battle-style"; import { CommandPhase } from "#app/phases/command-phase"; import { TurnInitPhase } from "#app/phases/turn-init-phase"; +import { SessionSaveData } from "#app/system/game-data"; +import GameManager from "../gameManager"; /** * Helper to allow reloading sessions in unit tests. */ export class ReloadHelper extends GameManagerHelper { + sessionData: SessionSaveData; + + constructor(game: GameManager) { + super(game); + + // Whenever the game saves the session, save it to the reloadHelper instead + vi.spyOn(game.scene.gameData, "saveAll").mockImplementation((scene) => { + return new Promise((resolve, reject) => { + this.sessionData = scene.gameData.getSessionSaveData(scene); + resolve(true); + }); + }); + } + /** * Simulate reloading the session from the title screen, until reaching the * beginning of the first turn (equivalent to running `startBattle()`) for @@ -17,7 +33,6 @@ export class ReloadHelper extends GameManagerHelper { */ async reloadSession() : Promise { const scene = this.game.scene; - const sessionData = scene.gameData.getSessionSaveData(scene); const titlePhase = new TitlePhase(scene); scene.clearPhaseQueue(); @@ -25,7 +40,7 @@ export class ReloadHelper extends GameManagerHelper { // Set the last saved session to the desired session data vi.spyOn(scene.gameData, "getSession").mockReturnValue( new Promise((resolve, reject) => { - resolve(sessionData); + resolve(this.sessionData); }) ); scene.unshiftPhase(titlePhase); diff --git a/src/tutorial.ts b/src/tutorial.ts index c4482839779..18d8291d227 100644 --- a/src/tutorial.ts +++ b/src/tutorial.ts @@ -1,7 +1,9 @@ import BattleScene from "./battle-scene"; import AwaitableUiHandler from "./ui/awaitable-ui-handler"; +import UiHandler from "./ui/ui-handler"; import { Mode } from "./ui/ui"; import i18next from "i18next"; +import Overrides from "#app/overrides"; export enum Tutorial { Intro = "INTRO", @@ -39,7 +41,7 @@ const tutorialHandlers = { scene.ui.showText(i18next.t("tutorial:starterSelect"), null, () => scene.ui.showText("", null, () => resolve()), null, true); }); }, - [Tutorial.Pokerus]: (scene: BattleScene) => { + [Tutorial.Pokerus]: (scene: BattleScene) => { return new Promise(resolve => { scene.ui.showText(i18next.t("tutorial:pokerus"), null, () => scene.ui.showText("", null, () => resolve()), null, true); }); @@ -63,26 +65,87 @@ const tutorialHandlers = { }, }; -export function handleTutorial(scene: BattleScene, tutorial: Tutorial): Promise { - return new Promise(resolve => { - if (!scene.enableTutorials) { - return resolve(false); - } +/** + * Run through the specified tutorial if it hasn't been seen before and mark it as seen once done + * This will show a tutorial overlay if defined in the current {@linkcode AwaitableUiHandler} + * The main menu will also get disabled while the tutorial is running + * @param scene the current {@linkcode BattleScene} + * @param tutorial the {@linkcode Tutorial} to play + * @returns a promise with result `true` if the tutorial was run and finished, `false` otherwise + */ +export async function handleTutorial(scene: BattleScene, tutorial: Tutorial): Promise { + if (!scene.enableTutorials && !Overrides.BYPASS_TUTORIAL_SKIP) { + return false; + } - if (scene.gameData.getTutorialFlags()[tutorial]) { - return resolve(false); - } + if (scene.gameData.getTutorialFlags()[tutorial] && !Overrides.BYPASS_TUTORIAL_SKIP) { + return false; + } - const handler = scene.ui.getHandler(); - if (handler instanceof AwaitableUiHandler) { - handler.tutorialActive = true; - } - tutorialHandlers[tutorial](scene).then(() => { - scene.gameData.saveTutorialFlag(tutorial, true); - if (handler instanceof AwaitableUiHandler) { - handler.tutorialActive = false; - } - resolve(true); - }); - }); + const handler = scene.ui.getHandler(); + const isMenuDisabled = scene.disableMenu; + + // starting tutorial, disable menu + scene.disableMenu = true; + if (handler instanceof AwaitableUiHandler) { + handler.tutorialActive = true; + } + + await showTutorialOverlay(scene, handler); + await tutorialHandlers[tutorial](scene); + await hideTutorialOverlay(scene, handler); + + // tutorial finished and overlay gone, re-enable menu, save tutorial as seen + scene.disableMenu = isMenuDisabled; + scene.gameData.saveTutorialFlag(tutorial, true); + if (handler instanceof AwaitableUiHandler) { + handler.tutorialActive = false; + } + + return true; } + +/** + * Show the tutorial overlay if there is one + * @param scene the current BattleScene + * @param handler the current UiHandler + * @returns `true` once the overlay has finished appearing, or if there is no overlay + */ +async function showTutorialOverlay(scene: BattleScene, handler: UiHandler) { + if (handler instanceof AwaitableUiHandler && handler.tutorialOverlay) { + scene.tweens.add({ + targets: handler.tutorialOverlay, + alpha: 0.5, + duration: 750, + ease: "Sine.easeOut", + onComplete: () => { + return true; + } + }); + } else { + return true; + } +} + +/** + * Hide the tutorial overlay if there is one + * @param scene the current BattleScene + * @param handler the current UiHandler + * @returns `true` once the overlay has finished disappearing, or if there is no overlay + */ +async function hideTutorialOverlay(scene: BattleScene, handler: UiHandler) { + if (handler instanceof AwaitableUiHandler && handler.tutorialOverlay) { + scene.tweens.add({ + targets: handler.tutorialOverlay, + alpha: 0, + duration: 500, + ease: "Sine.easeOut", + onComplete: () => { + return true; + } + }); + } else { + return true; + } +} + diff --git a/src/ui/abstact-option-select-ui-handler.ts b/src/ui/abstact-option-select-ui-handler.ts index c6abecda4c0..740830595ed 100644 --- a/src/ui/abstact-option-select-ui-handler.ts +++ b/src/ui/abstact-option-select-ui-handler.ts @@ -344,6 +344,7 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler { super.clear(); this.config = null; this.optionSelectContainer.setVisible(false); + this.scrollCursor = 0; this.eraseCursor(); } diff --git a/src/ui/achvs-ui-handler.ts b/src/ui/achvs-ui-handler.ts index 605b8c538a9..491c4acb523 100644 --- a/src/ui/achvs-ui-handler.ts +++ b/src/ui/achvs-ui-handler.ts @@ -1,12 +1,13 @@ -import BattleScene from "../battle-scene"; +import BattleScene from "#app/battle-scene"; import { Button } from "#enums/buttons"; import i18next from "i18next"; -import { Achv, achvs, getAchievementDescription } from "../system/achv"; -import { Voucher, getVoucherTypeIcon, getVoucherTypeName, vouchers } from "../system/voucher"; -import MessageUiHandler from "./message-ui-handler"; -import { addTextObject, TextStyle } from "./text"; -import { Mode } from "./ui"; -import { addWindow } from "./ui-theme"; +import { Achv, achvs, getAchievementDescription } from "#app/system/achv"; +import { Voucher, getVoucherTypeIcon, getVoucherTypeName, vouchers } from "#app/system/voucher"; +import MessageUiHandler from "#app/ui/message-ui-handler"; +import { addTextObject, TextStyle } from "#app/ui/text"; +import { Mode } from "#app/ui/ui"; +import { addWindow } from "#app/ui/ui-theme"; +import { ScrollBar } from "#app/ui/scroll-bar"; import { PlayerGender } from "#enums/player-gender"; enum Page { @@ -49,6 +50,7 @@ export default class AchvsUiHandler extends MessageUiHandler { private vouchersTotal: number; private currentTotal: number; + private scrollBar: ScrollBar; private scrollCursor: number; private cursorObj: Phaser.GameObjects.NineSlice | null; private currentPage: Page; @@ -91,7 +93,10 @@ export default class AchvsUiHandler extends MessageUiHandler { this.iconsBg = addWindow(this.scene, 0, this.headerBg.height, (this.scene.game.canvas.width / 6) - 2, (this.scene.game.canvas.height / 6) - this.headerBg.height - 68); this.iconsBg.setOrigin(0, 0); - this.iconsContainer = this.scene.add.container(6, this.headerBg.height + 6); + const yOffset = 6; + this.scrollBar = new ScrollBar(this.scene, this.iconsBg.width - 9, this.iconsBg.y + yOffset, 4, this.iconsBg.height - yOffset * 2, this.ROWS); + + this.iconsContainer = this.scene.add.container(5, this.headerBg.height + 8); this.icons = []; @@ -148,6 +153,7 @@ export default class AchvsUiHandler extends MessageUiHandler { this.mainContainer.add(this.headerText); this.mainContainer.add(this.headerActionText); this.mainContainer.add(this.iconsBg); + this.mainContainer.add(this.scrollBar); this.mainContainer.add(this.iconsContainer); this.mainContainer.add(titleBg); this.mainContainer.add(this.titleText); @@ -162,6 +168,7 @@ export default class AchvsUiHandler extends MessageUiHandler { this.currentPage = Page.ACHIEVEMENTS; this.setCursor(0); + this.setScrollCursor(0); this.mainContainer.setVisible(false); } @@ -175,6 +182,8 @@ export default class AchvsUiHandler extends MessageUiHandler { this.mainContainer.setVisible(true); this.setCursor(0); this.setScrollCursor(0); + this.scrollBar.setTotalRows(Math.ceil(this.currentTotal / this.COLS)); + this.scrollBar.setScrollCursor(0); this.getUi().moveTo(this.mainContainer, this.getUi().length - 1); @@ -224,6 +233,8 @@ export default class AchvsUiHandler extends MessageUiHandler { this.updateAchvIcons(); } this.setCursor(0, true); + this.scrollBar.setTotalRows(Math.ceil(this.currentTotal / this.COLS)); + this.scrollBar.setScrollCursor(0); this.mainContainer.update(); } if (button === Button.CANCEL) { @@ -237,32 +248,44 @@ export default class AchvsUiHandler extends MessageUiHandler { if (this.cursor < this.COLS) { if (this.scrollCursor) { success = this.setScrollCursor(this.scrollCursor - 1); + } else { + // Wrap around to the last row + success = this.setScrollCursor(Math.ceil(this.currentTotal / this.COLS) - this.ROWS); + let newCursorIndex = this.cursor + (this.ROWS - 1) * this.COLS; + if (newCursorIndex > this.currentTotal - this.scrollCursor * this.COLS -1) { + newCursorIndex -= this.COLS; + } + success = success && this.setCursor(newCursorIndex); } } else { success = this.setCursor(this.cursor - this.COLS); } break; case Button.DOWN: - const canMoveDown = (this.cursor + itemOffset) + this.COLS < this.currentTotal; + const canMoveDown = itemOffset + 1 < this.currentTotal; if (rowIndex >= this.ROWS - 1) { if (this.scrollCursor < Math.ceil(this.currentTotal / this.COLS) - this.ROWS && canMoveDown) { + // scroll down one row success = this.setScrollCursor(this.scrollCursor + 1); + } else { + // wrap back to the first row + success = this.setScrollCursor(0) && this.setCursor(this.cursor % this.COLS); } } else if (canMoveDown) { - success = this.setCursor(this.cursor + this.COLS); + success = this.setCursor(Math.min(this.cursor + this.COLS, this.currentTotal - itemOffset - 1)); } break; case Button.LEFT: - if (!this.cursor && this.scrollCursor) { - success = this.setScrollCursor(this.scrollCursor - 1) && this.setCursor(this.cursor + (this.COLS - 1)); - } else if (this.cursor) { + if (this.cursor % this.COLS === 0) { + success = this.setCursor(Math.min(this.cursor + this.COLS - 1, this.currentTotal - itemOffset - 1)); + } else { success = this.setCursor(this.cursor - 1); } break; case Button.RIGHT: - if (this.cursor + 1 === this.ROWS * this.COLS && this.scrollCursor < Math.ceil(this.currentTotal / this.COLS) - this.ROWS) { - success = this.setScrollCursor(this.scrollCursor + 1) && this.setCursor(this.cursor - (this.COLS - 1)); - } else if (this.cursor + itemOffset < this.currentTotal - 1) { + if ((this.cursor + 1) % this.COLS === 0 || (this.cursor + itemOffset) === (this.currentTotal - 1)) { + success = this.setCursor(this.cursor - this.cursor % this.COLS); + } else { success = this.setCursor(this.cursor + 1); } break; @@ -315,15 +338,22 @@ export default class AchvsUiHandler extends MessageUiHandler { } this.scrollCursor = scrollCursor; + this.scrollBar.setScrollCursor(this.scrollCursor); + + // Cursor cannot go farther than the last element in the list + const maxCursor = Math.min(this.cursor, this.currentTotal - this.scrollCursor * this.COLS - 1); + if (maxCursor !== this.cursor) { + this.setCursor(maxCursor); + } switch (this.currentPage) { case Page.ACHIEVEMENTS: this.updateAchvIcons(); - this.showAchv(achvs[Object.keys(achvs)[Math.min(this.cursor + this.scrollCursor * this.COLS, Object.values(achvs).length - 1)]]); + this.showAchv(achvs[Object.keys(achvs)[this.cursor + this.scrollCursor * this.COLS]]); break; case Page.VOUCHERS: this.updateVoucherIcons(); - this.showVoucher(vouchers[Object.keys(vouchers)[Math.min(this.cursor + this.scrollCursor * this.COLS, Object.values(vouchers).length - 1)]]); + this.showVoucher(vouchers[Object.keys(vouchers)[this.cursor + this.scrollCursor * this.COLS]]); break; } return true; @@ -411,6 +441,7 @@ export default class AchvsUiHandler extends MessageUiHandler { super.clear(); this.currentPage = Page.ACHIEVEMENTS; this.mainContainer.setVisible(false); + this.setScrollCursor(0); this.eraseCursor(); } diff --git a/src/ui/awaitable-ui-handler.ts b/src/ui/awaitable-ui-handler.ts index 2052c6e2ade..c6dc717aa3a 100644 --- a/src/ui/awaitable-ui-handler.ts +++ b/src/ui/awaitable-ui-handler.ts @@ -7,6 +7,7 @@ export default abstract class AwaitableUiHandler extends UiHandler { protected awaitingActionInput: boolean; protected onActionInput: Function | null; public tutorialActive: boolean = false; + public tutorialOverlay: Phaser.GameObjects.Rectangle; constructor(scene: BattleScene, mode: Mode | null = null) { super(scene, mode); @@ -24,4 +25,21 @@ export default abstract class AwaitableUiHandler extends UiHandler { return false; } + + /** + * Create a semi transparent overlay that will get shown during tutorials + * @param container the container to add the overlay to + */ + initTutorialOverlay(container: Phaser.GameObjects.Container) { + if (!this.tutorialOverlay) { + this.tutorialOverlay = new Phaser.GameObjects.Rectangle(this.scene, -1, -1, this.scene.scaledCanvas.width, this.scene.scaledCanvas.height, 0x070707); + this.tutorialOverlay.setName("tutorial-overlay"); + this.tutorialOverlay.setOrigin(0, 0); + this.tutorialOverlay.setAlpha(0); + } + + if (container) { + container.add(this.tutorialOverlay); + } + } } diff --git a/src/ui/battle-info.ts b/src/ui/battle-info.ts index 8e7e5bc4060..1d598f6ac4e 100644 --- a/src/ui/battle-info.ts +++ b/src/ui/battle-info.ts @@ -162,7 +162,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container { this.splicedIcon.setInteractive(new Phaser.Geom.Rectangle(0, 0, 12, 15), Phaser.Geom.Rectangle.Contains); this.add(this.splicedIcon); - this.statusIndicator = this.scene.add.sprite(0, 0, "statuses"); + this.statusIndicator = this.scene.add.sprite(0, 0, Utils.getLocalizedSpriteKey("statuses")); this.statusIndicator.setName("icon_status"); this.statusIndicator.setVisible(false); this.statusIndicator.setOrigin(0, 0); diff --git a/src/ui/battle-message-ui-handler.ts b/src/ui/battle-message-ui-handler.ts index 9a694d50b29..c27c6974192 100644 --- a/src/ui/battle-message-ui-handler.ts +++ b/src/ui/battle-message-ui-handler.ts @@ -83,12 +83,7 @@ export default class BattleMessageUiHandler extends MessageUiHandler { this.nameBoxContainer.add(this.nameText); messageContainer.add(this.nameBoxContainer); - const prompt = this.scene.add.sprite(0, 0, "prompt"); - prompt.setVisible(false); - prompt.setOrigin(0, 0); - messageContainer.add(prompt); - - this.prompt = prompt; + this.initPromptSprite(messageContainer); const levelUpStatsContainer = this.scene.add.container(0, 0); levelUpStatsContainer.setVisible(false); diff --git a/src/ui/egg-gacha-ui-handler.ts b/src/ui/egg-gacha-ui-handler.ts index 9497dfe58c6..b109eda5370 100644 --- a/src/ui/egg-gacha-ui-handler.ts +++ b/src/ui/egg-gacha-ui-handler.ts @@ -287,7 +287,6 @@ export default class EggGachaUiHandler extends MessageUiHandler { this.eggGachaContainer.add(this.eggGachaSummaryContainer); const gachaMessageBoxContainer = this.scene.add.container(0, 148); - this.eggGachaContainer.add(gachaMessageBoxContainer); const gachaMessageBox = addWindow(this.scene, 0, 0, 320, 32); gachaMessageBox.setOrigin(0, 0); @@ -301,8 +300,11 @@ export default class EggGachaUiHandler extends MessageUiHandler { this.message = gachaMessageText; + this.initTutorialOverlay(this.eggGachaContainer); this.eggGachaContainer.add(gachaMessageBoxContainer); + this.initPromptSprite(gachaMessageBoxContainer); + this.setCursor(0); } diff --git a/src/ui/evolution-scene-handler.ts b/src/ui/evolution-scene-handler.ts index ffbd06afde3..76d148d083e 100644 --- a/src/ui/evolution-scene-handler.ts +++ b/src/ui/evolution-scene-handler.ts @@ -45,12 +45,7 @@ export default class EvolutionSceneHandler extends MessageUiHandler { this.message = message; - const prompt = this.scene.add.sprite(0, 0, "prompt"); - prompt.setVisible(false); - prompt.setOrigin(0, 0); - this.messageContainer.add(prompt); - - this.prompt = prompt; + this.initPromptSprite(this.messageContainer); } show(_args: any[]): boolean { diff --git a/src/ui/menu-ui-handler.ts b/src/ui/menu-ui-handler.ts index b8c3cfd1364..0af527e518f 100644 --- a/src/ui/menu-ui-handler.ts +++ b/src/ui/menu-ui-handler.ts @@ -157,6 +157,9 @@ export default class MenuUiHandler extends MessageUiHandler { menuMessageText.setOrigin(0, 0); this.menuMessageBoxContainer.add(menuMessageText); + this.initTutorialOverlay(this.menuContainer); + this.initPromptSprite(this.menuMessageBoxContainer); + this.message = menuMessageText; // By default we use the general purpose message window @@ -433,6 +436,9 @@ export default class MenuUiHandler extends MessageUiHandler { this.scene.playSound("ui/menu_open"); + // Make sure the tutorial overlay sits above everything, but below the message box + this.menuContainer.bringToTop(this.tutorialOverlay); + this.menuContainer.bringToTop(this.menuMessageBoxContainer); handleTutorial(this.scene, Tutorial.Menu); this.bgmBar.toggleBgmBar(true); diff --git a/src/ui/message-ui-handler.ts b/src/ui/message-ui-handler.ts index 93e00cb6b70..54965a590fc 100644 --- a/src/ui/message-ui-handler.ts +++ b/src/ui/message-ui-handler.ts @@ -17,6 +17,23 @@ export default abstract class MessageUiHandler extends AwaitableUiHandler { this.pendingPrompt = false; } + /** + * Add the sprite to be displayed at the end of messages with prompts + * @param container the container to add the sprite to + */ + initPromptSprite(container: Phaser.GameObjects.Container) { + if (!this.prompt) { + const promptSprite = this.scene.add.sprite(0, 0, "prompt"); + promptSprite.setVisible(false); + promptSprite.setOrigin(0, 0); + this.prompt = promptSprite; + } + + if (container) { + container.add(this.prompt); + } + } + showText(text: string, delay?: integer | null, callback?: Function | null, callbackDelay?: integer | null, prompt?: boolean | null, promptDelay?: integer | null) { this.showTextInternal(text, delay, callback, callbackDelay, prompt, promptDelay); } @@ -180,7 +197,7 @@ export default abstract class MessageUiHandler extends AwaitableUiHandler { const lastLineWidth = lastLineTest.displayWidth; lastLineTest.destroy(); if (this.prompt) { - this.prompt.setPosition(lastLineWidth + 2, (textLinesCount - 1) * 18 + 2); + this.prompt.setPosition(this.message.x + lastLineWidth + 2, this.message.y + (textLinesCount - 1) * 18 + 2); this.prompt.play("prompt"); } this.pendingPrompt = false; diff --git a/src/ui/modifier-select-ui-handler.ts b/src/ui/modifier-select-ui-handler.ts index c9d3f195720..a1e10d74c64 100644 --- a/src/ui/modifier-select-ui-handler.ts +++ b/src/ui/modifier-select-ui-handler.ts @@ -160,7 +160,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { this.player = args[0]; - const partyHasHeldItem = this.player && !!this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && m.isTransferrable).length; + const partyHasHeldItem = this.player && !!this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && m.isTransferable).length; const canLockRarities = !!this.scene.findModifier(m => m instanceof LockModifierTiersModifier); this.transferButtonContainer.setVisible(false); @@ -277,13 +277,13 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { this.lockRarityButtonContainer.setVisible(canLockRarities); this.scene.tweens.add({ - targets: [ this.lockRarityButtonContainer, this.checkButtonContainer, this.continueButtonContainer ], + targets: [ this.checkButtonContainer, this.continueButtonContainer ], alpha: 1, duration: 250 }); this.scene.tweens.add({ - targets: [this.rerollButtonContainer], + targets: [this.rerollButtonContainer, this.lockRarityButtonContainer], alpha: this.rerollCost < 0 ? 0.5 : 1, duration: 250 }); diff --git a/src/ui/move-info-overlay.ts b/src/ui/move-info-overlay.ts index 77010f84528..a99e4c81e27 100644 --- a/src/ui/move-info-overlay.ts +++ b/src/ui/move-info-overlay.ts @@ -91,7 +91,7 @@ export default class MoveInfoOverlay extends Phaser.GameObjects.Container implem valuesBg.setOrigin(0, 0); this.val.add(valuesBg); - this.typ = this.scene.add.sprite(25, EFF_HEIGHT - 35, `types${Utils.verifyLang(i18next.language) ? `_${i18next.language}` : ""}`, "unknown"); + this.typ = this.scene.add.sprite(25, EFF_HEIGHT - 35, Utils.getLocalizedSpriteKey("types"), "unknown"); this.typ.setScale(0.8); this.val.add(this.typ); @@ -138,7 +138,7 @@ export default class MoveInfoOverlay extends Phaser.GameObjects.Container implem this.pow.setText(move.power >= 0 ? move.power.toString() : "---"); this.acc.setText(move.accuracy >= 0 ? move.accuracy.toString() : "---"); this.pp.setText(move.pp >= 0 ? move.pp.toString() : "---"); - this.typ.setTexture(`types${Utils.verifyLang(i18next.language) ? `_${i18next.language}` : ""}`, Type[move.type].toLowerCase()); + this.typ.setTexture(Utils.getLocalizedSpriteKey("types"), Type[move.type].toLowerCase()); this.cat.setFrame(MoveCategory[move.category].toLowerCase()); this.desc.setText(move?.effect || ""); diff --git a/src/ui/mystery-encounter-ui-handler.ts b/src/ui/mystery-encounter-ui-handler.ts index 307bab0a3af..08de740e3ec 100644 --- a/src/ui/mystery-encounter-ui-handler.ts +++ b/src/ui/mystery-encounter-ui-handler.ts @@ -6,7 +6,7 @@ import { Button } from "#enums/buttons"; import { addWindow, WindowVariant } from "./ui-theme"; import { MysteryEncounterPhase } from "../phases/mystery-encounter-phases"; import { PartyUiMode } from "./party-ui-handler"; -import MysteryEncounterOption from "../data/mystery-encounters/mystery-encounter-option"; +import MysteryEncounterOption from "#app/data/mystery-encounters/mystery-encounter-option"; import * as Utils from "../utils"; import { isNullOrUndefined } from "../utils"; import { getPokeballAtlasKey } from "../data/pokeball"; @@ -42,7 +42,8 @@ export default class MysteryEncounterUiHandler extends UiHandler { private encounterOptions: MysteryEncounterOption[] = []; private optionsMeetsReqs: boolean[]; - protected viewPartyIndex: integer = 0; + protected viewPartyIndex: number = 0; + protected viewPartyXPosition: number = 0; protected blockInput: boolean = true; @@ -300,11 +301,11 @@ export default class MysteryEncounterUiHandler extends UiHandler { } } - override getCursor(): integer { + override getCursor(): number { return this.cursor ? this.cursor : 0; } - override setCursor(cursor: integer): boolean { + override setCursor(cursor: number): boolean { const prevCursor = this.getCursor(); const changed = prevCursor !== cursor; if (changed) { @@ -319,7 +320,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { } if (cursor === this.viewPartyIndex) { - this.cursorObj.setPosition(246, -17); + this.cursorObj.setPosition(this.viewPartyXPosition, -17); } else if (this.optionsContainer.getAll()?.length === 3) { // 2 Options this.cursorObj.setPosition(-10.5 + (cursor % 2 === 1 ? 100 : 0), 15); } else if (this.optionsContainer.getAll()?.length === 4) { // 3 Options @@ -419,8 +420,10 @@ export default class MysteryEncounterUiHandler extends UiHandler { } // View Party Button - const viewPartyText = addBBCodeTextObject(this.scene, 256, -24, getBBCodeFrag(i18next.t("mysteryEncounterMessages:view_party_button"), TextStyle.PARTY), TextStyle.PARTY); + const viewPartyText = addBBCodeTextObject(this.scene, (this.scene.game.canvas.width) / 6, -24, getBBCodeFrag(i18next.t("mysteryEncounterMessages:view_party_button"), TextStyle.PARTY), TextStyle.PARTY); this.optionsContainer.add(viewPartyText); + viewPartyText.x -= (viewPartyText.displayWidth + 16); + this.viewPartyXPosition = viewPartyText.x - 10; // Description Window const titleTextObject = addBBCodeTextObject(this.scene, 0, 0, titleText ?? "", TextStyle.TOOLTIP_TITLE, { wordWrap: { width: 750 }, align: "center", lineSpacing: -8 }); diff --git a/src/ui/party-ui-handler.ts b/src/ui/party-ui-handler.ts index a793dad6a73..6b6ce2aa789 100644 --- a/src/ui/party-ui-handler.ts +++ b/src/ui/party-ui-handler.ts @@ -355,7 +355,7 @@ export default class PartyUiHandler extends MessageUiHandler { const newPokemon = this.scene.getParty()[p]; // this next line gets all of the transferable items from pokemon [p]; it does this by getting all the held modifiers that are transferable and checking to see if they belong to pokemon [p] const getTransferrableItemsFromPokemon = (newPokemon: PlayerPokemon) => - this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && (m as PokemonHeldItemModifier).isTransferrable && (m as PokemonHeldItemModifier).pokemonId === newPokemon.id) as PokemonHeldItemModifier[]; + this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && (m as PokemonHeldItemModifier).isTransferable && (m as PokemonHeldItemModifier).pokemonId === newPokemon.id) as PokemonHeldItemModifier[]; // this next bit checks to see if the the selected item from the original transfer pokemon exists on the new pokemon [p]; this returns undefined if the new pokemon doesn't have the item at all, otherwise it returns the pokemonHeldItemModifier for that item const matchingModifier = newPokemon.scene.findModifier(m => m instanceof PokemonHeldItemModifier && m.pokemonId === newPokemon.id && m.matchType(getTransferrableItemsFromPokemon(pokemon)[this.transferOptionCursor])) as PokemonHeldItemModifier; const partySlot = this.partySlots.filter(m => m.getPokemon() === newPokemon)[0]; // this gets pokemon [p] for us @@ -399,7 +399,7 @@ export default class PartyUiHandler extends MessageUiHandler { || (option === PartyOption.RELEASE && this.partyUiMode === PartyUiMode.RELEASE)) { let filterResult: string | null; const getTransferrableItemsFromPokemon = (pokemon: PlayerPokemon) => - this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && m.isTransferrable && m.pokemonId === pokemon.id) as PokemonHeldItemModifier[]; + this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && m.isTransferable && m.pokemonId === pokemon.id) as PokemonHeldItemModifier[]; if (option !== PartyOption.TRANSFER && option !== PartyOption.SPLICE) { filterResult = (this.selectFilter as PokemonSelectFilter)(pokemon); if (filterResult === null && (option === PartyOption.SEND_OUT || option === PartyOption.PASS_BATON)) { @@ -596,7 +596,7 @@ export default class PartyUiHandler extends MessageUiHandler { if (this.partyUiMode === PartyUiMode.MODIFIER_TRANSFER && !this.transferMode) { /** Initialize item quantities for the selected Pokemon */ const itemModifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier - && m.isTransferrable && m.pokemonId === this.scene.getParty()[this.cursor].id) as PokemonHeldItemModifier[]; + && m.isTransferable && m.pokemonId === this.scene.getParty()[this.cursor].id) as PokemonHeldItemModifier[]; this.transferQuantities = itemModifiers.map(item => item.getStackCount()); this.transferQuantitiesMax = itemModifiers.map(item => item.getStackCount()); } @@ -813,7 +813,7 @@ export default class PartyUiHandler extends MessageUiHandler { const itemModifiers = this.partyUiMode === PartyUiMode.MODIFIER_TRANSFER ? this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier - && m.isTransferrable && m.pokemonId === pokemon.id) as PokemonHeldItemModifier[] + && m.isTransferable && m.pokemonId === pokemon.id) as PokemonHeldItemModifier[] : []; if (this.options.length) { @@ -1272,7 +1272,7 @@ class PartySlot extends Phaser.GameObjects.Container { } if (this.pokemon.status) { - const statusIndicator = this.scene.add.sprite(0, 0, "statuses"); + const statusIndicator = this.scene.add.sprite(0, 0, Utils.getLocalizedSpriteKey("statuses")); statusIndicator.setFrame(StatusEffect[this.pokemon.status?.effect].toLowerCase()); statusIndicator.setOrigin(0, 0); statusIndicator.setPositionRelative(slotLevelLabel, this.slotIndex >= battlerCount ? 43 : 55, 0); diff --git a/src/ui/scroll-bar.ts b/src/ui/scroll-bar.ts index e756393ae1a..5ed79d0cdad 100644 --- a/src/ui/scroll-bar.ts +++ b/src/ui/scroll-bar.ts @@ -1,36 +1,65 @@ +/** + * A vertical scrollbar element that resizes dynamically based on the current scrolling + * and number of elements that can be shown on screen + */ export class ScrollBar extends Phaser.GameObjects.Container { - private bg: Phaser.GameObjects.Image; + private bg: Phaser.GameObjects.NineSlice; private handleBody: Phaser.GameObjects.Rectangle; - private handleBottom: Phaser.GameObjects.Image; - private pages: number; - private page: number; + private handleBottom: Phaser.GameObjects.NineSlice; + private currentRow: number; + private totalRows: number; + private maxRows: number; - constructor(scene: Phaser.Scene, x: number, y: number, pages: number) { + /** + * @param scene the current scene + * @param x the scrollbar's x position (origin: top left) + * @param y the scrollbar's y position (origin: top left) + * @param width the scrollbar's width + * @param height the scrollbar's height + * @param maxRows the maximum number of rows that can be shown at once + */ + constructor(scene: Phaser.Scene, x: number, y: number, width: number, height: number, maxRows: number) { super(scene, x, y); - this.bg = scene.add.image(0, 0, "scroll_bar"); + this.maxRows = maxRows; + + const borderSize = 2; + width = Math.max(width, 4); + + this.bg = scene.add.nineslice(0, 0, "scroll_bar", undefined, width, height, borderSize, borderSize, borderSize, borderSize); this.bg.setOrigin(0, 0); this.add(this.bg); - this.handleBody = scene.add.rectangle(1, 1, 3, 4, 0xaaaaaa); + this.handleBody = scene.add.rectangle(1, 1, width - 2, 4, 0xaaaaaa); this.handleBody.setOrigin(0, 0); this.add(this.handleBody); - this.handleBottom = scene.add.image(1, 1, "scroll_bar_handle"); + this.handleBottom = scene.add.nineslice(1, 1, "scroll_bar_handle", undefined, width - 2, 2, 2, 0, 0, 0); this.handleBottom.setOrigin(0, 0); this.add(this.handleBottom); } - setPage(page: number): void { - this.page = page; - this.handleBody.y = 1 + (this.bg.displayHeight - 1 - this.handleBottom.displayHeight) / this.pages * page; + /** + * Set the current row that is displayed + * Moves the bar handle up or down accordingly + * @param scrollCursor how many times the view was scrolled down + */ + setScrollCursor(scrollCursor: number): void { + this.currentRow = scrollCursor; + this.handleBody.y = 1 + (this.bg.displayHeight - 1 - this.handleBottom.displayHeight) / this.totalRows * this.currentRow; this.handleBottom.y = this.handleBody.y + this.handleBody.displayHeight; } - setPages(pages: number): void { - this.pages = pages; - this.handleBody.height = (this.bg.displayHeight - 1 - this.handleBottom.displayHeight) * 9 / this.pages; + /** + * Set the total number of rows to display + * If it's smaller than the maximum number of rows on screen the bar will get hidden + * Otherwise the scrollbar handle gets resized based on the ratio to the maximum number of rows + * @param rows how many rows of data there are in total + */ + setTotalRows(rows: number): void { + this.totalRows = rows; + this.handleBody.height = (this.bg.displayHeight - 1 - this.handleBottom.displayHeight) * this.maxRows / this.totalRows; - this.setVisible(this.pages > 9); + this.setVisible(this.totalRows > this.maxRows); } } diff --git a/src/ui/settings/abstract-control-settings-ui-handler.ts b/src/ui/settings/abstract-control-settings-ui-handler.ts index f8dab1bf7cc..efa262bb2e9 100644 --- a/src/ui/settings/abstract-control-settings-ui-handler.ts +++ b/src/ui/settings/abstract-control-settings-ui-handler.ts @@ -1,11 +1,12 @@ -import UiHandler from "../ui-handler"; -import BattleScene from "../../battle-scene"; -import {Mode} from "../ui"; -import {InterfaceConfig} from "../../inputs-controller"; -import {addWindow} from "../ui-theme"; -import {addTextObject, TextStyle} from "../text"; -import {getIconWithSettingName} from "#app/configs/inputs/configHandler"; -import NavigationMenu, {NavigationManager} from "#app/ui/settings/navigationMenu"; +import UiHandler from "#app/ui/ui-handler"; +import BattleScene from "#app/battle-scene"; +import { Mode } from "#app/ui/ui"; +import { InterfaceConfig } from "#app/inputs-controller"; +import { addWindow } from "#app/ui/ui-theme"; +import { addTextObject, TextStyle } from "#app/ui/text"; +import { ScrollBar } from "#app/ui/scroll-bar"; +import { getIconWithSettingName } from "#app/configs/inputs/configHandler"; +import NavigationMenu, { NavigationManager } from "#app/ui/settings/navigationMenu"; import { Device } from "#enums/devices"; import { Button } from "#enums/buttons"; import i18next from "i18next"; @@ -19,7 +20,7 @@ export interface LayoutConfig { inputsIcons: InputsIcons; settingLabels: Phaser.GameObjects.Text[]; optionValueLabels: Phaser.GameObjects.Text[][]; - optionCursors: integer[]; + optionCursors: number[]; keys: string[]; bindingSettings: Array; } @@ -31,8 +32,9 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler protected optionsContainer: Phaser.GameObjects.Container; protected navigationContainer: NavigationMenu; - protected scrollCursor: integer; - protected optionCursors: integer[]; + protected scrollBar: ScrollBar; + protected scrollCursor: number; + protected optionCursors: number[]; protected cursorObj: Phaser.GameObjects.NineSlice | null; protected optionsBg: Phaser.GameObjects.NineSlice; @@ -65,7 +67,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler protected device: Device; abstract saveSettingToLocalStorage(setting, cursor): void; - abstract setSetting(scene: BattleScene, setting, value: integer): boolean; + abstract setSetting(scene: BattleScene, setting, value: number): boolean; /** * Constructor for the AbstractSettingsUiHandler. @@ -241,7 +243,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler // Calculate the total available space for placing option labels next to their setting label // We reserve space for the setting label and then distribute the remaining space evenly - const totalSpace = (300 - labelWidth) - totalWidth / 6; + const totalSpace = (297 - labelWidth) - totalWidth / 6; // Calculate the spacing between options based on the available space divided by the number of gaps between labels const optionSpacing = Math.floor(totalSpace / (optionValueLabels[s].length - 1)); @@ -269,6 +271,11 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler // Add the options container to the overall settings container to be displayed in the UI. this.settingsContainer.add(optionsContainer); } + + // Add vertical scrollbar + this.scrollBar = new ScrollBar(this.scene, this.optionsBg.width - 9, this.optionsBg.y + 5, 4, this.optionsBg.height - 11, this.rowsToDisplay); + this.settingsContainer.add(this.scrollBar); + // Add the settings container to the UI. ui.add(this.settingsContainer); @@ -413,6 +420,8 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler this.optionCursors = layout.optionCursors; this.inputsIcons = layout.inputsIcons; this.bindingSettings = layout.bindingSettings; + this.scrollBar.setTotalRows(layout.settingLabels.length); + this.scrollBar.setScrollCursor(0); // Return true indicating the layout was successfully applied. return true; @@ -538,7 +547,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler * @param cursor - The cursor position to set. * @returns `true` if the cursor was set successfully. */ - setCursor(cursor: integer): boolean { + setCursor(cursor: number): boolean { const ret = super.setCursor(cursor); // If the optionsContainer is not initialized, return the result from the parent class directly. if (!this.optionsContainer) { @@ -547,7 +556,8 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler // Check if the cursor object exists, if not, create it. if (!this.cursorObj) { - this.cursorObj = this.scene.add.nineslice(0, 0, "summary_moves_cursor", undefined, (this.scene.game.canvas.width / 6) - 10, 16, 1, 1, 1, 1); + const cursorWidth = (this.scene.game.canvas.width / 6) - (this.scrollBar.visible? 16 : 10); + this.cursorObj = this.scene.add.nineslice(0, 0, "summary_moves_cursor", undefined, cursorWidth, 16, 1, 1, 1, 1); this.cursorObj.setOrigin(0, 0); // Set the origin to the top-left corner. this.optionsContainer.add(this.cursorObj); // Add the cursor to the options container. } @@ -564,7 +574,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler * @param scrollCursor - The scroll cursor position to set. * @returns `true` if the scroll cursor was set successfully. */ - setScrollCursor(scrollCursor: integer): boolean { + setScrollCursor(scrollCursor: number): boolean { // Check if the new scroll position is the same as the current one; if so, do not update. if (scrollCursor === this.scrollCursor) { return false; @@ -572,6 +582,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler // Update the internal scroll cursor state this.scrollCursor = scrollCursor; + this.scrollBar.setScrollCursor(this.scrollCursor); // Apply the new scroll position to the settings UI. this.updateSettingsScroll(); @@ -590,7 +601,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler * @param save - Whether to save the setting to local storage. * @returns `true` if the option cursor was set successfully. */ - setOptionCursor(settingIndex: integer, cursor: integer, save?: boolean): boolean { + setOptionCursor(settingIndex: number, cursor: number, save?: boolean): boolean { // Retrieve the specific setting using the settingIndex from the settingDevice enumeration. const setting = this.setting[Object.keys(this.setting)[settingIndex]]; diff --git a/src/ui/settings/abstract-settings-ui-handler.ts b/src/ui/settings/abstract-settings-ui-handler.ts index 570377eab43..975a32127ff 100644 --- a/src/ui/settings/abstract-settings-ui-handler.ts +++ b/src/ui/settings/abstract-settings-ui-handler.ts @@ -1,12 +1,13 @@ -import BattleScene from "../../battle-scene"; -import { hasTouchscreen, isMobile } from "../../touch-controls"; -import { TextStyle, addTextObject } from "../text"; -import { Mode } from "../ui"; -import UiHandler from "../ui-handler"; -import { addWindow } from "../ui-theme"; -import {Button} from "#enums/buttons"; -import {InputsIcons} from "#app/ui/settings/abstract-control-settings-ui-handler"; -import NavigationMenu, {NavigationManager} from "#app/ui/settings/navigationMenu"; +import BattleScene from "#app/battle-scene"; +import { hasTouchscreen, isMobile } from "#app/touch-controls"; +import { TextStyle, addTextObject } from "#app/ui/text"; +import { Mode } from "#app/ui/ui"; +import UiHandler from "#app/ui/ui-handler"; +import { addWindow } from "#app/ui/ui-theme"; +import { ScrollBar } from "#app/ui/scroll-bar"; +import { Button } from "#enums/buttons"; +import { InputsIcons } from "#app/ui/settings/abstract-control-settings-ui-handler"; +import NavigationMenu, { NavigationManager } from "#app/ui/settings/navigationMenu"; import { Setting, SettingKeys, SettingType } from "#app/system/settings/settings"; import i18next from "i18next"; @@ -19,11 +20,12 @@ export default class AbstractSettingsUiHandler extends UiHandler { private optionsContainer: Phaser.GameObjects.Container; private navigationContainer: NavigationMenu; - private scrollCursor: integer; + private scrollCursor: number; + private scrollBar: ScrollBar; private optionsBg: Phaser.GameObjects.NineSlice; - private optionCursors: integer[]; + private optionCursors: number[]; private settingLabels: Phaser.GameObjects.Text[]; private optionValueLabels: Phaser.GameObjects.Text[][]; @@ -117,7 +119,7 @@ export default class AbstractSettingsUiHandler extends UiHandler { const labelWidth = Math.max(78, this.settingLabels[s].displayWidth + 8); - const totalSpace = (300 - labelWidth) - totalWidth / 6; + const totalSpace = (297 - labelWidth) - totalWidth / 6; const optionSpacing = Math.floor(totalSpace / (this.optionValueLabels[s].length - 1)); let xOffset = 0; @@ -130,7 +132,11 @@ export default class AbstractSettingsUiHandler extends UiHandler { this.optionCursors = this.settings.map(setting => setting.default); + this.scrollBar = new ScrollBar(this.scene, this.optionsBg.width - 9, this.optionsBg.y + 5, 4, this.optionsBg.height - 11, this.rowsToDisplay); + this.scrollBar.setTotalRows(this.settings.length); + this.settingsContainer.add(this.optionsBg); + this.settingsContainer.add(this.scrollBar); this.settingsContainer.add(this.navigationContainer); this.settingsContainer.add(actionsBg); this.settingsContainer.add(this.optionsContainer); @@ -186,6 +192,7 @@ export default class AbstractSettingsUiHandler extends UiHandler { this.settingsContainer.setVisible(true); this.setCursor(0); + this.setScrollCursor(0); this.getUi().moveTo(this.settingsContainer, this.getUi().length - 1); @@ -301,11 +308,12 @@ export default class AbstractSettingsUiHandler extends UiHandler { * @param cursor - The cursor position to set. * @returns `true` if the cursor was set successfully. */ - setCursor(cursor: integer): boolean { + setCursor(cursor: number): boolean { const ret = super.setCursor(cursor); if (!this.cursorObj) { - this.cursorObj = this.scene.add.nineslice(0, 0, "summary_moves_cursor", undefined, (this.scene.game.canvas.width / 6) - 10, 16, 1, 1, 1, 1); + const cursorWidth = (this.scene.game.canvas.width / 6) - (this.scrollBar.visible? 16 : 10); + this.cursorObj = this.scene.add.nineslice(0, 0, "summary_moves_cursor", undefined, cursorWidth, 16, 1, 1, 1, 1); this.cursorObj.setOrigin(0, 0); this.optionsContainer.add(this.cursorObj); } @@ -323,7 +331,7 @@ export default class AbstractSettingsUiHandler extends UiHandler { * @param save - Whether to save the setting to local storage. * @returns `true` if the option cursor was set successfully. */ - setOptionCursor(settingIndex: integer, cursor: integer, save?: boolean): boolean { + setOptionCursor(settingIndex: number, cursor: number, save?: boolean): boolean { const setting = this.settings[settingIndex]; if (setting.key === SettingKeys.Touch_Controls && cursor && hasTouchscreen() && isMobile()) { @@ -359,12 +367,13 @@ export default class AbstractSettingsUiHandler extends UiHandler { * @param scrollCursor - The scroll cursor position to set. * @returns `true` if the scroll cursor was set successfully. */ - setScrollCursor(scrollCursor: integer): boolean { + setScrollCursor(scrollCursor: number): boolean { if (scrollCursor === this.scrollCursor) { return false; } this.scrollCursor = scrollCursor; + this.scrollBar.setScrollCursor(this.scrollCursor); this.updateSettingsScroll(); @@ -394,6 +403,7 @@ export default class AbstractSettingsUiHandler extends UiHandler { clear() { super.clear(); this.settingsContainer.setVisible(false); + this.setScrollCursor(0); this.eraseCursor(); this.getUi().bgmBar.toggleBgmBar(this.scene.showBgmBar); if (this.reloadRequired) { diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index ee56a3631dd..19527c9777f 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -627,7 +627,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { const starterBoxContainer = this.scene.add.container(speciesContainerX + 6, 9); //115 - this.starterSelectScrollBar = new ScrollBar(this.scene, 161, 12, 0); + this.starterSelectScrollBar = new ScrollBar(this.scene, 161, 12, 5, starterContainerWindow.height - 6, 9); starterBoxContainer.add(this.starterSelectScrollBar); @@ -760,7 +760,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.pokemonCaughtHatchedContainer.add(this.pokemonHatchedCountText); this.pokemonMovesContainer = this.scene.add.container(102, 16); - this.pokemonMovesContainer.setScale(0.5); + this.pokemonMovesContainer.setScale(0.375); for (let m = 0; m < 4; m++) { const moveContainer = this.scene.add.container(0, 14 * m); @@ -894,6 +894,9 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.message.setOrigin(0, 0); this.starterSelectMessageBoxContainer.add(this.message); + // arrow icon for the message box + this.initPromptSprite(this.starterSelectMessageBoxContainer); + this.statsContainer = new StatsContainer(this.scene, 6, 16); this.scene.add.existing(this.statsContainer); @@ -911,7 +914,11 @@ export default class StarterSelectUiHandler extends MessageUiHandler { y: this.scene.game.canvas.height / 6 - MoveInfoOverlay.getHeight(overlayScale) - 29, }); this.starterSelectContainer.add(this.moveInfoOverlay); + + // Filter bar sits above everything, except the tutorial overlay and message box this.starterSelectContainer.bringToTop(this.filterBarContainer); + this.initTutorialOverlay(this.starterSelectContainer); + this.starterSelectContainer.bringToTop(this.starterSelectMessageBoxContainer); this.scene.eventTarget.addEventListener(BattleSceneEventType.CANDY_UPGRADE_NOTIFICATION_CHANGED, (e) => this.onCandyUpgradeDisplayChanged(e)); @@ -2533,8 +2540,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler { } }); - this.starterSelectScrollBar.setPages(Math.max(Math.ceil(this.filteredStarterContainers.length / 9), 1)); - this.starterSelectScrollBar.setPage(0); + this.starterSelectScrollBar.setTotalRows(Math.max(Math.ceil(this.filteredStarterContainers.length / 9), 1)); + this.starterSelectScrollBar.setScrollCursor(0); // sort const sort = this.filterBar.getVals(DropDownColumn.SORT)[0]; @@ -2569,7 +2576,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { const onScreenFirstIndex = this.scrollCursor * maxColumns; const onScreenLastIndex = Math.min(this.filteredStarterContainers.length - 1, onScreenFirstIndex + maxRows * maxColumns -1); - this.starterSelectScrollBar.setPage(this.scrollCursor); + this.starterSelectScrollBar.setScrollCursor(this.scrollCursor); let pokerusCursorIndex = 0; this.filteredStarterContainers.forEach((container, i) => { diff --git a/src/ui/stats-container.ts b/src/ui/stats-container.ts index c6e0ea3a71c..06dd6e7c035 100644 --- a/src/ui/stats-container.ts +++ b/src/ui/stats-container.ts @@ -4,12 +4,15 @@ import { TextStyle, addBBCodeTextObject, addTextObject, getTextColor } from "./t import { PERMANENT_STATS, getStatKey } from "#app/enums/stat"; import i18next from "i18next"; + const ivChartSize = 24; const ivChartStatCoordMultipliers = [[0, -1], [0.825, -0.5], [0.825, 0.5], [-0.825, -0.5], [-0.825, 0.5], [0, 1]]; const speedLabelOffset = -3; const sideLabelOffset = 1; const ivLabelOffset = [0, sideLabelOffset, -sideLabelOffset, sideLabelOffset, -sideLabelOffset, speedLabelOffset]; +const ivChartLabelyOffset= [0, 5, 0, 5, 0, 0]; // doing this so attack does not overlap with (+N) const ivChartStatIndexes = [0, 1, 2, 5, 4, 3]; // swap special attack and speed + const defaultIvChartData = new Array(12).fill(null).map(() => 0); export class StatsContainer extends Phaser.GameObjects.Container { @@ -29,7 +32,6 @@ export class StatsContainer extends Phaser.GameObjects.Container { setup() { this.setName("stats"); const ivChartBgData = new Array(6).fill(null).map((_, i: integer) => [ ivChartSize * ivChartStatCoordMultipliers[ivChartStatIndexes[i]][0], ivChartSize * ivChartStatCoordMultipliers[ivChartStatIndexes[i]][1] ] ).flat(); - const ivChartBg = this.scene.add.polygon(48, 44, ivChartBgData, 0xd8e0f0, 0.625); ivChartBg.setOrigin(0, 0); @@ -55,12 +57,19 @@ export class StatsContainer extends Phaser.GameObjects.Container { this.ivStatValueTexts = []; for (const s of PERMANENT_STATS) { - const statLabel = addTextObject(this.scene, ivChartBg.x + (ivChartSize) * ivChartStatCoordMultipliers[s][0] * 1.325, ivChartBg.y + (ivChartSize) * ivChartStatCoordMultipliers[s][1] * 1.325 - 4 + ivLabelOffset[s], i18next.t(getStatKey(s)), TextStyle.TOOLTIP_CONTENT); + const statLabel = addTextObject( + this.scene, + ivChartBg.x + (ivChartSize) * ivChartStatCoordMultipliers[s][0] * 1.325 + (this.showDiff ? 0 : ivLabelOffset[s]), + ivChartBg.y + (ivChartSize) * ivChartStatCoordMultipliers[s][1] * 1.325 - 4 + (this.showDiff ? 0 : ivChartLabelyOffset[s]), + i18next.t(getStatKey(s)), + TextStyle.TOOLTIP_CONTENT + ); statLabel.setOrigin(0.5); - this.ivStatValueTexts[s] = addBBCodeTextObject(this.scene, statLabel.x, statLabel.y + 8, "0", TextStyle.TOOLTIP_CONTENT); + this.ivStatValueTexts[s] = addBBCodeTextObject(this.scene, statLabel.x - (this.showDiff ? 0 : ivLabelOffset[s]), statLabel.y + 8, "0", TextStyle.TOOLTIP_CONTENT); this.ivStatValueTexts[s].setOrigin(0.5); + this.add(statLabel); this.add(this.ivStatValueTexts[s]); } diff --git a/src/ui/summary-ui-handler.ts b/src/ui/summary-ui-handler.ts index 8ae72f08edd..fb9f1561447 100644 --- a/src/ui/summary-ui-handler.ts +++ b/src/ui/summary-ui-handler.ts @@ -214,7 +214,7 @@ export default class SummaryUiHandler extends UiHandler { this.statusContainer.add(statusLabel); - this.status = this.scene.add.sprite(91, 4, "statuses"); + this.status = this.scene.add.sprite(91, 4, Utils.getLocalizedSpriteKey("statuses")); this.status.setOrigin(0.5, 0); this.statusContainer.add(this.status); @@ -824,6 +824,7 @@ export default class SummaryUiHandler extends UiHandler { metFragment: i18next.t(`pokemonSummary:metFragment.${this.pokemon?.metBiome === -1? "apparently": "normal"}`, { biome: `${getBBCodeFrag(getBiomeName(this.pokemon?.metBiome!), TextStyle.SUMMARY_RED)}${closeFragment}`, // TODO: is this bang correct? level: `${getBBCodeFrag(this.pokemon?.metLevel.toString()!, TextStyle.SUMMARY_RED)}${closeFragment}`, // TODO: is this bang correct? + wave: `${getBBCodeFrag((this.pokemon?.metWave ? this.pokemon.metWave.toString()! : i18next.t("pokemonSummary:unknownTrainer")), TextStyle.SUMMARY_RED)}${closeFragment}`, }), natureFragment: i18next.t(`pokemonSummary:natureFragment.${rawNature}`, { nature: nature }) }); diff --git a/src/utils.ts b/src/utils.ts index a8206bf4dcf..e526d086316 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,5 @@ import { MoneyFormat } from "#enums/money-format"; +import { Moves } from "#enums/moves"; import i18next from "i18next"; export const MissingTextureKey = "__MISSING"; @@ -628,3 +629,12 @@ export function getLocalizedSpriteKey(baseKey: string) { export function isBetween(num: number, min: number, max: number): boolean { return num >= min && num <= max; } + +/** + * Helper method to return the animation filename for a given move + * + * @param move the move for which the animation filename is needed + */ +export function animationFileName(move: Moves): string { + return Moves[move].toLowerCase().replace(/\_/g, "-"); +} diff --git a/vite.config.ts b/vite.config.ts index 1fd85e2572f..946315c4b7b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -31,6 +31,7 @@ export default defineConfig(({mode}) => { return ({ ...defaultConfig, + base: '', esbuild: { pure: mode === 'production' ? ['console.log'] : [], keepNames: true,